Command Palette

Search for a command to run...

Hectal
Course outline
Topic 12.6Advanced ~22 min · step 6 of 7

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.

0/7 · 0%

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, mediump or highp.
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. 1

    Fix the shader

    Change 1 to 1.0 in the compile-error example. The error goes away and the plane appears white.

  2. 2

    A new debug mode

    Add view: "vec3(abs(vNormal.z))" to MODES. The centre pixel prints 254,254,254 (the ball faces the camera there, and the byte is rounded down) and the picture darkens towards the rim.

Code & diagrams

A real compile error New tab

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.

idle

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: 1
Colour as a debugger, and precision New tab

The precision lines are what this software GPU reports; a phone may say something else. The 16-bit values use `Math.f16round`.

idle

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 70000
onBeforeCompile and the program cache New tab

Left 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).

idle

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 changed

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

A bad chunk name for every ball

In the third example change the default hook of tinted to "#include <dithering_fragmen>".

terminal
$ Run
── what you'll see ──
[warn] onBeforeCompile: "#include <dithering_fragmen>" was not found, so nothing changed
...
own cache keys: left 108,112,116 | middle 108,111,115 | right 1,1,2 | programs: 3
The warning prints more than once per program, and the red and blue balls turn the same untinted grey (the pixels differ by one level only because the balls sit at different spots). Without our own check there would be no message at all.

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.onShaderError gives 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 as warning 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 uses warn (a link failure uses error and the words Shader 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 onBeforeCompile runs again. Keep it a pure text edit with no counters or one-time state.

  • ▸

    renderer.debug.checkShaderErrors = false skips the compile checks in production for a small speed-up. Leave it on while you develop.

Remember this

  1. 1

    The problem this solves: there is no console.log and no breakpoint in GLSL. Without a method, a black screen is a guess.

  2. 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. 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 as vec3(uv, 0.0), depth as vec3(gl_FragCoord.z). A ball with a normal colour reads about 130,130,255 at 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. 4

    3. Know the precision. Fragment shaders declare highp, mediump or lowp float precision. Desktops treat mediump as 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 to highp and falls back to mediump only when the GPU has no highp in fragment shaders. If you set precision: "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. 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 failed replace changes 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. 6

    5. WebGPU. onBeforeCompile works on WebGLRenderer only. WebGPURenderer uses node materials (Phase 13), so a paint made this way must be ported.

Explain it without notes

01

Why is the error on line 58 when your fragment shader has only 4 lines?

02

Why do two onBeforeCompile materials with different tints share one shader?

Practice

01

Show the UV of a mesh as colour in a MeshStandardMaterial.

02

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 mediump can do on a phone

  • Make onBeforeCompile fail loudly and give each material a cache key