Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

readme.md

materialGraph

A material described as nodes and links, compiled to the engine's own WGSL surface DSL.

Authoring a material here means writing fn surface(input: FragmentData) -> PbrSurface and a properties.yaml. That is WGSL. A graph says the same thing as a set of named nodes and a terminal, and the compiler writes the WGSL.

local materialGraph = require("@builtin::systems.materialGraph.materialGraph")

local graph = {
    name = "veined_marble",
    properties = {
        { name = "vein_color", type = "color", default = { 0.15, 0.15, 0.18, 1 } },
        { name = "scale",      type = "float", default = 8 },
    },
    nodes = {
        uv      = { op = "uv" },
        scaled  = { op = "multiply", a = "uv", b = { op = "property", property = "scale" } },
        ripple  = { op = "sin", value = { node = "scaled", swizzle = "x" } },
        veining = { op = "saturate", value = "ripple" },
        vein    = { op = "property", property = "vein_color" },
        albedo  = { op = "mix", a = { 0.9, 0.9, 0.88 }, b = { node = "vein", swizzle = "rgb" }, t = "veining" },
    },
    surface = {
        albedo = "albedo",
        roughness = 0.25,
        metallic = 0,
    },
}

local material = materialGraph.material(graph, { name = "marble" })
entity.spawn("pillar").component.add("Model", { model = "cube", material = material })

(A node can be written inline where an input is expected, as scaled.b does above, or declared under its own name in nodes and referenced by that name. Inline nodes are only reachable from the port they sit in.)

What is in a graph

KeyWhat it holds
nameThe name of the shader asset the graph compiles to.
propertiesThe material's parameters — what a material instance may set. Same declarations a hand-written properties.yaml carries: float, range, float2/3/4, color, int, bool, texture.
nodesA map of node name to { op = "...", <port> = <input>, ... }.
surfaceThe terminal. Each key is a channel of the shading model; its value is an input like any port's.

An input port takes a number, an array of 2-4 numbers, the name of another node, { node = "<name>", swizzle = "rgb" }, or a node written inline. A swizzle written beside any of the table forms reads part of what it resolves to.

Types

Every node's type — float, vec2, vec3, vec4 — is worked out from its inputs. A float meeting a vector is broadcast to the vector's width. Two vectors of different widths are refused, naming the node: there is no reading of that the author can have meant, and a silent guess is a material that is subtly wrong rather than one that failed to compile.

materialGraph.nodeTypes() lists every operation with its ports and result, and materialGraph.surfaceChannels() lists the terminal's channels.

What it compiles to

compile is pure: no asset, no renderer, no GPU. It returns the shader.wgsl and properties.yaml text, the nodes it emitted, and the nodes the terminal could not reach (declared and type-checked, but not in the shader).

install writes those two files as a real .shader asset, and material installs and then makes a material wearing it. Both produce ordinary assets, readable and hand-editable afterwards. Running either again over the same name is how a graph is iterated on: the shader is rewritten, and material also brings the material onto the rewritten shader's properties and applies opts.values over it.

The shader itself is compiled at the next frame boundary, the way any shader edit is. asset.ref(name, "shader"):compileStatus() is what reports that outcome.