Shaders & Textures

Texture Loading

Reactive hook for loading textures from URLs with automatic cleanup and error handling.


useTexture
is a Svelte hook that loads textures from URLs, exposes reactive loading/error state, and handles cancellation and resource cleanup automatically.

For texture declaration and configuration in materials, see Texture Definitions.

Basic usage

<script lang="ts">
  import { useFrame, useTexture } from '@motion-core/motion-gpu';

  const loaded = useTexture(['/assets/albedo.png']);

  useFrame((state) => {
    const tex = loaded.textures.current?.[0];
    state.setTexture('uAlbedo', tex ? { source: tex.source } : null);
  });
</script>
<script lang="ts">
  import { useFrame, useTexture } from '@motion-core/motion-gpu';

  const loaded = useTexture(['/assets/albedo.png']);

  useFrame((state) => {
    const tex = loaded.textures.current?.[0];
    state.setTexture('uAlbedo', tex ? { source: tex.source } : null);
  });
</script>

useTexture
starts loading immediately when called. It returns reactive stores that update as the load progresses.

URL input forms

Form Description
string[]
Static list of URLs
() => string[]
Lazy URL provider (evaluated on each load/reload)
// Static
const tex = useTexture(['/assets/a.png', '/assets/b.png']);

// Dynamic
const tex = useTexture(() => [
  `/assets/textures/${currentSet}/diffuse.png`,
  `/assets/textures/${currentSet}/normal.png`
]);
// Static
const tex = useTexture(['/assets/a.png', '/assets/b.png']);

// Dynamic
const tex = useTexture(() => [
  `/assets/textures/${currentSet}/diffuse.png`,
  `/assets/textures/${currentSet}/normal.png`
]);

Return value (
UseTextureResult
)

Field Type Description
textures
CurrentReadable<LoadedTexture[]
null>
Loaded textures in input order, or
null
on failure
loading
CurrentReadable<boolean>
true
while an active load request is running
error
CurrentReadable<Error
null>
Last loading error
reload
() => Promise<void>
Triggers a fresh load with current URL input

Using
.current
for synchronous access

All stores expose a

.current
getter for zero-subscription reads — ideal for
useFrame
callbacks:

useFrame((state) => {
  if (loaded.loading.current) return; // Skip while loading
  if (loaded.error.current) return;   // Skip on error

  const textures = loaded.textures.current;
  if (textures) {
    state.setTexture('uDiffuse', { source: textures[0].source });
    state.setTexture('uNormal', { source: textures[1].source });
  }
});
useFrame((state) => {
  if (loaded.loading.current) return; // Skip while loading
  if (loaded.error.current) return;   // Skip on error

  const textures = loaded.textures.current;
  if (textures) {
    state.setTexture('uDiffuse', { source: textures[0].source });
    state.setTexture('uNormal', { source: textures[1].source });
  }
});

Using Svelte subscriptions for reactive UI

<script lang="ts">
  const { textures, loading, error } = useTexture(['/assets/albedo.png']);
</script>

{#if $loading}
  <p>Loading textures...</p>
{:else if $error}
  <p>Error: {$error.message}</p>
{:else}
  <p>Loaded {$textures?.length ?? 0} textures</p>
{/if}
<script lang="ts">
  const { textures, loading, error } = useTexture(['/assets/albedo.png']);
</script>

{#if $loading}
  <p>Loading textures...</p>
{:else if $error}
  <p>Error: {$error.message}</p>
{:else}
  <p>Loaded {$textures?.length ?? 0} textures</p>
{/if}

TextureLoadOptions

Field Type Default Description
colorSpace
'srgb'
|
'linear'
'srgb'
Affects
colorSpaceConversion
during decode
requestInit
RequestInit
undefined
Forwarded to
fetch()
for custom headers, credentials, etc.
decode
TextureDecodeOptions
See below Bitmap decode settings
signal
AbortSignal
undefined
External cancellation signal
update
TextureUpdateMode
undefined
Metadata attached to loaded textures
flipY
boolean
undefined
Metadata attached to loaded textures
premultipliedAlpha
boolean
undefined
Metadata attached to loaded textures
generateMipmaps
boolean
undefined
Metadata attached to loaded textures

TextureDecodeOptions

Field Default Description
colorSpaceConversion
'none'
(linear) /
'default'
(srgb)
Colour space handling during bitmap decode
premultiplyAlpha
'default'
Alpha premultiplication during decode
imageOrientation
'none'
Bitmap orientation (
'none'
or
'flipY'
)

Example with options

const loaded = useTexture(
  ['/assets/hdr-env.png'],
  {
    colorSpace: 'linear',
    generateMipmaps: true,
    decode: {
      premultiplyAlpha: 'none'
    }
  }
);
const loaded = useTexture(
  ['/assets/hdr-env.png'],
  {
    colorSpace: 'linear',
    generateMipmaps: true,
    decode: {
      premultiplyAlpha: 'none'
    }
  }
);

LoadedTexture shape

Each loaded texture provides:

Field Type Description
url
string
Source URL
source
ImageBitmap
Decoded bitmap — pass to
state.setTexture
width
number
Bitmap width in pixels
height
number
Bitmap height in pixels
colorSpace
'srgb'
|
'linear'
Effective colour space
update
'once'
'onInvalidate'
'perFrame'
Effective update mode metadata
flipY
boolean
Effective flip-y metadata
premultipliedAlpha
boolean
Effective premultiplied alpha metadata
generateMipmaps
boolean
Effective mipmap metadata
dispose
() => void
Releases the bitmap resources

Abort, reload, and race safety

useTexture
is designed for safe async operation in a reactive environment:

Cancellation on reload

When

reload()
is called, any in-flight request is aborted before the new one starts. This prevents stale results from overwriting fresh data.

Request versioning

Each load increments an internal version counter. When a response arrives, it is only accepted if its version matches the current counter. This eliminates race conditions from overlapping loads.

Dispose on replacement

When new textures are loaded successfully, the previous bitmaps are disposed. When the component is destroyed, all current bitmaps are disposed.

Abort error suppression

AbortError
exceptions are treated as expected cancellation and are not surfaced in the
error
store. Only genuine failures (network errors, decode errors, etc.) appear.

Blob cache

Under the hood,

texture-loader.ts
maintains a reference-counted blob cache. This means:

  • If two
    useTexture
    calls request the same URL with the same options, only one network request is made.
  • The cache key is deterministic:
    JSON.stringify({ url, colorSpace, requestInit, decode })
    .
  • Entries are evicted when their reference count drops to zero (e.g., all components using that URL are destroyed).

Component lifecycle

useTexture
uses Svelte’s
onDestroy
internally:

  1. On destroy,
    disposed
    flag is set.
  2. The internal
    requestVersion
    is incremented (invalidating any in-flight result).
  3. The active
    AbortController
    is aborted.
  4. All current bitmap textures are disposed.

This means you don’t need to manually clean up — the hook handles its own lifecycle.

Error handling

Failure Source
createImageBitmap
unavailable
Runtime capability check
Non-OK HTTP response (4xx, 5xx) URL fetch
Invalid image data Bitmap decode
Abort / cancellation
AbortError
(suppressed in error store)