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 behavior, 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 with fragmentVisible: true (default) creates two WGSL bindings:

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

When fragmentVisible: false, the texture slot is excluded from fragment WGSL declarations and group(0) fragment bind-group bindings.

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 behavior
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
storage boolean false Marks texture as compute-writable storage texture
format GPUTextureFormat Color-space derived default Required when storage: true (must be storage-compatible)
width number undefined Optional explicit width (used for storage texture allocation)
height number undefined Optional explicit height (used for storage texture allocation)
fragmentVisible boolean true Controls whether this slot is bound as a fragment sampled texture in group(0)

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/svelte'; 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 Behavior
'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 overrideTextureData.update (from state.setTexture({ source, update: '...' }))
  2. Definition defaultTextureDefinition.update (from defineMaterial({ textures: { ... } }))
  3. Automatic fallbackHTMLVideoElementperFrame, everything else → once

Storage textures (compute)

When storage: true is enabled, the texture participates in compute bindings (group(2)):

  • format must be a storage-compatible format.
  • For PingPongComputePass, the target texture must have explicit width and height.
  • Storage textures are compute-managed and are not source-uploaded through normal texture update flow.
  • Use fragmentVisible: false for compute-only storage textures that should not consume fragment sampler/texture bindings.
const material = defineMaterial({
  fragment: `
fn frag(uv: vec2f) -> vec4f {
  return vec4f(uv, 0.0, 1.0);
}
`,
  textures: {
    simState: {
      storage: true,
      format: 'rgba16float',
      width: 256,
      height: 256,
      fragmentVisible: false // compute-only slot
    }
  }
});
const material = defineMaterial({
  fragment: `
fn frag(uv: vec2f) -> vec4f {
  return vec4f(uv, 0.0, 1.0);
}
`,
  textures: {
    simState: {
      storage: true,
      format: 'rgba16float',
      width: 256,
      height: 256,
      fragmentVisible: false // compute-only slot
    }
  }
});

Upload behavior

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'
Compute-only storage target storage: true + explicit format/width/height + fragmentVisible: false
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'