Skip to content

Three.js - Renderer and Responsiveness

Three.js - The Scene, Renderer, and Responsiveness

Section titled “Three.js - The Scene, Renderer, and Responsiveness”

The THREE.Scene object is the root of a structure called a scene graph. Think of it as a tree where the scene is the trunk, and every object you add—meshes, lights, cameras, even groups of other objects—are branches. This hierarchical structure is fundamental to how 3D engines organize and render content.

You can add any object that inherits from THREE.Object3D to the scene using the .add() method and remove it with .remove().

const scene = new THREE.Scene();
const mesh = new THREE.Mesh(/* ... */);
// Add the mesh to the scene
scene.add(mesh);
// The scene's children property is an array of all top-level objects
console.log(scene.children.length); // Output: 1
// Remove the mesh from the scene
scene.remove(mesh);
console.log(scene.children.length); // Output: 0

Calling scene.remove(object) only detaches the object from the scene graph. It does not free the memory used by its geometry, material, or textures. In applications where you frequently add and remove objects, this can lead to memory leaks. To properly clean up, you must call .dispose() on these resources.

function disposeObject(object) {
// Traverse the object and its children
object.traverse(child => {
if (child.isMesh) {
// Dispose of geometry and material
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
// If the material is an array, dispose of each one
if (Array.isArray(child.material)) {
child.material.forEach(material => material.dispose());
} else {
child.material.dispose();
}
}
}
});
// Finally, remove the object from the scene
scene.remove(object);
}
// Usage:
disposeObject(myMeshGroup);

The renderer is the component that takes your Scene and Camera and draws the result into a <canvas> element on your webpage. The most powerful and commonly used renderer is THREE.WebGLRenderer, which uses the WebGL API to leverage the user’s GPU for high-performance rendering.

While other renderers exist (like CSS2DRenderer and CSS3DRenderer), they are specialized tools, typically used for overlaying HTML-based UI elements on top of a WebGL canvas, rather than for rendering the main 3D scene.

A 3D scene must adapt to the browser window’s size, just like a normal website. This is achieved by listening to the window’s resize event and updating the camera and renderer accordingly.

window.addEventListener('resize', () => {
// 1. Update canvas and viewport sizes
const width = window.innerWidth;
const height = window.innerHeight;
// 2. Update camera's aspect ratio
camera.aspect = width / height;
camera.updateProjectionMatrix(); // This must be called after changing properties like 'aspect'
// 3. Update the renderer's size
renderer.setSize(width, height);
// 4. Adjust pixel ratio for high-DPI displays (e.g., Retina)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

The updateProjectionMatrix() call is crucial; it tells the camera to recalculate its perspective based on the new aspect ratio.

Example: Scene Management and Responsiveness

Section titled “Example: Scene Management and Responsiveness”

This example demonstrates adding and removing objects from a scene dynamically. It uses a clean, modern structure and includes the essential responsiveness logic.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js - Scene Management</title>
<style>
body { margin: 0; }
canvas.webgl { display: block; }
.ui-controls {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
}
button { margin: 5px; padding: 8px 12px; cursor: pointer; }
</style>
</head>
<body>
<div class="ui-controls">
<button id="add-cube">Add Cube</button>
<button id="remove-cube">Remove Last Cube</button>
</div>
<canvas class="webgl"></canvas>
<script type="module" src="/main.js"></script>
</body>
</html>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// --- Core Setup ---
const scene = new THREE.Scene();
const canvas = document.querySelector('canvas.webgl');
const sizes = { width: window.innerWidth, height: window.innerHeight };
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.set(10, 10, 15);
scene.add(camera);
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
// --- Lights & Environment ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// --- Scene Manipulation Logic ---
let cubeCounter = 0;
document.getElementById('add-cube').addEventListener('click', () => {
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random())
});
const cube = new THREE.Mesh(geometry, material);
// Give it a unique name for easy identification
cube.name = `cube-${cubeCounter++}`;
// Position randomly
cube.position.set(
(Math.random() - 0.5) * 20,
(Math.random()) * 5 + 1,
(Math.random() - 0.5) * 20
);
scene.add(cube);
console.log(`Added ${cube.name}. Total objects in scene: ${scene.children.length}`);
});
document.getElementById('remove-cube').addEventListener('click', () => {
// Find the last added cube
const lastCube = scene.children.find(obj => obj.name.startsWith('cube-'));
if (lastCube) {
console.log(`Removing ${lastCube.name}`);
// Proper cleanup
lastCube.geometry.dispose();
lastCube.material.dispose();
scene.remove(lastCube);
console.log(`Removed cube. Total objects in scene: ${scene.children.length}`);
} else {
console.log('No more cubes to remove.');
}
});
// --- Animation Loop ---
const tick = () => {
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();
// --- Responsiveness ---
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});