Cheatsheet — Three.js
Camera choice
| Situation | Use |
|---|
| Realistic 3D scene, perspective depth | PerspectiveCamera |
| CAD/blueprint view, 2D-ish UI overlays, isometric games | OrthographicCamera |
| Multi-view/split-screen (VR-style) rendering | ArrayCamera |
| Environment reflections / dynamic cubemaps | CubeCamera |
| Stereo/3D-glasses rendering | StereoCamera |
Light choice
| Situation | Use | Cost |
|---|
| Uniform scene fill, no direction | AmbientLight | Free (no shadows possible) |
| Sun-like distant light, parallel rays | DirectionalLight | Cheap; supports shadow maps |
| Bulb/lamp, radiates from a point | PointLight | Moderate; cubemap shadows are 6× cost |
| Flashlight/spotlight, cone-shaped | SpotLight | Moderate; single shadow map |
| Sky+ground gradient fill | HemisphereLight | Free (no shadows) |
| Physically-sized area light (panel/window) | RectAreaLight | Expensive; no shadow support, MeshStandardMaterial only |
Rule: start with AmbientLight/HemisphereLight + one DirectionalLight for the cheapest believable lighting; add PointLight/SpotLight only for local highlights, and enable shadows on the fewest lights possible (shadow maps are the single biggest lighting cost).
Geometry/instancing decision
- Same geometry+material, hundreds+ copies, no per-instance geometry variation →
InstancedMesh.
- Same geometry+material family but different materials per instance →
BatchedMesh.
- Distance-based detail swap (near = high-poly, far = low-poly/billboard) →
LOD.
- One-off unique object → plain
Mesh.
Loader by format
| Format | Loader |
|---|
| glTF/glb (recommended interchange format) | GLTFLoader |
| OBJ | OBJLoader (+ MTLLoader for materials) |
| FBX | FBXLoader |
| Draco-compressed geometry | DRACOLoader (used alongside GLTFLoader) |
| KTX2 compressed textures | KTX2Loader |
| Images (jpg/png/webp) | TextureLoader |
| HDR environment maps | HDRLoader (or RGBELoader in older versions) |
Fonts (for TextGeometry) | FontLoader |
| Point clouds | PCDLoader, PLYLoader |
Rule: prefer glTF for anything authored in a 3D DCC tool (Blender, etc.) — it's the format three.js's own tooling and most exporters target first, and it round-trips materials/animations/skinning most reliably.
Renderer choice
| Situation | Use |
|---|
| Maximum compatibility (older browsers/devices) | WebGLRenderer |
| TSL compute shaders, forward-looking WebGPU features | WebGPURenderer (has automatic WebGL2 fallback) |
Performance thresholds & defaults
- Default
Object3D.frustumCulled = true — leave it on; only disable for objects that must always render regardless of camera view (e.g. skyboxes attached to the camera).
- Default texture
wrapS/wrapT is ClampToEdgeWrapping, default magFilter/minFilter favor LinearFilter — switch to RepeatWrapping explicitly for tiling textures.
- Shadow map quality:
PCFShadowMap is the default and a reasonable quality/perf balance; BasicShadowMap is fastest/lowest-quality, PCFSoftShadowMap/VSMShadowMap cost more for softer edges.
- Always call
camera.updateProjectionMatrix() after changing fov/aspect/near/far (perspective) or the frustum bounds (orthographic) — property assignment alone does not recompute the matrix.
- Always
.dispose() geometries/materials/textures you're done with — three.js doesn't free GPU memory via JS garbage collection alone.
Tells & smells
- Black/invisible mesh with a normal-looking material → check lights exist in the scene, or that the material type actually responds to light (
MeshBasicMaterial ignores lights entirely — that's a common source of "why is nothing being lit" confusion, it's working as designed).
- Object appears in the wrong place after nesting in a Group/parent → local vs world coordinates; use
.getWorldPosition()/.localToWorld() when you need the resolved world-space value.
- Texture looks stretched/blurry at a distance → mipmaps disabled or
NearestFilter where LinearMipmapLinearFilter is expected; check generateMipmaps and texture dimensions (non-power-of-two textures have historically had mipmap restrictions on WebGL1).
- Raycasting hits nothing on an instanced/merged mesh →
Raycaster on InstancedMesh returns an instanceId in the intersection result; code written for plain meshes that ignores instanceId will misattribute hits.
- Animation plays once then freezes → check
AnimationAction.loop (defaults to LoopRepeat for AnimationMixer-driven clips, but explicit .setLoop(LoopOnce) calls need clampWhenFinished if you want it to hold the last frame instead of snapping back).