three-js-fundamentals
Three.js Fundamentals
When to use this
- You are building a 3D scene (product viewer, generative background, WebGL experience) in vanilla JS/TS without a framework wrapper, or building the underlying knowledge that a framework wrapper (R3F) abstracts.
- You need to get scene/camera/renderer/lighting/render-loop right from first principles, including the parts that are easy to get subtly wrong (color space, resize/DPR, disposal).
- You are debugging a memory leak or color-looks-wrong issue in an existing Three.js app and need the mental model of what's actually happening under the hood.
- Do NOT use this if you are working in React; go straight to
react-three-fiber-drei, which wraps all of this in a declarative, React-idiomatic API and handles most of the render-loop/resize/disposal bookkeeping for you. Do NOT use this for shader-only full-screen effects with no real 3D scene; seeglsl-fragment-backgroundsfor that narrower, lighter-weight case.
Mental model
Three.js's whole API is an abstraction over one core loop: a Scene graph (a tree of Object3Ds: meshes, lights, groups, cameras) gets projected through a Camera's view/projection matrices and rasterized by a WebGLRenderer into a <canvas>, once per animation frame. Nothing draws itself; you must explicitly call renderer.render(scene, camera) inside your own requestAnimationFrame loop every single frame, there is no automatic re-render on state change like there is in DOM/React.
A Mesh is geometry (the actual vertex/index buffer data describing shape) combined with a material (how those vertices are shaded: what color, how they respond to light, what texture). This separation matters practically: the same BufferGeometry can be reused across many meshes with different materials, and swapping a material at runtime is cheap while regenerating geometry is comparatively expensive.
Lighting only affects materials that are "lit" materials (MeshStandardMaterial, MeshPhysicalMaterial, MeshLambertMaterial, MeshPhongMaterial); MeshBasicMaterial ignores all scene lights entirely and just shows its own color/texture unlit, which is the single most common "why isn't my light doing anything" bug. Physically-based materials (MeshStandardMaterial) need real light intensity values calibrated to the renderer's exposure/tone-mapping settings to look correct, since Three.js since r152 defaults to a physically-based lighting unit system (renderer.useLegacyLights = false is now the default, meaning light intensities are in real-world-ish units like lumens/lux equivalents, not the old arbitrary 0-1 scale).
Color space is the other subtlety that trips up almost everyone coming from CSS/canvas 2D: renderer.outputColorSpace = THREE.SRGBColorSpace (default since r152) means the renderer expects your lit scene's linear color math internally but outputs sRGB-encoded pixels matching what monitors expect, and any texture holding color data (not normal maps or roughness maps, which are data, not color) must have texture.colorSpace = THREE.SRGBColorSpace set explicitly or colors will look washed out/wrong.
The render loop itself is a function you write; nothing in Three.js "runs" your animation for you beyond providing renderer.setAnimationLoop(callback) as a convenience wrapper around requestAnimationFrame that also correctly pauses in WebXR contexts.