Three.js - Renderer and Responsiveness
Three.js - The Scene, Renderer, and Responsiveness
Section titled “Three.js - The Scene, Renderer, and Responsiveness”The Scene Graph: Organizing Your World
Section titled “The Scene Graph: Organizing Your World”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.
Adding and Removing Objects
Section titled “Adding and Removing Objects”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 scenescene.add(mesh);
// The scene's children property is an array of all top-level objectsconsole.log(scene.children.length); // Output: 1
// Remove the mesh from the scenescene.remove(mesh);console.log(scene.children.length); // Output: 0Important: Memory Management and Disposal
Section titled “Important: Memory Management and Disposal”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: Bringing Your Scene to Life
Section titled “The Renderer: Bringing Your Scene to Life”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.
Making it Responsive
Section titled “Making it Responsive”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.
index.html
Section titled “index.html”<!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>main.js
Section titled “main.js”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));});