Course outline
Debugging Shaders
In one line
You cannot print from a shader. You read three.js's compile log with its line numbers, show values as colours, know what precision a phone really has, and treat onBeforeCompile as a powerful but fragile tool.
Think of it like this
Debugging a shader is like repairing a machine through a letterbox: you cannot step through it, so you look at what comes out. You change one thing and see what the picture does.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- compile log
- The GPU driver's list of errors for a shader, with a line number for each.
- precision
- How many bits a shader's floats have:
lowp,mediumporhighp. - onBeforeCompile
- A material hook that gives you the shader text just before it is compiled, so you can edit it.
- program cache
- three.js reuses one compiled shader for all materials that would produce the same text. The cache key decides what counts as the same.
Step by step
01Make an error
The first example writes float brightness = 1;. The compiler says it cannot convert const int to highp float and shows the source with a > on line 58.
02Print with colour
The second example draws a ball in three debug modes and reads the centre pixel of each, then asks the GPU for its highp and mediump precision and shows what a 16-bit float would keep.
03Edit three.js's shader
The third example adds a tint with onBeforeCompile. With one cache key all balls share the first program. With keys each gets its own. A typo in the chunk name finds nothing, so only our own console.warn tells us.
04Make each failure loud
Check every replacement, give each material a cache key, and test the precision story on a real phone before you ship.
Try it yourself
- 1
Fix the shader
Change
1to1.0in the compile-error example. The error goes away and the plane appears white. - 2
A new debug mode
Add
view: "vec3(abs(vNormal.z))"toMODES. The centre pixel prints254,254,254(the ball faces the camera there, and the byte is rounded down) and the picture darkens towards the rim.
Code & diagrams
The error is on line 58 because three.js adds 56 lines of its own before your `main`. The plane is not drawn; three.js still counts the draw call and keeps going.
Console
Run the scene to see its console.Expected console output
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
Material Name:
Material Type: ShaderMaterial
Program Info Log: Fragment shader is not compiled.
FRAGMENT
ERROR: 0:58: '=' : cannot convert from 'const int' to 'highp float'
53: const vec3 weights = vec3( 0.2126, 0.7152, 0.0722 );
54: return dot( weights, rgb );
55: }
56:
57: void main() {
> 58: float brightness = 1;
59: gl_FragColor = vec4(vec3(brightness), 1.0);
60: }
programs after render: 1 | draw calls: 1The precision lines are what this software GPU reports; a phone may say something else. The 16-bit values use `Math.f16round`.
Console
Run the scene to see its console.Expected console output
normal centre pixel: 130,130,255
uv centre pixel: 65,129,0
depth centre pixel: 245,245,245
this GPU, highp: range 2^127, 23 fraction bits
this GPU, mediump: range 2^15, 10 fraction bits
a 16-bit float keeps 1000.5 of 1000.7 and 0.00010002 of 0.0001 and Infinity of 70000Left is meant to be red and middle blue, but with the default key both come out red (one program). With their own keys they differ. The third ball (right, meant to be green) has a typo in the chunk name: it prints a warning and stays untinted grey (108,112,116). Before it exists, `right` reads the background (1,1,2).
Console
Run the scene to see its console.Expected console output
default cache key: left 108,22,23 | middle 108,22,23 | right 1,1,2 | programs: 1
own cache keys: left 108,22,23 | middle 22,45,115 | right 1,1,2 | programs: 3
onBeforeCompile: "#include <dithering_fragmen>" was not found, so nothing changed
with a typo: left 108,22,23 | middle 22,45,115 | right 108,112,116 | programs: 4
onBeforeCompile: "#include <dithering_fragmen>" was not found, so nothing changedBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A bad chunk name for every ball
In the third example change the default hook of tinted to "#include <dithering_fragmen>".
Myth vs fact
Myth
If it works on my desktop it works on phones.
Fact
Desktops silently give you 32-bit floats for mediump. Phones often do not, and large numbers, small differences and big coordinates are where it breaks first.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
renderer.debug.onShaderErrorgives you the raw GL context, program and shaders when a compile fails, so you can send the info log to your own error tracking instead of reading the console. - ▸
Warnings are not errors. On Chrome for Windows (measured), three.js may log
THREE.WebGLProgram: Program Info Log:followed by lines such aswarning X4122: sum of 1 and -1.49272e-017 cannot be represented accurately in double precision. Chrome translates your GLSL to Direct3D's HLSL through ANGLE, and the HLSL compiler adds up two constants in double precision and notes that the exact sum has no exact float form. The program linked, so three.js useswarn(a link failure useserrorand the wordsShader Error). The picture is unaffected, and Mac, Linux and most phones print nothing. In the showroom build the warning also appears with the paint removed, so it comes from another built-in program in the scene, not from your shader. Fix real errors, and do not chase this one. - ▸
Context loss. When the browser loses the GL context and restores it, three.js recompiles every program, so
onBeforeCompileruns again. Keep it a pure text edit with no counters or one-time state. - ▸
renderer.debug.checkShaderErrors = falseskips the compile checks in production for a small speed-up. Leave it on while you develop.
Remember this
- 1
The problem this solves: there is no
console.logand no breakpoint in GLSL. Without a method, a black screen is a guess. - 2
1. Read the compile error. A shader that fails to compile does not throw. three.js logs
THREE.WebGLProgram: Shader Error ...with the GPU's message and your source with line numbers, and the mesh is not drawn. The line numbers count three.js's own lines first: in the first example the bad line is line 58 of the fragment shader even though it is line 2 of yours. Read the>marker, not your own count. - 3
2. Use colour as a debugger. Turn the value you want to see into a colour: normals as
n * 0.5 + 0.5, UVs asvec3(uv, 0.0), depth asvec3(gl_FragCoord.z). A ball with a normal colour reads about130,130,255at its centre (it faces the camera, so the normal is (0, 0, 1)). If the colour is wrong the number is wrong; if the picture is black the value is 0 or NaN. - 4
3. Know the precision. Fragment shaders declare
highp,mediumporlowpfloat precision. Desktops treatmediumpas 32 bit. Many phones really use 16-bit floats there: 10 fraction bits, a largest value of about 65504. 1000.7 becomes 1000.5, and 70000 becomes Infinity. three.js defaults tohighpand falls back tomediumponly when the GPU has nohighpin fragment shaders. If you setprecision: "mediump"on a material to save power, test on a phone: a shader that works on the desktop can band, flicker or turn black there. - 5
**4.
onBeforeCompile.** It lets you edit the shader text of a built-in material, as the paint does. It is powerful because you keep all the lighting. It is fragile for three reasons. The chunk names (#include <normal_fragment_maps>) can change between versions. A failedreplacechanges nothing and says nothing. And the program cache is keyed on the text of your function, so two materials made by the same function but different captured values share one program (the example shows both blue and red balls turning red). Fix:customProgramCacheKey, and a check that every replacement was found. - 6
5. WebGPU.
onBeforeCompileworks onWebGLRendereronly.WebGPURendereruses node materials (Phase 13), so a paint made this way must be ported.
Explain it without notes
Why is the error on line 58 when your fragment shader has only 4 lines?
Why do two onBeforeCompile materials with different tints share one shader?
Practice
Show the UV of a mesh as colour in a MeshStandardMaterial.
Make insertAfter log the chunk name and the line where it inserted.
Done when you can
Find the real line of a compile error
Show normals, UVs and depth as colours
Say what
mediumpcan do on a phoneMake
onBeforeCompilefail loudly and give each material a cache key