---
name: threejs-builder
description: >
Creates simple Three.js web apps with scene setup, lighting, geometries, materials,
animations, and responsive rendering. Use for: "Create a threejs scene/app/showcase"
or when user wants 3D web content. Supports ES modules, modern Three.js r150+ APIs.
---
# Three.js Builder
A focused skill for creating simple, performant Three.js web applications using modern ES module patterns.
## Reference Files
> **Important**: Read the appropriate reference file when working on specific topics.
| Topic | File | Use When |
|-------|------|----------|
| **GLTF Models** | [gltf-loading-guide.md](references/gltf-loading-guide.md) | Loading, caching, cloning 3D models, SkeletonUtils |
| **Reference Frames** | [reference-frame-contract.md](references/reference-frame-contract.md) | Calibration, anchoring, axis correctness, debugging |
| **Game Development** | [game-patterns.md](references/game-patterns.md) | State machines, animation switching, parallax, object pooling |
| **Advanced Topics** | [advanced-topics.md](references/advanced-topics.md) | Post-processing, shaders, physics, instancing |
| **Calibration Helpers** | [scripts/README.md](scripts/README.md) | GLTF calibration helper installation and usage |
---
## Philosophy: The Scene Graph Mental Model
Three.js is built on the **scene graph**—a hierarchical tree of objects where parent transformations affect children. Understanding this mental model is key to effective 3D web development.
**Before creating a Three.js app, ask**:
- What is the **core visual element**? (geometry, shape, model)
- What **interaction** does the user need? (none, orbit controls, custom input)
- What **performance** constraints exist? (mobile, desktop, WebGL capabilities)
- What **animation** brings it to life? (rotation, movement, transitions)
**Core principles**:
1. **Scene Graph First**: Everything added to `scene` renders. Use `Group` for hierarchical transforms.
2. **Primitives as Building Blocks**: Built-in geometries (Box, Sphere, Torus) cover 80% of simple use cases.
3. **Animation as Transformation**: Change position/rotation/scale over time using `requestAnimationFrame` or `renderer.setAnimationLoop`.
4. **Performance Through Simplicity**: Fewer objects, fewer draw calls, reusable geometries/materials.
---
## Three.js Coordinate System (CRITICAL)
Understanding Three.js's right-handed coordinate system is **essential** to avoid inverted movement, wrong-facing models, and broken collision detection.
### The Axes
```
+Y (up)
|
|
|_______ +X (right)
/
/
+Z (toward camera/viewer)
```
**Memory aid**: Point your thumb (+X), index finger (+Y), middle finger (+Z) - that's right-handed coordinates.
| Axis | Direction | Common Usage |
|------|-----------|--------------|
| +X | Right | Strafe right, spawn right |
| -X | Left | Strafe left, spawn left |
| +Y | Up | Jump, height |
| -Y | Down | Fall, gravity |
| +Z | Toward camera | Approach viewer, "forward" in many setups |
| -Z | Away from camera | Retreat, **GLTF models face -Z by default** |
### GLTF Model Default Orientation
**CRITICAL**: GLTF models exported from Blender/Maya face **-Z** (into the screen) by default.
```javascript
// GLTF model faces -Z. To face +Z (toward camera):
model.rotation.y = Math.PI; // 180° rotation
// To face +X (right):
model.rotation.y = -Math.PI / 2; // -90°
// To face -X (left):
model.rotation.y = Math.PI / 2; // +90°
```
### Camera-Relative Movement (CRITICAL for Games)
**PROBLEM**: When camera is at an angle (e.g., isometric view), raw WASD input moves wrong!
```javascript
// ❌ WRONG - Input is world-axis relative, not camera-relative
if (keyW) player.position.z -= speed; // Moves toward -Z, not "forward" from player's view
if (keyD) player.position.x += speed; // Moves +X, not "right" from camera's view
// ✓ CORRECT - Calculate camera-relative directions
function updateMovement(deltaTime) {
// Get camera's forward direction, projected onto ground (XZ plane)
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
// Calculate right vector (cross product of forward and world up)
const right = new THREE.Vector3();
right.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
// Apply input relative to camera orientation
const velocity = new THREE.Vector3();
if (inputState.up) velocity.add(forward);
if (inputState.down) velocity.sub(forward);
if (inputState.right) velocity.add(right);
if (inputState.left) velocity.sub(right);
if (velocity.length() > 0) {
velocity.normalize().multiplyScalar(speed * deltaTime);
player.position.add(velocity);
// Face movement direction
player.rotation.y = Math.atan2(velocity.x, velocity.z);
}
}
```
**Why this matters**: With camera at `(8, 11, -6)` looking at `(0, 1, 3)`:
- "Forward" visually is NOT `-Z`, it's roughly `+Z`
- "Right" visually is NOT `+X`, it's roughly `-X + Z`
- Raw axis input feels completely inverted to players
---
## Quick Start: Essential Setup
### Minimal HTML Template
```html
Three.js App
```
---
## Geometries
Built-in primitives cover most simple app needs. Use `BufferGeometry` only for custom shapes.
**Common primitives**:
- `BoxGeometry(width, height, depth)` - cubes, boxes
- `SphereGeometry(radius, widthSegments, heightSegments)` - balls, planets
- `CylinderGeometry(radiusTop, radiusBottom, height)` - tubes, cylinders
- `TorusGeometry(radius, tube)` - donuts, rings
- `PlaneGeometry(width, height)` - floors, walls, backgrounds
- `ConeGeometry(radius, height)` - spikes, cones
- `IcosahedronGeometry(radius, detail)` - low-poly spheres (detail=0)
**Usage**:
```javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
```
---
## Materials
Choose material based on lighting needs and visual style.
**Material selection guide**:
- `MeshBasicMaterial` - No lighting, flat colors. Use for: UI, wireframes, unlit effects
- `MeshStandardMaterial` - PBR lighting. Default for realistic surfaces
- `MeshPhysicalMaterial` - Advanced PBR with clearcoat, transmission. Glass, water
- `MeshNormalMaterial` - Debug, rainbow colors based on normals
- `MeshPhongMaterial` - Legacy, shininess control. Faster than Standard
**Common material properties**:
```javascript
{
color: 0x44aa88, // Hex color
roughness: 0.5, // 0=glossy, 1=matte (Standard/Physical)
metalness: 0.0, // 0=non-metal, 1=metal (Standard/Physical)
emissive: 0x000000, // Self-illumination color
wireframe: false, // Show edges only
transparent: false, // Enable transparency
opacity: 1.0, // 0=invisible, 1=opaque (needs transparent:true)
side: THREE.FrontSide // FrontSide, BackSide, DoubleSide
}
```
---
## Lighting
No light = black screen (except BasicMaterial/NormalMaterial).
**Light types**:
- `AmbientLight(intensity)` - Base illumination everywhere. Use 0.3-0.5
- `DirectionalLight(color, intensity)` - Sun-like, parallel rays. Cast shadows
- `PointLight(color, intensity, distance)` - Light bulb, emits in all directions
- `SpotLight(color, intensity, angle, penumbra)` - Flashlight, cone of light
**Typical lighting setup**:
```javascript
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(5, 10, 7);
scene.add(mainLight);
const fillLight = new THREE.DirectionalLight(0x88ccff, 0.5);
fillLight.position.set(-5, 0, -5);
scene.add(fillLight);
```
**Shadows** (advanced, use when needed):
```javascript
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mesh.castShadow = true;
mesh.receiveShadow = true;
```
---
## Animation
Transform objects over time using the animation loop.
**Animation patterns**:
1. **Continuous rotation**:
```javascript
renderer.setAnimationLoop((time) => {
mesh.rotation.x = time * 0.001;
mesh.rotation.y = time * 0.0005;
renderer.render(scene, camera);
});
```
2. **Wave/bobbing motion**:
```javascript
renderer.setAnimationLoop((time) => {
mesh.position.y = Math.sin(time * 0.002) * 0.5;
renderer.render(scene, camera);
});
```
3. **Mouse interaction**:
```javascript
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
renderer.setAnimationLoop(() => {
mesh.rotation.x = mouse.y * 0.5;
mesh.rotation.y = mouse.x * 0.5;
renderer.render(scene, camera);
});
```
---
## Camera Controls
Import OrbitControls from examples for interactive camera movement:
```html
```
---
## Common Scene Patterns
### Rotating Cube (Hello World)
```javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
renderer.setAnimationLoop((time) => {
cube.rotation.x = time * 0.001;
cube.rotation.y = time * 0.001;
renderer.render(scene, camera);
});
```
### Floating Particle Field
```javascript
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 50;
positions[i + 1] = (Math.random() - 0.5) * 50;
positions[i + 2] = (Math.random() - 0.5) * 50;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 });
const particles = new THREE.Points(geometry, material);
scene.add(particles);
```
### Animated Background with Foreground Object
```javascript
// Background grid
const gridHelper = new THREE.GridHelper(50, 50, 0x444444, 0x222222);
scene.add(gridHelper);
// Foreground object
const mainGeometry = new THREE.IcosahedronGeometry(1, 0);
const mainMaterial = new THREE.MeshStandardMaterial({
color: 0xff6600,
flatShading: true
});
const mainMesh = new THREE.Mesh(mainGeometry, mainMaterial);
scene.add(mainMesh);
```
---
## Colors
Three.js uses hexadecimal color format: `0xRRGGBB`
Common hex colors:
- Black: `0x000000`, White: `0xffffff`
- Red: `0xff0000`, Green: `0x00ff00`, Blue: `0x0000ff`
- Cyan: `0x00ffff`, Magenta: `0xff00ff`, Yellow: `0xffff00`
- Orange: `0xff8800`, Purple: `0x8800ff`, Pink: `0xff0088`
---
## Anti-Patterns to Avoid
### Basic Setup Mistakes
❌ **Not importing OrbitControls from correct path**
Why bad: Controls won't load, `THREE.OrbitControls` is undefined in modern Three.js
Better: Use `import { OrbitControls } from 'three/addons/controls/OrbitControls.js'` or unpkg examples/jsm path
❌ **Forgetting to add object to scene**
Why bad: Object won't render, silent failure
Better: Always call `scene.add(object)` after creating meshes/lights
❌ **Using old `requestAnimationFrame` pattern instead of `setAnimationLoop`**
Why bad: More verbose, doesn't handle XR/WebXR automatically
Better: `renderer.setAnimationLoop((time) => { ... })`
### Performance Issues
❌ **Creating new geometries in animation loop**
Why bad: Massive memory allocation, frame rate collapse
Better: Create geometry once, reuse it. Transform only position/rotation/scale
❌ **Using too many segments on primitives**
Why bad: Unnecessary vertices, GPU overhead
Better: Default segments are usually fine. `SphereGeometry(1, 32, 16)` not `SphereGeometry(1, 128, 64)`
❌ **Not setting pixelRatio cap**
Why bad: 4K/5K displays run at full resolution, poor performance
Better: `Math.min(window.devicePixelRatio, 2)`
### Code Organization
❌ **Everything in one giant function**
Why bad: Hard to modify, hard to debug
Better: Separate setup into functions: `createScene()`, `createLights()`, `createMeshes()`
❌ **Hardcoding all values**
Why bad: Difficult to tweak and experiment
Better: Define constants at top: `const CONFIG = { color: 0x00ff88, speed: 0.001 }`
---
## Variation Guidance
**IMPORTANT**: Each Three.js app should feel unique and context-appropriate.
**Vary by scenario**:
- **Portfolio/showcase**: Elegant, smooth animations, muted colors
- **Game/interactive**: Bright colors, snappy controls, particle effects
- **Data visualization**: Clean lines, grid helpers, clear labels
- **Background effect**: Subtle, slow movement, dark/gradient backgrounds
- **Product viewer**: Realistic lighting, PBR materials, smooth orbit
**Vary visual elements**:
- **Geometry choice**: Not everything needs to be a cube. Explore spheres, tori, icosahedra
- **Material style**: Mix flat shaded, glossy, metallic, wireframe
- **Color palettes**: Use complementary, analogous, or monochromatic schemes
- **Animation style**: Rotation, oscillation, wave motion, mouse tracking
**Avoid converging on**:
- Default green cube as first example every time
- Same camera angle (front-facing, z=5)
- Identical lighting setup (always directional light at 1,1,1)
---
## Remember
**Three.js is a tool for interactive 3D on the web.**
Effective Three.js apps:
- Start with the scene graph mental model
- Use primitives as building blocks
- Keep animations simple and performant
- Vary visual style based on purpose
- Import from modern ES module paths
**Modern Three.js (r150+) uses ES modules from `three` package or CDN.** CommonJS patterns and global `THREE` variable are legacy.
**Claude is capable of creating elegant, performant 3D web experiences. These patterns guide the way—they don't limit the result.**
For specific topics, see the **Reference Files** table at the top of this document.