Course outline
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.
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. ItsoutputNodesays 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
Drop the cube that is not compatible
In the first example change
scene.add(plain, raw, edited)toscene.add(plain, edited). The black cube disappears, and the first not-compatible error goes with it. The composer'sRawShaderMaterialerror remains, because its last pass is made of one. - 2
Make the flakes big
In the paint example change
createPaintTSL()tocreatePaintTSL({ flakeScale: 40, flakeStrength: 1 }). There are fewer, larger flakes. The shader did not change, only the uniform values.
Code & diagrams
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.
Console
Run the scene to see its console.`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 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.
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).
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.
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 anot compatibleconsole line. Filter the console forNodeBuilderafter 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, readMeshPhysicalNodeMaterial.jsfor what it offers.
Remember this
- 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
ShaderMaterial and RawShaderMaterial. These hold GLSL text for WebGL.
WebGPURendererhas no way to turn GLSL into its node system, so it logsTHREE.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.jscallsrenderer.library.fromMaterial(material), getsnullfor an unknown type, logs that error and substitutes a blankNodeMaterial. - 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
EffectComposer. The classic composer draws passes with
ShaderMaterialandRawShaderMaterialfull-screen shaders. WithWebGPURendererit builds without complaint, butcomposer.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 isRenderPipeline, whoseoutputNodeis apass(scene, camera)with effects layered on top. (Before r183 it was calledPostProcessing.) Phase 15 builds it. - 5
Also changes.
PMREMGeneratorfrom"three"is WebGL only: withWebGPURendererit fails withCannot read properties of undefined (reading 'buffers'). Either importPMREMGeneratorfromthree/webgpu, or skip it: settexture.mapping = EquirectangularReflectionMappingandscene.environment = texture, and the renderer prepares the reflections itself. The showroom'sloadEnvironmentis changed this way in the build. - 6
The migration path. For each GLSL effect, ask which node slot it belongs to: colour tweaks go in
colorNode,roughnessNode,emissiveNode; surface direction innormalNode; vertex moves inpositionNode. Whole-screen effects become aRenderPipeline. If you must keep a hand-written snippet,glslFnandwgslFnwrap a function of GLSL or WGSL text as a node, but then that part is tied to one backend.
Explain it without notes
Name the three WebGL features that do not carry over to WebGPURenderer, and what each one does when you try it.
How would you find every place a project needs porting before switching to WebGPURenderer?
Practice
A material uses onBeforeCompile to turn the surface red where vWorldPos.y > 1.0. Write the node-material version.
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
WebGLRendererfor projects that depend on a large GLSL code base or the classic composer.
Done when you can
Say what
ShaderMaterial,onBeforeCompileandEffectComposerdo underWebGPURendererFind unported code by its names and by
not compatibleconsole linesMap GLSL pieces to node slots
Port a small
onBeforeCompileeffect to a node material