---
title: "Material graphs"
description: "Part of the rendering guide (guides { path = \"topics/rendering\" }), which lists every part."
section: "Topics"
slug: "topics-rendering-material-graphs"
canonical: "https://origozero.ai/docs/topics-rendering-material-graphs"
updated: "2026-10-15T11:08:40.198740277+00:00"
tags: ["documentation", "guide", "rendering"]
---

# Material graphs

## 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.

```lua
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.
