Shaders & Textures

Textures

Declaring textures, configuring samplers, and managing runtime texture updates.


This page covers texture declarations in

defineMaterial
, the runtime texture value forms, update modes, upload behaviour, mipmap generation, and sampler configuration.

For loading textures from URLs, see Loading Textures.

Declaring textures

Textures are declared in the

textures
field of
defineMaterial
:

const material = defineMaterial({
  fragment: `
fn frag(uv: vec2f) -> vec4f {
  let color = textureSample(uAlbedo, uAlbedoSampler, uv);
  return color;
}
`,
  textures: {
    uAlbedo: {
      colorSpace: 'srgb',
      filter: 'linear',
      generateMipmaps: true
    }
  }
});
const material = defineMaterial({
  fragment: `
fn frag(uv: vec2f) -> vec4f {
  let color = textureSample(uAlbedo, uAlbedoSampler, uv);
  return color;
}
`,
  textures: {
    uAlbedo: {
      colorSpace: 'srgb',
      filter: 'linear',
      generateMipmaps: true
    }
  }
});

Each texture named

uFoo
creates two WGSL bindings:

  • uFoo: texture_2d<f32>
    — the texture
  • uFooSampler: sampler
    — the associated sampler

TextureDefinition fields

Field Type Default Description
source
TextureValue
null
Initial texture binding value
colorSpace
'srgb'
|
'linear'
'srgb'
Determines GPU format:
rgba8unorm-srgb
vs
rgba8unorm
flipY
boolean
true
Flips texture vertically during upload
generateMipmaps
boolean
false
Enables CPU-generated mip chain after upload
premultipliedAlpha
boolean
false
Upload premultiplication behaviour
update
'once'
|
'onInvalidate'
|
'perFrame'
Inferred from source Runtime refresh policy
anisotropy
number
1
Anisotropic filtering level (clamped to
1..16
)
filter
GPUFilterMode
'linear'
Min/mag filter mode (
'nearest'
or
'linear'
)
addressModeU
GPUAddressMode
'clamp-to-edge'
Horizontal wrap mode
addressModeV
GPUAddressMode
'clamp-to-edge'
Vertical wrap mode

Runtime texture value forms

You can set texture sources at definition time or at runtime via

state.setTexture()
:

Form Description
ImageBitmap
Pre-decoded bitmap (from
createImageBitmap
or
useTexture
)
HTMLImageElement
Standard
<img>
element
HTMLCanvasElement
2D canvas element
HTMLVideoElement
Video element (auto-infers
perFrame
update mode)
{ source, width?, height?, ... }
Source with per-value overrides for
width
,
height
,
colorSpace
,
flipY
,
premultipliedAlpha
,
generateMipmaps
,
update
null
Unbinds user source; a fallback 1×1 texture remains valid in the shader

Example: setting a texture from a video

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

  let video: HTMLVideoElement;

  useFrame((state) => {
    if (video && video.readyState >= 2) {
      state.setTexture('uVideo', video);
    }
  });
</script>

<video bind:this={video} src="/assets/loop.mp4" autoplay loop muted playsinline />
<script lang="ts">
  import { useFrame } from '@motion-core/motion-gpu';

  let video: HTMLVideoElement;

  useFrame((state) => {
    if (video && video.readyState >= 2) {
      state.setTexture('uVideo', video);
    }
  });
</script>

<video bind:this={video} src="/assets/loop.mp4" autoplay loop muted playsinline />

Because the source is an

HTMLVideoElement
, the update mode is automatically set to
perFrame
.

Update modes

The update mode controls when the texture is re-uploaded to the GPU:

Mode Behaviour
'once'
Upload only on first bind or when the source object / dimensions / format change
'onInvalidate'
Upload when the scheduler fires an invalidation, or on source change
'perFrame'
Upload every frame, regardless of change detection

Resolution precedence

  1. Runtime override
    TextureData.update
    (from
    state.setTexture({ source, update: '...' })
    )
  2. Definition default
    TextureDefinition.update
    (from
    defineMaterial({ textures: { ... } })
    )
  3. Automatic fallback
    HTMLVideoElement
    perFrame
    , everything else →
    once

Upload behaviour

The renderer decides how to handle each texture per frame:

Scenario Action
First bind Allocate GPU texture + upload
Source object reference changed Reallocate if size/format differ, then upload
Size or format changed Reallocate + upload
update = 'perFrame'
Upload every frame
update = 'onInvalidate'
Upload when invalidation is pending
update = 'once'
No re-upload unless source/size/format change

The renderer maintains a fallback 1×1 opaque white texture for each binding. If the user source is

null
, the fallback is used so the shader always has a valid binding.

Mipmap generation

When

generateMipmaps: true
is set:

  1. The base level (mip 0) is uploaded normally with
    copyExternalImageToTexture
    .
  2. Each subsequent mip level is generated by drawing into an offscreen canvas, halving dimensions each time.
  3. Each downscaled level is uploaded individually.

The implementation uses

OffscreenCanvas
when available, falling back to a regular
<canvas>
element.

Mipmap generation adds upload cost proportional to ~33% of the base texture size. Use it when texture minification is visible (e.g., a texture displayed at varying scales).

Sampler configuration

The sampler created for each texture is configured from the

TextureDefinition
fields:

Field Effect
filter
Sets both
magFilter
and
minFilter
addressModeU
Horizontal wrap:
'clamp-to-edge'
,
'repeat'
, or
'mirror-repeat'
addressModeV
Vertical wrap: same options
anisotropy
Maximum anisotropic filtering samples (1 = disabled)

If

generateMipmaps
is enabled,
mipmapFilter
is also set to the same value as
filter
.

Naming rules

Texture identifiers follow the same rules as uniforms:

[A-Za-z_][A-Za-z0-9_]*
. Invalid names throw at material definition time.

Practical recommendations

Use case Recommended config
Static image (photo, sprite)
update: 'once'
,
generateMipmaps: true
when minification is visible
Event-driven updates
update: 'onInvalidate'
+ explicit
invalidate(token)
Video / camera / canvas stream
update: 'perFrame'
Alpha-correct compositing Set
premultipliedAlpha
intentionally and keep source pipeline consistent
Pixel art
filter: 'nearest'
to avoid bilinear smoothing
Tiling patterns
addressModeU: 'repeat'
,
addressModeV: 'repeat'