Patterns

Patterns — Three.js

Geometry + Material + Mesh

When to use: Every visible 3D object. How: new Mesh(geometry, material) then scene.add(mesh). Geometry and material are independently reusable/shareable across meshes. Trade-offs: Sharing geometry/material instances across many meshes saves GPU memory and draw-call setup, but any mutation (e.g. editing a shared material's color) affects every mesh using it — clone when independence is needed.

Render loop with requestAnimationFrame

When to use: Any animated scene. How: renderer.setAnimationLoop(() => { update(); renderer.render(scene, camera); }) — preferred over manual requestAnimationFrame because it integrates with WebXR sessions automatically. Trade-offs: setAnimationLoop couples your loop to the renderer; for non-XR apps a manual RAF loop works identically and is easier to pause/resume explicitly.

Raycaster for interaction/picking

When to use: Mouse/touch selection, hover effects, click-to-place. How: Convert screen coordinates to normalized device coordinates, raycaster.setFromCamera(ndc, camera), then raycaster.intersectObjects(objects, recursive). Trade-offs: intersectObjects walks the full object graph each call — for large scenes, restrict the candidate list or use spatial partitioning (Octree) instead of raycasting against everything.

Instancing for repeated geometry

When to use: Hundreds/thousands of visually-similar objects (grass, particles, crowd, voxels). How: new InstancedMesh(geometry, material, count), then set each instance's transform via setMatrixAt(i, matrix) and mark instanceMatrix.needsUpdate = true. Trade-offs: All instances share one material (with per-instance color/attributes as an escape hatch) — if instances need truly independent materials, instancing isn't the right tool; consider BatchedMesh (independent materials, still one draw call family) instead.

Disposal / memory management

When to use: Any app that creates/destroys geometry, materials, or textures at runtime (level streaming, dynamic content). How: Call .dispose() on geometries, materials, and textures no longer referenced — three.js does NOT garbage-collect GPU resources automatically just because the JS object is unreferenced. Trade-offs: Over-eager disposal (disposing a shared resource still in use elsewhere) causes visual corruption; track reference counts or ownership explicitly in non-trivial scenes.

Node-based materials (TSL) vs classic materials

When to use: Classic materials (MeshStandardMaterial) cover most needs with zero shader code. Reach for *NodeMaterial + TSL when you need custom vertex displacement, procedural texturing, or logic the built-in material properties can't express. How: Use a *NodeMaterial variant and assign TSL graphs to its colorNode/positionNode/normalNode/opacityNode etc. (see Ch 797–799). Trade-offs: TSL materials are more powerful and portable (compile to both WebGL2 and WebGPU) but have a steeper learning curve than tweaking classic material properties.

Loading external assets

When to use: Any model/texture/font not authored as inline three.js code (glTF, textures, fonts, point clouds). How: Instantiate the matching *Loader, call .load(url, onLoad, onProgress, onError) (or the promise-friendly .loadAsync), and add the result's .scene/mesh to your scene graph. Trade-offs: Loaders are async by nature — design your scene setup to handle "not yet loaded" states (placeholders, loading UI) rather than assuming synchronous availability.

Post-processing pipelines

When to use: Bloom, depth-of-field, anti-aliasing beyond MSAA, color grading, screen-space effects. How: Classic API: EffectComposer + chained *Pass instances. Node system: PostProcessing + composed TSL effect functions (ao(), afterImage(), ...). Trade-offs: Every additional pass costs a full-screen render — profile before stacking many effects, and prefer combining effects into fewer passes where TSL composition allows it.

Accessibility / responsive canvas sizing

When to use: Any app embedded in a resizable layout. How: Listen for container/window resize, update camera.aspect (perspective) or the ortho frustum bounds, call camera.updateProjectionMatrix(), and call renderer.setSize(width, height). Trade-offs: Forgetting updateProjectionMatrix() after changing aspect/frustum properties is one of the most common three.js bugs — the camera object doesn't recompute its matrix automatically on property assignment.