Theme:

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