Command Palette

Search for a command to run...

Hectal
Course outline
Topic 19.2Advanced ~21 min · step 2 of 5

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.

0/5 · 0%

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. 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 prints orange: from inside, the walls' back faces face you, and BackSide draws exactly those. (That is how sky domes and room interiors are built.)

  2. 2

    Find the near that is safe

    In the second example, add check("near 0.1", 0.1, 1000, 30, 0.001);. It prints 2 steps apart: better than 0.01, still fragile. near 1 gave 18.

Code & diagrams

It is black: five causes, measured New tab

Pixels and triangle counts, the same on every GPU for these flat cases.

idle

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: 12
It flickers: what the depth buffer can tell apart New tab

The 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
It leaks: a texture per click New tab

`renderer.info.memory.textures` counts textures on the GPU right now.

idle

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 KiB

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

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.

terminal
$ Press Run
── what you'll see ──
clean: new textures on the GPU after 5, 10, 15, 20 clicks: 5, 10, 15, 20
The leak is back, and the swatch still looks right: three.js uploads the disposed texture again when it draws it.

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.info answer "what was drawn"; the browser's Performance panel answers "what took the time". Use both before changing code.

Remember this

  1. 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. 2

    It is black. Read the centre pixel and renderer.info.render.triangles. The first example measures five causes: no light gives 0, 0, 0 with 12 triangles drawn (rendered, unlit); camera inside the box gives the background with 12 triangles (back faces culled); near too far and "behind the camera" give 0 triangles (culled); scale 0 gives the background with 12 triangles of zero area.

  3. 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 near 0.01, lands on the same step as the panel; raise near to 1 and they are 18 steps apart. Fixes: raise near (it matters far more than far), 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 at near 1 and 5; polygonOffset removed them at every size. The exercise uses it.

  4. 4

    It leaks. Watch renderer.info.memory while 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. 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: setPixelRatio missing, a canvas stretched by CSS without resizing, or a pixel ratio capped too low. Compare renderer.domElement.width with clientWidth × devicePixelRatio.

Explain it without notes

01

Your scene is black. Give the checks you'd run, in order.

02

A configurator gets slower and slower the longer it is used. What's your hypothesis and test?

Practice

01

Write a function whyBlack(renderer, scene, camera) that renders, reads the centre pixel and the triangle count, and returns the likely group of causes.

02

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