Command Palette

Search for a command to run...

Hectal
Course outline
Topic 13.2Advanced ~21 min · step 2 of 6

Node Materials

In one line

A node material describes its look as a graph of small operations: colorNode, positionNode, normalNode and more. three.js turns the graph into a shader for whichever backend runs, and a uniform() node changes without recompiling.

0/6 · 0%

Think of it like this

A recipe card can be written as a list of steps: take the flour, mix with water, bake. Each step takes ingredients from earlier steps. A node material is a recipe card for a surface: take the uv, mix two colours by it, multiply by the light. The kitchen (the renderer) reads the card and cooks it its own way, WGSL on WebGPU and GLSL on WebGL 2.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

node
A small object that stands for one value or operation computed on the GPU, such as uv(), time or mix(a, b, t).
graph
Nodes connected together: each node's inputs are other nodes. The renderer walks it to write the shader.
node material
A material whose properties can take nodes. In three.js, classes such as MeshStandardNodeMaterial from three/webgpu.
uniform
A value the shader reads that stays the same for all vertices and pixels in a draw, and that JavaScript can change between frames.
program
One compiled shader stage. renderer.info.memory.programs counts them: a vertex and a fragment program per distinct material.
view space
Coordinates measured from the camera (Topic 2.4). normalNode is expected in it.

Step by step

01Choose the node class

Take the node version of the material you want: MeshStandardNodeMaterial for lit surfaces, MeshBasicNodeMaterial for flat colour, MeshPhysicalNodeMaterial for clear coat. Its normal properties (roughness, side) still work.

02Fill a slot with a graph

material.colorNode = mix(color("#2a9df4"), color("#ff6b2c"), uv().x) says: blend blue to orange by the pixel's horizontal uv. uv() is a function call: it makes the node. Forgetting the parentheses is the most common first mistake.

03Move vertices with positionNode

positionLocal.add(vec3(0, 0, wave)) starts from each vertex's local position and adds a push along z. time is a node that holds the seconds since the first frame, so the wave moves with no JavaScript in the loop.

04Let a uniform be the knob

const amount = uniform(0.12) makes a value you can read and write from JavaScript. Use it for anything a slider or a button will change, such as a paint colour or a flake size.

05Count the programs

The second example logs renderer.info.memory.programs. Two materials give 4 (two stages each). A new uniform value keeps 4. A new graph gives 6. A second plain material of an already compiled kind adds none.

Try it yourself

  1. 1

    Make the wave tall

    In the first example change uniform(0.12) to uniform(0.4). The wave swings 0.4 m up and down on a 1.2 m plane. The console lines do not change, because the geometry in JavaScript is still flat.

  2. 2

    Use a plain material

    Change new THREE.MeshStandardNodeMaterial( to new THREE.MeshStandardMaterial(. The console prints is a node material: undefined, but the plane still shows the gradient and the wave: WebGPURenderer converted the material and copied colorNode and positionNode with it. On WebGLRenderer those two properties would be ignored.

Code & diagrams

colorNode and positionNode on one material r186+ New tab

A wave moves along the plane and the colour runs from blue to orange. The console shows the CPU still sees a flat plane. No `output` because of the fallback warning line.

idle

Console

Run the scene to see its console.
Conversion, uniforms and programs New tab

`forceWebGL` keeps the numbers the same everywhere. Programs count shader stages: a vertex stage and a fragment stage for each distinct material.

idle

Console

Run the scene to see its console.

Expected console output

MeshStandardMaterial -> MeshStandardNodeMaterial | colour copied: true
programs before the first frame: 0
after the first frame: 4
after changing the uniform: 4
after replacing the colorNode: 6
The slots, in one listjs

From NodeMaterial.js and MeshStandardNodeMaterial.js in three r186.

const m = new MeshStandardNodeMaterial();
m.colorNode     = ...; // base colour (vec3 or colour)
m.roughnessNode = ...; // number 0 to 1
m.metalnessNode = ...; // number 0 to 1
m.emissiveNode  = ...; // colour the surface gives off by itself
m.opacityNode   = ...; // number 0 to 1 (set m.transparent = true)
m.normalNode    = ...; // direction, in VIEW space
m.positionNode  = ...; // replaces positionLocal, before the camera
// null means: use the ordinary property, such as m.color or m.roughness

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the parentheses on uv

In the first example change uv().x to uv.x.

terminal
$ Run
── what you'll see ──
[error] THREE.TSL: TypeError: Cannot read properties of null (reading 'build') "MathNode.generate()" at ...
(then a stack trace) and the plane is drawn black

Break #2

Replace a node without needsUpdate

In the second example delete the node.needsUpdate = true; line.

terminal
$ Run
── what you'll see ──
MeshStandardMaterial -> MeshStandardNodeMaterial | colour copied: true
programs before the first frame: 0
after the first frame: 4
after changing the uniform: 4
after replacing the colorNode: 4

Myth vs fact

Myth

Node materials run in JavaScript, so they are slow.

Fact

JavaScript only builds the description, once. The GPU runs the generated shader, as fast as one written by hand for the same maths.

Myth

Changing a node's value recompiles the shader.

Fact

A uniform() node's .value changes freely. Only changing the graph (a different node tree) compiles a new program.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Materials that produce the same shader share one compiled program, each keeping its own uniform values. In a test run, adding a second plain MeshStandardMaterial after the first frame added no programs.

  • ▸

    MeshStandardNodeMaterial.normalNode expects view space: a normal made in object space needs transformNormalToView(...) first. Topic 13.4 does exactly that for the paint flakes.

  • ▸

    Node materials are not a WebGL feature: they need WebGPURenderer, on either of its backends. A project that must keep WebGLRenderer cannot use them.

Remember this

  1. 1

    The problem this solves: MeshStandardMaterial has fixed knobs: a colour, a roughness, a map. To change how colour or position is made you needed GLSL text edits (Phase 12). A node material has the same knobs as hooks you can fill with your own graph, in JavaScript, with no shader text.

  2. 2

    What a node is. A node is a small object that stands for a value computed on the GPU: a number, a vector, the uv of this pixel, the current time, the result of mix(a, b, t). You combine nodes with functions and methods, and the result is another node. Nothing is calculated in JavaScript: you are building a description, and three.js writes the shader from it when the object is first drawn.

  3. 3

    The slots. MeshStandardNodeMaterial is MeshStandardMaterial with extra properties that take a node: colorNode (the base colour), roughnessNode, metalnessNode, emissiveNode (light the surface gives off), opacityNode, normalNode (the surface direction, in view space), and positionNode (replaces each vertex's local position before the camera transform). Anything you leave null uses the normal property, so you replace only what you need.

  4. 4

    Plain materials still work. WebGPURenderer converts every classic material it meets into the matching node material and copies its properties over (renderer.library.fromMaterial(material)), which is why your whole showroom works unchanged. A side effect on this renderer: node properties you put on a plain material are copied too, so they happen to work. Do not rely on that: write the node class, so the code also reads right.

  5. 5

    Uniforms are values you can change. uniform(0.12) is a node holding a number the GPU reads every frame. Changing amount.value costs nothing. Replacing a node in the graph is different: the shader text changes, so three.js must compile a new program. The second example counts this: programs stay at 4 when the uniform changes and rise to 6 when colorNode is replaced and needsUpdate is set.

  6. 6

    Limits to know. positionNode moves vertices on the GPU only: the CPU copy of the geometry does not change (the first example prints vertex 10 z on the CPU: 0), so bounding boxes, raycasting and frustum culling use the undisplaced shape, as in Phase 12. Moving vertices does not update normals either: add a normalNode if lighting must follow.

Explain it without notes

01

What is a node, and why is a node material described as a graph?

02

When does changing a node material cost a shader compile, and when is it free?

Practice

01

Make a MeshStandardNodeMaterial whose roughness goes from 0.1 at the left of a plane to 0.9 at the right.

02

Make the plane's colour pulse between orange and blue once per second with no JavaScript in the animation loop.

Done when you can

  • Name the main slots of a node material and what each replaces

  • Build a colour from uv() and mix and a vertex wave from positionNode

  • Tell when a change is free (uniform) and when it recompiles (new graph)

  • Explain why positionNode leaves the CPU geometry unchanged