Three.js From Zero · Article s14-02

Debug UI Mastery

Debug UI Mastery is Article s14-02 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS14-02 · Setup & Tooling

Season 14 · Article 02 · Setup & Tooling

lil-gui is the rebooted dat.GUI — every Bruno tutorial uses it. Adds a tweakable UI panel to your scene in five lines. The fastest way to find the right shader uniforms, light intensities, and camera angles.

Setup

npm install lil-gui
import GUI from 'lil-gui';
const gui = new GUI();
gui.add(mesh.position, 'y', -2, 2, 0.01);
gui.add(material, 'metalness', 0, 1, 0.01);
gui.addColor(material, 'color');

gui.add(target, propertyName, min, max, step). The target's property gets a slider. Changes apply live — no save, no reload. Type-aware: numbers → slider, booleans → checkbox, strings → dropdown (with options), colors → color picker.

Folders for organization

const camFolder = gui.addFolder('Camera');
camFolder.add(camera, 'fov', 20, 100).onChange(() => camera.updateProjectionMatrix());
camFolder.add(camera.position, 'z', 1, 20);

const matFolder = gui.addFolder('Material');
matFolder.add(material, 'wireframe');
matFolder.addColor(material, 'color');
matFolder.add(material, 'metalness', 0, 1);
matFolder.add(material, 'roughness', 0, 1);

camFolder.close();   // collapse by default
matFolder.open();

Folders are essential past 5 sliders. Group by what they control: camera, lights, materials, post-processing, debug. Close folders you're not actively tweaking — keeps the panel scannable.

Custom button/action

const actions = {
  regenerate: () => rebuildScene(),
  exportPositions: () => console.log(JSON.stringify(scene.children.map(c => c.position))),
};
gui.add(actions, 'regenerate');
gui.add(actions, 'exportPositions');

Functions on an object become buttons. The export-positions pattern is the secret — once you find the perfect camera angle / light positions in the GUI, click "export" and paste the JSON into your code. Tuning becomes a one-way function: tweak in GUI, freeze in code.

onChange callbacks

gui.add(camera, 'fov', 20, 100).onChange((value) => {
  camera.updateProjectionMatrix();   // FOV changes need this
});

gui.add(uniforms.uTime, 'value', 0, 10).listen();    // .listen() = reflect external changes

.listen() makes the slider reflect external mutations to the value — useful when you're animating it elsewhere and want the GUI to show the current value.

Save / load presets

// Save current GUI state
const preset = gui.save();
localStorage.setItem('gui-preset', JSON.stringify(preset));

// Load on startup
const stored = localStorage.getItem('gui-preset');
if (stored) gui.load(JSON.parse(stored));

The undersold killer feature. Tweak your scene, save the preset, share the JSON with a teammate. They load it, see exactly your config. Better than screenshots.

Multiple GUIs / different titles

const debugGUI = new GUI({ title: 'Debug', width: 320 });
const camGUI = new GUI({ title: 'Camera', container: someDiv });
const hideAll = () => { debugGUI.hide(); camGUI.hide(); };

Multiple panels for big scenes. Toggle them with a key (e.g., 'h' for hide) so you can screenshot without the UI.

Common first-time pitfalls

"Slider doesn't move my mesh." You're sliding mesh.position (the object), not mesh.position.x (the value). lil-gui needs a property name: gui.add(mesh.position, 'x', -5, 5).
"Color picker shows but doesn't change material color." Three.js Color objects need onChange to actually update (gui.addColor mutates a different property internally). Use: gui.addColor(params, 'color').onChange(v => material.color.set(v)).
"GUI hides under the canvas." Canvas has higher z-index. Set gui.domElement.style.zIndex = '100' or render canvas inside a parent with lower z.

Exercises

  1. Tune a shader. Take a custom shader with 5 uniforms. Bind them all to lil-gui sliders. Find a beautiful preset. Save it.
  2. Camera position freezer. A button "Print Camera" that console.logs the current camera.position and controls.target. Copy-paste into your code for the perfect initial camera.
  3. URL hash preset. Encode the GUI preset to base64 in location.hash. Shareable scene configurations via URL.