Command Palette

Search for a command to run...

Hectal
Course outline
Topic 3.6Beginner ~18 min · step 6 of 7

Debug Tools

In one line

Sliders, axes, grids and counters let you see a scene instead of guessing. You use lil-gui to tweak values live, axes and grid helpers to read directions, renderer.info to count the GPU's work, and meet the three.js editor, DevTools and Spector.js.

0/7 · 0%

Think of it like this

A mechanic does not guess why an engine rattles. They put a gauge on it, listen with a stethoscope and turn one screw at a time. Debug tools are your gauges: they let you change one thing and watch what it does.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

lil-gui
A tiny library that shows sliders, checkboxes and colour pickers bound to your objects' properties.
helper
An object that draws something you cannot normally see: axes, a grid, a light's direction, a bounding box.
renderer.info
Counters kept by the renderer: draw calls, triangles, geometries, textures and shader programs.
AxesHelper
Three coloured lines: red along x, green along y, blue along z, from the origin.

Step by step

01Draw the axes and a grid

scene.add(new THREE.AxesHelper(1.5), new THREE.GridHelper(4, 8)). Now you can see where y = 0 is, and which way x and z go. Red is x, green is y, blue is z (RGB = XYZ).

02Add sliders for the values you are unsure about

const gui = new GUI(); gui.add(cube.position, "x", -1.5, 1.5, 0.01); makes a slider that writes straight into cube.position.x. A checkbox is made from a boolean property: gui.add(cube, "visible").

03Add a colour

Keep the colour in a plain object (settings.color), make the picker with addColor, and in onChange call cube.material.color.set(value).

04Read the receipt after a render

renderer.render(scene, camera), then renderer.info.render.calls. Read it after, not before: the counters describe the last render.

Try it yourself

  1. 1

    Add a slider that turns the cube

    In the first example add gui.add(cube.rotation, "y", -3.14, 3.14, 0.01); before the loop. The console says 6 controls, but the slider seems to do nothing: the loop writes cube.rotation.y every frame (two writers for one value). Delete the loop's cube.rotation.y = ... line and the slider turns the cube.

  2. 2

    Share one geometry

    In the second example change the second loop to create meshes with shape instead of a new BoxGeometry. Now the line +5 meshes with their own shape shows calls 10 and geometries 1: draw calls still grow with meshes, but the GPU holds one shape.

Code & diagrams

Sliders, axes and a grid New tab

Use the panel: drag x, change the height, turn spin to 0 and back. Both helpers are objects in the scene, so they are in the draw call count.

idle

Console

Run the scene to see its console.

Expected console output

controls in the panel: 5
helpers are objects too: scene holds 5 children (2 lights, 2 helpers, 1 cube)
Read the receipt: calls, triangles, geometries, programs New tab

The same scene grows in steps and prints `renderer.info` after each. The last two lines are the "value that does nothing" and the autoReset trap.

idle

Console

Run the scene to see its console.

Expected console output

empty scene                        | calls  0 | triangles   0 | geometries 0 | programs 0
5 meshes sharing a shape           | calls  5 | triangles  60 | geometries 1 | programs 1
+5 meshes with their own shape     | calls 10 | triangles 120 | geometries 6 | programs 1
+1 sphere with another material    | calls 11 | triangles 1080 | geometries 7 | programs 2
parameters.width = 3, real width: 0.5
calls after two renders: 11 (it resets on every render())

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

Bind a slider to a copy

In the first example replace gui.add(cube.position, "x", -1.5, 1.5, 0.01); with const p = cube.position.clone(); gui.add(p, "x", -1.5, 1.5, 0.01);.

terminal
$ Run
── what you'll see ──
The slider moves and shows numbers, but the cube never moves. No error.
The control is bound to the copy `p`.

Myth vs fact

Myth

renderer.info.render.calls is the number of objects in the scene.

Fact

It counts draw calls for the last render. Hidden and culled objects add none; a mesh with a material array adds one per group; helpers add theirs.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Every lil-gui control has onChange, onFinishChange and listen(), which polls the property each frame so the panel follows code that changes it. Use listen() only while debugging.

  • ▸

    info.memory.geometries and .textures are the numbers to watch for leaks: if they climb every time you open and close a viewer, you are not calling dispose() (Phase 17).

Remember this

  1. 1

    The problem this solves: most 3D bugs are not errors, they are wrong pictures. Reading code does not find them. You need to see numbers, directions and costs, and change them while the page runs.

  2. 2

    lil-gui (import GUI from "lil-gui") is a panel of sliders, checkboxes and colour pickers. gui.add(object, "property", min, max, step) binds a slider to a property; gui.addColor(settings, "color") makes a picker (copy it into a three.js Color in onChange). Bind to the live object (cube.position, not a copy) or the slider moves a number nobody reads.

  3. 3

    Helpers draw what you cannot see. AxesHelper(size) draws red x, green y and blue z; GridHelper(size, divisions) draws the floor grid (y = 0). They are ordinary objects, so they are in the tree and cost draw calls (the example's scene holds two). Others show a light (DirectionalLightHelper), a camera frustum (CameraHelper) and a box (BoxHelper). Remove them before shipping.

  4. 4

    **renderer.info** is the GPU's receipt: info.render has calls, triangles, points, lines, frame for the last render; info.memory has geometries and textures held on the GPU; info.programs lists compiled shader programs. In the second example five meshes sharing a shape give 5 calls and 1 geometry; five with their own shapes give 10 calls and 6 geometries; a new material compiles a second program.

  5. 5

    Two traps. info.render resets on every render() (info.autoReset), so with two renders per frame (Phase 15) you see only the last; set autoReset = false and call info.reset() once per frame to total them. And info.memory counts what is uploaded, not what JavaScript still holds.

  6. 6

    A value that "does nothing" usually means you changed a copy or a record. BoxGeometry.parameters only records the constructor's arguments: changing parameters.width does not change the shape (the example prints the real width, 0.5). Change scale or build a new geometry. Other causes: a value read only once (needsUpdate, Phase 5), or an object not in the scene.

  7. 7

    Outside the code. The three.js editor (threejs.org/editor) inspects and tweaks a model. DevTools show the canvas size, the console and the Performance frame timeline. Spector.js records every WebGL call of one frame: use it when renderer.info says what and you need why. Phase 17 returns to them.

Explain it without notes

01

Your slider moves but the object does not. Give two possible causes.

02

What do info.render.calls and info.memory.geometries tell you, and why can the first be larger than the second?

Practice

01

Add a Box3Helper or BoxHelper around the cube in the first example. How does it change the child count?

02

Write a loop line that shows the draw call count in the page title every frame.

Done when you can

  • Add a lil-gui panel bound to live objects

  • Use axes and grid helpers to read directions and the floor

  • Read renderer.info and explain calls, triangles, geometries and programs

  • Name what the three.js editor, DevTools and Spector.js are for