Skip to content

Search

Documentation

Shaders and materials: WESL on wgpu

Shaders in Balaur are WESL (WGSL with imports and variants), linked at run time. A material asset names a shader and its values; meshes take one.

På den här sidan

Shaders are WESL: WGSL plus imports, @if variants and dead-code elimination, linked to WGSL at run time. A material asset names a shader and its values. sprite, shape2d, shape3d, mesh and tilemap take one. A node can pass one to its subtree. A node with neither draws with the built-in material.

A material and its shader

# materials/glow.toml
type = "material"
shader = "shaders/glow.wesl"

[params]
speed = 2.0
rings = 5.0
tint = "#ffb347"
// shaders/glow.wesl

struct Params { speed: f32, rings: f32, tint: vec4<f32> }
@group(3) @binding(0) var<uniform> params: Params;

@vertex fn vs_main(in: VertexInput) -> VertexOutput {
    return vertex(in);
}

@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let uv: vec2<f32> = in.uv;
    let dist: f32 = length(uv - vec2<f32>(0.5, 0.5)) * 2.0;
    let wave: f32 = 0.5 + 0.5 * sin(dist * params.rings * 6.2832 - time() * params.speed);
    let ramp: vec4<f32> = params.tint * wave * wave;
    let albedo: vec4<f32> = sample_albedo(uv) * tint(in);
    return albedo + ramp * albedo.a;
}
Import What it binds
package::sprite The 2D contract: frame and object uniforms, vertex inputs, vertex and place, sample_albedo, tint, time
package::mesh The 3D contract, with shade(in) for the scene's lights and fog
package::post A pass over the whole frame: fullscreen, frame(uv) and texel()
constants The engine's limits, MAX_LIGHTS, MAX_PROBES and MAX_JOINTS: the numbers its own buffers are sized by

A shader file holds its two entry points; the rest is imported.

  • [params] are the fields of the shader's Params struct, read off the linked shader.
  • A number is an f32, an array a vec2, vec3 or vec4, a #rrggbb string a vec4.
  • A param the shader does not read warns. One it declares and the material omits is zero.
  • [features] are the @if flags that pick a variant at link time.
  • A plugin can register shader modules for a project's shader to import.
  • A contract's helpers marked private stay inside it. Importing one fails when the shader links, naming the helper.

A material for a whole subtree

The material component goes on any node, including one that draws nothing. The node and its subtree draw with it.

[[nodes]]
id = "rig"
name = "Rig"

[nodes.material]
source = "materials/metal.toml"
  • The nearest one up wins.
  • A renderable's own material does not pass down. The split is the one between color and tint (Godot's self_modulate and modulate).
  • node.set_material(path) sets it from a script. node.material() reads the node's own, and node.effective_material() reads what it draws with.
  • The contract a shader imports says which nodes it draws. A 3D material on a 2D node leaves it on the built-in material and warns once.
  • In the inspector, an empty material field shows what the node inherits and from where, as metal ← Rig.

A pass over the whole frame

A material named on camera.post draws over the frame. It imports package::post, has no mesh or texture of its own, and keeps its Params at @group(1).


struct Params { amount: f32 }
@group(1) @binding(0) var<uniform> params: Params;

@vertex fn vs_main(@builtin(vertex_index) index: u32) -> PostInput {
    return fullscreen(index);
}

@fragment fn fs_main(in: PostInput) -> @location(0) vec4<f32> {
    let c: vec4<f32> = frame(in.uv);
    return vec4<f32>(mix(c.rgb, vec3<f32>(1.0) - c.rgb, params.amount), 1.0);
}
  • frame(uv) is the pass's input: linear light before tonemap, the finished picture after it.
  • texel() is the size of one texel.
  • The same material may sit on both sides of tonemap. It is compiled once per side.

In the editor

  • The inspector lists a material's params off its linked shader, with an editor per type, and writes them back to the file.
  • The shader opens in the code editor with WESL highlighting. On save, every material naming it relinks and every node drawing with it is rebuilt.
  • A shader that fails to link is reported in the Problems dock with file, line and column. The node draws with the built-in material until it links. render::check_material(path) gives the same answer to a tool.
Way How
A line's value Click a line number in the gutter; the viewport draws that line's value for every pixel that reaches it: a vec2 as red and green, an f32 as grey, a vec4 as colour. The line must declare a typed value (let dist: f32 = ...) or a constructor. Click again to restore the picture
Channels render::set_debug_view("normals") draws the scene's normals. "uv", "depth" and "albedo" are the others; "" is the picture. The command palette has one command per channel

Determinism

  • A shader is outside the digest: rendering feeds nothing back to the simulation. time() is render time, not the tick.
  • A function marked @const runs on the CPU in headless tests.
  • A call to an undeclared name fails to link.

examples/shaders is the material above on a sprite, beside a plain one.