Command Palette

Search for a command to run...

Hectal
Course outline
Topic 13.4Advanced ~24 min · step 4 of 6

What Does Not Carry Over

In one line

WebGPURenderer does not run ShaderMaterial, ignores onBeforeCompile and cannot use EffectComposer. You see what each one really does, how to translate it to nodes, and port the Phase 12 metal-flake paint to TSL.

0/6 · 0%

Think of it like this

You move to a new country with the same electric kettle. The plug does not fit the socket. The kettle is fine and the socket is fine, but they were made for different systems. Phase 12's shader code was made for the WebGL socket. WebGPURenderer has a different one, so some of it needs an adapter, and some needs rewriting.

Words you'll meet

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

ShaderMaterial
A three.js material that runs GLSL source you write. It is a WebGL concept.
onBeforeCompile
A hook that lets you rewrite a built-in material's WebGL shader text just before it is compiled.
EffectComposer
The classic chain of full-screen post-processing passes for WebGLRenderer.
RenderPipeline
The node-based post-processing object for WebGPURenderer. Its outputNode says what ends up on screen.
NodeBuilder
The part of three.js that walks a node graph and writes the shader for the active backend.
port
To rewrite code for a different system so it does the same job there.

Step by step

01Try each one

The first example puts three cubes side by side: a normal MeshStandardMaterial (blue), a ShaderMaterial (middle) and a standard material with an onBeforeCompile hook (orange). Then it builds an EffectComposer. Read the console after it runs.

02Look at the results

Blue draws normally. The middle cube is a black box and the console has a not compatible error. The orange cube draws, and onBeforeCompile ran: false says the edit never happened. The composer prints more not-compatible errors and does not throw.

03Translate the paint, line by line

Phase 12's paint made flakes by editing the shader. In TSL, the same recipe is a normal node material: the flake hash is an Fn, the cell is floor(positionLocal * scale), and the tilt goes into normalNode. The table below maps each GLSL line to its node.

04Adjust what changed underneath

The paint is a MeshPhysicalNodeMaterial, not a patched MeshPhysicalMaterial. The environment is an equirectangular texture assigned directly. customProgramCacheKey is not needed: the graph decides the program.

05Check both backends

Run the paint example on WebGPU and with forceWebGL: true. The node graph is the same, so the picture should be, too. A difference on one backend is a bug to report, not a feature.

Try it yourself

  1. 1

    Drop the cube that is not compatible

    In the first example change scene.add(plain, raw, edited) to scene.add(plain, edited). The black cube disappears, and the first not-compatible error goes with it. The composer's RawShaderMaterial error remains, because its last pass is made of one.

  2. 2

    Make the flakes big

    In the paint example change createPaintTSL() to createPaintTSL({ flakeScale: 40, flakeStrength: 1 }). There are fewer, larger flakes. The shader did not change, only the uniform values.

Code & diagrams

What stops working r186+ New tab

The console shows one not-compatible error per unsupported material, `onBeforeCompile ran: false`, and the composer's errors. No `output`: the fallback warning appears in only some browsers. The composer runs on a second renderer so it cannot blank this canvas.

idle

Console

Run the scene to see its console.
Phase 12 GLSL next to its TSL portjs

`normalNode` expects view space, so the random tilt (made in object space) goes through `transformNormalToView` first.

// GLSL, paint.glsl.js (Phase 12)              // TSL, paint.tsl.js (this phase)
uniform float uFlakeScale;                      const scale = uniform(140);
vFlakePos = position; (vertex)                  positionLocal   // already the object-space point
floor(vFlakePos * uFlakeScale)                  floor(positionLocal.mul(scale))
step(flakeHash(cell + 7.0).x, uFlakeDensity)    step(flakeHash(cell.add(7)).x, density)
normal = normalize(normal + k * tiltView);      material.normalNode = normalize(normalView.add(transformNormalToView(tilt).mul(k).mul(present)))
roughnessFactor *= mix(1.0, 0.5, present);      material.roughnessNode = mix(0.4, 0.2, present)
#include <normal_fragment_maps> (the hook)      (none: normalNode is the hook)
customProgramCacheKey = () => "lumen-paint-1"   (none: the graph decides)
new THREE.MeshPhysicalMaterial({ ... })         new MeshPhysicalNodeMaterial({ ... })
The Lumen paint in TSL r186+ New tab

The code of `src/materials/paint.tsl.js` is joined onto the top of this example. Left: clear coat and no flakes (`flakeStrength: 0`). Right: the same paint with flakes. Changing `flake.density.value` needs no new shader. No `output` because of the fallback warning.

idle

Console

Run the scene to see its console.

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

Use the composer on the visible renderer

In the first example change new EffectComposer(other) to new EffectComposer(renderer).

terminal
$ Run
── what you'll see ──
[error] THREE.NodeBuilder: Material "ShaderMaterial" is not compatible.
onBeforeCompile ran: false (the material is drawn, with the edit ignored)
[error] THREE.NodeBuilder: Material "ShaderMaterial" is not compatible.
[error] THREE.NodeBuilder: Material "RawShaderMaterial" is not compatible.
composer.render() returned without throwing

Break #2

Use the WebGL environment helper

Make a small file that imports PMREMGenerator from "three" (not three/webgpu) and calls pmrem.fromScene(new RoomEnvironment(), 0.04) with a started WebGPURenderer.

terminal
$ Run
── what you'll see ──
threw: TypeError: Cannot read properties of undefined (reading 'buffers')

Myth vs fact

Myth

If my code runs on WebGPURenderer without errors, it is ported.

Fact

onBeforeCompile is ignored with no message, and a composer or ShaderMaterial logs an error but does not throw. Search the project for the three names, and look at the console.

Myth

Node materials are slower than my GLSL.

Fact

The GPU runs generated shaders like hand-written ones. The cost of the port is your time, not frame time.

Pro corner

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

  • ▸

    Because unknown materials become a blank NodeMaterial, a missing object in a WebGPU migration is nearly always a not compatible console line. Filter the console for NodeBuilder after every upgrade.

  • ▸

    A port loses something: patched shader chunks let you reach any variable inside MeshPhysicalMaterial. Node slots expose most useful ones (colour, roughness, normal, emissive, position, clearcoat), but not every internal. When a slot is missing, read MeshPhysicalNodeMaterial.js for what it offers.

Remember this

  1. 1

    The problem this solves: an upgrade that breaks without a word is the worst kind. If you know the three things that do not carry over, and what each one does when you try it, you can find them in your own code before they reach a visitor.

  2. 2

    ShaderMaterial and RawShaderMaterial. These hold GLSL text for WebGL. WebGPURenderer has no way to turn GLSL into its node system, so it logs THREE.NodeBuilder: Material "ShaderMaterial" is not compatible. and draws the object with an empty node material: a black silhouette. It does not throw. Read the three.js source if you doubt it: NodeBuilder.js calls renderer.library.fromMaterial(material), gets null for an unknown type, logs that error and substitutes a blank NodeMaterial.

  3. 3

    onBeforeCompile. It edits the text of three.js's built-in WebGL shaders. There is no such text on the node path, so the callback is never called. In the example it prints onBeforeCompile ran: false, and the material is drawn as if the edit were not there. No error is shown, which makes it the easiest of the three to miss.

  4. 4

    EffectComposer. The classic composer draws passes with ShaderMaterial and RawShaderMaterial full-screen shaders. With WebGPURenderer it builds without complaint, but composer.render() logs a not-compatible error for the shader materials it meets and the picture goes black (the break-it below). The node-based replacement is RenderPipeline, whose outputNode is a pass(scene, camera) with effects layered on top. (Before r183 it was called PostProcessing.) Phase 15 builds it.

  5. 5

    Also changes. PMREMGenerator from "three" is WebGL only: with WebGPURenderer it fails with Cannot read properties of undefined (reading 'buffers'). Either import PMREMGenerator from three/webgpu, or skip it: set texture.mapping = EquirectangularReflectionMapping and scene.environment = texture, and the renderer prepares the reflections itself. The showroom's loadEnvironment is changed this way in the build.

  6. 6

    The migration path. For each GLSL effect, ask which node slot it belongs to: colour tweaks go in colorNode, roughnessNode, emissiveNode; surface direction in normalNode; vertex moves in positionNode. Whole-screen effects become a RenderPipeline. If you must keep a hand-written snippet, glslFn and wgslFn wrap a function of GLSL or WGSL text as a node, but then that part is tied to one backend.

Explain it without notes

01

Name the three WebGL features that do not carry over to WebGPURenderer, and what each one does when you try it.

02

How would you find every place a project needs porting before switching to WebGPURenderer?

Practice

01

A material uses onBeforeCompile to turn the surface red where vWorldPos.y > 1.0. Write the node-material version.

02

Write the one line that gives a WebGPURenderer scene an HDR environment from a HDRLoader result called hdr.

Trade-offs

  • ↔

    Moving to node materials gives you one source for two backends and removes shader-text hacks, but the port takes effort and some internals are not reachable. Keep WebGLRenderer for projects that depend on a large GLSL code base or the classic composer.

Done when you can

  • Say what ShaderMaterial, onBeforeCompile and EffectComposer do under WebGPURenderer

  • Find unported code by its names and by not compatible console lines

  • Map GLSL pieces to node slots

  • Port a small onBeforeCompile effect to a node material