Log inGet started

Material graphs

Updated October 15, 2026

Material graphs — a material as nodes

A surface shader is WGSL. @builtin::systems.materialGraph.materialGraph lets a material be written as nodes and the links between them instead, and compiles it to the same two files a hand-written .shader asset holds — the material a graph produces is an ordinary material, drawn by an ordinary program.

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

local material = materialGraph.material({
    name = "rusty_metal",
    properties = {
        { name = "tint",       type = "color",   default = { 0.8, 0.4, 0.2, 1 } },
        { name = "wear",       type = "range",   default = 0.5, min = 0, max = 1 },
        { name = "albedo_map", type = "texture", default = "white" },
    },
    nodes = {
        base  = { op = "sampleTexture", texture = "albedo_map" },
        tint  = { op = "property", property = "tint" },
        mixed = { op = "multiply", a = { node = "base", swizzle = "rgb" }, b = { node = "tint", swizzle = "rgb" } },
        wear  = { op = "property", property = "wear" },
    },
    surface = { albedo = "mixed", roughness = "wear", metallic = 1 },
}, { name = "rusty" })

entity.spawn("pillar").component.add("Model", { model = "cube", material = material })

A port takes a number, an array of 2-4 numbers, the name of another node, { node = "<name>", swizzle = "rgb" }, or a node written inline where it is used. A node's type — float, vec2, vec3, vec4 — comes from its inputs: in an arithmetic node the widest input decides the result and a float is broadened to it, while two different vector widths are refused, naming the node.

compile(graph) is the same work with nothing installed — it returns the shader.wgsl and properties.yaml text, the nodes it emitted, and the nodes the terminal could not reach. check(graph) hands an error back instead of raising. nodeTypes() lists every operation with the inputs a node of it is written with and which of those it cannot be written without; surfaceChannels() lists what the surface terminal accepts. The package's own readme has the full reference.

Calling material again with the same material name is how a graph is iterated on: the shader is rewritten from the graph, the material is brought onto that shader's properties, and opts.values is applied over it. The shader itself compiles at the next frame boundary, the way any shader edit does — asset.ref(name, "shader"):compileStatus() reports that outcome.

  • documentation
  • guide
  • rendering