Course outline
Debugging Rounds
In one line
Debugging rounds hand you a broken scene. Five symptoms cover most of them: it is black, it is slow, it leaks, it is blurry, it flickers. Each has a short checklist, and each check is a measurement: a pixel, a triangle count, renderer.info, the pixel ratio, a depth calculation.
Think of it like this
A mechanic with a squeaking bike. They don't take it apart; they spin the wheel, squeeze the brake, lift the saddle, listening each time. Each test rules out a cause. A debugging round rewards the same habit: test, rule out, repeat.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- z-fighting
- Two surfaces so close that the depth buffer can't tell which is in front, so pixels flicker between them.
- depth step
- One of the 2²⁴ values a 24-bit depth buffer can store; two depths on the same step look equal.
- leak
- Memory that keeps growing because old resources are never freed; on the GPU,
dispose()is the free. - back-face culling
- Skipping triangles that face away from the camera; from inside a box, every face does.
Step by step
01Black: read a pixel and a count
Run the first example. Note how the five causes split into three groups: black and drawn, background and drawn, nothing drawn. Each group has different fixes.
02Flicker: compute the steps
The second example shows what the depth buffer stores for two surfaces 1 mm apart. Change near and watch the steps separate.
03Leak: count while repeating
The third example clicks the colour picker 20 times in each version and counts textures. Growth with every click is a leak; a flat line is not.
04Say it out loud
In the interview, narrate: "I read the centre pixel: pure black, 12 triangles drawn, so it rendered but nothing lit it. Checking lights…". The narration is half the score.
Try it yourself
- 1
See the walls from inside
In the first example, change the camera-inside case to
({ camera, box }) => { camera.position.set(0, 0, 0.2); box.material.side = THREE.BackSide; }. It now printsorange: from inside, the walls' back faces face you, andBackSidedraws exactly those. (That is how sky domes and room interiors are built.) - 2
Find the near that is safe
In the second example, add
check("near 0.1", 0.1, 1000, 30, 0.001);. It prints2 steps apart: better than 0.01, still fragile.near1 gave 18.
Code & diagrams
Pixels and triangle counts, the same on every GPU for these flat cases.
Console
Run the scene to see its console.Expected console output
fine -> centre: orange | triangles drawn: 12
no light -> centre: 0, 0, 0 | triangles drawn: 12
camera inside the box -> centre: 11, 17, 24 | triangles drawn: 12
near plane too far -> centre: 11, 17, 24 | triangles drawn: 0
box behind the camera -> centre: 11, 17, 24 | triangles drawn: 0
scale 0 -> centre: 11, 17, 24 | triangles drawn: 12The same formula the GPU uses for a perspective camera and a 24-bit depth buffer. Whether a fight is visible on a given GPU varies; equal steps always risk it.
Console
(nothing was printed)Expected console output
decal 1 mm above a panel near 0.01 far 1000 at 30 m, 1 mm apart: steps 16771790 and 16771790 -> SAME: they fight
the same, near raised near 1 far 1000 at 30 m, 1 mm apart: steps 16234209 and 16234227 -> 18 steps apart
the same, near and far tight near 1 far 60 at 30 m, 1 mm apart: steps 16492855 and 16492874 -> 19 steps apart
decal 1 cm above near 0.01 far 1000 at 30 m, 10 mm apart: steps 16771790 and 16771792 -> 2 steps apart
close to the camera near 0.01 far 1000 at 0.5 m, 1 mm apart: steps 16441835 and 16442505 -> 670 steps apart`renderer.info.memory.textures` counts textures on the GPU right now.
Console
Run the scene to see its console.Expected console output
leaky: new textures on the GPU after 5, 10, 15, 20 clicks: 5, 10, 15, 20
clean: new textures on the GPU after 5, 10, 15, 20 clicks: 1, 1, 1, 1
one 256 x 256 RGBA texture with mipmaps is about 341 KiBBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Dispose the texture still in use
In the third example's pickClean, call swatch.material.map.dispose() after assigning the new texture instead of before.
Myth vs fact
Myth
Z-fighting is fixed by making far smaller.
Fact
Depth precision is crowded near the camera, so near dominates: raising near from 0.01 to 1 separated the surfaces by 18 steps; tightening far from 1000 to 60 only added 1.
When it breaks
Black after a three.js upgrade
What you see
A scene lit with old intensity values goes dark after r155's physically based lights.
Fix & prevent
Read the migration guide for the version jump; scale light intensities (Topic 5.2) and compare with a known-good screenshot.
Slow only on phones
What you see
60 fps on desktop, 20 on a mid-range phone: the canvas is drawn at pixel ratio 3.
Fix & prevent
Cap the pixel ratio at 2 (or 1.5 on low-end), measure again, then look at draw calls (Topic 17.5).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
For leaks, better than dispose-and-recreate is reuse: keep one canvas texture, redraw the canvas, set
texture.needsUpdate = true. One upload, no new GPU object. - ▸
Spector.js and
renderer.infoanswer "what was drawn"; the browser's Performance panel answers "what took the time". Use both before changing code.
Remember this
- 1
The problem this solves: under pressure, people change code at random. A checklist of measurements turns "it's broken" into a cause in minutes, and shows the interviewer how you think.
- 2
It is black. Read the centre pixel and
renderer.info.render.triangles. The first example measures five causes: no light gives0, 0, 0with 12 triangles drawn (rendered, unlit); camera inside the box gives the background with 12 triangles (back faces culled);neartoo far and "behind the camera" give 0 triangles (culled); scale 0 gives the background with 12 triangles of zero area. - 3
It flickers. Two surfaces at nearly the same depth fight over pixels (z-fighting). A 24-bit depth buffer stores 16,777,216 steps, crowded near the camera. The second example computes it: a decal 1 mm above a panel, 30 m away, with
near0.01, lands on the same step as the panel; raisenearto 1 and they are 18 steps apart. Fixes: raisenear(it matters far more thanfar), separate the surfaces,polygonOffset, or a logarithmic depth buffer (Topic 7.2). The calculation is the theory; rasterisers add their own rounding. Measured on this site's software GPU, a 7 × 7-segment decal over a one-quad panel still showed bands atnear1 and 5;polygonOffsetremoved them at every size. The exercise uses it. - 4
It leaks. Watch
renderer.info.memorywhile repeating an action. The third example's colour picker makes a canvas texture per click: 20 clicks, 20 textures on the GPU, each about 341 KiB. Disposing the old one first keeps it at 1. Leaks hide in anything that creates on change: textures, geometries, render targets, materials with new shaders. - 5
It is slow / it is blurry. Slow: count draw calls and triangles first (Topic 17.2), then pixels (pixel ratio × size, Topic 17.5), then shader cost. Blurry: the drawing buffer is smaller than the screen:
setPixelRatiomissing, a canvas stretched by CSS without resizing, or a pixel ratio capped too low. Comparerenderer.domElement.widthwithclientWidth × devicePixelRatio.
Explain it without notes
Your scene is black. Give the checks you'd run, in order.
A configurator gets slower and slower the longer it is used. What's your hypothesis and test?
Practice
Write a function whyBlack(renderer, scene, camera) that renders, reads the centre pixel and the triangle count, and returns the likely group of causes.
Write a leak test: call an action N times and fail if renderer.info.memory.textures grew by more than one.
Done when you can
You can diagnose a black scene by pixel and triangle count
You can explain z-fighting with depth steps and fix it
You can prove a leak and its fix with renderer.info
You narrate your checks out loud