Skill: openlark/threejs-3d (version 1.0.0)

--- name: threejs-3d description: Comprehensive Three.js 3D graphics reference. Use when building 3D web apps, games, or visualizations with Three.js. Covers: scene setup, cameras, geometries, materials, lights, shadows, post-processing, controls, animation, loaders, textures, TSL/node materials, math utils, and debugging. --- # Three.js 3D ## Quick Start ```js import * as THREE from 'three'; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({antialias: true}); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); scene.add(new THREE.Mesh( new THREE.BoxGeometry(1,1,1), new THREE.MeshStandardMaterial({color: 0x44aa88}) )); scene.add(new THREE.DirectionalLight(0xffffff, 3)); scene.add(new THREE.AmbientLight(0x404040, 0.5)); camera.position.z = 5; function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); ``` ## Import ```bash npm install three ``` ```js import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; ``` ## Key Patterns ### Object3D (everything extends this) ```js obj.position.set(x,y,z); obj.rotation.set(x,y,z); obj.scale.set(x,y,z); // Euler rad obj.quaternion.set(x,y,z,w); // No gimbal lock obj.lookAt(target); // -Z → target, +Y up parent.add(child); parent.remove(child); parent.attach(child); // Reparent w/o visual change obj.getWorldPosition(v); obj.localToWorld(v); obj.worldToLocal(v); ``` ### Color ```js new THREE.Color(0xff0000); new THREE.Color('#ff0000'); new THREE.Color(1,0,0); color.setHSL(hue, saturation, lightness); // 0–1 ``` ### Dispose (always!) ```js geometry.dispose(); material.dispose(); texture.dispose(); renderTarget.dispose(); ``` ## Reference Files Read the relevant file based on the task — each is concise and domain-focused: | File | Topic | |---|---| | [getting-started.md](references/getting-started.md) | Responsive design, multi-canvas, render-on-demand, fog, disposal | | [cameras.md](references/cameras.md) | PerspectiveCamera, OrthographicCamera, CubeCamera, gotchas | | [geometries.md](references/geometries.md) | All built-in primitives, BufferGeometry, merge, vertex colors | | [materials.md](references/materials.md) | Material selection guide, properties, transparency fixes, ShaderMaterial | | [lights.md](references/lights.md) | Light types, three-point lighting, environment maps, skyboxes | | [objects.md](references/objects.md) | Object3D API, Mesh, InstancedMesh, SkinnedMesh, LOD, scene graph | | [textures.md](references/textures.md) | Properties, types, color space, KTX2, memory management | | [loaders.md](references/loaders.md) | GLTF/OBJ/FBX, Draco, KTX2, loading manager, gotchas | | [animation.md](references/animation.md) | AnimationMixer/Action, skeletal, morph targets, crossfading | | [controls.md](references/controls.md) | OrbitControls, TransformControls, HTML labels, sprites/facades | | [math.md](references/math.md) | Vector/Matrix/Quat/Color/Raycaster patterns + gotchas | | [renderers.md](references/renderers.md) | WebGL/WebGPU setup, post-processing, shadows, render targets, GPU picking | | [nodes.md](references/nodes.md) | TSL node materials, compute shaders | | [helpers.md](references/helpers.md) | Debug helpers, scene graph dump, GLSL debugging, lil-gui patterns |

path: references/animation.md
content: # Animation

## AnimationMixer (the player)

```js
const mixer = new THREE.AnimationMixer(rootObject);

// In render loop:
const delta = clock.getDelta();
mixer.update(delta);

// Properties
mixer.timeScale = 1;      // Speed multiplier
mixer.time;               // Current time
```

## AnimationAction (controls playback)

```js
const action = mixer.clipAction(animationClip);

// Playback
action.play();
action.pause();
action.stop();
action.reset();
action.halt(duration);    // Gradual stop

// Properties
action.time;              // Current time in clip
action.timeScale = 1;     // Speed (negative = reverse)
action.weight = 1;        // Blend weight 0–1
action.loop = THREE.LoopRepeat | THREE.LoopOnce | THREE.LoopPingPong;
action.repetitions = Infinity;
action.paused = false;
action.enabled = true;
action.clampWhenFinished = false;    // Hold last frame
action.zeroSlopeAtStart = true;     // Smooth start
action.zeroSlopeAtEnd = true;       // Smooth end
action.blendMode = THREE.NormalAnimationBlendMode | THREE.AdditiveAnimationBlendMode;

// Cross-fading
action.crossFadeTo(otherAction, duration, warp);
action.fadeIn(duration);
action.fadeOut(duration);
action.startAt(startTime);
```

## Animation State Machine
```js
const actions = { idle: mixer.clipAction(idleClip), walk: mixer.clipAction(walkClip) };
let current = actions.idle;
current.play();

function switchTo(name, dur = 0.3) {
  const prev = current;
  current = actions[name];
  current.reset().play();
  prev.crossFadeTo(current, dur, true);
}
```

## Skeletal Animation (from glTF)
```js
loader.load('character.glb', gltf => {
  const mixer = new THREE.AnimationMixer(gltf.scene);
  gltf.animations.forEach(c => mixer.clipAction(c).play());
  // In loop: mixer.update(delta);
  // Access bones: gltf.scene.traverse(c => { if(c.isSkinnedMesh) console.log(c.skeleton.bones); });
});
```

## Morph Target Animation
```js
mesh.morphTargetInfluences[0] = 0.5;  // Blend first morph target 50%

// Generate clips from morph sequences:
const clips = THREE.AnimationClip.CreateClipsFromMorphTargetSequences(meshes, fps, false);
```

## AnimationClip (keyframe data)
```js
const clip = new THREE.AnimationClip(name, duration, tracks);
clip.tracks;              // KeyframeTrack[]
clip.duration;            // Seconds

// Tracks:
new THREE.VectorKeyframeTrack('.position', times, values);
new THREE.QuaternionKeyframeTrack('.quaternion', times, values);
new THREE.NumberKeyframeTrack('.material.opacity', times, values);
new THREE.ColorKeyframeTrack('.material.color', times, values);
```

## Addons
```js
import { CCDIKSolver } from 'three/addons/animation/CCDIKSolver.js';       // Inverse kinematics
import { AnimationClipCreator } from 'three/addons/animation/AnimationClipCreator.js';
// CreateRotationAnimation(period, axis), CreateScaleAxisAnimation, CreateShakeAnimation, CreatePulsationAnimation
```

path: references/cameras.md
content: # Cameras

## PerspectiveCamera (3D view)
```js
const cam = new THREE.PerspectiveCamera(fov, aspect, near, far);
// fov: vertical field of view in degrees (not radians!)
// aspect: width/height
// near/far: clipping planes

cam.zoom = 1;                   // Zoom factor (>1 = zoom in)
// After changing: cam.updateProjectionMatrix()

// Stereo / split-screen view offset:
cam.setViewOffset(fullW, fullH, x, y, w, h);
cam.clearViewOffset();
```

## OrthographicCamera (parallel projection)
```js
const aspect = innerWidth / innerHeight;
const size = 10;
const cam = new THREE.OrthographicCamera(
  -size * aspect / 2, size * aspect / 2,  // left, right
  size / 2, -size / 2,                     // top, bottom
  0.1, 1000                                // near, far
);

// 2D pixel-perfect (top-left origin):
const cam = new THREE.OrthographicCamera(0, canvasW, 0, canvasH, -1, 1);
```

## CubeCamera (dynamic cubemap/environment)
```js
const cubeRT = new THREE.WebGLCubeRenderTarget(256);
const cubeCam = new THREE.CubeCamera(near, far, cubeRT);
// In loop: obj.visible=false; cubeCam.update(renderer, scene); obj.visible=true;
// Use cubeRT.texture as envMap
```

## StereoCamera (VR, used internally by WebXR)
```js
const stereo = new THREE.StereoCamera();
stereo.eyeSep = 0.064;           // IPD in meters
stereo.update(camera);
// stereo.cameraL / cameraR for each eye
```

## ArrayCamera (multiple cameras, single scene)
```js
new THREE.ArrayCamera([cam1, cam2]);
```

## Camera Helper
```js
const helper = new THREE.CameraHelper(camera);
scene.add(helper);
helper.update();
```

## Gotchas
- **fov** is degrees (unique in Three.js where everything else uses radians)
- `lookAt` points **-Z** axis at target — set `camera.up.set(0,0,1)` before for top-down views
- `near` should be as large as possible to maximize depth buffer precision
- For huge scenes with z-fighting: `renderer.logarithmicDepthBuffer = true` (WebGL only)
- `updateProjectionMatrix()` must be called after changing fov/aspect/near/far/zoom

path: references/controls.md
content: # Controls & Labels

## OrbitControls (most common)

```js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);

// Setup
controls.target.set(0, 0, 0);
controls.enableDamping = true;     // Inertia (requires controls.update() in loop)
controls.dampingFactor = 0.05;

// Limits
controls.minDistance = 1;  controls.maxDistance = Infinity;
controls.minPolarAngle = 0; controls.maxPolarAngle = Math.PI;        // Vertical
controls.minAzimuthAngle = -Infinity; controls.maxAzimuthAngle = Infinity; // Horizontal
controls.enableZoom = true;  controls.zoomSpeed = 1.0;
controls.enableRotate = true; controls.rotateSpeed = 0.5;
controls.enablePan = true;    controls.panSpeed = 0.5;
controls.autoRotate = false;  controls.autoRotateSpeed = 2.0;

// State
controls.saveState();
controls.reset();    // Restore saved state
controls.dispose();

// Button mapping
controls.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
controls.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN };
```

## Other Controls
```js
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';    // Free rotation
import { MapControls } from 'three/addons/controls/MapControls.js';                // Map-style (same API as Orbit)
import { FlyControls } from 'three/addons/controls/FlyControls.js';                // WASD flight
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; // FPS
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // FPS + pointer lock
import { ArcballControls } from 'three/addons/controls/ArcballControls.js';        // Advanced with gizmos
import { TransformControls } from 'three/addons/controls/TransformControls.js';    // Move/rotate/scale gizmo
import { DragControls } from 'three/addons/controls/DragControls.js';              // Drag objects on plane
```

## TransformControls (editor gizmo)
```js
const gizmo = new TransformControls(camera, renderer.domElement);
gizmo.attach(targetObject);
gizmo.setMode('translate' | 'rotate' | 'scale');
gizmo.space = 'world' | 'local';
scene.add(gizmo);  // Add to scene to render

// Disable orbit during transform:
gizmo.addEventListener('dragging-changed', e => { orbitControls.enabled = !e.value; });
```

## DragControls
```js
const drag = new DragControls(objects, camera, renderer.domElement);
drag.addEventListener('dragstart', e => { orbitControls.enabled = false; });
drag.addEventListener('dragend', e => { orbitControls.enabled = true; });
```

## HTML Labels Aligned to 3D

Position DOM elements over 3D points via `Vector3.project()`:

```js
// Container: <div id="c"><canvas/><div id="labels"/></div>
// CSS: #c{position:relative} #labels{position:absolute;left:0;top:0}
// Labels: #labels>div{position:absolute;left:0;top:0;white-space:nowrap;text-shadow:...}

const tempV = new THREE.Vector3();
function updateLabel(obj, elem, camera, canvas) {
  obj.getWorldPosition(tempV);
  tempV.project(camera);

  if (Math.abs(tempV.z) > 1) { elem.style.display = 'none'; return; }
  elem.style.display = '';

  const x = (tempV.x * 0.5 + 0.5) * canvas.clientWidth;
  const y = (tempV.y * -0.5 + 0.5) * canvas.clientHeight;
  elem.style.transform = `translate(-50%,-50%) translate(${x}px,${y}px)`;
  elem.style.zIndex = ((-tempV.z * 0.5 + 0.5) * 100000) | 0;
}
```

**Occlusion check:** Raycaster from camera through projected point — hide if not first hit.
**Sphere backface cull:** `dot(relativePos, cameraToPoint) < 0.2` → hide label.

## Sprites (billboards — always face camera)
```js
const mat = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(mat);
sprite.position.set(x, y, z);
sprite.scale.set(w, h, 1);
sprite.center.set(0.5, 0.5);
```

### Facade (render 3D object to sprite texture)
```js
const rt = new THREE.WebGLRenderTarget(size, size);
const cam = new THREE.PerspectiveCamera(fov, 1, 0.1, 1000);
// Position cam to frame object, render to rt, use rt.texture as SpriteMaterial.map
// Saves draw calls for distant objects
```

path: references/geometries.md
content: # Geometries

## Built-in Primitives

| Geometry | Constructor | Default |
|---|---|---|
| Box | `BoxGeometry(w, h, d, segW, segH, segD)` | `(1,1,1,1,1,1)` |
| Sphere | `SphereGeometry(r, segW, segH, phiS, phiL, thetaS, thetaL)` | `(1,32,16,0,2π,0,π)` |
| Plane | `PlaneGeometry(w, h, segW, segH)` | `(1,1,1,1)` |
| Circle | `CircleGeometry(r, seg, thetaS, thetaL)` | `(1,32,0,2π)` |
| Cylinder | `CylinderGeometry(rTop, rBot, h, segR, segH, open)` | `(1,1,1,32,1,false)` |
| Cone | `ConeGeometry(r, h, segR, segH, open)` | `(1,1,32,1,false)` |
| Capsule | `CapsuleGeometry(r, len, capSeg, radSeg)` | `(1,1,4,8)` |
| Ring | `RingGeometry(inR, outR, segT, segP, tS, tL)` | `(0.5,1,32,1,0,2π)` |
| Torus | `TorusGeometry(R, r, segR, segT, arc)` | `(1,0.4,12,48,2π)` |
| TorusKnot | `TorusKnotGeometry(R, r, segT, segR, p, q)` | `(1,0.4,64,8,2,3)` |
| Tube | `TubeGeometry(path, seg, r, segR, closed)` | path is Curve |
| Lathe | `LatheGeometry(points, seg, phiS, phiL)` | spin points around Y |
| Shape | `ShapeGeometry(shapes, seg)` | 2D shapes |
| Extrude | `ExtrudeGeometry(shapes, opts)` | depth + bevel |

**Polyhedra:** `IcosahedronGeometry(r, detail)`, `DodecahedronGeometry(r, detail)`, `OctahedronGeometry(r, detail)`, `TetrahedronGeometry(r, detail)`, `PolyhedronGeometry(verts, indices, r, detail)`

**Wireframe:** `EdgesGeometry(geo, thresholdAngle)` — boundary edges only | `WireframeGeometry(geo)` — all edges

## BufferGeometry (custom)

```js
const geo = new THREE.BufferGeometry();
const verts = new Float32Array([...]);                // Flat array [x,y,z, x,y,z, ...]
geo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
geo.setIndex([0,1,2, 2,1,3, ...]);                    // Indexed triangles (optional)
geo.computeVertexNormals();                            // Auto normals if not provided
geo.computeBoundingBox(); geo.computeBoundingSphere();

// Named attributes: position, normal, uv, color, uv2
// Each is a BufferAttribute(typedArray, componentsPerVertex)

// Transform geometry in place:
geo.translate(x,y,z); geo.scale(x,y,z);
geo.rotateX(r); geo.rotateY(r); geo.rotateZ(r);
geo.center();
geo.applyMatrix4(matrix);
geo.applyQuaternion(q);

// Dynamic update:
const posAttr = new THREE.BufferAttribute(positions, 3);
posAttr.setUsage(THREE.DynamicDrawUsage);
geo.setAttribute('position', posAttr);
// In loop: modify positions array, then posAttr.needsUpdate = true;
```

## Optimize: Merge Geometries (1 draw call)
```js
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const merged = mergeGeometries(geos, false);

// For per-object colors in merged geometry:
const colors = new Uint8Array(3 * numVerts);
const rgb = [r*255, g*255, b*255];
for (let i=0; i<colors.length; i++) colors[i] = rgb[i%3];
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
// Then: new THREE.MeshBasicMaterial({ vertexColors: true })
```

## Sphere Positioning Helper
```js
// Efficiently place N objects on sphere surface:
const lonH = new THREE.Object3D(), latH = new THREE.Object3D(), posH = new THREE.Object3D();
lonH.add(latH); latH.add(posH); posH.position.z = radius;
// For each point: lonH.rotation.y = long; latH.rotation.x = lat; posH.updateWorldMatrix(true,false);
// mesh.applyMatrix4(posH.matrixWorld);
```

## Disposal
```js
geometry.dispose();
// Also disposes underlying buffer attributes
```

path: references/getting-started.md
content: # Getting Started — Advanced Patterns

## Minimal Scene

```js
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

const mesh = new THREE.Mesh(
  new THREE.BoxGeometry(1,1,1),
  new THREE.MeshStandardMaterial({color: 0x44aa88})
);
scene.add(mesh);
scene.add(new THREE.DirectionalLight(0xffffff, 3));
scene.add(new THREE.AmbientLight(0x404040, 0.5));
camera.position.z = 5;

function animate() {
  requestAnimationFrame(animate);
  mesh.rotation.x += 0.01; mesh.rotation.y += 0.01;
  renderer.render(scene, camera);
}
animate();
```

## Installation
```js
// npm
import * as THREE from 'three';

// Addons
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
```

## Responsive Design
```js
function resizeRendererToDisplaySize(renderer) {
  const canvas = renderer.domElement;
  const w = canvas.clientWidth, h = canvas.clientHeight;
  const need = canvas.width !== w || canvas.height !== h;
  if (need) renderer.setSize(w, h, false);
  return need;
}

// In render loop:
if (resizeRendererToDisplaySize(renderer)) {
  camera.aspect = canvas.clientWidth / canvas.clientHeight;
  camera.updateProjectionMatrix();
}
// Pass `false` to setSize — let CSS control display size, match drawingBuffer to CSS
```

### HD-DPI
```js
const pr = window.devicePixelRatio;
const w = Math.floor(canvas.clientWidth * pr);
const h = Math.floor(canvas.clientHeight * pr);
if (canvas.width !== w || canvas.height !== h) renderer.setSize(w, h, false);

// Cap resolution to prevent GPU overload:
const maxPixels = 3840 * 2160;
const scale = w*h > maxPixels ? Math.sqrt(maxPixels/(w*h)) : 1;
renderer.setSize(Math.floor(w*scale), Math.floor(h*scale), false);
```

## Rendering on Demand (no continuous loop)
```js
let renderRequested = false;
function requestRenderIfNotRequested() {
  if (!renderRequested) { renderRequested = true; requestAnimationFrame(render); }
}
function render() { renderRequested = false; /* ... render ... */ }
render(); // Initial
controls.addEventListener('change', requestRenderIfNotRequested);
window.addEventListener('resize', requestRenderIfNotRequested);
// For damped controls: call controls.update() inside render()
```

## Multiple Canvases (single WebGL context)
Browser limits WebGL contexts (~8). Use one canvas with scissor/viewport per "virtual" canvas:

```js
const sceneElems = [];
function addScene(elem, fn) { sceneElems.push({elem, fn}); }

function render(time) {
  renderer.setScissorTest(false); renderer.clear(true, true); renderer.setScissorTest(true);
  renderer.domElement.style.transform = `translateY(${scrollY}px)`; // scroll sync

  for (const {elem, fn} of sceneElems) {
    const r = elem.getBoundingClientRect();
    if (r.bottom < 0 || r.top > innerHeight || r.right < 0 || r.left > innerWidth) continue;
    const y = renderer.domElement.clientHeight - r.bottom;
    renderer.setScissor(r.left, y, r.width, r.height);
    renderer.setViewport(r.left, y, r.width, r.height);
    fn(time, r);
  }
  requestAnimationFrame(render);
}
```

## Fog
```js
scene.fog = new THREE.Fog(color, near, far);       // Linear (most common)
scene.fog = new THREE.FogExp2(color, density);     // Exponential (realistic)
scene.background = new THREE.Color(fogColor);       // Must match fog color!
// Per-material exclusion:
material.fog = false;  // For interiors viewed from inside
```

## Clock / Timer
```js
const clock = new THREE.Clock();
// In loop: const delta = clock.getDelta(); // seconds, auto-start
// Timer (new): timer.update(timestamp); timer.getDelta(); timer.getElapsed();
```

## Disposal (prevent memory leaks)
```js
geometry.dispose();
material.dispose();
texture.dispose();
renderTarget.dispose();

// Dispose materials includes textures:
function disposeMaterial(mat) {
  for (const k of Object.keys(mat)) {
    if (mat[k]?.isTexture) mat[k].dispose();
  }
  mat.dispose();
}
// Full scene:
scene.traverse(obj => {
  if (obj.geometry) obj.geometry.dispose();
  if (obj.material) { /* disposeMaterial */ }
});
renderer.dispose();
```

## Capacibilities / WebXR
```js
import { WebGL } from 'three/addons/capabilities/WebGL.js';
if (!WebGL.isWebGL2Available()) document.body.appendChild(WebGL.getErrorMessage());

// VR:
renderer.xr.enabled = true;
renderer.setAnimationLoop(() => renderer.render(scene, camera));
```

path: references/helpers.md
content: # Helpers & Debugging

## Visualization Helpers
```js
scene.add(new THREE.AxesHelper(5));                           // RGB axes
scene.add(new THREE.GridHelper(10, 10));                      // XZ grid
scene.add(new THREE.BoxHelper(mesh, 0xffff00));               // Wireframe bounds
scene.add(new THREE.Box3Helper(box3, 0xffff00));              // Visualize Box3
scene.add(new THREE.CameraHelper(camera));                    // Frustum
scene.add(new THREE.DirectionalLightHelper(light, 0.5));
scene.add(new THREE.PointLightHelper(light, 0.5));
scene.add(new THREE.SpotLightHelper(light));
scene.add(new THREE.SkeletonHelper(skinnedMesh));
const arrow = new THREE.ArrowHelper(dir, origin, length, color); // Debug vectors
```

All helpers need `.update()` after their target changes.

## Stats (FPS Monitor)
```js
import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats(); stats.showPanel(0);
document.body.appendChild(stats.dom);
// In loop: stats.begin(); renderer.render(...); stats.end();
```

## Debugging

### Nothing Renders? Check:
1. Camera near/far too tight? → temporarily `near=0.001, far=1000000`
2. Object behind camera? → add OrbitControls
3. Needs lights? → try `MeshBasicMaterial` first
4. NaN in transforms? → `console.log(mesh.matrixWorld)`
5. `requestAnimationFrame` at **end** of render (errors stop loop → bugs visible)
6. Unit mismatch: Three.js assumes 1 unit = 1 meter

### On-Screen Logger (per-frame self-clearing)
```js
class ClearingLogger {
  constructor(elem) { this.elem = elem; this.lines = []; }
  log(...args) { this.lines.push(args.join(' ')); }
  render() { this.elem.textContent = this.lines.join('\n'); this.lines = []; }
}
```

### Dump Scene Graph
```js
function dumpObject(obj, lines=[], isLast=true, prefix='') {
  const m = isLast ? '└─' : '├─';
  lines.push(`${prefix}${m}${obj.name||'*no-name*'} [${obj.type}]`);
  const p = prefix + (isLast?'  ':'│ ');
  obj.children.forEach((c,i)=>dumpObject(c,lines,i===obj.children.length-1,p));
  return lines;
}
console.log(dumpObject(scene).join('\n'));
```

### GLSL Shader Debugging
1. **Solid color test:** `gl_FragColor = vec4(1,0,0,1);` → if object appears, bug is in fragment shader
2. **Visualize inputs:**
   ```glsl
   gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1);   // Normals → RGB
   gl_FragColor = vec4(fract(vUv), 0, 1);           // UVs → RG
   ```
3. **Shader Editor** extension (Chrome) for live shader editing
4. **Simplify:** Can you draw with MeshBasicMaterial? Add shader changes incrementally.

### NaN Sources
- `SplineCurve` (2D) fed to `Vector3.set()` → z=NaN → use `CatmullRomCurve3`
- Division by zero in custom shaders
- Uninitialized `Vector3` components

### Memory Leak Check
```js
setInterval(() => {
  console.log('geometries:', renderer.info.memory.geometries, 'textures:', renderer.info.memory.textures);
}, 2000);
// Always dispose: geo.dispose(), mat.dispose(), tex.dispose(), rt.dispose()
```

### Debug UI (lil-gui)
```js
import GUI from 'lil-gui';
const gui = new GUI();
gui.addColor(params, 'color').onChange(v => material.color.set(v));
gui.add(params, 'speed', 0, 0.1);
gui.add(mesh.position, 'x', -5, 5);
gui.add(mesh, 'visible');
```
**lil-gui helpers for Three.js:**
- `ColorGUIHelper` — bridges hex string ↔ THREE.Color
- `DegRadHelper` — bridges degrees ↔ radians
- `MinMaxGUIHelper` — enforces near<far, etc.

## Renderer Capabilities
```js
renderer.capabilities.maxTextureSize;         // Max texture resolution
renderer.capabilities.maxTextures;            // Texture units
renderer.capabilities.isWebGL2;               // WebGL 2.0 available

// Check WebGL/WebGPU support:
import { WebGL } from 'three/addons/capabilities/WebGL.js';
if (!WebGL.isWebGL2Available()) document.body.appendChild(WebGL.getErrorMessage());
```

path: references/lights.md
content: # Lights & Environments

## Light Types

| Light | Constructor | Shadow | Notes |
|---|---|---|---|
| AmbientLight | `(color, intensity)` | No | Uniform omnidirectional |
| HemisphereLight | `(skyColor, groundColor, intensity)` | No | Sky/ground gradient |
| DirectionalLight | `(color, intensity)` | Yes (ortho) | Parallel rays (sun) |
| PointLight | `(color, intensity, distance, decay)` | Yes (cubemap, 6x render!) | Omnidirectional from point |
| SpotLight | `(color, intensity, distance, angle, penumbra, decay)` | Yes (persp) | Cone with falloff |
| RectAreaLight | `(color, intensity, width, height)` | No | Realistic soft, needs `RectAreaLightUniformsLib.init()` |
| IESSpotLight | `(color, intensity, distance, angle, penumbra, decay)` | No | IES profile via `iesMap` texture |
| ProjectorLight | `(color, intensity, distance)` | No | Projects texture like video projector |

All lights extend `Object3D` — use `position`/`rotation`, `lookAt` (except RectAreaLight uses rotation).

## DirectionalLight (most common for shadows)
```js
const light = new THREE.DirectionalLight(0xffffff, 3); // color, intensity
light.position.set(2, 5, 3);
light.target.position.set(0, 0, 0); scene.add(light.target);
scene.add(light);
scene.add(new THREE.DirectionalLightHelper(light, 0.5));
```

## SpotLight
```js
const light = new THREE.SpotLight(0xffffff, 50, 0, Math.PI/6, 0.3, 2);
// intensity, distance (0=infinite), angle, penumbra (0–1), decay
light.target.position.set(0, 0, 0); scene.add(light.target);
```

## PointLight
```js
new THREE.PointLight(color, intensity, distance, decay);
// Shadows: 6 renders per light (cubemap) — very expensive; use sparingly
```

## RectAreaLight
```js
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
RectAreaLightUniformsLib.init();  // Required before use
const light = new THREE.RectAreaLight(color, 5, 12, 4); // w, h
light.rotation.x = -Math.PI/2;  // Use rotation, not target
```

## Lighting Patterns

**Three-point:**
```js
const key = new THREE.DirectionalLight(0xffffff, 3); key.position.set(2,5,3);
const fill = new THREE.DirectionalLight(0x8888ff, 1); fill.position.set(-2,3,-1);
const rim = new THREE.DirectionalLight(0xff8888, 1.5); rim.position.set(0,2,-3);
scene.add(new THREE.AmbientLight(0x404040, 0.5));
```

## Environment Maps (PBR)
```js
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileCubemapShader(); // Pre-compile

// From equirectangular HDR
const tex = new RGBELoader().load('env.hdr', t => {
  scene.environment = pmrem.fromEquirectangular(t).texture;
  scene.background = t; // Optional
  t.dispose();
});

// From cubemap
const cubeTex = new THREE.CubeTextureLoader().load(['px.jpg',...]);
scene.environment = pmrem.fromCubemap(cubeTex).texture;

// Bake current scene as environment (for reflections)
scene.environment = pmrem.fromScene(scene, 0.04).texture;
```

## Backgrounds
```js
// Solid color
scene.background = new THREE.Color(0x222222);

// Cubemap skybox
scene.background = new THREE.CubeTextureLoader().load(['px.jpg','nx.jpg','py.jpg','ny.jpg','pz.jpg','nz.jpg']);

// Equirectangular
loader.load('env.jpg', t => { t.mapping = THREE.EquirectangularReflectionMapping; scene.background = t; });

// Aspect-correct texture background (needs update in loop):
const canvasAsp = canvas.clientWidth / canvas.clientHeight;
const imgAsp = t.image?.width / t.image?.height || 1;
const asp = imgAsp / canvasAsp;
t.repeat.set(asp>1 ? 1/asp : 1, asp>1 ? 1 : asp);
t.offset.set(asp>1 ? (1-1/asp)/2 : 0, asp>1 ? 0 : (1-asp)/2);
```

## Light Helpers
```js
scene.add(new THREE.DirectionalLightHelper(light, 0.5));
scene.add(new THREE.PointLightHelper(light, 0.5));
scene.add(new THREE.SpotLightHelper(light));
scene.add(new THREE.HemisphereLightHelper(light, 0.5));
// For shadow camera frustum: scene.add(new THREE.CameraHelper(light.shadow.camera));
```

path: references/loaders.md
content: # Loaders

## LoadingManager (track all loads)
```js
const manager = new THREE.LoadingManager();
manager.onLoad = () => console.log('All done');
manager.onProgress = (url, loaded, total) => { /* progress bar */ };
manager.onError = (url) => console.error('Failed:', url);

const loader = new THREE.TextureLoader(manager); // Pass to any loader
```

## Texture Loaders
```js
// Basic
const tex = new THREE.TextureLoader().load('tex.jpg');
tex.colorSpace = THREE.SRGBColorSpace;          // Color textures
tex.colorSpace = THREE.LinearSRGBColorSpace;    // Normal/roughness/metal/AO maps

// Cubemap
new THREE.CubeTextureLoader().setPath('skybox/')
  .load(['px.jpg','nx.jpg','py.jpg','ny.jpg','pz.jpg','nz.jpg']);

// HDR/EXR environment maps
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
new RGBELoader().setDataType(THREE.HalfFloatType).load('env.hdr', tex => {
  tex.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = tex;
});
```

## GLTFLoader (primary model format)

```js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';

const loader = new GLTFLoader();

// Draco (mesh compression)
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
loader.setDRACOLoader(dracoLoader);

// KTX2 (texture compression)
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('https://unpkg.com/three@0.170.0/examples/jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);
loader.setKTX2Loader(ktx2Loader);

loader.load('model.glb', gltf => {
  scene.add(gltf.scene);
  // gltf.animations, gltf.cameras, gltf.asset
  // Play all: gltf.animations.forEach(c => new THREE.AnimationMixer(gltf.scene).clipAction(c).play());
});
```

### glTF Gotchas
- `.glb` = binary (single file, compact), `.gltf` = JSON + separate assets
- **Artists should clear transforms** before export — scaling/rotating nodes breaks runtime positioning
- Remove unnecessary wrapper nodes (orientation_matrix, model_correction_matrix) from export
- For animated models with broken origins: reparent into fresh Object3D at world position
```js
root.updateMatrixWorld();
for (const child of loadedGroup.children.slice()) {
  const wrapper = new THREE.Object3D();
  child.getWorldPosition(wrapper.position);
  child.position.set(0, 0, 0);
  wrapper.add(child);
  scene.add(wrapper);
}
```

## OBJ Loader

```js
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';

new MTLLoader().load('model.mtl', mtl => {
  mtl.preload();
  Object.values(mtl.materials).forEach(m => m.side = THREE.DoubleSide); // Common fix
  const objLoader = new OBJLoader().setMaterials(mtl);
  objLoader.load('model.obj', root => scene.add(root));
});
```

### OBJ/MTL Common Fixes
- `map_Bump` → `norm` in .mtl for normal maps
- Convert TGA textures to JPG/PNG (TGA unsupported, very large)
- Auto-frame after load: `const box = new THREE.Box3().setFromObject(root);` then adjust camera

## Other Model Loaders
```js
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';  // Point clouds
```

## SVG / Font / Audio
```js
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TTFLoader } from 'three/addons/loaders/TTFLoader.js';
new THREE.AudioLoader().load('sound.mp3', buffer => { audio.setBuffer(buffer); audio.play(); });
```

## Serialization
```js
new THREE.BufferGeometryLoader().load('geo.json', geo => {});
new THREE.ObjectLoader().load('obj.json', obj => {});
new THREE.MaterialLoader().load('mat.json', mat => {});
```

## Cache
```js
THREE.Cache.enabled = true;  // Default on
THREE.Cache.clear();         // Clear all
THREE.Cache.remove(key);     // Remove specific
```

path: references/materials.md
content: # Materials

## Quick Selection Guide

| Need | Use | Notes |
|---|---|---|
| Unlit / wireframe / background | `MeshBasicMaterial` | No lights, fastest |
| Diffuse only | `MeshLambertMaterial` | Per-vertex, fast |
| Specular highlights | `MeshPhongMaterial` | `shininess` (0–150) |
| Toon/cel shading | `MeshToonMaterial` | Needs `gradientMap` (3-pixel texture) |
| PBR (standard) | `MeshStandardMaterial` | `roughness` + `metalness` (0–1) |
| PBR + clearcoat/glass/aniso | `MeshPhysicalMaterial` | Adds `clearcoat`, `transmission`, `sheen`, `iridescence`, `anisotropy` |
| MatCap (view-dependent) | `MeshMatcapMaterial` | No lights needed, uses `matcap` texture |
| Normals debug | `MeshNormalMaterial` | RGB = XYZ normals |
| Depth visualization | `MeshDepthMaterial` | Near=black, far=white |
| Shadow receiver only | `ShadowMaterial` | `opacity` controls shadow darkness |
| Custom GLSL | `ShaderMaterial` | Full control, must declare uniforms |
| Custom GLSL (no builtins) | `RawShaderMaterial` | Must declare ALL uniforms |
| Particles | `PointsMaterial` | `size`, `sizeAttenuation`, `map` (sprite sheet) |
| Lines | `LineBasicMaterial` | `linewidth` often ignored (WebGL limit) |
| Dashed lines | `LineDashedMaterial` | Requires `geometry.computeLineDistances()` |
| Sprites (billboards) | `SpriteMaterial` | Always faces camera |

**Speed ranking:** Basic → Lambert → Phong → Standard → Physical

## Key Material Properties

```js
material.side = THREE.FrontSide | THREE.BackSide | THREE.DoubleSide;
material.transparent = true;     // Enable opacity/alpha
material.opacity = 0.5;
material.alphaTest = 0.5;       // Discard fragments below threshold (no depth issues)
material.blending = THREE.NormalBlending | THREE.AdditiveBlending | THREE.MultiplyBlending;
material.depthWrite = true;      // false for transparent to avoid z-fighting
material.depthTest = true;
material.wireframe = false;
material.flatShading = false;
material.fog = true;             // false for interior objects
material.clippingPlanes = [plane1, plane2];
material.clipIntersection = false;

// Textures on Standard/Phong:
material.map;                     // Diffuse/albedo
material.normalMap;               // RGB normal
material.roughnessMap;            // Grayscale (Standard only)
material.metalnessMap;            // Grayscale (Standard only)
material.aoMap;                   // Ambient occlusion
material.emissiveMap;
material.bumpMap;                 // Grayscale
material.displacementMap;
material.envMap;                  // Environment reflection
material.alphaMap;

// Physical material extras:
material.clearcoat;               // 0–1
material.clearcoatRoughness;      // 0–1
material.transmission;            // 0–1 (glass)
material.thickness;               // Volume thickness
material.ior;                     // Index of refraction (default 1.5)
material.sheen;                   // 0–1 (fabric)
material.iridescence;             // 0–1 (thin film)
material.anisotropy;              // 0–1 (brushed metal)
```

## Texture Color Space (critical!)

```js
// Color textures → sRGB
texture.colorSpace = THREE.SRGBColorSpace;

// Data textures (normal, roughness, metalness, AO, displacement) → Linear
texture.colorSpace = THREE.LinearSRGBColorSpace;
```

## Transparency Gotchas

**Problem:** Transparent objects at object level are sorted back→front, but individual triangles within an object are NOT sorted → back faces disappear.

**Fix 1 — Double render (convex objects):**
```js
[THREE.BackSide, THREE.FrontSide].forEach(side => {
  scene.add(new THREE.Mesh(geo, new THREE.MeshPhongMaterial({opacity:0.5, transparent:true, side})));
});
```

**Fix 2 — alphaTest (textured objects):**
```js
material.alphaTest = 0.5;  // Discard below threshold → no depth sorting needed
// Best for: leaves, grass, decals, text
```

**Fix 3 — Split intersecting planes** into non-overlapping halves with shared parent Object3D.

## Multi-Material (per-group)
```js
const mesh = new THREE.Mesh(geometry, [mat0, mat1, mat2]);
// geometry.groups: [{start, count, materialIndex}, ...]
// BoxGeometry has 6 groups (one per face), Cylinder has 3, Cone has 2
```

## ShaderMaterial Pattern
```js
new THREE.ShaderMaterial({
  uniforms: { time: {value:0}, uColor: {value: new THREE.Color()} },
  vertexShader: `varying vec2 vUv; void main() { vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader: `uniform float time; uniform vec3 uColor; varying vec2 vUv; void main(){gl_FragColor=vec4(uColor,1.0);}`,
});
// builtin uniforms: projectionMatrix, modelViewMatrix, normalMatrix, cameraPosition, viewMatrix
// builtin attributes: position, normal, uv
```

path: references/math.md
content: # Math Utilities

Three.js math classes are standard. Key notes:

## Common Patterns

```js
// position/rotation/scale
obj.position.set(x, y, z);
obj.rotation.set(x, y, z);         // Euler, radians
obj.quaternion.set(x, y, z, w);    // Avoids gimbal lock
obj.scale.set(x, y, z);
obj.lookAt(target);                // Points -Z at target, +Y as up
```

## Matrix4 Composition
```js
// Compose/decompose transforms
const m = new THREE.Matrix4();
m.compose(position, quaternion, scale);
m.decompose(position, quaternion, scale);
m.makePerspective(left, right, top, bottom, near, far); // for shadow cameras
m.makeRotationFromQuaternion(q);  // Quaternion → Matrix
```

## Quaternion
```js
q.setFromAxisAngle(axis, angle);   // Axis-angle rotation
q.slerp(qb, t);                    // Spherical interpolation (no gimbal lock)
q.setFromUnitVectors(from, to);    // Rotation between two directions
q.rotateTowards(target, step);     // Step-limited rotation
```

## Color
```js
new THREE.Color(0xff0000);        // Hex
new THREE.Color('#ff0000');       // CSS
new THREE.Color('rgb(255,0,0)');  // CSS
new THREE.Color(1, 0, 0);         // RGB 0-1
color.setHSL(h, s, l);            // h=0..1, s=0..1, l=0..1

// Color space conversion (for proper PBR rendering)
color.convertSRGBToLinear();
color.convertLinearToSRGB();
```

## Raycaster

```js
const raycaster = new THREE.Raycaster();

// Mouse → ray:
const mouse = new THREE.Vector2();
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; // flip Y
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(scene.children, true);

// Each hit: { distance, point, face, faceIndex, object, uv, normal, instanceId }

// Intersection with specific object types:
raycaster.params.Points.threshold = 1;  // point size tolerance
raycaster.params.Line.threshold = 1;    // line width tolerance
```

## Box3 / Sphere (Bounding Volumes)

```js
// Auto-compute bounding box of hierarchy
const box = new THREE.Box3().setFromObject(root);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());

// Frustum culling check
const frustum = new THREE.Frustum();
frustum.setFromProjectionMatrix(
  new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
);
frustum.intersectsObject(obj);     // true if visible
frustum.containsPoint(point);
```

## MathUtils

```js
THREE.MathUtils.clamp(v, min, max);
THREE.MathUtils.lerp(a, b, t);
THREE.MathUtils.mapLinear(x, a1, a2, b1, b2);
THREE.MathUtils.smoothstep(x, min, max);
THREE.MathUtils.degToRad(d);     // degrees → radians
THREE.MathUtils.radToDeg(r);     // radians → degrees
THREE.MathUtils.damp(x, y, lambda, dt);  // Smooth damping
THREE.MathUtils.randFloat(low, high);
THREE.MathUtils.randFloatSpread(range);
THREE.MathUtils.generateUUID();
THREE.MathUtils.DEG2RAD;         // Math.PI / 180
THREE.MathUtils.RAD2DEG;         // 180 / Math.PI
```

## Gotchas

- **all angles are radians** except PerspectiveCamera.fov (degrees)
- **Euler order** defaults to `'XYZ'` — change with `new Euler(x,y,z,'YXZ')` for FPS-style
- **Color.setRGB/setHSL** takes 0–1, not 0–255
- **Vector3.project(camera)** returns NDC: x,y in [-1,1], z in [-1,1] (not [0,1]!)
- **lookAt** points -Z axis at target with +Y up — adjust with `camera.up.set()` before
- **SplineCurve** is 2D — feeding into Vector3.set gives NaN on z → use CatmullRomCurve3 for 3D

path: references/nodes.md
content: # Node Materials & TSL (Three.js Shading Language)

TSL = composable JS-level shader nodes. Compiles to both GLSL (WebGL) and WGSL (WebGPU).

## Core Imports
```js
import {
  // Values
  float, int, uint, bool, vec2, vec3, vec4, ivec2, ivec3, ivec4, mat3, mat4, color,

  // Inputs
  uniform, attribute, varying, output,
  positionLocal, positionWorld, positionView,
  normalLocal, normalWorld, normalView,
  uv, vertexIndex, instanceIndex, frontFacing, faceDirection,

  // Math ops (chainable)
  add, sub, mul, div, rem, pow, sqrt, abs, sign, floor, ceil, round,
  fract, mod, min, max, clamp, saturate, negate,
  sin, cos, tan, asin, acos, atan, atan2,
  exp, exp2, log, log2, radians, degrees,
  cross, dot, normalize, length, distance, reflect, refract,
  transpose, determinant, inverse, mix, step, smoothstep,
  equal, notEqual, lessThan, lessThanEqual, greaterThan, greaterThanEqual,
  and, or, not, xor, select,

  // Textures
  texture, textureLoad, textureStore, textureSample, cubeTexture,

  // Environment
  time, deltaTime, frameId, resolution,
  cameraNear, cameraFar, cameraPosition,
  cameraViewMatrix, cameraProjectionMatrix,
  modelWorldMatrix, modelNormalMatrix, modelViewMatrix,

  // Functions
  Fn, tslFn, func, wgslFn, glslFn, glsl,
  If, Else, ElseIf, loop, Break, Continue, Discard, Return,

  // Materials
  MeshStandardNodeMaterial, MeshPhysicalNodeMaterial,
  MeshPhongNodeMaterial, MeshLambertNodeMaterial, MeshToonNodeMaterial,
  MeshBasicNodeMaterial, MeshNormalNodeMaterial, MeshMatcapNodeMaterial,
  PointsNodeMaterial, SpriteNodeMaterial,
  LineBasicNodeMaterial, LineDashedNodeMaterial,
  ShadowNodeMaterial, VolumeNodeMaterial, MeshSSSNodeMaterial,
  NodeMaterial,

  // Compute
  compute, workgroupSize,
} from 'three/tsl';
```

## Basic Usage

```js
// Set nodes on material
const mat = new MeshStandardNodeMaterial();
mat.colorNode = color(0xff0000);           // Constant color
mat.roughnessNode = float(0.2);
mat.metalnessNode = float(0.8);

// Uniforms (update at runtime)
const myColor = uniform(new THREE.Color(0x00ff00));
mat.colorNode = myColor;
myColor.value.set(0xff0000); // Update later

// Texture
mat.colorNode = texture(loadedTexture);

// Animated color
mat.colorNode = vec3(sin(time.mul(2)), sin(time.mul(3).add(1)), sin(time.mul(4).add(2))).mul(0.5).add(0.5);

// Mix with position
mat.colorNode = mix(color(0xff0000), color(0x0000ff), positionLocal.y.add(1).mul(0.5));
```

## Custom TSL Functions

```js
const myFn = Fn(({ normal, lightDir, lightColor }) => {
  const NdotL = dot(normalize(normal), normalize(lightDir)).max(0);
  return vec4(lightColor.mul(NdotL), 1.0);
});
// Call: myFn({ normal: ..., lightDir: ..., lightColor: ... })
```

## Vertex Displacement
```js
mat.positionNode = positionLocal.add(
  vec3(0, sin(time.mul(3).add(positionLocal.x.mul(5))).mul(0.2), 0)
);
```

## Custom NodeMaterial (full shader)
```js
const mat = new NodeMaterial();
mat.vertexNode = ...;    // Custom vertex
mat.fragmentNode = ...;  // Custom fragment
mat.lights = true;       // Include lighting
mat.normals = true;      // Include normals
```

## Compute Shaders
```js
const computeShader = Fn(() => {
  // ... GPU computation ...
})();
const computeNode = compute(computeShader, workgroupSize(64));
renderer.compute(computeNode);
```

## TSL Tips
- TSL compiles to both GLSL and WGSL = works with WebGLRenderer AND WebGPURenderer
- Use `uniform()` for runtime-changeable values
- Classic materials (MeshStandardMaterial) can't use TSL — use `MeshStandardNodeMaterial` instead
- Node materials can be slower to initialize (compile time) but offer richer customization

path: references/objects.md
content: # Objects & Scene Graph

## Object3D (base class for everything)

```js
// Transform
obj.position.set(x, y, z);
obj.rotation.set(x, y, z);          // Euler, radians
obj.quaternion.set(x, y, z, w);     // No gimbal lock
obj.scale.set(x, y, z);

// Hierarchy
parent.add(child);                   // Add child (local space)
parent.remove(child);                // Remove
parent.attach(child);                // Reparent preserving world transform
obj.parent;                          // Access parent
obj.children;                        // Array

// Space conversion
obj.localToWorld(v);                 // Local → world
obj.worldToLocal(v);                 // World → local
obj.getWorldPosition(target);        // World position
obj.getWorldQuaternion(target);      // World rotation
obj.getWorldDirection(target);       // -Z direction in world

// Transform helpers
obj.lookAt(x,y,z | vector | object); // -Z points at target, +Y up
obj.translateX(d); obj.translateY(d); obj.translateZ(d);
obj.translateOnAxis(axis, d);
obj.rotateX(r); obj.rotateY(r); obj.rotateZ(r);

// Visibility
obj.visible = true;
obj.castShadow = false;
obj.receiveShadow = false;
obj.frustumCulled = true;            // Skip if outside camera frustum
obj.renderOrder = 0;                 // Sort order (higher = later)
obj.layers.set(0);                   // Camera must share a layer to see

// Traversal
obj.traverse(cb);                    // Visit all descendants
obj.traverseVisible(cb);             // Skip invisible
obj.traverseAncestors(cb);           // Upward
scene.getObjectByName('name');       // First match, recursive
scene.getObjectById(id);             // By numeric ID

// Manual matrix update
obj.updateMatrix();
obj.updateMatrixWorld();
obj.matrixAutoUpdate = true;         // Default; set false for static objects
obj.applyMatrix4(matrix4);           // Apply transform to object

// Lifecycle callbacks
obj.onBeforeRender = (renderer, scene, camera, geometry, material, group) => {};
obj.onAfterRender = (renderer, scene, camera, geometry, material, group) => {};
```

## Mesh
```js
new THREE.Mesh(geometry, material);   // Single material
new THREE.Mesh(geometry, [m0, m1]);   // Per-group materials
mesh.geometry; mesh.material;
```

## Group
```js
const group = new THREE.Group();
group.add(obj1, obj2, obj3);
// Group has no geometry/material — just a transform node
```

## Line / Points / Sprite
```js
new THREE.Line(geo, mat);                     // Line
new THREE.LineLoop(geo, mat);                 // Closed loop
new THREE.LineSegments(geo, mat);             // Paired vertices

new THREE.Points(geo, new THREE.PointsMaterial({size:1}));  // Particles
new THREE.Sprite(new THREE.SpriteMaterial({map:tex}));       // Always faces camera
sprite.center.set(0.5, 0.5);                                 // Anchor (default bottom-left)
```

## InstancedMesh (many copies, 1 draw call)
```js
const mesh = new THREE.InstancedMesh(geo, mat, count);
mesh.setMatrixAt(i, matrix4);
mesh.setColorAt(i, color);
mesh.instanceMatrix.needsUpdate = true;
mesh.instanceColor.needsUpdate = true;  // if using colors
```

## BatchedMesh (many different geometries, 1 draw call)
```js
const batch = new THREE.BatchedMesh(maxGeos, maxVerts, maxIndices, mat);
const geoId = batch.addGeometry(geo);
const instId = batch.addInstance(geoId);
batch.setMatrixAt(instId, matrix);
batch.deleteInstance(instId); // Can delete and reuse slots
```

## SkinnedMesh (bone animation)
```js
const skinnedMesh = new THREE.SkinnedMesh(geo, mat);
skinnedMesh.bind(skeleton);
skinnedMesh.skeleton.bones;          // Bone array
skinnedMesh.skeleton.pose();         // Reset to rest pose
```

## LOD (Level of Detail)
```js
const lod = new THREE.LOD();
lod.addLevel(highMesh, 0);            // Show when distance < 10
lod.addLevel(medMesh, 10);            // Show when distance < 50
lod.addLevel(lowMesh, 50);            // Show when distance >= 50
lod.autoUpdate = true;
```

## Scene Graph Patterns

### Reparent preserving world transform
```js
parent2.attach(child);  // One-liner
// Equivalent:
const wp = new THREE.Vector3(), wq = new THREE.Quaternion(), ws = new THREE.Vector3();
child.getWorldPosition(wp); child.getWorldQuaternion(wq); child.getWorldScale(ws);
child.removeFromParent();
parent2.add(child);
child.position.copy(wp); child.quaternion.copy(wq); child.scale.copy(ws);
```

### Debug: force all objects to one material
```js
scene.overrideMaterial = new THREE.MeshNormalMaterial();
// Remove: scene.overrideMaterial = null;
```

### EventDispatcher (base class)
```js
obj.addEventListener('change', cb);
obj.hasEventListener('change', cb);
obj.removeEventListener('change', cb);
obj.dispatchEvent({type: 'change'});
```

path: references/renderers.md
content: # Renderers, Post-Processing & Shadows

## WebGLRenderer Setup

```js
const renderer = new THREE.WebGLRenderer({
  antialias: true,           // MSAA
  alpha: false,              // Transparent canvas background
  preserveDrawingBuffer: false, // true for toDataURL/screenshots
  powerPreference: 'high-performance',
});
renderer.setSize(w, h);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap for perf
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;   // Recommended
renderer.toneMappingExposure = 1.0;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
```

**Tone Mapping options:** `NoToneMapping` | `LinearToneMapping` | `ReinhardToneMapping` | `CineonToneMapping` | `ACESFilmicToneMapping` | `AgXToneMapping` | `NeutralToneMapping`

## WebGPURenderer

```js
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init();
renderer.setSize(w, h);
```
Note: WebGPURenderer requires async init. Some classic materials need TSL node variants.

## Performance Tracking
```js
renderer.info.render.calls;       // Draw calls
renderer.info.render.triangles;   // Triangle count
renderer.info.memory.geometries;  // Geometry count in memory
renderer.info.memory.textures;    // Texture count in memory
renderer.info.reset();            // Reset per-frame
```

## Render Targets
```js
const rt = new THREE.WebGLRenderTarget(w, h, {
  minFilter: THREE.LinearFilter,
  magFilter: THREE.LinearFilter,
  format: THREE.RGBAFormat,
  depthBuffer: true,
  samples: 0,              // MSAA (0=off)
});

// Render to texture:
renderer.setRenderTarget(rt);
renderer.render(scene, camera);
renderer.setRenderTarget(null);  // Back to screen

// Use result:
material.map = rt.texture;
rt.dispose();

// Special types:
new THREE.WebGLCubeRenderTarget(256);         // Cubemap
new THREE.WebGL3DRenderTarget(w, h, depth);   // 3D
```

## Post-Processing (EffectComposer)

**Architecture:** Creates two render targets (rtA/rtB), ping-pongs between passes:

```js
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// ... add effect passes here ...
composer.addPass(new OutputPass()); // Must be last: color space + tone mapping

// In loop:
composer.setSize(canvas.width, canvas.height);
composer.render(deltaTime);
// Pass options: pass.enabled, pass.needsSwap, pass.clear, pass.renderToScreen
```

**Custom ShaderPass:**
```js
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
const myPass = new ShaderPass({
  uniforms: { tDiffuse: {value:null}, uColor: {value: new THREE.Color()} },
  vertexShader: `varying vec2 vUv; void main(){vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1);}`,
  fragmentShader: `varying vec2 vUv; uniform sampler2D tDiffuse; uniform vec3 uColor; void main(){vec4 p=texture2D(tDiffuse,vUv); gl_FragColor=vec4(p.rgb*uColor,p.a);}`,
});
```

**Available passes:** `UnrealBloomPass`, `GlitchPass`, `OutlinePass`, `SAOPass`, `SSAOPass`, `AfterimagePass`, `BokehPass`, `FilmPass`, `LutPass`, `MaskPass`/`ClearMaskPass`, `TexturePass`, `HalftonePass`

**Available built-in shaders:** `RGBShiftShader`, `FXAAShader`, `VignetteShader`, `ColorCorrectionShader`, `SepiaShader`, `DotScreenShader`

## Shadows

### Enable
```js
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Basic | PCF | PCFSoft | VSM

light.castShadow = true;
mesh.castShadow = true;
mesh.receiveShadow = true;
```

### DirectionalLight Shadow (orthographic shadow camera)
```js
light.shadow.mapSize.set(1024, 1024);
light.shadow.camera.near = 0.5;  light.shadow.camera.far = 50;
light.shadow.camera.left = -10;  light.shadow.camera.right = 10;
light.shadow.camera.top = 10;    light.shadow.camera.bottom = -10;
light.shadow.bias = -0.001;       // Reduce acne, increase peter-panning
light.shadow.normalBias = 0.02;
light.shadow.radius = 3;          // PCF soft blur (PCFSoftShadowMap only)
light.shadow.blurSamples = 8;     // VSM samples
```

### SpotLight Shadow (perspective shadow camera)
```js
// fov = light.angle (automatically), aspect = mapSize ratio
light.shadow.mapSize.set(512, 512);
light.shadow.camera.near = 0.5;  light.shadow.camera.far = 50;
light.shadow.bias = -0.0001;
```

### PointLight Shadow (6 renders — expensive!)
```js
// Same properties as SpotLight; internally renders 6 faces (cubemap)
light.shadow.mapSize.set(512, 512);
```

### Debug Shadow Camera Frustum
```js
const helper = new THREE.CameraHelper(light.shadow.camera);
scene.add(helper);
```

### Key Shadow Rules
- **Shadow map size** bigger = sharper but more memory. Keep as small as possible.
- **Shadow camera area** smaller = higher shadow resolution. Tight-fit to visible area.
- **PointLight shadows** are 6x more expensive (one render per face).
- **Only 1 directional light** should cast shadows in most scenes.
- For missing shadows: check `castShadow` on BOTH light and mesh, `receiveShadow` on receiver, and that shadow camera covers the area.

## Fake Shadows (cheap alternative)
```js
// Plane with circular shadow texture, parented to object, positioned just above ground
const shadowTex = loader.load('roundshadow.png');
const shadowMesh = new THREE.Mesh(
  new THREE.PlaneGeometry(1, 1),
  new THREE.MeshBasicMaterial({ map: shadowTex, transparent: true, depthWrite: false })
);
shadowMesh.rotation.x = -Math.PI / 2;
shadowMesh.position.y = 0.001;
shadowMesh.material.opacity = 1; // Fade as object rises
```

## GPU-Based Picking (alternative to Raycaster)
Render each pickable object with unique color ID to 1×1 render target:

```js
const pickRT = new THREE.WebGLRenderTarget(1, 1);
const pixel = new Uint8Array(4);

// Per-object: emissive = ID as hex, color=black, blending=NoBlending
// Render only pixel under cursor with camera.setViewOffset(...)
renderer.setRenderTarget(pickRT);
renderer.render(pickingScene, camera);
renderer.setRenderTarget(null);
renderer.readRenderTargetPixels(pickRT, 0, 0, 1, 1, pixel);
const id = (pixel[0]<<16)|(pixel[1]<<8)|pixel[2];
// Lookup: idToObject[id]
```
Advantage: handles shader deformations and alphaTest transparency.

path: references/textures.md
content: # Textures

## Core Properties
```js
const t = new THREE.Texture(image);
t.wrapS = THREE.ClampToEdgeWrapping | THREE.RepeatWrapping | THREE.MirroredRepeatWrapping;
t.wrapT = /* same */;
t.repeat.set(u, v);              // Tile repeat
t.offset.set(u, v);              // UV offset
t.rotation = Math.PI/4;          // Radians
t.center.set(0.5, 0.5);         // Rotation pivot

t.magFilter = THREE.LinearFilter | THREE.NearestFilter;
t.minFilter = THREE.LinearMipmapLinearFilter | THREE.NearestFilter | ...;
t.generateMipmaps = true;        // false for NPOT textures
t.anisotropy = 4;               // >1 needs EXT_texture_filter_anisotropic

t.format = THREE.RGBAFormat | THREE.RGBFormat | THREE.RedFormat | ...;
t.type = THREE.UnsignedByteType | THREE.HalfFloatType | THREE.FloatType | ...;

// CRITICAL: color space
t.colorSpace = THREE.SRGBColorSpace;         // Color textures (maps)
t.colorSpace = THREE.LinearSRGBColorSpace;   // Data (normal, roughness, metalness, AO, displacement)
t.flipY = true;                              // Default; false for render-to-texture
t.needsUpdate = true;                        // After modifying image data
```

## Texture Sources

```js
// Image
const t = new THREE.TextureLoader().load('img.jpg');

// Canvas
const t = new THREE.CanvasTexture(canvas);
t.needsUpdate = true;  // After canvas drawing

// Video
const t = new THREE.VideoTexture(videoElement);
t.minFilter = THREE.LinearFilter; t.magFilter = THREE.LinearFilter;

// Data (raw typed array)
const t = new THREE.DataTexture(data, w, h, format, type);
t.needsUpdate = true;

// Render target result
const t = renderTarget.texture;
```

## Cubemap / Environment
```js
// 6 separate images
new THREE.CubeTextureLoader().load(['px.jpg','nx.jpg','py.jpg','ny.jpg','pz.jpg','nz.jpg']);

// Equirectangular (360° photo)
const t = loader.load('equirect.jpg');
t.mapping = THREE.EquirectangularReflectionMapping; // or RefractionMapping
t.colorSpace = THREE.SRGBColorSpace;
```

## Compressed Textures (KTX2)
```js
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const loader = new KTX2Loader();
loader.setTranscoderPath('https://unpkg.com/three@0.170.0/examples/jsm/libs/basis/');
loader.detectSupport(renderer);
loader.load('texture.ktx2', t => { material.map = t; material.needsUpdate = true; });
```

## Texture Gotchas

- **Power-of-two (POT) dimensions** (256, 512, 1024) required for mipmaps. NPOT textures: set `generateMipmaps=false`, use `LinearFilter`
- **Memory:** `width × height × 4 × 1.33 = bytes`. A 3024×3761 JPEG = 60MB in VRAM despite 157KB download
- **Normal maps** must use `LinearSRGBColorSpace` (not sRGB)
- **White 1×1 pixel fallback:** `new THREE.DataTexture(new Uint8Array([255,255,255,255]), 1, 1)` for materials switching from no-texture to textured
- **CanvasTexture** for labels: set `minFilter=LinearFilter, wrapS=ClampToEdgeWrapping` since canvas is usually NPOT
- **VideoTexture**: set `colorSpace=SRGBColorSpace`, use `LinearFilter` to avoid mipmap issues

## Memory Management
```js
texture.dispose();          // Free GPU memory
// Track: renderer.info.memory.textures
```

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)