Shaders
Backend.Shader
Write custom WebGPU shaders without GPU boilerplate. You provide WGSL code and a uniforms object — the engine handles buffer creation, pipeline setup, uniform packing, and rendering.
Minimal Example
const Backend = world.renderer;
const mesh = new U.Mesh(new U.PlaneGeometry(1, 1), new Material());
Backend.Shader({
uniforms: {
color: new U.Color(1, 0, 0),
modelMatrix: new U.Matrix4(),
},
code: /* wgsl */ `
struct VIn {
@location(0) position: vec3f,
@location(1) uv: vec2f,
}
struct VOut {
@builtin(position) pos: vec4f,
@location(0) uv: vec2f,
}
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
o.pos = frame.viewProjection * uniforms.modelMatrix * vec4f(v.position, 1.0);
o.uv = v.uv;
return o;
}
@fragment fn fs_main(v: VOut) -> @location(0) vec4f {
return vec4f(uniforms.color, 1.0);
}
`,
}).attach(mesh);
world.add(mesh);
What You Write vs What Gets Auto-Generated
You write the shader logic — VIn, VOut, vs_main, fs_main, and any helper functions. The system auto-prepends:
// Auto-generated Frame struct (camera, lighting, shadows, time)
struct Frame {
view: mat4x4f,
projection: mat4x4f,
viewProjection: mat4x4f,
cameraPos: vec4f,
time: vec4f, // time.x = elapsed seconds
skyLightDirection: vec4f,
skyLightColor: vec4f,
ambientLightColor: vec4f,
lightViewProjection0: mat4x4f,
lightViewProjection1: mat4x4f,
lightViewProjection2: mat4x4f,
}
// Auto-generated from your JS uniforms object (alignment handled for you)
struct Uniforms {
color: vec3f,
modelMatrix: mat4x4f,
}
@group(0) @binding(0) var<uniform> frame: Frame;
@group(1) @binding(0) var<uniform> uniforms: Uniforms;
// --- your code starts here ---
You reference frame.* and uniforms.* directly in your shader code.
API
const shader = Backend.Shader({
// Required
code: "...", // Your WGSL (vs_main + fs_main)
// Optional
label: "MyShader", // Debug label (default: 'CustomShader')
uniforms: {}, // JS object → auto-generates WGSL struct
attributes: ["position", "uv"], // Geometry attributes to bind (default)
depthWrite: true, // Write to depth buffer (default: true)
depthCompare: "less", // Depth test function (default: 'less')
cullMode: "back", // Face culling (default: 'back')
topology: "triangle-list", // Primitive topology (default: 'triangle-list')
blend: undefined, // GPUBlendState for transparency (default: opaque)
});
shader.attach(mesh); // Creates GPU buffers from mesh.geometry, sets mesh.render
shader.uniforms; // Same object you passed in — mutate it per-frame
Uniform Types
The uniforms object maps JS types to WGSL types automatically:
| JS Type | WGSL Type | Example |
|---|---|---|
number |
f32 |
opacity: 1.0 |
U.Vector2 |
vec2f |
offset: new U.Vector2() |
U.Vector3 |
vec3f |
direction: new U.Vector3() |
U.Vector4 |
vec4f |
clipPlane: new U.Vector4() |
U.Color |
vec3f |
color: new U.Color(1,0,0) |
U.Matrix4 |
mat4x4f |
modelMatrix: new U.Matrix4() |
Struct alignment and padding are handled automatically. You do not need _pad fields.
Updating Uniforms Per-Frame
Mutate the uniforms object directly. Changes are uploaded to the GPU every frame.
// In your update loop:
shader.uniforms.opacity = 0.5;
shader.uniforms.color.set(0, 1, 0);
shader.uniforms.modelMatrix.copy(mesh.matrixWorld);
For modelMatrix, call mesh.updateMatrixWorld(true) before copying:
mesh.updateMatrixWorld(true);
shader.uniforms.modelMatrix.copy(mesh.matrixWorld);
Vertex Inputs
By default, the system binds position (vec3f) and uv (vec2f) from the mesh geometry:
struct VIn {
@location(0) position: vec3f, // geometry.attributes.position
@location(1) uv: vec2f, // geometry.attributes.uv
}
To use different or additional attributes, pass attributes:
Backend.Shader({
attributes: ["position", "uv", "normal"],
code: /* wgsl */ `
struct VIn {
@location(0) position: vec3f,
@location(1) uv: vec2f,
@location(2) normal: vec3f,
}
// ...
`,
});
The @location(N) index matches the order in the attributes array.
Transparency
Pass a blend state for transparent rendering:
Backend.Shader({
blend: {
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
},
uniforms: {
opacity: 0.5,
color: new U.Color(1, 1, 1),
modelMatrix: new U.Matrix4(),
},
code: /* wgsl */ `
struct VIn { @location(0) position: vec3f, @location(1) uv: vec2f }
struct VOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
o.pos = frame.viewProjection * uniforms.modelMatrix * vec4f(v.position, 1.0);
o.uv = v.uv;
return o;
}
@fragment fn fs_main(v: VOut) -> @location(0) vec4f {
return vec4f(uniforms.color, uniforms.opacity);
}
`,
}).attach(mesh);
Full Example: Animated Grid
const gridUniforms = {
gridColor: new U.Color("#23262e"),
baseColor: new U.Color("#30353d"),
elementWidth: 0.05,
elementHeight: 0.05,
lineWidth: 0.004,
scroll: new U.Vector2(0, 0),
modelMatrix: new U.Matrix4(),
};
Backend.Shader({
label: "AnimatedGrid",
uniforms: gridUniforms,
code: /* wgsl */ `
struct VIn {
@location(0) position: vec3f,
@location(1) uv: vec2f,
}
struct VOut {
@builtin(position) pos: vec4f,
@location(0) uv: vec2f,
}
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
o.pos = frame.viewProjection * uniforms.modelMatrix * vec4f(v.position, 1.0);
o.uv = v.uv;
return o;
}
fn linear_to_srgb(color: vec4f) -> vec4f {
let cutoff = color.rgb <= vec3f(0.0031308);
let higher = pow(color.rgb, vec3f(0.41666)) * 1.055 - vec3f(0.055);
let lower = color.rgb * 12.92;
return vec4f(select(higher, lower, cutoff), color.a);
}
@fragment fn fs_main(v: VOut) -> @location(0) vec4f {
var uv = v.uv + uniforms.scroll;
let coord = uv / vec2f(uniforms.elementWidth, uniforms.elementHeight);
let grid = fract(coord);
let fw = fwidth(coord);
let lw = vec2f(
uniforms.lineWidth * uniforms.elementWidth,
uniforms.lineWidth * uniforms.elementHeight,
);
let smoothGrid = abs(grid - 0.5) - (0.5 - lw / vec2f(uniforms.elementWidth, uniforms.elementHeight));
let edge = fw * 2.0;
let g = min(
smoothstep(-edge.x, edge.x, smoothGrid.x) + smoothstep(-edge.y, edge.y, smoothGrid.y),
1.0,
);
let color = mix(uniforms.baseColor, uniforms.gridColor, g);
return linear_to_srgb(vec4f(color, 1.0));
}
`,
}).attach(gridMesh);
// Per-frame update
gridUniforms.scroll.set(xScroll, yScroll);
gridMesh.updateMatrixWorld(true);
gridUniforms.modelMatrix.copy(gridMesh.matrixWorld);
Available Frame Uniforms
These are available in any shader via frame.*:
| Field | Type | Description |
|---|---|---|
frame.view |
mat4x4f |
Camera view matrix |
frame.projection |
mat4x4f |
Camera projection matrix |
frame.viewProjection |
mat4x4f |
Combined view * projection |
frame.cameraPos |
vec4f |
Camera world position (xyz) |
frame.time |
vec4f |
Elapsed time in .x |
frame.skyLightDirection |
vec4f |
Directional light direction |
frame.skyLightColor |
vec4f |
Directional light color * intensity |
frame.ambientLightColor |
vec4f |
Ambient light color * intensity |
frame.lightViewProjection0 |
mat4x4f |
Shadow cascade 0 matrix |
frame.lightViewProjection1 |
mat4x4f |
Shadow cascade 1 matrix |
frame.lightViewProjection2 |
mat4x4f |
Shadow cascade 2 matrix |