MotionGPU’s material preprocessor supports #includedefines
Include system
Declaring includes
Includes are named WGSL source chunks declared in the material definition:
const material = defineMaterial({
fragment: `
#include <noise>
fn frag(uv: vec2f) -> vec4f {
let n = fbm(uv * 4.0 + motiongpuFrame.time * 0.5);
return vec4f(vec3f(n), 1.0);
}
`,
includes: {
noise: `
fn hash(p: vec2f) -> f32 {
let h = dot(p, vec2f(127.1, 311.7));
return fract(sin(h) * 43758.5453);
}
fn noise(p: vec2f) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2f(1.0, 0.0)), u.x),
mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
u.y
);
}
fn fbm(p: vec2f) -> f32 {
var value = 0.0;
var amplitude = 0.5;
var pos = p;
for (var i = 0; i < 5; i++) {
value += amplitude * noise(pos);
pos *= 2.0;
amplitude *= 0.5;
}
return value;
}
`
}
});const material = defineMaterial({
fragment: `
#include <noise>
fn frag(uv: vec2f) -> vec4f {
let n = fbm(uv * 4.0 + motiongpuFrame.time * 0.5);
return vec4f(vec3f(n), 1.0);
}
`,
includes: {
noise: `
fn hash(p: vec2f) -> f32 {
let h = dot(p, vec2f(127.1, 311.7));
return fract(sin(h) * 43758.5453);
}
fn noise(p: vec2f) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2f(1.0, 0.0)), u.x),
mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
u.y
);
}
fn fbm(p: vec2f) -> f32 {
var value = 0.0;
var amplitude = 0.5;
var pos = p;
for (var i = 0; i < 5; i++) {
value += amplitude * noise(pos);
pos *= 2.0;
amplitude *= 0.5;
}
return value;
}
`
}
});Directive syntax
The directive must appear on its own line:
#include <name>#include <name>Where
nameincludes[A-Za-z_][A-Za-z0-9_]*Recursive includes
Includes can themselves contain
#includeincludes: {
math_utils: `
fn remap(value: f32, low1: f32, high1: f32, low2: f32, high2: f32) -> f32 {
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
`,
color_utils: `
#include <math_utils>
fn gammaCorrect(color: vec3f, gamma: f32) -> vec3f {
return pow(color, vec3f(1.0 / gamma));
}
fn contrastAdjust(color: vec3f, contrast: f32) -> vec3f {
return vec3f(
remap(color.r, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5),
remap(color.g, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5),
remap(color.b, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5)
);
}
`
}includes: {
math_utils: `
fn remap(value: f32, low1: f32, high1: f32, low2: f32, high2: f32) -> f32 {
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
`,
color_utils: `
#include <math_utils>
fn gammaCorrect(color: vec3f, gamma: f32) -> vec3f {
return pow(color, vec3f(1.0 / gamma));
}
fn contrastAdjust(color: vec3f, contrast: f32) -> vec3f {
return vec3f(
remap(color.r, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5),
remap(color.g, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5),
remap(color.b, 0.0, 1.0, 0.5 - contrast * 0.5, 0.5 + contrast * 0.5)
);
}
`
}Validation rules
| Rule | Behaviour |
|---|---|
| Unknown include key | Throws Unknown include "name" referenced in fragment shader. |
| Circular include chain | Throws with full stack: Circular include detected for "X". Include stack: A -> B -> X. |
| Invalid identifier | Throws naming validation error |
| Empty source string | Throws Include source must be a non-empty WGSL string. |
Include expansion order
- The fragment source is scanned line by line.
- Each line is replaced with the full source of that include chunk.
#include <name> - The process is recursive — included chunks can contain their own directives.
#include - A line map is built during expansion, tracking which generated line came from which source (fragment line N, include “X” line M, etc.).
This line map is used for diagnostics — when WGSL compilation fails, the error message points back to the original source location (see Error Handling).
Define system
Declaring defines
Defines are compile-time constants injected as top-level WGSL
constconst material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
var color = vec3f(0.0);
if (ENABLE_GLOW) {
let dist = distance(uv, vec2f(0.5, 0.5));
let glow = GLOW_INTENSITY / (dist * dist + 0.001);
color += vec3f(glow);
}
for (var i = 0i; i < ITERATIONS; i++) {
color += vec3f(0.01);
}
return vec4f(color, 1.0);
}
`,
defines: {
ENABLE_GLOW: true,
GLOW_INTENSITY: 0.02,
ITERATIONS: { type: 'i32', value: 8 }
}
});const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
var color = vec3f(0.0);
if (ENABLE_GLOW) {
let dist = distance(uv, vec2f(0.5, 0.5));
let glow = GLOW_INTENSITY / (dist * dist + 0.001);
color += vec3f(glow);
}
for (var i = 0i; i < ITERATIONS; i++) {
color += vec3f(0.01);
}
return vec4f(color, 1.0);
}
`,
defines: {
ENABLE_GLOW: true,
GLOW_INTENSITY: 0.02,
ITERATIONS: { type: 'i32', value: 8 }
}
});Value forms and emitted WGSL
| Input form | Emitted WGSL |
|---|---|
truefalse | const NAME: bool = true;const NAME: bool = false; |
42 | const NAME: f32 = 42.0; |
3.14 | const NAME: f32 = 3.14; |
{ type: 'bool', value: true } | const NAME: bool = true; |
{ type: 'f32', value: 1.5 } | const NAME: f32 = 1.5; |
{ type: 'i32', value: 8 } | const NAME: i32 = 8; |
{ type: 'u32', value: 16 } | const NAME: u32 = 16u; |
Validation rules for defines
| Rule | Result |
|---|---|
| Numeric values must be finite | Throws if NaNInfinity |
i32u32 | Throws if fractional |
u32>= 0 | Throws if negative |
| Invalid identifier name | Throws naming validation error |
Expansion order
Defines are sorted alphabetically by key and prepended before the expanded fragment source. This means:
- All declarations from defines appear first.
const - An empty separator line follows.
- The expanded fragment (with includes resolved) comes next.
This is deterministic and part of the material signature, meaning changing a define value always triggers a renderer rebuild (unlike uniforms, which only update a buffer).
Defines vs. uniforms: when to use which
| Use case | Use defines | Use uniforms |
|---|---|---|
Feature toggles (ENABLE_X | ✅ | ❌ |
| Loop iteration counts | ✅ | ❌ |
| Values that change every frame | ❌ | ✅ |
| Values controlled by user interaction | ❌ | ✅ |
| Performance-critical constants | ✅ (enables compiler optimisation) |
Key difference: Defines are baked into the shader source at material definition time. Changing a define requires creating a new material and triggers a full pipeline rebuild. Uniforms are updated per-frame via buffer writes with no recompilation cost.
Source-map diagnostics
When a WGSL compilation error occurs, MotionGPU uses the line map to report the original source location:
- Fragment line errors → reported as
fragment line X - Include errors → reported as
include <name> line X - Define errors → reported as
define "NAME" line X
This means you see meaningful error locations even when your fragment expands to hundreds of generated WGSL lines. See Error Handling and Diagnostics for the full error report structure.
Practical tips
- Keep includes general-purpose — utility functions (noise, SDF helpers, colour space conversions) are ideal candidates.
- Name includes clearly — the key is used in error messages and diagnostics.
- Use defines for branch elimination — the WGSL compiler can optimise away dead branches when a bool constant is .
false - Use typed defines for integer loops — avoids the default
{ type: 'i32', value: N }inference.f32 - Avoid putting inside includes — the entry point should stay in the main fragment for clarity.
fn frag(...)