This page covers texture declarations in
defineMaterialFor loading textures from URLs, see Loading Textures.
Declaring textures
Textures are declared in the
texturesdefineMaterialconst 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- — the texture
uFoo: texture_2d<f32> - — the associated sampler
uFooSampler: sampler
TextureDefinition fields
| Field | Type | Default | Description |
|---|---|---|---|
source | TextureValue | null | Initial texture binding value |
colorSpace | 'srgb''linear' | 'srgb' | Determines GPU format: rgba8unorm-srgbrgba8unorm |
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''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 createImageBitmapuseTexture |
HTMLImageElement | Standard <img> |
HTMLCanvasElement | 2D canvas element |
HTMLVideoElement | Video element (auto-infers perFrame |
{ source, width?, height?, ... } | Source with per-value overrides for widthheightcolorSpaceflipYpremultipliedAlphagenerateMipmapsupdate |
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
HTMLVideoElementperFrameUpdate 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
- Runtime override — (from
TextureData.update)state.setTexture({ source, update: '...' }) - Definition default — (from
TextureDefinition.update)defineMaterial({ textures: { ... } }) - Automatic fallback — →
HTMLVideoElement, everything else →perFrameonce
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
nullMipmap generation
When
generateMipmaps: true- The base level (mip 0) is uploaded normally with .
copyExternalImageToTexture - Each subsequent mip level is generated by drawing into an offscreen canvas, halving dimensions each time.
- Each downscaled level is uploaded individually.
The implementation uses
OffscreenCanvas<canvas>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| Field | Effect |
|---|---|
filter | Sets both magFilterminFilter |
addressModeU | Horizontal wrap: 'clamp-to-edge''repeat''mirror-repeat' |
addressModeV | Vertical wrap: same options |
anisotropy | Maximum anisotropic filtering samples (1 = disabled) |
If
generateMipmapsmipmapFilterfilterNaming rules
Texture identifiers follow the same rules as uniforms:
[A-Za-z_][A-Za-z0-9_]*Practical recommendations
| Use case | Recommended config |
|---|---|
| Static image (photo, sprite) | update: 'once'generateMipmaps: true |
| Event-driven updates | update: 'onInvalidate'invalidate(token) |
| Video / camera / canvas stream | update: 'perFrame' |
| Alpha-correct compositing | Set premultipliedAlpha |
| Pixel art | filter: 'nearest' |
| Tiling patterns | addressModeU: 'repeat'addressModeV: 'repeat' |