Skip to content

Three.js - Debug and Stats

Three.js - Debugging and Performance Monitoring

Section titled “Three.js - Debugging and Performance Monitoring”

When building 3D applications, being able to tweak values in real-time and monitor performance is crucial. It drastically speeds up development and helps identify bottlenecks. In this chapter, we’ll explore two essential tools for this: a graphical user interface (GUI) for live code manipulation and a statistics panel for performance monitoring.

Experimenting with variables like an object’s position, color, or scale by constantly changing code and refreshing the browser is a slow and tedious process. A debug UI panel solves this. The classic dat.GUI has been succeeded by the more modern and lightweight lil-gui, which we’ll use. It integrates seamlessly with Three.js and allows you to create a simple UI to change variables in your code live.

In a modern JavaScript project, we manage dependencies using a package manager like npm or yarn. This is the recommended approach.

# Using npm
npm install three lil-gui
# Using yarn
yarn add three lil-gui

After installation, you can import it into your JavaScript module.

import * as THREE from 'three';
import GUI from 'lil-gui';

First, initialize the GUI. This will create a panel, typically in the top-right corner of the screen.

const gui = new GUI();

Next, you can add a controller for a property you want to manipulate. The .add() method takes the object and the property name (as a string) that you want to control.

// Assuming 'cube' is a THREE.Mesh object
gui.add(cube.position, 'y'); // Adds a controller for the cube's y-position

You can enhance controllers by chaining methods to set minimum/maximum values, the step increment, and a display name.

gui.add(cube.position, 'x')
.min(-3)
.max(3)
.step(0.01)
.name('Cube X Position');

For properties that are not numbers, lil-gui provides specific controllers. For example, use .addColor() for colors and a simple .add() for booleans (which creates a checkbox).

// For a boolean value (e.g., wireframe)
gui.add(cube.material, 'wireframe');
// For colors, it's best to control a separate object and use an onChange event
const colorParams = {
color: 0xff0000
}
gui.addColor(colorParams, 'color').onChange(() => {
cube.material.color.set(colorParams.color);
});

When you have many controllable properties, your UI panel can get crowded. You can organize it using folders to group related controls.

const cubeFolder = gui.addFolder('Cube Properties');
const positionFolder = cubeFolder.addFolder('Position');
positionFolder.add(cube.position, 'x', -5, 5);
positionFolder.add(cube.position, 'y', -5, 5);
positionFolder.add(cube.position, 'z', -5, 5);
const materialFolder = cubeFolder.addFolder('Material');
materialFolder.add(cube.material, 'wireframe');
// ... add color controller here

Another crucial debugging tool is Stats.js, which provides a simple panel to monitor your application’s performance, showing frames per second (FPS), memory usage, and frame time. A high and stable FPS (typically 60) indicates a smooth experience.

First, install the package:

npm install stats.js

Then, import it, create an instance, add it to your page, and update it on every frame within your animation loop.

import Stats from 'stats.js';
// 1. Initialization
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
// 2. Update in animation loop
function animate() {
stats.begin(); // Start monitoring
// ... your animation code ...
renderer.render(scene, camera);
stats.end(); // End monitoring
requestAnimationFrame(animate);
}
animate();

Here is a complete example integrating Three.js, lil-gui, and stats.js in a modern project setup. This code would typically reside in a main.js file.

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import GUI from 'lil-gui';
import Stats from 'stats.js';
/**
* Base Setup
*/
// Debug
const gui = new GUI();
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
// Canvas
const canvas = document.querySelector('canvas.webgl');
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x262626);
/**
* Object
*/
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
/**
* Lights
*/
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 0.8);
pointLight.position.set(2, 3, 4);
scene.add(pointLight);
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};
window.addEventListener('resize', () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
scene.add(camera);
/**
* Controls
*/
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* Debug UI
*/
const cubeFolder = gui.addFolder('Cube');
cubeFolder.add(cube.position, 'y').min(-3).max(3).step(0.01).name('elevation');
cubeFolder.add(cube, 'visible');
cubeFolder.add(material, 'wireframe');
const colorParams = { color: 0x00ff00 };
cubeFolder.addColor(colorParams, 'color').onChange(() => {
material.color.set(colorParams.color);
});
/**
* Animate
*/
const clock = new THREE.Clock();
const tick = () => {
stats.begin();
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
stats.end();
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
  • Nothing is visible: Check your browser’s developer console (F12) for errors. Often, it’s a typo in a property name or an incorrect import.
  • GUI not appearing: Make sure you have instantiated it with new GUI(). Check for any CSS on your page that might be hiding it (e.g., overflow: hidden on the <body>).
  • Performance is low: Use the Stats.js panel. If FPS is consistently below 60, you might have too many objects, too complex geometry, or an expensive operation in your animation loop. Try simplifying your scene to identify the cause.
  • Visualizing Axes: To better understand object orientation and position, add an AxesHelper to your scene: const axesHelper = new THREE.AxesHelper(2); scene.add(axesHelper);