defineMaterialFragMaterialFragCanvasBasic usage
import { defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.5, 1.0);
}
`
});import { defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.5, 1.0);
}
`
});Input fields
| Field | Type | Required | Description |
|---|---|---|---|
fragment | string | Yes | WGSL fragment source containing fn frag(uv: vec2f) -> vec4f |
uniforms | UniformMap | No | Named uniform declarations with initial values |
textures | TextureDefinitionMap | No | Named texture declarations with sampler/upload config |
defines | MaterialDefines | No | Compile-time constants injected as const |
includes | MaterialIncludes | No | Named WGSL source chunks for #include |
Fragment contract
The fragment source must contain this function signature:
fn frag(uv: vec2f) -> vec4ffn frag(uv: vec2f) -> vec4fdefineMaterial- missing entrypoint
frag - wrong parameter list (must be exactly )
uv: vec2f - wrong return type (must be )
vec4f
What defineMaterial does internally
defineMaterial- Validates the fragment contract — checks entrypoint name, parameter contract, and return type.
- Normalizes uniforms — validates identifier names, infers types from values, sorts alphabetically.
- Normalizes textures — validates identifier names and clones definitions.
- Normalizes defines — validates identifier names, checks value constraints (finite numbers, integer checks for /
i32, non-negative foru32).u32 - Normalizes includes — validates identifier names, checks for non-empty source strings.
- Freezes the output object and its top-level maps (,
uniforms,textures,defines).includes
Immutability
The returned
FragMaterialObject.freeze- You cannot modify the material after creation.
- The ,
uniforms,textures, anddefinessub-objects are also frozen.includes - To change a material, create a new one with .
defineMaterial(...)
This immutability is critical for MotionGPU’s caching: the material signature is computed once and used to detect when the renderer needs rebuilding.
Material signatures
When
FragCanvas- The preprocessed fragment (after include/define expansion).
- The uniform layout (sorted name/type sequence).
- The texture key list (sorted).
- The normalized texture sampling/upload config for each texture.
This signature is combined with
outputColorSpaceWhat triggers rebuilds vs. buffer updates
| Change | Triggers rebuild? |
|---|---|
| Fragment shader text change | Yes |
| Adding/removing a uniform | Yes |
| Adding/removing a texture | Yes |
| Changing texture sampler config | Yes |
| Changing a define value | Yes |
Changing outputColorSpaceFragCanvas | Yes |
| Changing a uniform value at runtime | No — buffer update only |
| Changing a texture source at runtime | No — upload only |
Full example with all fields
const material = defineMaterial({
fragment: `
#include <sdf>
fn frag(uv: vec2f) -> vec4f {
let aspect = motiongpuFrame.resolution.x / motiongpuFrame.resolution.y;
var p = uv - 0.5;
p.x *= aspect;
let d = sdCircle(p, 0.2 + 0.05 * sin(motiongpuUniforms.uTime * 3.0));
let edge = smoothstep(EDGE_WIDTH, 0.0, abs(d));
let texColor = textureSample(uBackground, uBackgroundSampler, uv);
let finalColor = mix(texColor.rgb, motiongpuUniforms.uColor, edge);
return vec4f(finalColor, 1.0);
}
`,
uniforms: {
uTime: 0,
uColor: [0.2, 0.8, 1.0]
},
textures: {
uBackground: {
filter: 'linear',
addressModeU: 'repeat',
addressModeV: 'repeat'
}
},
defines: {
EDGE_WIDTH: 0.003
},
includes: {
sdf: `
fn sdCircle(p: vec2f, r: f32) -> f32 {
return length(p) - r;
}
`
}
});const material = defineMaterial({
fragment: `
#include <sdf>
fn frag(uv: vec2f) -> vec4f {
let aspect = motiongpuFrame.resolution.x / motiongpuFrame.resolution.y;
var p = uv - 0.5;
p.x *= aspect;
let d = sdCircle(p, 0.2 + 0.05 * sin(motiongpuUniforms.uTime * 3.0));
let edge = smoothstep(EDGE_WIDTH, 0.0, abs(d));
let texColor = textureSample(uBackground, uBackgroundSampler, uv);
let finalColor = mix(texColor.rgb, motiongpuUniforms.uColor, edge);
return vec4f(finalColor, 1.0);
}
`,
uniforms: {
uTime: 0,
uColor: [0.2, 0.8, 1.0]
},
textures: {
uBackground: {
filter: 'linear',
addressModeU: 'repeat',
addressModeV: 'repeat'
}
},
defines: {
EDGE_WIDTH: 0.003
},
includes: {
sdf: `
fn sdCircle(p: vec2f, r: f32) -> f32 {
return length(p) - r;
}
`
}
});Misuse guards
| Invalid input | Result |
|---|---|
Object not created with defineMaterial | Throws |
| Invalid identifier in uniforms/textures/defines/includes | Throws |
| Invalid uniform value shape (e.g., 5-element array) | Throws |
Missing fragment contract (fn frag(...) | Throws |
| Non-finite define number | Throws |
Fractional i32u32 | Throws |
Negative u32 | Throws |
| Empty include source | Throws |
Practical guidance
- Keep material objects stable — reuse the same instance. Creating a new material with the same content still matches the same signature, but unnecessary allocations add overhead.
- Put shape/type changes in — uniform types and texture bindings should be declared upfront.
defineMaterial - Put value changes in — animation, interaction, and dynamic state go through
useFrameandstate.setUniform().state.setTexture() - Use defines for compile-time branches — the compiler can eliminate dead code paths.
- Use includes for shared utility code — noise functions, SDF helpers, colour transforms.