Installation

Install via CLI

npx @vector-labs/skills add threejs

Or target a specific tool

npx @vector-labs/skills add threejs --tool cursor
View on GitHub

Skill Files (11)

SKILL.md 4.0 KB
---
name: threejs
description: >-
  Guia completo de desenvolvimento Three.js com padroes obrigatorios:
  disposal de recursos, cap de pixel ratio em 2x, color spaces por tipo de
  textura e template scaffold aprovado. Cobre fundamentals, geometria,
  materiais, iluminacao, shaders, interacao e pos-processamento.
license: Apache-2.0
compatibility: claude-code
allowed-tools: Read Write Edit Glob Bash
metadata:
  author: vector-labs
  version: "1.0"
tags: [3d, webgl, graphics]
complexity: advanced
---

# Three.js Best Practices

## Quick Start

```typescript
import * as THREE from "three";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);

// Mesh
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// Light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.3));

camera.position.z = 5;

// Animation loop
const clock = new THREE.Clock();
function animate() {
  const delta = clock.getDelta();
  cube.rotation.x += delta;
  cube.rotation.y += delta;
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);

// Resize
window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
```

## Topic References

Read the relevant reference file based on the task at hand:

- [references/fundamentals.md](references/fundamentals.md) - Scene, camera, renderer, Object3D, math utilities, coordinate system, cleanup/disposal patterns
- [references/geometry.md](references/geometry.md) - Built-in shapes, custom BufferGeometry, InstancedMesh, points, lines, edges, geometry utilities
- [references/materials.md](references/materials.md) - All material types, PBR workflow (Standard/Physical), environment maps, material properties, multiple materials
- [references/lighting-and-shadows.md](references/lighting-and-shadows.md) - Light types (Ambient, Hemisphere, Directional, Point, Spot, RectArea), shadow setup, IBL/HDR, lighting setups
- [references/textures.md](references/textures.md) - Texture loading/config, color spaces, HDR, render targets, UV mapping, texture atlas, memory management
- [references/animation.md](references/animation.md) - AnimationMixer/Clip/Action, skeletal animation, morph targets, animation blending, procedural animation
- [references/shaders.md](references/shaders.md) - ShaderMaterial, GLSL uniforms/varyings, common shader patterns, extending built-in materials, instanced shaders
- [references/interaction.md](references/interaction.md) - Raycasting, camera controls (Orbit/Fly/PointerLock), TransformControls, DragControls, selection, coordinate conversion
- [references/postprocessing.md](references/postprocessing.md) - EffectComposer, bloom, DOF, SSAO, FXAA/SMAA, custom ShaderPass, selective bloom, multi-pass rendering
- [references/loaders.md](references/loaders.md) - GLTF/Draco loading, OBJ/FBX/STL formats, LoadingManager, async patterns, caching, error handling

## Essential Patterns

**Always dispose resources when done:**
```typescript
geometry.dispose();
material.dispose();
texture.dispose();
renderer.dispose();
```

**Frame-rate-independent animation:** Always use `clock.getDelta()` or `clock.getElapsedTime()`.

**Pixel ratio:** Always `renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))` to cap at 2x.

**Color spaces:** Set `texture.colorSpace = THREE.SRGBColorSpace` for color/albedo maps. Leave data maps (normal, roughness, metalness) as default.
references/
animation.md 11.8 KB
# Three.js Animation

## Animation System Overview

Three.js uses a three-part system for animations:
- **AnimationClip**: Contains keyframe data (tracks)
- **AnimationMixer**: Manages animation playback for an object
- **AnimationAction**: Controls individual clip playback (play, pause, blend, etc.)

```javascript
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(animationClip);
action.play();

// In animation loop
mixer.update(deltaTime);
```

## AnimationClip

An AnimationClip contains keyframe tracks that animate object properties.

```javascript
// Create clip manually
const positionKF = new THREE.VectorKeyframeTrack(
  '.position',
  [0, 1, 2],           // times
  [0, 0, 0, 10, 0, 0, 0, 5, 0]  // values (x,y,z for each time)
);

const clip = new THREE.AnimationClip('move', 2, [positionKF]);
```

### KeyframeTrack Types

```javascript
// NumberKeyframeTrack - single values
new THREE.NumberKeyframeTrack('.material.opacity', [0, 1], [1, 0]);

// VectorKeyframeTrack - position, scale (x,y,z)
new THREE.VectorKeyframeTrack('.position', [0, 1], [0,0,0, 5,2,0]);

// QuaternionKeyframeTrack - rotation
new THREE.QuaternionKeyframeTrack('.quaternion', [0, 1], [0,0,0,1, 0,0.707,0,0.707]);

// ColorKeyframeTrack - colors
new THREE.ColorKeyframeTrack('.material.color', [0, 1], [1,0,0, 0,0,1]);

// BooleanKeyframeTrack - on/off
new THREE.BooleanKeyframeTrack('.visible', [0, 0.5, 1], [true, false, true]);

// StringKeyframeTrack - discrete values
new THREE.StringKeyframeTrack('.morphTargetInfluences[0]', [0, 1], ['smile', 'frown']);
```

### Property Path Syntax

```javascript
'.position'                    // Root object position
'.scale[0]'                    // X scale component
'.material.opacity'            // Material property
'.bones[2].position'           // Bone position
'.morphTargetInfluences[0]'    // Morph target
```

### Interpolation Modes

```javascript
import { InterpolateLinear, InterpolateSmooth, InterpolateDiscrete } from 'three';

track.setInterpolation(InterpolateLinear);   // Linear (default)
track.setInterpolation(InterpolateSmooth);   // Smooth/cubic
track.setInterpolation(InterpolateDiscrete); // Step/no interpolation
```

## AnimationMixer

Manages all animations for a single object or hierarchy.

```javascript
const mixer = new THREE.AnimationMixer(model);

// Update in animation loop
const clock = new THREE.Clock();
function animate() {
  const delta = clock.getDelta();
  mixer.update(delta);
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
```

### Mixer Events

```javascript
mixer.addEventListener('finished', (e) => {
  console.log('Animation finished:', e.action.getClip().name);
});

mixer.addEventListener('loop', (e) => {
  console.log('Loop completed:', e.action.getClip().name);
});
```

### Mixer Methods

```javascript
mixer.stopAllAction();           // Stop all animations
mixer.update(deltaTime);         // Update animations
mixer.setTime(seconds);          // Set global time
mixer.uncacheClip(clip);         // Remove clip from cache
mixer.uncacheRoot(model);        // Remove all clips for object
```

## AnimationAction

Controls playback of a single AnimationClip.

```javascript
const action = mixer.clipAction(clip);

// Basic playback
action.play();
action.stop();
action.reset();
action.paused = true;
```

### Action Properties

```javascript
action.timeScale = 1.0;      // Speed (2.0 = 2x, 0.5 = half speed, -1 = reverse)
action.weight = 1.0;         // Blend weight (0-1)
action.time = 0;             // Current time
action.enabled = true;       // Enable/disable action
action.clampWhenFinished = true;  // Stay on last frame when finished
action.repetitions = 3;      // Number of times to play (with LoopRepeat)
```

### Loop Modes

```javascript
import { LoopOnce, LoopRepeat, LoopPingPong } from 'three';

action.setLoop(LoopOnce);      // Play once and stop
action.setLoop(LoopRepeat, 5); // Repeat 5 times (Infinity for endless)
action.setLoop(LoopPingPong);  // Forward then backward
```

### Fading and Crossfading

```javascript
// Fade in over 0.5 seconds
action.fadeIn(0.5);

// Fade out over 0.5 seconds
action.fadeOut(0.5);

// Crossfade between actions
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);

idleAction.play();
walkAction.play();
idleAction.crossFadeTo(walkAction, 0.3);  // 0.3 second crossfade
```

## Loading GLTF Animations

```javascript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  const mixer = new THREE.AnimationMixer(model);

  // Play all animations
  gltf.animations.forEach((clip) => {
    mixer.clipAction(clip).play();
  });

  // Play specific animation
  const clip = THREE.AnimationClip.findByName(gltf.animations, 'Walk');
  const action = mixer.clipAction(clip);
  action.play();

  // Store mixer for update loop
  model.userData.mixer = mixer;
});

// In animation loop
scene.traverse((obj) => {
  if (obj.userData.mixer) {
    obj.userData.mixer.update(delta);
  }
});
```

## Skeletal Animation

### Accessing Skeleton and Bones

```javascript
const skinnedMesh = model.getObjectByName('Character');
const skeleton = skinnedMesh.skeleton;
const bones = skeleton.bones;

// Find specific bone
const handBone = bones.find(b => b.name === 'Hand_R');

// Visualize skeleton
const helper = new THREE.SkeletonHelper(skinnedMesh);
scene.add(helper);
```

### Programmatic Bone Animation

```javascript
// Rotate arm bone
const armBone = skeleton.getBoneByName('UpperArm_R');
armBone.rotation.z = Math.sin(time) * 0.5;

// Update skeleton
skeleton.update();
```

### Bone Attachments

Attach objects to bones (e.g., weapon to hand).

```javascript
const handBone = skeleton.getBoneByName('Hand_R');
const weapon = new THREE.Mesh(swordGeometry, swordMaterial);

// Position relative to bone
weapon.position.set(0, 0.5, 0);
weapon.rotation.x = Math.PI / 2;

handBone.add(weapon);
```

## Morph Targets

Morph targets (blend shapes) deform geometry between states.

```javascript
// Access morph targets
const mesh = model.getObjectByName('Face');
console.log(mesh.morphTargetDictionary); // { smile: 0, frown: 1, ... }

// Manually set influence (0-1)
mesh.morphTargetInfluences[0] = 0.5;  // 50% smile

// Animate with keyframes
const morphTrack = new THREE.NumberKeyframeTrack(
  '.morphTargetInfluences[0]',
  [0, 1, 2],
  [0, 1, 0]
);
const clip = new THREE.AnimationClip('smile', 2, [morphTrack]);
```

### Morph Animation from GLTF

```javascript
loader.load('face.glb', (gltf) => {
  const face = gltf.scene.getObjectByName('Face');

  // Morph targets included in animations
  const mixer = new THREE.AnimationMixer(face);
  const smileClip = THREE.AnimationClip.findByName(gltf.animations, 'Smile');
  mixer.clipAction(smileClip).play();
});
```

## Animation Blending

### Weight-based Blending

Blend between multiple animations (idle, walk, run).

```javascript
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);

// Start all actions
idleAction.play();
walkAction.play();
runAction.play();

// Blend based on speed
function updateBlending(speed) {
  if (speed < 0.5) {
    idleAction.weight = 1 - speed * 2;
    walkAction.weight = speed * 2;
    runAction.weight = 0;
  } else {
    idleAction.weight = 0;
    walkAction.weight = 1 - (speed - 0.5) * 2;
    runAction.weight = (speed - 0.5) * 2;
  }
}

updateBlending(0.7); // 70% speed = blend walk/run
```

### Additive Blending

Layer animations on top of base animation (e.g., waving while walking).

```javascript
import { AnimationUtils } from 'three';

// Make clip additive
const waveClip = AnimationUtils.makeClipAdditive(originalWaveClip);

const walkAction = mixer.clipAction(walkClip);
const waveAction = mixer.clipAction(waveClip);

walkAction.play();
waveAction.play();

// Additive weight controls blend amount
waveAction.weight = 0.8;
```

## Animation Utilities

```javascript
import { AnimationUtils } from 'three';

// Find clip by name
const clip = THREE.AnimationClip.findByName(clips, 'Walk');

// Create subclip (extract portion)
const subClip = AnimationUtils.subclip(clip, 'WalkFirstHalf', 0, 30);

// Make additive
const additiveClip = AnimationUtils.makeClipAdditive(clip);

// Optimize clip (remove redundant keyframes)
clip.optimize();

// Sort tracks
clip.resetDuration(); // Recalculate duration from tracks
```

## Procedural Animation Patterns

### Smooth Damping

Smooth movement toward target value.

```javascript
function smoothDamp(current, target, velocity, smoothTime, deltaTime) {
  const omega = 2 / smoothTime;
  const x = omega * deltaTime;
  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
  const change = current - target;
  const temp = (velocity + omega * change) * deltaTime;

  velocity = (velocity - omega * temp) * exp;
  const result = target + (change + temp) * exp;

  return { value: result, velocity };
}

// Usage
let pos = 0;
let vel = 0;

function animate() {
  const result = smoothDamp(pos, targetPos, vel, 0.3, deltaTime);
  pos = result.value;
  vel = result.velocity;

  object.position.x = pos;
}
```

### Spring Physics

Simple spring-based animation.

```javascript
class Spring {
  constructor(stiffness = 100, damping = 10) {
    this.stiffness = stiffness;
    this.damping = damping;
    this.velocity = 0;
    this.value = 0;
  }

  update(target, deltaTime) {
    const force = (target - this.value) * this.stiffness;
    const dampingForce = this.velocity * this.damping;
    this.velocity += (force - dampingForce) * deltaTime;
    this.value += this.velocity * deltaTime;
    return this.value;
  }
}

// Usage
const spring = new Spring(150, 15);

function animate() {
  const pos = spring.update(targetPos, deltaTime);
  object.position.x = pos;
}
```

### Oscillation Patterns

```javascript
// Sine wave (bobbing)
object.position.y = Math.sin(time * 2) * amplitude;

// Bounce (elastic)
const bounce = Math.abs(Math.sin(time * Math.PI)) * amplitude;
object.position.y = bounce;

// Circular orbit
object.position.x = Math.cos(time) * radius;
object.position.z = Math.sin(time) * radius;

// Figure-8 (lissajous)
object.position.x = Math.sin(time) * radius;
object.position.y = Math.sin(time * 2) * radius;

// Damped oscillation
const decay = Math.exp(-time * 0.5);
object.position.y = Math.sin(time * 5) * amplitude * decay;
```

## Performance Tips

### Share Animation Clips

```javascript
// Don't create new clips for each instance
const clip = clips[0];

characters.forEach(char => {
  const mixer = new THREE.AnimationMixer(char);
  mixer.clipAction(clip).play(); // Reuse same clip
});
```

### Optimize Clips

```javascript
// Remove redundant keyframes
clip.optimize();

// Remove unused tracks
clip.tracks = clip.tracks.filter(track => track.times.length > 1);
```

### Disable Off-screen Animations

```javascript
function animate() {
  characters.forEach(char => {
    if (isVisible(char)) {
      char.userData.mixer.update(delta);
    }
  });
}
```

### Cache Clips

```javascript
const clipCache = new Map();

function getClip(name, animations) {
  if (!clipCache.has(name)) {
    const clip = THREE.AnimationClip.findByName(animations, name);
    clipCache.set(name, clip);
  }
  return clipCache.get(name);
}
```

### Limit Update Frequency

```javascript
let accumulatedTime = 0;
const updateInterval = 1/30; // 30 FPS for animations

function animate() {
  const delta = clock.getDelta();
  accumulatedTime += delta;

  while (accumulatedTime >= updateInterval) {
    mixer.update(updateInterval);
    accumulatedTime -= updateInterval;
  }

  renderer.render(scene, camera);
}
```

## See Also

- [Fundamentals](fundamentals.md) - Animation loop, Clock, and delta time
- [Shaders](shaders.md) - Animated shader uniforms and procedural effects
- [Loaders](loaders.md) - Loading animated GLTF/FBX models
- [Interaction](interaction.md) - User-triggered animations and controls
fundamentals.md 12.3 KB
# Three.js Fundamentals Reference

## Scene

```javascript
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000); // Color or null
scene.background = textureLoader.load('path/to/texture.jpg'); // Texture
scene.environment = hdrTexture; // For PBR materials (IBL)
scene.fog = new THREE.Fog(0xffffff, 1, 1000); // color, near, far
scene.fog = new THREE.FogExp2(0xffffff, 0.002); // color, density
```

**Key properties:**
- `scene.add(object)` / `scene.remove(object)`
- `scene.getObjectByName(name)` / `scene.getObjectById(id)`
- `scene.traverse(callback)` - recursively visit all descendants

## Cameras

### PerspectiveCamera
```javascript
const camera = new THREE.PerspectiveCamera(
  75,                                    // fov (degrees)
  window.innerWidth / window.innerHeight, // aspect ratio
  0.1,                                   // near clipping plane
  1000                                   // far clipping plane
);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix(); // Call after changing fov, aspect, near, far
```

**Common gotchas:**
- Must call `updateProjectionMatrix()` after changing projection parameters
- Z-fighting occurs when near/far ratio is too large (keep near > 0.1, far reasonable)
- Default position is (0,0,0), default up is (0,1,0)

### OrthographicCamera
```javascript
const frustumSize = 10;
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.OrthographicCamera(
  frustumSize * aspect / -2,  // left
  frustumSize * aspect / 2,   // right
  frustumSize / 2,            // top
  frustumSize / -2,           // bottom
  0.1,                        // near
  1000                        // far
);
camera.zoom = 1; // Increase to zoom in
camera.updateProjectionMatrix();
```

### ArrayCamera
```javascript
// For split-screen rendering
const camera1 = new THREE.PerspectiveCamera(50, 0.5, 1, 1000);
const camera2 = new THREE.PerspectiveCamera(50, 0.5, 1, 1000);
const arrayCamera = new THREE.ArrayCamera([camera1, camera2]);
```

### CubeCamera
```javascript
// For dynamic environment maps (reflections)
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);

// In render loop:
cubeCamera.update(renderer, scene);
reflectiveMesh.material.envMap = cubeRenderTarget.texture;
```

## WebGLRenderer

```javascript
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector('#canvas'),
  antialias: true,
  alpha: true,           // Transparent background
  powerPreference: 'high-performance'
});

// Essential setup
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap at 2 for performance
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // or PCFShadowMap, VSMShadowMap

// Color management (Three.js r152+)
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

// Other useful properties
renderer.physicallyCorrectLights = true; // Deprecated in r152, use useLegacyLights = false
renderer.useLegacyLights = false; // r152+
```

**Shadow types:**
- `THREE.BasicShadowMap` - fastest, lowest quality
- `THREE.PCFShadowMap` - default, filtered
- `THREE.PCFSoftShadowMap` - softer, slower
- `THREE.VSMShadowMap` - variance shadow maps, can have artifacts

## Object3D

Base class for most Three.js objects (Mesh, Light, Camera, Group, etc.)

```javascript
const obj = new THREE.Object3D();

// Transform properties (do NOT modify directly in most cases)
obj.position.set(x, y, z);
obj.rotation.set(x, y, z); // Euler angles in radians
obj.scale.set(x, y, z);
obj.quaternion.setFromEuler(new THREE.Euler(x, y, z)); // For rotation

// Hierarchy
obj.add(childObject);
obj.remove(childObject);
obj.parent // Reference to parent
obj.children // Array of children

// Visibility and rendering
obj.visible = true;
obj.layers.set(0); // Default layer is 0
obj.layers.enable(1); // Add to layer 1
obj.layers.toggle(2); // Toggle layer 2
obj.renderOrder = 0; // Higher values render last (for transparency)

// Traversal
obj.traverse((child) => {
  if (child.isMesh) {
    // Do something with meshes
  }
});

// World space transforms
obj.getWorldPosition(target); // Get world position into target Vector3
obj.getWorldQuaternion(target);
obj.getWorldScale(target);
obj.getWorldDirection(target);

// Update matrices (usually automatic)
obj.updateMatrix(); // Local transform
obj.updateMatrixWorld(); // World transform (includes parent)
obj.matrixAutoUpdate = false; // Disable auto-update for manual control
```

**Important:**
- Rotation uses Euler angles (gimbal lock possible), Quaternion for interpolation
- `matrixAutoUpdate` should stay `true` unless you're manually managing matrices
- World transforms require traversing parent chain

## Group

Convenience class for organizing objects.

```javascript
const group = new THREE.Group();
group.add(mesh1, mesh2, mesh3);
scene.add(group);

// Transform entire group
group.position.set(0, 5, 0);
group.rotation.y = Math.PI / 4;
```

## Mesh

```javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);

// Shadows
mesh.castShadow = true;
mesh.receiveShadow = true;

// Raycasting
mesh.raycast(raycaster, intersects); // Usually called via scene.raycast

// Frustum culling
mesh.frustumCulled = true; // Default, disable if doing custom culling
```

## Coordinate System

Three.js uses a **right-handed** coordinate system:
- **+X** is right
- **+Y** is up
- **+Z** is out of the screen (toward camera in default view)
- Camera looks down **-Z** by default

```javascript
// Common camera setup (looking at origin from +Z)
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
```

## Math Utilities

### Vector3
```javascript
const v = new THREE.Vector3(x, y, z);
v.set(x, y, z);
v.copy(otherVector);
v.add(otherVector); // v += other
v.sub(otherVector); // v -= other
v.multiplyScalar(scalar);
v.normalize(); // Make unit length
v.length(); // Magnitude
v.distanceTo(otherVector);
v.dot(otherVector);
v.cross(otherVector); // Cross product
v.lerp(targetVector, alpha); // Linear interpolation
v.applyMatrix4(matrix);
v.applyQuaternion(quaternion);
v.project(camera); // To NDC
v.unproject(camera); // From NDC
```

### Matrix4
```javascript
const m = new THREE.Matrix4();
m.identity();
m.makeTranslation(x, y, z);
m.makeRotationX(radians);
m.makeScale(x, y, z);
m.multiply(otherMatrix);
m.invert();
m.transpose();
m.decompose(position, quaternion, scale); // Extract TRS
m.compose(position, quaternion, scale); // Build from TRS
```

### Quaternion
```javascript
const q = new THREE.Quaternion();
q.setFromEuler(new THREE.Euler(x, y, z));
q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
q.multiply(otherQuaternion);
q.slerp(targetQuaternion, alpha); // Spherical interpolation
q.normalize();
```

### Euler
```javascript
const euler = new THREE.Euler(x, y, z, 'XYZ'); // Rotation order: XYZ, YXZ, ZXY, etc.
euler.setFromQuaternion(quaternion);
```

### Color
```javascript
const color = new THREE.Color(0xff0000);
const color2 = new THREE.Color('rgb(255, 0, 0)');
const color3 = new THREE.Color('hsl(0, 100%, 50%)');
color.set(0x00ff00);
color.setHex(0x0000ff);
color.setRGB(r, g, b); // 0-1 range
color.setHSL(h, s, l); // h: 0-1, s: 0-1, l: 0-1
color.lerp(targetColor, alpha);
color.getHex(); // Returns number
color.getHexString(); // Returns string without #
```

### MathUtils
```javascript
THREE.MathUtils.degToRad(degrees);
THREE.MathUtils.radToDeg(radians);
THREE.MathUtils.clamp(value, min, max);
THREE.MathUtils.lerp(start, end, alpha);
THREE.MathUtils.smoothstep(x, min, max);
THREE.MathUtils.mapLinear(x, a1, a2, b1, b2);
THREE.MathUtils.randFloat(low, high);
THREE.MathUtils.randInt(low, high);
THREE.MathUtils.seededRandom(seed); // Returns random function
THREE.MathUtils.isPowerOfTwo(value);
```

## Common Patterns

### Cleanup and Disposal
```javascript
// Dispose geometry and material
mesh.geometry.dispose();
mesh.material.dispose();

// Dispose textures
mesh.material.map?.dispose();
mesh.material.normalMap?.dispose();
mesh.material.envMap?.dispose();

// Dispose render targets
renderTarget.dispose();

// Remove from scene
scene.remove(mesh);

// Full cleanup function
function dispose(obj) {
  obj.traverse((child) => {
    if (child.geometry) child.geometry.dispose();
    if (child.material) {
      if (Array.isArray(child.material)) {
        child.material.forEach(m => m.dispose());
      } else {
        child.material.dispose();
      }
    }
  });
}
```

**Critical:** Always dispose when removing objects to prevent memory leaks.

### Clock for Animation
```javascript
const clock = new THREE.Clock();

function animate() {
  const deltaTime = clock.getDelta(); // Time since last call
  const elapsedTime = clock.getElapsedTime(); // Total time since start

  mesh.rotation.y += deltaTime; // Frame-rate independent rotation

  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();
```

### Responsive Canvas
```javascript
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);

// Or for specific container
function onWindowResize() {
  const container = renderer.domElement.parentElement;
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
}
```

### LoadingManager
```javascript
const manager = new THREE.LoadingManager();

manager.onStart = (url, loaded, total) => {
  console.log(`Started loading: ${url}`);
};

manager.onLoad = () => {
  console.log('All assets loaded');
};

manager.onProgress = (url, loaded, total) => {
  console.log(`Loading: ${loaded / total * 100}%`);
};

manager.onError = (url) => {
  console.error(`Error loading: ${url}`);
};

const textureLoader = new THREE.TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);
```

### Render Loop with Cleanup
```javascript
let animationId;

function animate() {
  animationId = requestAnimationFrame(animate);

  // Update logic
  controls?.update();

  renderer.render(scene, camera);
}

function cleanup() {
  cancelAnimationFrame(animationId);
  renderer.dispose();
  // Dispose all objects...
}
```

## Performance Tips

### Geometry
- Reuse geometries when possible (instancing)
- Use `BufferGeometry` (default in modern Three.js)
- Merge static geometries with `BufferGeometryUtils.mergeGeometries()`
- Use LOD (Level of Detail) for distant objects

### Materials
- Reuse materials when possible
- Use `MeshBasicMaterial` for unlit objects
- Limit `MeshStandardMaterial` / `MeshPhysicalMaterial` count
- Disable features you don't need (shadows, fog, etc.)

### Textures
- Use power-of-two dimensions (256, 512, 1024, 2048)
- Enable mipmaps for textures (default)
- Use compressed formats (KTX2, Basis)
- Set `texture.minFilter = THREE.NearestFilter` to disable mipmaps if not needed
- Dispose unused textures

### Rendering
- Cap `renderer.setPixelRatio()` at 2
- Use `renderer.info` to monitor draw calls, triangles
- Disable `shadowMap` if not needed
- Use `renderer.setAnimationLoop()` for VR/AR (better than requestAnimationFrame)
- Enable frustum culling (default)
- Use `renderer.compile(scene, camera)` to pre-compile shaders

### General
- Limit `traverse()` calls in render loop
- Use object pooling for frequently created/destroyed objects
- Batch updates (don't call `updateMatrixWorld()` multiple times per frame)
- Use `InstancedMesh` for many identical objects
- Use `Points` for particle systems (not individual meshes)

### Monitoring
```javascript
console.log(renderer.info.render); // Draw calls, triangles, etc.
console.log(renderer.info.memory); // Geometries, textures
console.log(renderer.info.programs.length); // Shader programs compiled
```

**Rule of thumb:** Keep draw calls < 100-200, triangles < 1M for 60fps on average hardware.

## See Also

- [Geometry](geometry.md) - Built-in shapes and custom BufferGeometry
- [Materials](materials.md) - Material types and PBR workflow
- [Lighting & Shadows](lighting-and-shadows.md) - Light types and shadow setup
- [Animation](animation.md) - Animation loop, mixer, and procedural motion
geometry.md 13.3 KB
# Three.js Geometry

## Built-in Geometries

### Basic Shapes
```javascript
// Box: width, height, depth, widthSegments, heightSegments, depthSegments
new THREE.BoxGeometry(1, 1, 1, 1, 1, 1);

// Sphere: radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI * 2, 0, Math.PI);

// Plane: width, height, widthSegments, heightSegments
new THREE.PlaneGeometry(1, 1, 1, 1);

// Circle: radius, segments, thetaStart, thetaLength
new THREE.CircleGeometry(1, 32, 0, Math.PI * 2);

// Cylinder: radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
new THREE.CylinderGeometry(1, 1, 2, 32, 1, false, 0, Math.PI * 2);

// Cone: radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
new THREE.ConeGeometry(1, 2, 32, 1, false, 0, Math.PI * 2);

// Torus: radius, tube, radialSegments, tubularSegments, arc
new THREE.TorusGeometry(1, 0.4, 16, 100, Math.PI * 2);

// TorusKnot: radius, tube, tubularSegments, radialSegments, p, q
new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3);

// Ring: innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength
new THREE.RingGeometry(0.5, 1, 32, 1, 0, Math.PI * 2);
```

### Advanced Shapes
```javascript
// Capsule: radius, length, capSegments, radialSegments
new THREE.CapsuleGeometry(1, 1, 4, 8);

// Dodecahedron: radius, detail
new THREE.DodecahedronGeometry(1, 0);

// Icosahedron: radius, detail
new THREE.IcosahedronGeometry(1, 0);

// Octahedron: radius, detail
new THREE.OctahedronGeometry(1, 0);

// Tetrahedron: radius, detail
new THREE.TetrahedronGeometry(1, 0);

// Polyhedron: vertices, indices, radius, detail
new THREE.PolyhedronGeometry(vertices, indices, 1, 0);
```

### Path-based Geometries
```javascript
// Lathe: points, segments, phiStart, phiLength
const points = [new THREE.Vector2(0, 0), new THREE.Vector2(1, 0.5), new THREE.Vector2(0, 1)];
new THREE.LatheGeometry(points, 32, 0, Math.PI * 2);

// Extrude with bevel: shapes, options
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(0, 1);
shape.lineTo(1, 1);
shape.lineTo(1, 0);
shape.lineTo(0, 0);
const extrudeSettings = {
  depth: 2,
  bevelEnabled: true,
  bevelThickness: 0.1,
  bevelSize: 0.1,
  bevelSegments: 3,
  steps: 1
};
new THREE.ExtrudeGeometry(shape, extrudeSettings);

// Tube from curve: path, tubularSegments, radius, radialSegments, closed
const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-1, 0, 0),
  new THREE.Vector3(0, 1, 0),
  new THREE.Vector3(1, 0, 0)
]);
new THREE.TubeGeometry(path, 64, 0.2, 8, false);
```

### TextGeometry
```javascript
const loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', (font) => {
  const geometry = new THREE.TextGeometry('Hello', {
    font: font,
    size: 1,
    height: 0.2,
    curveSegments: 12,
    bevelEnabled: true,
    bevelThickness: 0.03,
    bevelSize: 0.02,
    bevelSegments: 5
  });
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});
```

## BufferGeometry

### Custom Geometry from Arrays
```javascript
const geometry = new THREE.BufferGeometry();

// Positions (3 values per vertex: x, y, z)
const positions = new Float32Array([
  -1, -1, 0,
   1, -1, 0,
   1,  1, 0,
  -1,  1, 0
]);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

// Indices (triangle vertex order)
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));

// Normals (3 values per vertex)
const normals = new Float32Array([
  0, 0, 1,
  0, 0, 1,
  0, 0, 1,
  0, 0, 1
]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));

// UVs (2 values per vertex)
const uvs = new Float32Array([
  0, 0,
  1, 0,
  1, 1,
  0, 1
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

// Vertex colors (3 values per vertex: r, g, b)
const colors = new Float32Array([
  1, 0, 0,
  0, 1, 0,
  0, 0, 1,
  1, 1, 0
]);
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
```

### BufferAttribute Types
```javascript
// Float32Array: positions, normals, uvs, colors
new THREE.BufferAttribute(new Float32Array([...]), itemSize);

// Uint16Array: indices (up to 65535 vertices)
new THREE.BufferAttribute(new Uint16Array([...]), 1);

// Uint32Array: indices (larger meshes)
new THREE.BufferAttribute(new Uint32Array([...]), 1);

// Item sizes: position=3, normal=3, uv=2, color=3 or 4, index=1
```

### Modifying Geometry at Runtime
```javascript
const positionAttribute = geometry.getAttribute('position');

// Modify individual vertex
positionAttribute.setXYZ(vertexIndex, x, y, z);

// Mark for GPU update
positionAttribute.needsUpdate = true;

// Recompute normals after position changes
geometry.computeVertexNormals();

// Recompute bounding sphere for culling
geometry.computeBoundingSphere();

// Direct array access
const positions = positionAttribute.array;
positions[vertexIndex * 3] = x;
positions[vertexIndex * 3 + 1] = y;
positions[vertexIndex * 3 + 2] = z;
positionAttribute.needsUpdate = true;
```

### Interleaved Buffers
```javascript
// Combine multiple attributes in single array
const interleavedData = new Float32Array([
  // x, y, z, nx, ny, nz, u, v
  -1, -1, 0, 0, 0, 1, 0, 0,
   1, -1, 0, 0, 0, 1, 1, 0,
   1,  1, 0, 0, 0, 1, 1, 1
]);

const interleavedBuffer = new THREE.InterleavedBuffer(interleavedData, 8);
geometry.setAttribute('position', new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0));
geometry.setAttribute('normal', new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 3));
geometry.setAttribute('uv', new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 6));
```

## EdgesGeometry & WireframeGeometry

```javascript
// EdgesGeometry: only edges where angle exceeds threshold
const edges = new THREE.EdgesGeometry(geometry, 30); // 30 degree threshold
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x000000 }));

// WireframeGeometry: all triangle edges
const wireframe = new THREE.WireframeGeometry(geometry);
const line = new THREE.LineSegments(wireframe, new THREE.LineBasicMaterial({ color: 0x000000 }));
```

## Points (Point Clouds)

```javascript
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);

for (let i = 0; i < count; i++) {
  positions[i * 3] = Math.random() * 10 - 5;
  positions[i * 3 + 1] = Math.random() * 10 - 5;
  positions[i * 3 + 2] = Math.random() * 10 - 5;

  colors[i * 3] = Math.random();
  colors[i * 3 + 1] = Math.random();
  colors[i * 3 + 2] = Math.random();
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const material = new THREE.PointsMaterial({
  size: 0.1,
  vertexColors: true,
  transparent: true,
  opacity: 0.8,
  sizeAttenuation: true // scale with distance
});

const points = new THREE.Points(geometry, material);
scene.add(points);
```

## Lines

```javascript
// Line: continuous line through points
const points = [
  new THREE.Vector3(-1, 0, 0),
  new THREE.Vector3(0, 1, 0),
  new THREE.Vector3(1, 0, 0)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }));

// LineLoop: closed loop
const loop = new THREE.LineLoop(geometry, new THREE.LineBasicMaterial({ color: 0x00ff00 }));

// LineSegments: disconnected segments (pairs of points)
const segments = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ color: 0x0000ff }));
```

## InstancedMesh

```javascript
// Setup: geometry, material, instance count
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

// Set transforms per instance
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
  dummy.position.set(
    Math.random() * 100 - 50,
    Math.random() * 100 - 50,
    Math.random() * 100 - 50
  );
  dummy.rotation.set(
    Math.random() * Math.PI,
    Math.random() * Math.PI,
    Math.random() * Math.PI
  );
  dummy.scale.setScalar(Math.random() * 2 + 0.5);
  dummy.updateMatrix();
  instancedMesh.setMatrixAt(i, dummy.matrix);
}

// Per-instance colors
instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(
  new Float32Array(count * 3),
  3
);
for (let i = 0; i < count; i++) {
  instancedMesh.setColorAt(i, new THREE.Color(Math.random(), Math.random(), Math.random()));
}

scene.add(instancedMesh);

// Runtime updates
dummy.position.y += 0.1;
dummy.updateMatrix();
instancedMesh.setMatrixAt(instanceId, dummy.matrix);
instancedMesh.instanceMatrix.needsUpdate = true;

// Raycasting
raycaster.intersectObject(instancedMesh); // returns array with instanceId property
```

## InstancedBufferGeometry

```javascript
// Custom per-instance attributes
const geometry = new THREE.InstancedBufferGeometry();

// Base geometry (shared)
const baseGeometry = new THREE.BoxGeometry(1, 1, 1);
geometry.index = baseGeometry.index;
geometry.attributes.position = baseGeometry.attributes.position;
geometry.attributes.normal = baseGeometry.attributes.normal;
geometry.attributes.uv = baseGeometry.attributes.uv;

// Per-instance offsets
const offsets = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
  offsets[i * 3] = Math.random() * 100 - 50;
  offsets[i * 3 + 1] = Math.random() * 100 - 50;
  offsets[i * 3 + 2] = Math.random() * 100 - 50;
}
geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3));

// Per-instance colors
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
  colors[i * 3] = Math.random();
  colors[i * 3 + 1] = Math.random();
  colors[i * 3 + 2] = Math.random();
}
geometry.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(colors, 3));

// Custom shader to use instance attributes
const material = new THREE.ShaderMaterial({
  vertexShader: `
    attribute vec3 offset;
    attribute vec3 instanceColor;
    varying vec3 vColor;
    void main() {
      vColor = instanceColor;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position + offset, 1.0);
    }
  `,
  fragmentShader: `
    varying vec3 vColor;
    void main() {
      gl_FragColor = vec4(vColor, 1.0);
    }
  `
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
```

## Geometry Utilities

```javascript
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';

// Merge multiple geometries into one
const geometries = [geometry1, geometry2, geometry3];
const merged = mergeGeometries(geometries);

// Merge with materials (creates groups for multi-material)
const merged = mergeGeometries(geometries, true);

// Compute tangents for normal mapping
import { computeTangents } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
computeTangents(geometry);
```

## Common Patterns

### Center Geometry
```javascript
geometry.center(); // center around origin
geometry.computeBoundingBox();
const center = geometry.boundingBox.getCenter(new THREE.Vector3());
geometry.translate(-center.x, -center.y, -center.z);
```

### Scale to Fit
```javascript
geometry.computeBoundingBox();
const size = geometry.boundingBox.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = desiredSize / maxDim;
geometry.scale(scale, scale, scale);
```

### Clone and Transform
```javascript
const clone = geometry.clone();
clone.translate(x, y, z);
clone.rotateX(angle);
clone.scale(sx, sy, sz);
```

### Morph Targets
```javascript
// Create base geometry
const geometry = new THREE.BoxGeometry(1, 1, 1);

// Create morph target (same vertex count)
const morphTarget = geometry.attributes.position.array.slice();
// Modify morphTarget array...
geometry.morphAttributes.position = [
  new THREE.BufferAttribute(morphTarget, 3)
];

// Animate in material
const material = new THREE.MeshStandardMaterial({ morphTargets: true });
const mesh = new THREE.Mesh(geometry, material);

// Control influence (0 to 1)
mesh.morphTargetInfluences[0] = 0.5;
```

## Performance Tips

1. **Use indexed geometry**: Reuse vertices with index buffer
2. **Merge static geometry**: Combine objects that don't move
3. **Use InstancedMesh**: For many copies of same geometry
4. **Reduce segment counts**: Lower poly counts for distant objects
5. **Dispose unused geometry**: Call `geometry.dispose()` when done
6. **Avoid frequent attribute updates**: Batch changes, update once per frame
7. **Use interleaved buffers**: Better cache performance for GPU
8. **Frustum culling**: Three.js automatic, ensure bounding spheres are correct
9. **LOD (Level of Detail)**: Use THREE.LOD for distance-based geometry switching
10. **Reuse geometries**: Share geometry instances across multiple meshes

```javascript
// Dispose pattern
geometry.dispose();
material.dispose();
texture.dispose();

// LOD example
const lod = new THREE.LOD();
lod.addLevel(highPolyMesh, 0);
lod.addLevel(mediumPolyMesh, 50);
lod.addLevel(lowPolyMesh, 100);
scene.add(lod);
```

## See Also

- [Fundamentals](fundamentals.md) - Scene setup, Object3D hierarchy, coordinate system
- [Materials](materials.md) - Material types to apply to geometries
- [Shaders](shaders.md) - Custom vertex/fragment shaders for geometry effects
- [Loaders](loaders.md) - Loading external 3D model geometry
interaction.md 16.8 KB
# Three.js Interaction

## Raycaster

### Basic Setup
```typescript
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

function onPointerMove(event) {
  // Full window
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  // Canvas-specific
  const rect = canvas.getBoundingClientRect();
  pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}

function checkIntersections() {
  raycaster.setFromCamera(pointer, camera);
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length > 0) {
    const hit = intersects[0];
    // hit.distance - distance from camera
    // hit.point - Vector3 world position
    // hit.face - Face3 (normal, materialIndex)
    // hit.object - intersected Object3D
    // hit.uv - texture coordinates
    // hit.instanceId - for InstancedMesh
  }
}
```

### Touch Support
```typescript
function onTouchMove(event) {
  event.preventDefault();
  const touch = event.touches[0];
  pointer.x = (touch.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(touch.clientY / window.innerHeight) * 2 + 1;
}
```

### Raycaster Options
```typescript
raycaster.near = 0.1;
raycaster.far = 1000;
raycaster.params.Line.threshold = 0.1; // Line detection sensitivity
raycaster.params.Points.threshold = 0.1; // Point cloud sensitivity
raycaster.layers.set(1); // Only intersect objects on layer 1
```

### Throttled Raycasting for Hover
```typescript
let lastRaycastTime = 0;
const raycastThrottle = 50; // ms

function animate(time) {
  if (time - lastRaycastTime > raycastThrottle) {
    checkIntersections();
    lastRaycastTime = time;
  }
  renderer.render(scene, camera);
}
```

## Camera Controls

### OrbitControls
```typescript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 5;
controls.maxDistance = 50;
controls.maxPolarAngle = Math.PI / 2; // Prevent going below ground
controls.autoRotate = true;
controls.autoRotateSpeed = 2;

function animate() {
  controls.update(); // Required when damping or auto-rotate enabled
  renderer.render(scene, camera);
}
```

### FlyControls
```typescript
import { FlyControls } from 'three/examples/jsm/controls/FlyControls';

const controls = new FlyControls(camera, renderer.domElement);
controls.movementSpeed = 10;
controls.rollSpeed = Math.PI / 6;
controls.dragToLook = true;

const clock = new THREE.Clock();
function animate() {
  controls.update(clock.getDelta());
  renderer.render(scene, camera);
}
```

### FirstPersonControls
```typescript
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls';

const controls = new FirstPersonControls(camera, renderer.domElement);
controls.movementSpeed = 10;
controls.lookSpeed = 0.1;
controls.lookVertical = true;
controls.constrainVertical = true;
controls.verticalMin = 1.0;
controls.verticalMax = 2.0;

const clock = new THREE.Clock();
function animate() {
  controls.update(clock.getDelta());
  renderer.render(scene, camera);
}
```

### PointerLockControls with WASD
```typescript
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls';

const controls = new PointerLockControls(camera, document.body);

// Lock pointer on click
document.addEventListener('click', () => controls.lock());

const moveState = { forward: false, backward: false, left: false, right: false };
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();

document.addEventListener('keydown', (e) => {
  if (e.code === 'KeyW') moveState.forward = true;
  if (e.code === 'KeyS') moveState.backward = true;
  if (e.code === 'KeyA') moveState.left = true;
  if (e.code === 'KeyD') moveState.right = true;
});

document.addEventListener('keyup', (e) => {
  if (e.code === 'KeyW') moveState.forward = false;
  if (e.code === 'KeyS') moveState.backward = false;
  if (e.code === 'KeyA') moveState.left = false;
  if (e.code === 'KeyD') moveState.right = false;
});

const clock = new THREE.Clock();
function animate() {
  const delta = clock.getDelta();

  velocity.x -= velocity.x * 10.0 * delta;
  velocity.z -= velocity.z * 10.0 * delta;

  direction.z = Number(moveState.forward) - Number(moveState.backward);
  direction.x = Number(moveState.right) - Number(moveState.left);
  direction.normalize();

  if (moveState.forward || moveState.backward) velocity.z -= direction.z * 400.0 * delta;
  if (moveState.left || moveState.right) velocity.x -= direction.x * 400.0 * delta;

  controls.moveRight(-velocity.x * delta);
  controls.moveForward(-velocity.z * delta);

  renderer.render(scene, camera);
}
```

### TrackballControls
```typescript
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls';

const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;

function animate() {
  controls.update();
  renderer.render(scene, camera);
}
```

### MapControls
```typescript
import { MapControls } from 'three/examples/jsm/controls/MapControls';

const controls = new MapControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 10;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;

function animate() {
  controls.update();
  renderer.render(scene, camera);
}
```

## TransformControls

```typescript
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';

const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);

// Attach to object
transformControls.attach(selectedObject);

// Switch modes
transformControls.setMode('translate'); // or 'rotate', 'scale'
transformControls.setSpace('world'); // or 'local'

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  if (e.key === 'g') transformControls.setMode('translate');
  if (e.key === 'r') transformControls.setMode('rotate');
  if (e.key === 's') transformControls.setMode('scale');
  if (e.key === 'Escape') transformControls.detach();
});

// Disable OrbitControls during drag
transformControls.addEventListener('dragging-changed', (event) => {
  orbitControls.enabled = !event.value;
});

// Object changed event
transformControls.addEventListener('objectChange', () => {
  console.log('Object transformed', selectedObject.position);
});
```

## DragControls

```typescript
import { DragControls } from 'three/examples/jsm/controls/DragControls';

const draggableObjects = [mesh1, mesh2, mesh3];
const dragControls = new DragControls(draggableObjects, camera, renderer.domElement);

// Events
dragControls.addEventListener('dragstart', (event) => {
  orbitControls.enabled = false;
  event.object.material.opacity = 0.5;
});

dragControls.addEventListener('drag', (event) => {
  // Constrain to ground plane
  event.object.position.y = 0;
});

dragControls.addEventListener('dragend', (event) => {
  orbitControls.enabled = true;
  event.object.material.opacity = 1.0;
});

// Disable/enable
dragControls.enabled = false;
```

## Selection System

### Click-to-Select with Visual Feedback
```typescript
let selectedObject = null;
const originalMaterials = new Map();

function onPointerClick(event) {
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const intersects = raycaster.intersectObjects(selectableObjects);

  // Deselect previous
  if (selectedObject) {
    selectedObject.material = originalMaterials.get(selectedObject);
    selectedObject = null;
  }

  // Select new
  if (intersects.length > 0) {
    selectedObject = intersects[0].object;
    originalMaterials.set(selectedObject, selectedObject.material);
    selectedObject.material = selectedObject.material.clone();
    selectedObject.material.emissive.setHex(0x555555);
  }
}
```

### Box Selection
```typescript
import { SelectionBox } from 'three/examples/jsm/interactive/SelectionBox';
import { SelectionHelper } from 'three/examples/jsm/interactive/SelectionHelper';

const selectionBox = new SelectionBox(camera, scene);
const helper = new SelectionHelper(renderer, 'selectBox');

let startPoint = new THREE.Vector2();
let isSelecting = false;

document.addEventListener('pointerdown', (e) => {
  if (e.shiftKey) {
    isSelecting = true;
    startPoint.set(e.clientX, e.clientY);
    selectionBox.startPoint.set(
      (e.clientX / window.innerWidth) * 2 - 1,
      -(e.clientY / window.innerHeight) * 2 + 1,
      0.5
    );
  }
});

document.addEventListener('pointermove', (e) => {
  if (isSelecting) {
    selectionBox.endPoint.set(
      (e.clientX / window.innerWidth) * 2 - 1,
      -(e.clientY / window.innerHeight) * 2 + 1,
      0.5
    );
    helper.onSelectMove(startPoint, new THREE.Vector2(e.clientX, e.clientY));
  }
});

document.addEventListener('pointerup', () => {
  if (isSelecting) {
    isSelecting = false;
    const selected = selectionBox.select();
    console.log('Selected objects:', selected);
    helper.onSelectOver();
  }
});
```

### Hover Effects with Cursor Change
```typescript
let hoveredObject = null;

function onPointerMove(event) {
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const intersects = raycaster.intersectObjects(hoverableObjects);

  // Remove previous hover
  if (hoveredObject && hoveredObject !== selectedObject) {
    hoveredObject.material.emissive.setHex(0x000000);
    hoveredObject = null;
    document.body.style.cursor = 'default';
  }

  // Apply new hover
  if (intersects.length > 0) {
    hoveredObject = intersects[0].object;
    if (hoveredObject !== selectedObject) {
      hoveredObject.material.emissive.setHex(0x222222);
    }
    document.body.style.cursor = 'pointer';
  }
}
```

## Keyboard Input

### Key State Tracking for WASD
```typescript
class KeyboardState {
  private keys: Map<string, boolean> = new Map();

  constructor() {
    document.addEventListener('keydown', (e) => this.keys.set(e.code, true));
    document.addEventListener('keyup', (e) => this.keys.set(e.code, false));
  }

  isPressed(code: string): boolean {
    return this.keys.get(code) || false;
  }

  isAnyPressed(...codes: string[]): boolean {
    return codes.some(code => this.isPressed(code));
  }
}

const keyboard = new KeyboardState();

function animate() {
  if (keyboard.isPressed('KeyW')) moveForward();
  if (keyboard.isPressed('KeyS')) moveBackward();
  if (keyboard.isPressed('KeyA')) moveLeft();
  if (keyboard.isPressed('KeyD')) moveRight();
  if (keyboard.isPressed('Space')) jump();
}
```

## World-Screen Coordinate Conversion

### worldToScreen (Position HTML over 3D)
```typescript
function worldToScreen(position: THREE.Vector3, camera: THREE.Camera): { x: number, y: number } {
  const vector = position.clone().project(camera);

  return {
    x: (vector.x * 0.5 + 0.5) * window.innerWidth,
    y: (-vector.y * 0.5 + 0.5) * window.innerHeight
  };
}

// Usage: Position HTML label
const screenPos = worldToScreen(mesh.position, camera);
labelElement.style.left = `${screenPos.x}px`;
labelElement.style.top = `${screenPos.y}px`;
```

### screenToWorld (Unproject)
```typescript
function screenToWorld(x: number, y: number, z: number, camera: THREE.Camera): THREE.Vector3 {
  const vector = new THREE.Vector3(
    (x / window.innerWidth) * 2 - 1,
    -(y / window.innerHeight) * 2 + 1,
    z
  );

  return vector.unproject(camera);
}

// Usage: Get ray direction
const near = screenToWorld(event.clientX, event.clientY, 0, camera);
const far = screenToWorld(event.clientX, event.clientY, 1, camera);
const direction = far.sub(near).normalize();
```

### Ray-Plane Intersection for Ground Positioning
```typescript
function getGroundPosition(event: MouseEvent, camera: THREE.Camera): THREE.Vector3 | null {
  const pointer = new THREE.Vector2(
    (event.clientX / window.innerWidth) * 2 - 1,
    -(event.clientY / window.innerHeight) * 2 + 1
  );

  raycaster.setFromCamera(pointer, camera);

  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
  const intersection = new THREE.Vector3();

  return raycaster.ray.intersectPlane(plane, intersection);
}

// Usage: Place object on ground at mouse position
const groundPos = getGroundPosition(event, camera);
if (groundPos) {
  object.position.copy(groundPos);
}
```

## Event Handling Best Practices

### InteractionManager Class Pattern
```typescript
class InteractionManager {
  private raycaster = new THREE.Raycaster();
  private pointer = new THREE.Vector2();
  private selectedObject: THREE.Object3D | null = null;
  private hoveredObject: THREE.Object3D | null = null;

  constructor(
    private camera: THREE.Camera,
    private scene: THREE.Scene,
    private canvas: HTMLCanvasElement
  ) {
    this.setupEventListeners();
  }

  private setupEventListeners() {
    this.canvas.addEventListener('pointermove', this.onPointerMove.bind(this));
    this.canvas.addEventListener('click', this.onClick.bind(this));
  }

  private updatePointer(event: PointerEvent) {
    const rect = this.canvas.getBoundingClientRect();
    this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
  }

  private onPointerMove(event: PointerEvent) {
    this.updatePointer(event);
    this.updateHover();
  }

  private onClick(event: PointerEvent) {
    this.updatePointer(event);
    this.updateSelection();
  }

  private getIntersections(): THREE.Intersection[] {
    this.raycaster.setFromCamera(this.pointer, this.camera);
    return this.raycaster.intersectObjects(this.scene.children, true);
  }

  private updateHover() {
    const intersects = this.getIntersections();

    if (this.hoveredObject) {
      this.hoveredObject.dispatchEvent({ type: 'hoverout' });
      this.hoveredObject = null;
    }

    if (intersects.length > 0) {
      this.hoveredObject = intersects[0].object;
      this.hoveredObject.dispatchEvent({ type: 'hoverover' });
    }
  }

  private updateSelection() {
    const intersects = this.getIntersections();

    if (this.selectedObject) {
      this.selectedObject.dispatchEvent({ type: 'deselect' });
      this.selectedObject = null;
    }

    if (intersects.length > 0) {
      this.selectedObject = intersects[0].object;
      this.selectedObject.dispatchEvent({ type: 'select' });
    }
  }

  dispose() {
    this.canvas.removeEventListener('pointermove', this.onPointerMove.bind(this));
    this.canvas.removeEventListener('click', this.onClick.bind(this));
  }
}

// Usage
const manager = new InteractionManager(camera, scene, renderer.domElement);

mesh.addEventListener('select', () => console.log('Selected!'));
mesh.addEventListener('hoverover', () => console.log('Hover!'));
```

## Performance Tips

### Throttle Raycasts
```typescript
// Use requestAnimationFrame for hover detection instead of raw mousemove
let rafId: number | null = null;

canvas.addEventListener('pointermove', (e) => {
  if (rafId !== null) return;

  rafId = requestAnimationFrame(() => {
    updatePointer(e);
    checkIntersections();
    rafId = null;
  });
});
```

### Use Layers
```typescript
// Interactive objects on layer 1
mesh.layers.set(1);

// Configure raycaster to only check layer 1
raycaster.layers.set(1);

// Camera must also see the layer
camera.layers.enable(1);
```

### Simple Collision Meshes
```typescript
// Use invisible simplified geometry for raycasting
const interactionMesh = new THREE.Mesh(
  new THREE.BoxGeometry(10, 10, 10),
  new THREE.MeshBasicMaterial({ visible: false })
);

const detailedMesh = new THREE.Mesh(
  complexGeometry,
  material
);

const group = new THREE.Group();
group.add(interactionMesh);
group.add(detailedMesh);

// Raycast only against interactionMesh
raycaster.intersectObject(interactionMesh);
```

### Disable Unused Controls
```typescript
// Disable controls when not needed
orbitControls.enabled = false;

// Or remove event listeners
orbitControls.dispose();

// Re-enable when needed
orbitControls.enabled = true;
```

### Limit Raycast Recursion
```typescript
// Don't recursively check all children
raycaster.intersectObjects(scene.children, false); // Only direct children

// Or maintain array of only interactive objects
const interactiveObjects: THREE.Object3D[] = [mesh1, mesh2, mesh3];
raycaster.intersectObjects(interactiveObjects);
```

## See Also

- [Fundamentals](fundamentals.md) - Coordinate systems and Object3D hierarchy
- [Animation](animation.md) - User-triggered animation playback
- [Geometry](geometry.md) - Raycasting targets and bounding boxes
- [Postprocessing](postprocessing.md) - Outline and selection visual effects
lighting-and-shadows.md 11.2 KB
# Three.js Lighting & Shadows

## Light Types Overview

| Type | Description | Shadow Support | Performance Cost |
|------|-------------|----------------|------------------|
| AmbientLight | Uniform illumination, no direction | No | Very Low |
| HemisphereLight | Sky/ground gradient | No | Low |
| DirectionalLight | Parallel rays (sun-like) | Yes | Medium |
| PointLight | Omnidirectional (bulb-like) | Yes | Medium-High |
| SpotLight | Cone-shaped beam | Yes | Medium-High |
| RectAreaLight | Rectangular area light | No (native) | High |

## AmbientLight

Provides uniform lighting to all objects equally. No direction or shadows.

```typescript
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
```

**Use case:** Base illumination, fill shadows, prevent pure black areas.

## HemisphereLight

Creates gradient lighting from sky color to ground color. Ideal for outdoor scenes.

```typescript
const hemiLight = new THREE.HemisphereLight(
  0x87ceeb, // sky color (light blue)
  0x8b4513, // ground color (brown)
  0.6       // intensity
);
hemiLight.position.set(0, 50, 0);
scene.add(hemiLight);
```

**Use case:** Natural outdoor ambient lighting, sky/ground color bounce.

## DirectionalLight

Emits parallel rays like sunlight. Uses orthographic shadow camera.

```typescript
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;

// Shadow camera setup
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 50;

// Shadow quality
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.bias = -0.0001;
dirLight.shadow.normalBias = 0.02;

scene.add(dirLight);

// Visualize shadow camera
const helper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(helper);
```

**Key points:**
- Position determines light direction
- Tight shadow frustum improves quality
- Adjust bias to fix shadow acne

## PointLight

Omnidirectional light like a light bulb. Uses cube shadow map (6 renders).

```typescript
const pointLight = new THREE.PointLight(0xffffff, 1, 100, 2);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;

// Shadow setup
pointLight.shadow.mapSize.width = 1024;
pointLight.shadow.mapSize.height = 1024;
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 100;
pointLight.shadow.bias = -0.001;

// Distance and decay
pointLight.distance = 100; // 0 = infinite
pointLight.decay = 2;      // physically correct

scene.add(pointLight);
```

**Parameters:**
- `distance`: Maximum range (0 = infinite)
- `decay`: Light falloff (2 = physically accurate)

## SpotLight

Cone-shaped light with falloff. Uses perspective shadow camera.

```typescript
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(10, 10, 10);
spotLight.target.position.set(0, 0, 0);
spotLight.castShadow = true;

// Cone shape
spotLight.angle = Math.PI / 6;      // 30 degrees
spotLight.penumbra = 0.2;           // soft edge
spotLight.distance = 50;
spotLight.decay = 2;

// Shadow setup
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 50;
spotLight.shadow.bias = -0.0001;

scene.add(spotLight);
scene.add(spotLight.target);
```

**Parameters:**
- `angle`: Cone angle in radians
- `penumbra`: Edge softness (0-1)
- `target`: Point the light aims at

## RectAreaLight

Rectangular area light for soft realistic lighting. Only works with MeshStandardMaterial and MeshPhysicalMaterial.

```typescript
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';

// Required initialization
RectAreaLightUniformsLib.init();

const rectLight = new THREE.RectAreaLight(0xffffff, 5, 4, 2);
rectLight.position.set(0, 5, 0);
rectLight.lookAt(0, 0, 0);
scene.add(rectLight);

// Helper
const helper = new RectAreaLightHelper(rectLight);
rectLight.add(helper);
```

**Limitations:**
- No native shadow support (use contact shadows workaround)
- Only Standard/Physical materials
- Requires UniformsLib initialization

## Shadow Setup

### Enable Shadows

```typescript
// 1. Enable on renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 2. Enable on light
light.castShadow = true;

// 3. Enable on objects
mesh.castShadow = true;
mesh.receiveShadow = true;
```

### Shadow Map Types

```typescript
THREE.BasicShadowMap      // Fast, hard edges
THREE.PCFShadowMap        // Default, filtered
THREE.PCFSoftShadowMap    // Softer shadows
THREE.VSMShadowMap        // Variance Shadow Maps, can blur
```

### Tight Frustum Optimization

```typescript
// Fit shadow camera tightly around scene
const box = new THREE.Box3().setFromObject(sceneGroup);
const size = box.getSize(new THREE.Vector3());

dirLight.shadow.camera.left = -size.x / 2;
dirLight.shadow.camera.right = size.x / 2;
dirLight.shadow.camera.top = size.y / 2;
dirLight.shadow.camera.bottom = -size.y / 2;
dirLight.shadow.camera.updateProjectionMatrix();
```

### Shadow Acne Fixes

```typescript
// Adjust bias values to fix artifacts
light.shadow.bias = -0.0001;        // Offset shadow depth
light.shadow.normalBias = 0.02;     // Offset along normal
```

### Contact Shadows (Fake/Fast)

```typescript
import { ContactShadows } from 'three/addons/objects/ContactShadows.js';

const contactShadows = new ContactShadows({
  opacity: 0.5,
  scale: 10,
  blur: 1,
  far: 10
});
contactShadows.position.y = 0;
scene.add(contactShadows);
```

## Light Helpers

```typescript
// All light types have helpers
const ambientHelper = new THREE.HemisphereLightHelper(hemiLight, 5);
const dirHelper = new THREE.DirectionalLightHelper(dirLight, 5);
const pointHelper = new THREE.PointLightHelper(pointLight, 1);
const spotHelper = new THREE.SpotLightHelper(spotLight);
const rectHelper = new RectAreaLightHelper(rectLight);

scene.add(ambientHelper);
scene.add(dirHelper);
scene.add(pointHelper);
scene.add(spotHelper);

// Update spot helper after changes
spotHelper.update();
```

## Environment Lighting (IBL)

### HDR Environment Maps

```typescript
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

const rgbeLoader = new RGBELoader();
rgbeLoader.load('/path/to/env.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping;

  scene.background = texture;
  scene.environment = texture; // PBR reflections

  // Optional blur
  scene.backgroundBlurriness = 0.5;
  scene.backgroundIntensity = 1.0;
  scene.environmentIntensity = 1.0;
});
```

### PMREM Generator (Prefiltered Mipmaps)

```typescript
import { PMREMGenerator } from 'three';

const pmremGenerator = new PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();

rgbeLoader.load('/env.hdr', (texture) => {
  const envMap = pmremGenerator.fromEquirectangular(texture).texture;

  scene.environment = envMap;

  texture.dispose();
  pmremGenerator.dispose();
});
```

### Cube Texture Environment

```typescript
const cubeLoader = new THREE.CubeTextureLoader();
const envMap = cubeLoader.load([
  'px.jpg', 'nx.jpg',
  'py.jpg', 'ny.jpg',
  'pz.jpg', 'nz.jpg'
]);

scene.background = envMap;
scene.environment = envMap;
```

## Light Probes

Baked ambient lighting from environment map.

```typescript
import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js';

// From cube texture
const cubeTexture = cubeLoader.load([...]);
const lightProbe = LightProbeGenerator.fromCubeTexture(cubeTexture);
scene.add(lightProbe);

// From equirectangular
const probe = LightProbeGenerator.fromCubeRenderTarget(
  renderer,
  cubeRenderTarget
);
scene.add(probe);
```

## Common Lighting Setups

### Three-Point Lighting

```typescript
// Key light (main)
const keyLight = new THREE.DirectionalLight(0xffffff, 1);
keyLight.position.set(5, 5, 5);
keyLight.castShadow = true;

// Fill light (soften shadows)
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-5, 0, 0);

// Back light (rim/separation)
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
backLight.position.set(0, 3, -5);

scene.add(keyLight, fillLight, backLight);
```

### Outdoor Daylight

```typescript
// Sky gradient
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x8b7355, 0.6);
scene.add(hemiLight);

// Sun
const sunLight = new THREE.DirectionalLight(0xfff4e6, 1);
sunLight.position.set(10, 20, 5);
sunLight.castShadow = true;
sunLight.shadow.camera.left = -20;
sunLight.shadow.camera.right = 20;
sunLight.shadow.camera.top = 20;
sunLight.shadow.camera.bottom = -20;
scene.add(sunLight);
```

### Indoor Studio

```typescript
// Multiple soft area lights
const light1 = new THREE.RectAreaLight(0xffffff, 5, 4, 2);
light1.position.set(-3, 3, 0);
light1.lookAt(0, 0, 0);

const light2 = new THREE.RectAreaLight(0xffffff, 3, 4, 2);
light2.position.set(3, 3, 0);
light2.lookAt(0, 0, 0);

const fillAmbient = new THREE.AmbientLight(0xffffff, 0.2);

scene.add(light1, light2, fillAmbient);
```

## Light Animation

### Orbit Animation

```typescript
function animate(time) {
  const radius = 10;
  pointLight.position.x = Math.cos(time) * radius;
  pointLight.position.z = Math.sin(time) * radius;
}
```

### Pulse Intensity

```typescript
function animate(time) {
  light.intensity = 0.5 + Math.sin(time * 2) * 0.5;
}
```

### Color Cycle

```typescript
function animate(time) {
  const hue = (time * 0.1) % 1;
  light.color.setHSL(hue, 1, 0.5);
}
```

### Animated Target

```typescript
function animate(time) {
  spotLight.target.position.x = Math.cos(time) * 5;
  spotLight.target.position.z = Math.sin(time) * 5;
  spotLight.target.updateMatrixWorld();
}
```

## Performance Tips

1. **Limit light count**: Aim for 3-5 real-time lights maximum
2. **Bake static lighting**: Use lightmaps for non-moving lights
3. **Smaller shadow maps**: 1024x1024 often sufficient, use 2048+ only for hero lights
4. **Tight shadow frustums**: Minimize shadow camera bounds for better quality
5. **Disable shadows on distant objects**: Use layers or distance checks
6. **Use light layers**: Selective lighting with Object3D.layers
7. **Environment maps over lights**: IBL cheaper than multiple lights
8. **Ambient + single shadow**: Often sufficient for mobile
9. **Shadow map type**: PCFShadowMap good balance, avoid PCFSoft on mobile
10. **Disable unnecessary shadows**: Not all objects need to cast/receive

### Light Layers Example

```typescript
// Setup layers
camera.layers.enable(0);
camera.layers.enable(1);

light.layers.set(1); // Only affects layer 1

object1.layers.set(0); // Not affected by light
object2.layers.set(1); // Affected by light
```

### Conditional Shadows

```typescript
function updateShadows(camera) {
  scene.traverse((obj) => {
    if (obj.isMesh) {
      const distance = obj.position.distanceTo(camera.position);
      obj.castShadow = distance < 20;
    }
  });
}
```

## See Also

- [Materials](materials.md) - PBR materials that respond to lighting
- [Textures](textures.md) - HDR environment maps and IBL textures
- [Postprocessing](postprocessing.md) - Bloom, SSAO, and other light-related effects
- [Fundamentals](fundamentals.md) - Scene setup and renderer configuration
loaders.md 13.5 KB
# Three.js Loaders

## LoadingManager

Coordinates multiple loaders with unified callbacks:

```javascript
const manager = new THREE.LoadingManager();

manager.onStart = (url, itemsLoaded, itemsTotal) => {
  console.log(`Started loading: ${url}`);
};

manager.onLoad = () => {
  console.log('All assets loaded');
};

manager.onProgress = (url, itemsLoaded, itemsTotal) => {
  console.log(`Loading: ${itemsLoaded}/${itemsTotal}`);
};

manager.onError = (url) => {
  console.error(`Error loading: ${url}`);
};

// Use with any loader
const textureLoader = new THREE.TextureLoader(manager);
const gltfLoader = new THREE.GLTFLoader(manager);
```

## Texture Loading

### TextureLoader

```javascript
// Callback style
const loader = new THREE.TextureLoader();
loader.load(
  'texture.jpg',
  (texture) => {
    material.map = texture;
    material.needsUpdate = true;
  },
  undefined,
  (error) => console.error(error)
);

// Synchronous style (starts loading immediately)
const texture = loader.load('texture.jpg');
material.map = texture;
```

### Texture Configuration

```javascript
const texture = loader.load('texture.jpg');

// Color space (important for correct rendering)
texture.colorSpace = THREE.SRGBColorSpace; // For color textures
texture.colorSpace = THREE.LinearSRGBColorSpace; // For data textures (normal, roughness, etc.)

// Wrapping
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);

// Filtering
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;

// Anisotropic filtering (better quality at angles)
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
texture.anisotropy = maxAnisotropy;

// Flip Y (some formats need this)
texture.flipY = false;
```

### CubeTextureLoader

```javascript
const cubeLoader = new THREE.CubeTextureLoader();
const envMap = cubeLoader.load([
  'px.jpg', 'nx.jpg',
  'py.jpg', 'ny.jpg',
  'pz.jpg', 'nz.jpg'
]);

scene.background = envMap;
material.envMap = envMap;
```

### HDR/EXR Loading

```javascript
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';

// HDR
const rgbeLoader = new RGBELoader();
rgbeLoader.load('environment.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.background = texture;
  scene.environment = texture;
});

// EXR
const exrLoader = new EXRLoader();
exrLoader.load('environment.exr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = texture;
});
```

### PMREMGenerator for PBR

```javascript
import { PMREMGenerator } from 'three';

const pmremGenerator = new PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();

rgbeLoader.load('environment.hdr', (texture) => {
  const envMap = pmremGenerator.fromEquirectangular(texture).texture;
  scene.environment = envMap;
  texture.dispose();
  pmremGenerator.dispose();
});
```

## GLTF/GLB Loading

### Basic GLTFLoader

```javascript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  // Access animations
  const mixer = new THREE.AnimationMixer(model);
  gltf.animations.forEach((clip) => {
    mixer.clipAction(clip).play();
  });

  // Access cameras
  const camera = gltf.cameras[0];

  // Asset info
  console.log(gltf.asset);
});
```

### GLTF with Draco Compression

```javascript
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/'); // Path to decoder files
dracoLoader.setDecoderConfig({ type: 'js' }); // or 'wasm'

const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);

gltfLoader.load('compressed.glb', (gltf) => {
  scene.add(gltf.scene);
});

// Cleanup when done
dracoLoader.dispose();
```

### GLTF with KTX2 Textures

```javascript
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';

const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);

const gltfLoader = new GLTFLoader();
gltfLoader.setKTX2Loader(ktx2Loader);

gltfLoader.load('model.glb', (gltf) => {
  scene.add(gltf.scene);
});

// Cleanup
ktx2Loader.dispose();
```

### Processing GLTF Content

```javascript
loader.load('model.glb', (gltf) => {
  const model = gltf.scene;

  // Enable shadows via traverse
  model.traverse((node) => {
    if (node.isMesh) {
      node.castShadow = true;
      node.receiveShadow = true;
    }
  });

  // Find by name
  const specificMesh = model.getObjectByName('MeshName');

  // Adjust materials
  model.traverse((node) => {
    if (node.isMesh && node.material) {
      node.material.roughness = 0.5;
      node.material.metalness = 1.0;
    }
  });

  // Center and scale
  const box = new THREE.Box3().setFromObject(model);
  const center = box.getCenter(new THREE.Vector3());
  const size = box.getSize(new THREE.Vector3());

  model.position.sub(center); // Center
  const maxDim = Math.max(size.x, size.y, size.z);
  model.scale.setScalar(2 / maxDim); // Scale to fit

  scene.add(model);
});
```

## Other Model Formats

### OBJ+MTL

```javascript
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';

const mtlLoader = new MTLLoader();
mtlLoader.load('model.mtl', (materials) => {
  materials.preload();

  const objLoader = new OBJLoader();
  objLoader.setMaterials(materials);
  objLoader.load('model.obj', (object) => {
    scene.add(object);
  });
});
```

### FBX

```javascript
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';

const loader = new FBXLoader();
loader.load('model.fbx', (object) => {
  // FBX often needs scaling
  object.scale.setScalar(0.01);

  // Access animations
  if (object.animations.length > 0) {
    const mixer = new THREE.AnimationMixer(object);
    object.animations.forEach((clip) => {
      mixer.clipAction(clip).play();
    });
  }

  scene.add(object);
});
```

### STL

```javascript
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';

const loader = new STLLoader();
loader.load('model.stl', (geometry) => {
  const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});
```

### PLY

```javascript
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';

const loader = new PLYLoader();
loader.load('model.ply', (geometry) => {
  geometry.computeVertexNormals();

  // PLY can contain vertex colors
  const material = new THREE.MeshStandardMaterial({
    vertexColors: geometry.hasAttribute('color')
  });

  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});
```

## Async/Promise Loading

### Promisified Loader Pattern

```javascript
function loadGLTF(url) {
  return new Promise((resolve, reject) => {
    const loader = new GLTFLoader();
    loader.load(url, resolve, undefined, reject);
  });
}

// Usage
async function init() {
  try {
    const gltf = await loadGLTF('model.glb');
    scene.add(gltf.scene);
  } catch (error) {
    console.error('Failed to load model:', error);
  }
}

// Or with loadAsync (available in newer versions)
const gltf = await loader.loadAsync('model.glb');
```

### Loading Multiple Assets

```javascript
async function loadAssets() {
  const textureLoader = new THREE.TextureLoader();
  const gltfLoader = new GLTFLoader();

  const [texture, model, envMap] = await Promise.all([
    textureLoader.loadAsync('texture.jpg'),
    gltfLoader.loadAsync('model.glb'),
    new RGBELoader().loadAsync('env.hdr')
  ]);

  return { texture, model, envMap };
}
```

## Caching

### Built-in Cache

```javascript
// Enable global cache
THREE.Cache.enabled = true;

// Same URL will be loaded only once
loader.load('texture.jpg'); // Loads from network
loader.load('texture.jpg'); // Returns from cache

// Clear cache
THREE.Cache.clear();

// Remove specific file
THREE.Cache.remove('texture.jpg');
```

### Custom AssetManager

```javascript
class AssetManager {
  constructor() {
    this.textures = new Map();
    this.models = new Map();
    this.textureLoader = new THREE.TextureLoader();
    this.gltfLoader = new GLTFLoader();
  }

  async loadTexture(url, clone = false) {
    if (this.textures.has(url)) {
      const cached = this.textures.get(url);
      return clone ? cached.clone() : cached;
    }

    const texture = await this.textureLoader.loadAsync(url);
    this.textures.set(url, texture);
    return texture;
  }

  async loadModel(url, clone = false) {
    if (this.models.has(url)) {
      const cached = this.models.get(url);
      return clone ? cached.scene.clone() : cached.scene;
    }

    const gltf = await this.gltfLoader.loadAsync(url);
    this.models.set(url, gltf);
    return gltf.scene;
  }

  dispose() {
    this.textures.forEach(texture => texture.dispose());
    this.models.forEach(gltf => {
      gltf.scene.traverse(node => {
        if (node.geometry) node.geometry.dispose();
        if (node.material) {
          if (Array.isArray(node.material)) {
            node.material.forEach(mat => mat.dispose());
          } else {
            node.material.dispose();
          }
        }
      });
    });
    this.textures.clear();
    this.models.clear();
  }
}

// Usage
const assets = new AssetManager();
const texture = await assets.loadTexture('texture.jpg');
const model = await assets.loadModel('model.glb', true); // Clone for instancing
```

## Loading from Different Sources

### Data URL/Base64

```javascript
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANS...';
const texture = textureLoader.load(dataUrl);
```

### Blob URL

```javascript
const response = await fetch('texture.jpg');
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);

const texture = textureLoader.load(blobUrl);

// Cleanup when done
URL.revokeObjectURL(blobUrl);
```

### ArrayBuffer

```javascript
const response = await fetch('model.glb');
const buffer = await response.arrayBuffer();

const loader = new GLTFLoader();
loader.parse(buffer, '', (gltf) => {
  scene.add(gltf.scene);
}, (error) => {
  console.error(error);
});
```

### Custom Paths

```javascript
// Set base path for all loads
loader.setPath('assets/models/');
loader.load('model.glb'); // Loads from assets/models/model.glb

// Set resource path (for referenced assets like textures)
loader.setResourcePath('assets/textures/');

// Custom URL modification
loader.setURLModifier((url) => {
  // Add CDN prefix, authentication tokens, etc.
  return `https://cdn.example.com/${url}?token=abc123`;
});
```

## Error Handling

### Graceful Fallback

```javascript
async function loadWithFallback(primary, fallback) {
  const loader = new GLTFLoader();

  try {
    return await loader.loadAsync(primary);
  } catch (error) {
    console.warn(`Failed to load ${primary}, trying fallback`);
    try {
      return await loader.loadAsync(fallback);
    } catch (fallbackError) {
      console.error('Both primary and fallback failed');
      // Return default/placeholder model
      const geometry = new THREE.BoxGeometry();
      const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
      const placeholder = new THREE.Mesh(geometry, material);
      return { scene: placeholder };
    }
  }
}
```

### Retry Logic with Exponential Backoff

```javascript
async function loadWithRetry(url, maxRetries = 3) {
  const loader = new GLTFLoader();

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await loader.loadAsync(url);
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      console.warn(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

### Timeout with AbortController

```javascript
async function loadWithTimeout(url, timeoutMs = 10000) {
  const loader = new GLTFLoader();

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Load timeout')), timeoutMs);
  });

  const loadPromise = loader.loadAsync(url);

  return Promise.race([loadPromise, timeoutPromise]);
}
```

## Performance Tips

### Use Draco Compression

Reduces GLTF file size by 60-90% for geometry-heavy models. Requires DRACOLoader setup.

### Use KTX2/Basis Textures

GPU-compressed textures reduce memory usage and loading time. Requires KTX2Loader setup.

### Progressive Loading with Placeholders

```javascript
// Show low-res placeholder immediately
const placeholder = textureLoader.load('thumbnail.jpg');
material.map = placeholder;

// Load high-res in background
textureLoader.load('full-res.jpg', (texture) => {
  material.map = texture;
  material.needsUpdate = true;
  placeholder.dispose();
});
```

### Lazy Loading

Only load assets when needed:

```javascript
class LazyLoader {
  constructor() {
    this.loader = new GLTFLoader();
    this.loaded = new Map();
  }

  async getModel(name) {
    if (!this.loaded.has(name)) {
      const gltf = await this.loader.loadAsync(`models/${name}.glb`);
      this.loaded.set(name, gltf);
    }
    return this.loaded.get(name).scene.clone();
  }
}
```

### Enable Cache

```javascript
THREE.Cache.enabled = true;
```

Prevents re-downloading the same asset multiple times across your application.

## See Also

- [Geometry](geometry.md) - Geometry processing after loading models
- [Materials](materials.md) - Material setup for loaded models
- [Animation](animation.md) - Playing animations from loaded GLTF/FBX
- [Textures](textures.md) - Texture loading and compression formats
materials.md 15.7 KB
# Three.js Materials

## Material Types Overview

| Material Type | Use Case | Lighting Support |
|--------------|----------|------------------|
| MeshBasicMaterial | Unlit surfaces, sprites, skybox | None |
| MeshLambertMaterial | Low-cost lit surfaces | Diffuse only |
| MeshPhongMaterial | Shiny surfaces, legacy PBR | Diffuse + Specular |
| MeshStandardMaterial | Modern PBR workflow | Full PBR |
| MeshPhysicalMaterial | Advanced materials (glass, car paint) | Full PBR + Extensions |
| MeshToonMaterial | Cel-shaded cartoon style | Custom gradient |
| MeshNormalMaterial | Debugging normals | None |
| MeshDepthMaterial | Debugging depth, shadows | None |
| PointsMaterial | Particle systems | None |
| LineBasicMaterial | Lines and wireframes | None |
| LineDashedMaterial | Dashed lines | None |
| ShaderMaterial | Custom GLSL with helpers | Custom |
| RawShaderMaterial | Full manual GLSL control | Custom |

## MeshBasicMaterial

Unlit material, ignores all lights. Best for performance or flat designs.

```typescript
const material = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  transparent: true,
  opacity: 0.5,
  wireframe: true,
  map: texture,
  alphaMap: alphaTexture,
  envMap: cubeTexture, // Reflection without lighting
  combine: THREE.MultiplyOperation, // MixOperation, AddOperation
  reflectivity: 0.8,
  refractionRatio: 0.98
});
```

Common uses: Skyboxes, UI elements, debug wireframes, sprites.

## MeshLambertMaterial

Diffuse-only lighting model. Cheap but no specular highlights.

```typescript
const material = new THREE.MeshLambertMaterial({
  color: 0x00ff00,
  emissive: 0x222222, // Self-illumination
  emissiveIntensity: 0.5,
  map: texture,
  emissiveMap: emissiveTexture,
  envMap: cubeTexture
});
```

Use for matte surfaces where performance matters more than realism.

## MeshPhongMaterial

Adds specular highlights. Legacy but still useful for stylized looks.

```typescript
const material = new THREE.MeshPhongMaterial({
  color: 0x0000ff,
  specular: 0x555555, // Highlight color
  shininess: 30, // 0-100+, higher = tighter highlight
  emissive: 0x000000,
  map: texture,
  normalMap: normalTexture,
  normalScale: new THREE.Vector2(1, 1),
  bumpMap: bumpTexture,
  bumpScale: 1,
  displacementMap: dispTexture,
  displacementScale: 1,
  specularMap: specTexture,
  envMap: cubeTexture
});
```

Better than Lambert for shiny surfaces, cheaper than Standard.

## MeshStandardMaterial (PBR)

Modern physically-based rendering. Roughness/metalness workflow.

```typescript
const material = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  roughness: 0.5, // 0 = mirror, 1 = diffuse
  metalness: 0.0, // 0 = dielectric, 1 = metal

  // Full texture set
  map: diffuseTexture,
  roughnessMap: roughnessTexture,
  metalnessMap: metalnessTexture,
  normalMap: normalTexture,
  normalScale: new THREE.Vector2(1, 1),

  aoMap: aoTexture, // Requires UV2
  aoMapIntensity: 1.0,

  displacementMap: dispTexture,
  displacementScale: 1,

  emissive: 0x000000,
  emissiveMap: emissiveTexture,
  emissiveIntensity: 1,

  envMap: cubeTexture,
  envMapIntensity: 1.0
});

// AO map requires UV2 attribute
geometry.setAttribute('uv2', geometry.attributes.uv);
```

### Roughness/Metalness Workflows

**Roughness**: Controls surface microsurface detail
- 0.0 = Perfect mirror (glass, polished metal)
- 0.3 = Glossy plastic, wet surfaces
- 0.7 = Wood, stone
- 1.0 = Chalk, fabric

**Metalness**: Separates metals from dielectrics
- 0.0 = Non-metal (plastic, wood, skin, stone)
- 1.0 = Metal (iron, gold, copper)
- Avoid in-between values (0-1) unless fading

### Environment Map Setup

```typescript
// Cube texture
const cubeLoader = new THREE.CubeTextureLoader();
const envMap = cubeLoader.load([
  'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'
]);
material.envMap = envMap;

// Scene-wide environment (recommended)
scene.environment = envMap; // Auto-applies to all PBR materials
```

## MeshPhysicalMaterial (Advanced PBR)

Extends StandardMaterial with advanced effects.

```typescript
const material = new THREE.MeshPhysicalMaterial({
  // All StandardMaterial properties +

  // Clearcoat (car paint, varnish)
  clearcoat: 1.0, // 0-1
  clearcoatRoughness: 0.1,
  clearcoatMap: clearcoatTexture,
  clearcoatRoughnessMap: clearcoatRoughTexture,
  clearcoatNormalMap: clearcoatNormalTexture,
  clearcoatNormalScale: new THREE.Vector2(1, 1),

  // Transmission (glass, transparent materials)
  transmission: 1.0, // 0-1, use instead of opacity for glass
  transmissionMap: transmissionTexture,
  thickness: 0.5, // Volume thickness for refraction
  thicknessMap: thicknessTexture,
  ior: 1.5, // Index of refraction (glass = 1.5, water = 1.33)

  // Sheen (fabric, velvet)
  sheen: 1.0,
  sheenRoughness: 0.5,
  sheenColor: new THREE.Color(0xffffff),

  // Iridescence (soap bubbles, oil slicks)
  iridescence: 1.0,
  iridescenceIOR: 1.3,
  iridescenceThicknessRange: [100, 400],

  // Anisotropy (brushed metal)
  anisotropy: 1.0,
  anisotropyRotation: 0, // Radians

  // Specular tint (colored reflections)
  specularIntensity: 1.0,
  specularColor: new THREE.Color(0xffffff)
});
```

### Glass Example

```typescript
const glass = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  metalness: 0,
  roughness: 0,
  transmission: 1.0, // Fully transparent via refraction
  thickness: 0.5,
  ior: 1.5,
  envMapIntensity: 1.0,
  transparent: false // Use transmission instead
});
```

### Car Paint Example

```typescript
const carPaint = new THREE.MeshPhysicalMaterial({
  color: 0xff0000,
  metalness: 0.3,
  roughness: 0.2,
  clearcoat: 1.0,
  clearcoatRoughness: 0.1,
  envMapIntensity: 1.5
});
```

## MeshToonMaterial

Cel-shading with custom gradient control.

```typescript
// Create custom gradient for banding
const gradientData = new Uint8Array([0, 128, 255]); // 3-band gradient
const gradientTexture = new THREE.DataTexture(
  gradientData,
  gradientData.length,
  1,
  THREE.RedFormat
);
gradientTexture.minFilter = THREE.NearestFilter;
gradientTexture.magFilter = THREE.NearestFilter;
gradientTexture.needsUpdate = true;

const material = new THREE.MeshToonMaterial({
  color: 0xff6347,
  gradientMap: gradientTexture, // Controls shading bands
  map: texture,
  emissive: 0x000000,
  emissiveMap: emissiveTexture
});
```

Without gradientMap, defaults to 2-tone shading.

## MeshNormalMaterial / MeshDepthMaterial

Debug materials for visualizing geometry data.

```typescript
// Normals as RGB
const normalMat = new THREE.MeshNormalMaterial({
  flatShading: false
});

// Depth visualization
const depthMat = new THREE.MeshDepthMaterial({
  depthPacking: THREE.BasicDepthPacking // or RGBADepthPacking
});
```

Useful for debugging normal maps, depth sorting, and geometry issues.

## PointsMaterial

For particle systems and point clouds.

```typescript
const material = new THREE.PointsMaterial({
  color: 0xffffff,
  size: 0.1,
  sizeAttenuation: true, // Scale with distance
  map: texture, // Particle texture
  alphaMap: alphaTexture,
  transparent: true,
  opacity: 0.8,
  alphaTest: 0.5,
  depthWrite: false, // Prevent sorting issues
  blending: THREE.AdditiveBlending // For glowing effects
});

const points = new THREE.Points(geometry, material);
```

## LineBasicMaterial, LineDashedMaterial

```typescript
const basicLine = new THREE.LineBasicMaterial({
  color: 0x0000ff,
  linewidth: 1 // Note: Only works on some platforms
});

const dashedLine = new THREE.LineDashedMaterial({
  color: 0xff0000,
  dashSize: 3,
  gapSize: 1,
  linewidth: 1
});

// LineDashedMaterial requires computeLineDistances
const line = new THREE.Line(geometry, dashedLine);
line.computeLineDistances();
```

## ShaderMaterial

Custom GLSL with automatic uniform handling.

```typescript
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 },
    color: { value: new THREE.Color(0xff0000) },
    texture1: { value: texture }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float time;
    uniform vec3 color;
    uniform sampler2D texture1;
    varying vec2 vUv;

    void main() {
      vec4 texColor = texture2D(texture1, vUv);
      gl_FragColor = vec4(color * texColor.rgb, 1.0);
    }
  `,
  transparent: true,
  side: THREE.DoubleSide
});

// Update uniforms in animation loop
material.uniforms.time.value = clock.getElapsedTime();
```

### Built-in Uniforms (Automatically Provided)

```glsl
// Matrices
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat3 normalMatrix;

// Camera
uniform vec3 cameraPosition;

// Fog
uniform vec3 fogColor;
uniform float fogNear;
uniform float fogFar;

// Time (if USE_TIME defined)
uniform float time;
```

### Built-in Attributes

```glsl
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
attribute vec2 uv2;
attribute vec4 color;
attribute vec3 tangent;
```

## RawShaderMaterial

Full manual control, no automatic uniforms or attributes.

```typescript
const material = new THREE.RawShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    precision mediump float;
    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;
    attribute vec3 position;

    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    precision mediump float;

    void main() {
      gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
  `
});
```

Use when you need full control or are targeting WebGL2.

## Common Material Properties

```typescript
const material = new THREE.MeshStandardMaterial({
  // Visibility
  visible: true,

  // Transparency
  transparent: false,
  opacity: 1.0,
  alphaTest: 0.5, // Discard pixels below threshold (0-1)

  // Rendering
  side: THREE.FrontSide, // BackSide, DoubleSide
  depthTest: true,
  depthWrite: true,

  // Blending
  blending: THREE.NormalBlending,
  // NoBlending, AdditiveBlending, SubtractiveBlending, MultiplyBlending

  // Advanced
  flatShading: false,
  fog: true, // Affected by scene fog
  toneMapped: true,

  // Stencil buffer
  stencilWrite: false,
  stencilFunc: THREE.AlwaysStencilFunc,
  stencilRef: 0,
  stencilMask: 0xff,

  // Polygon offset (Z-fighting fix)
  polygonOffset: false,
  polygonOffsetFactor: 0,
  polygonOffsetUnits: 0
});
```

### Side Rendering

- `THREE.FrontSide`: Default, cull back faces
- `THREE.BackSide`: Render only back faces (interior views)
- `THREE.DoubleSide`: Render both sides (slower, avoid if possible)

### Alpha Blending Modes

```typescript
// Solid (default)
blending: THREE.NormalBlending

// Additive (glow effects)
blending: THREE.AdditiveBlending
depthWrite: false

// Multiply (shadows, tint)
blending: THREE.MultiplyBlending

// Custom
blending: THREE.CustomBlending
blendSrc: THREE.SrcAlphaFactor
blendDst: THREE.OneMinusSrcAlphaFactor
```

## Multiple Materials

Use geometry groups with material array.

```typescript
const geometry = new THREE.BoxGeometry(1, 1, 1);

// Define groups (start, count, materialIndex)
geometry.clearGroups();
geometry.addGroup(0, 6, 0);  // First 6 vertices use material[0]
geometry.addGroup(6, 6, 1);  // Next 6 use material[1]

const materials = [
  new THREE.MeshStandardMaterial({ color: 0xff0000 }),
  new THREE.MeshStandardMaterial({ color: 0x0000ff })
];

const mesh = new THREE.Mesh(geometry, materials);
```

Common use: Different materials per face or mesh section.

## Environment Maps

### Cube Texture Loader

```typescript
const loader = new THREE.CubeTextureLoader();
const envMap = loader.load([
  'px.jpg', 'nx.jpg', // positive/negative X
  'py.jpg', 'ny.jpg', // positive/negative Y
  'pz.jpg', 'nz.jpg'  // positive/negative Z
]);

material.envMap = envMap;
```

### HDR Environment Maps

```typescript
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';

const loader = new RGBELoader();
loader.load('environment.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping;

  // Apply to scene (recommended)
  scene.environment = texture;
  scene.background = texture; // Optional: use as skybox

  // Or apply to individual materials
  material.envMap = texture;
});
```

### Scene Environment (Recommended)

```typescript
// Auto-applies to all PBR materials
scene.environment = envMap;

// Control intensity per material
material.envMapIntensity = 1.5;
```

## Material Cloning and needsUpdate

```typescript
// Clone material (shares textures, copies properties)
const material2 = material.clone();

// Modify cloned material
material2.color.set(0x00ff00);

// Force shader recompilation when changing certain properties
material.needsUpdate = true; // Required after changing:
// - vertexShader/fragmentShader
// - uniforms structure
// - side, flatShading, fog
// - Adding/removing maps
```

### When needsUpdate is Required

Changes that need `material.needsUpdate = true`:
- Changing shader code
- Adding/removing texture maps
- Changing `side`, `flatShading`, `fog`
- Changing material type properties that affect shader compilation

Changes that don't need it:
- Uniform values (`material.uniforms.time.value`)
- Color values (`material.color.set()`)
- Numeric properties (`roughness`, `metalness`, `opacity`)

## Performance Tips

### Material Reuse for Batching

```typescript
// Share material across meshes for better batching
const sharedMat = new THREE.MeshStandardMaterial({ color: 0xff0000 });

for (let i = 0; i < 1000; i++) {
  const mesh = new THREE.Mesh(geometry, sharedMat); // Reuse
  scene.add(mesh);
}
```

### Alpha Test vs Transparency

```typescript
// Faster: Use alphaTest for cutout textures (leaves, grass)
const cutoutMat = new THREE.MeshStandardMaterial({
  map: texture,
  alphaTest: 0.5,
  transparent: false, // Not needed with alphaTest
  side: THREE.DoubleSide
});

// Slower: Full transparency requires sorting
const transparentMat = new THREE.MeshStandardMaterial({
  transparent: true,
  opacity: 0.5,
  depthWrite: false
});
```

### Use Simplest Material Possible

Performance hierarchy (fast to slow):
1. MeshBasicMaterial (no lighting)
2. MeshLambertMaterial (diffuse only)
3. MeshPhongMaterial (specular)
4. MeshStandardMaterial (PBR)
5. MeshPhysicalMaterial (advanced PBR)
6. ShaderMaterial (custom)

### Material Pooling Pattern

```typescript
class MaterialPool {
  private pool: Map<string, THREE.Material> = new Map();

  get(key: string, factory: () => THREE.Material): THREE.Material {
    if (!this.pool.has(key)) {
      this.pool.set(key, factory());
    }
    return this.pool.get(key)!;
  }

  dispose() {
    this.pool.forEach(mat => mat.dispose());
    this.pool.clear();
  }
}

// Usage
const pool = new MaterialPool();
const mat = pool.get('red-metal', () =>
  new THREE.MeshStandardMaterial({
    color: 0xff0000,
    metalness: 1,
    roughness: 0.2
  })
);
```

### Texture Optimization

```typescript
// Reuse textures across materials
const texture = textureLoader.load('diffuse.jpg');
texture.minFilter = THREE.LinearMipMapLinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

// Share across materials
const mat1 = new THREE.MeshStandardMaterial({ map: texture });
const mat2 = new THREE.MeshStandardMaterial({ map: texture });

// Dispose when done
material.dispose();
texture.dispose();
```

### Avoid DoubleSide When Possible

```typescript
// Slower: Renders twice
material.side = THREE.DoubleSide;

// Faster: Duplicate geometry with flipped normals if needed
geometry = geometry.clone();
geometry.scale(-1, 1, 1); // Flip inside-out
```

## See Also

- [Textures](textures.md) - Texture loading, UV mapping, and memory management
- [Lighting & Shadows](lighting-and-shadows.md) - How lights interact with materials
- [Shaders](shaders.md) - Custom ShaderMaterial and extending built-in materials
- [Geometry](geometry.md) - Geometry types that materials are applied to
postprocessing.md 15.1 KB
# Three.js Post-Processing

## EffectComposer Setup

```javascript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';

// Create composer
const composer = new EffectComposer(renderer);

// ALWAYS add RenderPass first
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// Add other passes...

// In animation loop: use composer.render() instead of renderer.render()
function animate() {
  requestAnimationFrame(animate);
  composer.render(); // NOT renderer.render(scene, camera)
}

// Handle resize
window.addEventListener('resize', () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
});
```

## Common Effects

### Bloom (UnrealBloomPass)

```javascript
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5,    // strength
  0.4,    // radius
  0.85    // threshold (0-1, higher = less bloom)
);
composer.addPass(bloomPass);

// Adjustable properties
bloomPass.strength = 1.5;
bloomPass.radius = 0.4;
bloomPass.threshold = 0.85;
```

### Selective Bloom (Layer-based)

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';

// Setup layers
const BLOOM_LAYER = 1;
const bloomLayer = new THREE.Layers();
bloomLayer.set(BLOOM_LAYER);

// Mark objects for bloom
glowingMesh.layers.enable(BLOOM_LAYER);

// Create two composers
const finalComposer = new EffectComposer(renderer);
const bloomComposer = new EffectComposer(renderer);

// Bloom composer setup
bloomComposer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5, 0.4, 0.85
);
bloomComposer.addPass(bloomPass);

// Final composer setup
finalComposer.addPass(new RenderPass(scene, camera));
const mixPass = new ShaderPass(
  new THREE.ShaderMaterial({
    uniforms: {
      baseTexture: { value: null },
      bloomTexture: { value: bloomComposer.renderTarget2.texture }
    },
    vertexShader: `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform sampler2D baseTexture;
      uniform sampler2D bloomTexture;
      varying vec2 vUv;
      void main() {
        gl_FragColor = texture2D(baseTexture, vUv) + vec4(1.0) * texture2D(bloomTexture, vUv);
      }
    `
  }), 'baseTexture'
);
mixPass.needsSwap = true;
finalComposer.addPass(mixPass);

// Render function with darken/restore pattern
function render() {
  // Render bloom
  camera.layers.set(BLOOM_LAYER);
  bloomComposer.render();

  // Render final scene
  camera.layers.set(0);
  finalComposer.render();
}
```

### FXAA (Fast Approximate Anti-Aliasing)

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';

const fxaaPass = new ShaderPass(FXAAShader);
const pixelRatio = renderer.getPixelRatio();

fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);

composer.addPass(fxaaPass);

// Update on resize
function onResize() {
  const pixelRatio = renderer.getPixelRatio();
  fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
  fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
}
```

### SMAA (Subpixel Morphological Anti-Aliasing)

```javascript
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass';

const smaaPass = new SMAAPass(
  window.innerWidth * renderer.getPixelRatio(),
  window.innerHeight * renderer.getPixelRatio()
);
composer.addPass(smaaPass);

// Update on resize
function onResize() {
  smaaPass.setSize(
    window.innerWidth * renderer.getPixelRatio(),
    window.innerHeight * renderer.getPixelRatio()
  );
}
```

### SSAO (Screen Space Ambient Occlusion)

```javascript
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass';

const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
ssaoPass.kernelRadius = 16;
ssaoPass.minDistance = 0.005;
ssaoPass.maxDistance = 0.1;
ssaoPass.output = SSAOPass.OUTPUT.Default; // Default, SSAO, Blur, Beauty, Depth, Normal

composer.addPass(ssaoPass);
```

### Depth of Field (BokehPass)

```javascript
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass';

const bokehPass = new BokehPass(scene, camera, {
  focus: 1.0,       // focus distance
  aperture: 0.025,  // aperture size (lower = more blur)
  maxblur: 0.01     // max blur amount
});

composer.addPass(bokehPass);

// Adjustable
bokehPass.uniforms['focus'].value = 1.0;
bokehPass.uniforms['aperture'].value = 0.025;
bokehPass.uniforms['maxblur'].value = 0.01;
```

### Film Grain

```javascript
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass';

const filmPass = new FilmPass(
  0.35,   // noise intensity
  0.025,  // scanline intensity
  648,    // scanline count
  false   // grayscale
);

composer.addPass(filmPass);
```

### Vignette

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { VignetteShader } from 'three/examples/jsm/shaders/VignetteShader';

const vignettePass = new ShaderPass(VignetteShader);
vignettePass.uniforms['offset'].value = 1.0;   // vignette offset
vignettePass.uniforms['darkness'].value = 1.0; // vignette darkness

composer.addPass(vignettePass);
```

### Color Correction

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { ColorCorrectionShader } from 'three/examples/jsm/shaders/ColorCorrectionShader';

const colorCorrectionPass = new ShaderPass(ColorCorrectionShader);
colorCorrectionPass.uniforms['powRGB'].value = new THREE.Vector3(2.0, 2.0, 2.0);
colorCorrectionPass.uniforms['mulRGB'].value = new THREE.Vector3(1.0, 1.0, 1.0);
colorCorrectionPass.uniforms['addRGB'].value = new THREE.Vector3(0.0, 0.0, 0.0);

composer.addPass(colorCorrectionPass);
```

### Gamma Correction

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader';

const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaCorrectionPass);
```

### Pixelation

```javascript
import { RenderPixelatedPass } from 'three/examples/jsm/postprocessing/RenderPixelatedPass';

const pixelPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size
composer.addPass(pixelPass);
```

### Glitch Effect

```javascript
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass';

const glitchPass = new GlitchPass();
glitchPass.goWild = false; // enable for constant glitching

composer.addPass(glitchPass);
```

### Halftone Effect

```javascript
import { HalftonePass } from 'three/examples/jsm/postprocessing/HalftonePass';

const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, {
  shape: 1,           // 1 = dot, 2 = ellipse, 3 = line, 4 = square
  radius: 4,          // dot radius
  rotateR: Math.PI / 12,  // rotation angles for CMYK
  rotateG: Math.PI / 12 * 2,
  rotateB: Math.PI / 12 * 3,
  scatter: 0          // scatter amount
});

composer.addPass(halftonePass);
```

### Outline Effect

```javascript
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';

const outlinePass = new OutlinePass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  scene,
  camera
);

outlinePass.edgeStrength = 3.0;
outlinePass.edgeGlow = 0.0;
outlinePass.edgeThickness = 1.0;
outlinePass.pulsePeriod = 0;
outlinePass.visibleEdgeColor.set('#ffffff');
outlinePass.hiddenEdgeColor.set('#190a05');

// Add objects to outline
outlinePass.selectedObjects = [mesh1, mesh2];

composer.addPass(outlinePass);
```

## Custom ShaderPass

### Basic Custom Effect Structure

```javascript
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';

const customShader = {
  uniforms: {
    tDiffuse: { value: null },  // REQUIRED: input texture from previous pass
    amount: { value: 1.0 }      // custom uniforms
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform float amount;
    varying vec2 vUv;

    void main() {
      vec4 color = texture2D(tDiffuse, vUv);
      // Apply effect...
      gl_FragColor = color;
    }
  `
};

const customPass = new ShaderPass(customShader);
composer.addPass(customPass);
```

### Invert Colors Example

```javascript
const invertShader = {
  uniforms: {
    tDiffuse: { value: null }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    varying vec2 vUv;

    void main() {
      vec4 color = texture2D(tDiffuse, vUv);
      gl_FragColor = vec4(1.0 - color.rgb, color.a);
    }
  `
};

const invertPass = new ShaderPass(invertShader);
composer.addPass(invertPass);
```

### Chromatic Aberration Example

```javascript
const chromaticAberrationShader = {
  uniforms: {
    tDiffuse: { value: null },
    amount: { value: 0.005 }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform float amount;
    varying vec2 vUv;

    void main() {
      vec2 offset = vec2(amount, 0.0);
      vec4 cr = texture2D(tDiffuse, vUv + offset);
      vec4 cga = texture2D(tDiffuse, vUv);
      vec4 cb = texture2D(tDiffuse, vUv - offset);

      gl_FragColor = vec4(cr.r, cga.g, cb.b, cga.a);
    }
  `
};

const chromaticPass = new ShaderPass(chromaticAberrationShader);
composer.addPass(chromaticPass);
```

## Combining Multiple Effects

```javascript
// Example pipeline: RenderPass > Bloom > Vignette > Gamma > FXAA
// ALWAYS add anti-aliasing (FXAA/SMAA) LAST

const composer = new EffectComposer(renderer);

// 1. RenderPass (always first)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// 2. Bloom
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5, 0.4, 0.85
);
composer.addPass(bloomPass);

// 3. Vignette
const vignettePass = new ShaderPass(VignetteShader);
vignettePass.uniforms['offset'].value = 1.0;
vignettePass.uniforms['darkness'].value = 1.0;
composer.addPass(vignettePass);

// 4. Gamma correction
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaCorrectionPass);

// 5. FXAA (always last for anti-aliasing)
const fxaaPass = new ShaderPass(FXAAShader);
const pixelRatio = renderer.getPixelRatio();
fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
composer.addPass(fxaaPass);
```

## Render to Texture (WebGLRenderTarget)

```javascript
// Create render target
const renderTarget = new THREE.WebGLRenderTarget(
  window.innerWidth,
  window.innerHeight,
  {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBAFormat,
    stencilBuffer: false
  }
);

// Use with composer
const composer = new EffectComposer(renderer, renderTarget);

// Manual render to texture
renderer.setRenderTarget(renderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);

// Use texture in material
const material = new THREE.MeshBasicMaterial({
  map: renderTarget.texture
});
```

## Multi-Pass Rendering (Multiple Composers)

```javascript
// Different scenes/layers
const mainComposer = new EffectComposer(renderer);
mainComposer.addPass(new RenderPass(mainScene, camera));

const uiComposer = new EffectComposer(renderer);
uiComposer.addPass(new RenderPass(uiScene, camera));

// Render in order
function animate() {
  requestAnimationFrame(animate);

  // Render main scene with effects
  mainComposer.render();

  // Render UI on top without clearing
  renderer.autoClear = false;
  uiComposer.render();
  renderer.autoClear = true;
}
```

## WebGPU Post-Processing (Node-based, r150+)

```javascript
import { pass, bloom, fxaa, output } from 'three/nodes';

// Create post-processing chain using nodes
const scenePass = pass(scene, camera);
const bloomPass = bloom(scenePass, 1.5, 0.4, 0.85);
const fxaaPass = fxaa(bloomPass);

// Use in render loop
renderer.compute(fxaaPass);
renderer.render(scene, camera);

// Or use postProcessing chain
const postProcessing = scenePass.bloom(1.5, 0.4, 0.85).fxaa();
renderer.compute(postProcessing);
```

## Performance Tips

1. **Limit passes**: Each pass = full-screen render. Combine shaders when possible.

2. **Lower resolution for blur effects**:
```javascript
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2), // half resolution
  1.5, 0.4, 0.85
);
```

3. **Toggle effects conditionally**:
```javascript
bloomPass.enabled = highQualityMode;
```

4. **FXAA over MSAA**: FXAA is cheaper than multi-sample anti-aliasing.

5. **Profile and disable**: Use Chrome DevTools Performance to identify bottlenecks.

6. **Reuse render targets**:
```javascript
const renderTarget = new THREE.WebGLRenderTarget(width, height);
composer1.renderTarget1 = renderTarget;
composer2.renderTarget1 = renderTarget;
```

7. **Adaptive quality**:
```javascript
function updateQuality(fps) {
  if (fps < 30) {
    bloomPass.enabled = false;
    ssaoPass.enabled = false;
  }
}
```

## Handle Resize

```javascript
function onWindowResize() {
  const width = window.innerWidth;
  const height = window.innerHeight;

  // Update camera
  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(width, height);

  // Update composer
  composer.setSize(width, height);

  // Update FXAA resolution
  const pixelRatio = renderer.getPixelRatio();
  fxaaPass.material.uniforms['resolution'].value.x = 1 / (width * pixelRatio);
  fxaaPass.material.uniforms['resolution'].value.y = 1 / (height * pixelRatio);

  // Update bloom resolution (if using lower res)
  bloomPass.resolution.set(width / 2, height / 2);

  // Update SMAA
  smaaPass.setSize(width * pixelRatio, height * pixelRatio);

  // Update SSAO
  ssaoPass.setSize(width, height);

  // Update render targets
  renderTarget.setSize(width, height);
}

window.addEventListener('resize', onWindowResize);
```

## See Also

- [Shaders](shaders.md) - Writing custom GLSL for ShaderPass
- [Lighting & Shadows](lighting-and-shadows.md) - Light sources that drive bloom/SSAO
- [Textures](textures.md) - Render targets and framebuffer textures
- [Fundamentals](fundamentals.md) - Renderer setup and pixel ratio
shaders.md 12.5 KB
# Three.js Shaders

## ShaderMaterial vs RawShaderMaterial

**ShaderMaterial**: Auto-provides common uniforms and attributes, adds precision statements.

```javascript
const material = new THREE.ShaderMaterial({
  vertexShader: `
    // Auto-provided: position, normal, uv attributes
    // Auto-provided: modelMatrix, viewMatrix, projectionMatrix, etc.
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    varying vec2 vUv;
    void main() {
      gl_FragColor = vec4(vUv, 0.0, 1.0);
    }
  `
});
```

**RawShaderMaterial**: Requires manual declaration of everything.

```javascript
const material = new THREE.RawShaderMaterial({
  vertexShader: `
    precision mediump float;
    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;
    attribute vec3 position;
    attribute vec2 uv;
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    precision mediump float;
    varying vec2 vUv;
    void main() {
      gl_FragColor = vec4(vUv, 0.0, 1.0);
    }
  `
});
```

## Uniforms

All uniform types supported:

```javascript
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0.0 },
    uSpeed: { value: 1.0 },
    uResolution: { value: new THREE.Vector2(800, 600) },
    uColor: { value: new THREE.Color(0xff0000) },
    uTexture: { value: texture },
    uMatrix: { value: new THREE.Matrix4() },
    uFloatArray: { value: [1.0, 2.0, 3.0] },
    uVec3Array: { value: [new THREE.Vector3(), new THREE.Vector3()] }
  },
  vertexShader: `...`,
  fragmentShader: `
    uniform float uTime;
    uniform vec2 uResolution;
    uniform vec3 uColor;
    uniform sampler2D uTexture;
    void main() {
      vec4 texColor = texture2D(uTexture, vUv);
      gl_FragColor = vec4(uColor * texColor.rgb, 1.0);
    }
  `
});

// Update at runtime
material.uniforms.uTime.value += deltaTime;
material.uniforms.uColor.value.set(0x00ff00);
```

## Varyings

Pass data from vertex to fragment shader:

```javascript
vertexShader: `
  varying vec2 vUv;
  varying vec3 vNormal;
  varying vec3 vPosition;
  varying vec3 vWorldPosition;

  void main() {
    vUv = uv;
    vNormal = normalize(normalMatrix * normal);
    vPosition = position;
    vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`,
fragmentShader: `
  varying vec2 vUv;
  varying vec3 vNormal;
  varying vec3 vPosition;
  varying vec3 vWorldPosition;

  void main() {
    gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);
  }
`
```

## Common Shader Patterns

### Texture Sampling

```glsl
uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
  vec4 texColor = texture2D(uTexture, vUv);
  gl_FragColor = texColor;
}
```

### Vertex Displacement (Wave)

```glsl
uniform float uTime;
varying vec2 vUv;

void main() {
  vUv = uv;
  vec3 pos = position;
  pos.z += sin(pos.x * 5.0 + uTime) * 0.1;
  pos.z += cos(pos.y * 5.0 + uTime) * 0.1;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
```

### Fresnel Effect

```glsl
varying vec3 vNormal;
varying vec3 vPosition;
uniform vec3 cameraPosition;

void main() {
  vec3 viewDirection = normalize(cameraPosition - vPosition);
  float fresnel = pow(1.0 - dot(vNormal, viewDirection), 3.0);
  gl_FragColor = vec4(vec3(fresnel), 1.0);
}
```

### Noise-Based Effects

```glsl
// Random function
float random(vec2 st) {
  return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

// Value noise
float noise(vec2 st) {
  vec2 i = floor(st);
  vec2 f = fract(st);
  float a = random(i);
  float b = random(i + vec2(1.0, 0.0));
  float c = random(i + vec2(0.0, 1.0));
  float d = random(i + vec2(1.0, 1.0));
  vec2 u = smoothstep(0.0, 1.0, f);
  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

void main() {
  float n = noise(vUv * 10.0);
  gl_FragColor = vec4(vec3(n), 1.0);
}
```

### Gradients

```glsl
// Linear gradient
void main() {
  vec3 colorA = vec3(1.0, 0.0, 0.0);
  vec3 colorB = vec3(0.0, 0.0, 1.0);
  vec3 color = mix(colorA, colorB, vUv.x);
  gl_FragColor = vec4(color, 1.0);
}

// Radial gradient
void main() {
  float dist = distance(vUv, vec2(0.5));
  vec3 color = mix(vec3(1.0), vec3(0.0), smoothstep(0.0, 0.5, dist));
  gl_FragColor = vec4(color, 1.0);
}
```

### Rim Lighting

```glsl
varying vec3 vNormal;
varying vec3 vPosition;
uniform vec3 cameraPosition;

void main() {
  vec3 viewDirection = normalize(cameraPosition - vPosition);
  float rimPower = 2.0;
  float rim = 1.0 - max(0.0, dot(vNormal, viewDirection));
  rim = pow(rim, rimPower);
  vec3 rimColor = vec3(0.0, 1.0, 1.0);
  gl_FragColor = vec4(rimColor * rim, 1.0);
}
```

### Dissolve Effect with Edge Glow

```glsl
uniform float uProgress;
uniform sampler2D uNoiseTexture;
varying vec2 vUv;

void main() {
  float noise = texture2D(uNoiseTexture, vUv).r;
  float threshold = uProgress;
  float edge = 0.05;

  if (noise < threshold) discard;

  float edgeFactor = smoothstep(threshold, threshold + edge, noise);
  vec3 edgeColor = vec3(1.0, 0.5, 0.0);
  vec3 baseColor = vec3(1.0);
  vec3 color = mix(edgeColor, baseColor, edgeFactor);

  gl_FragColor = vec4(color, 1.0);
}
```

## Extending Built-in Materials

Use `onBeforeCompile` to modify existing materials:

```javascript
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });

material.onBeforeCompile = (shader) => {
  // Add custom uniforms
  shader.uniforms.uTime = { value: 0.0 };

  // Modify vertex shader
  shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
    #include <begin_vertex>
    transformed.z += sin(transformed.x * 5.0 + uTime) * 0.1;
    `
  );

  // Modify fragment shader
  shader.fragmentShader = shader.fragmentShader.replace(
    '#include <color_fragment>',
    `
    #include <color_fragment>
    diffuseColor.rgb *= vec3(vUv, 1.0);
    `
  );

  // Store reference for updates
  material.userData.shader = shader;
};

// Update in animation loop
material.userData.shader.uniforms.uTime.value += deltaTime;
```

Common injection points:
- `#include <begin_vertex>` - Modify vertex position
- `#include <beginnormal_vertex>` - Modify normals
- `#include <color_fragment>` - Modify diffuse color
- `#include <emissivemap_fragment>` - Add emissive effects
- `#include <roughnessmap_fragment>` - Modify roughness
- `#include <metalnessmap_fragment>` - Modify metalness

## GLSL Built-in Functions

### Math Functions

```glsl
abs(x)           // Absolute value
sign(x)          // -1, 0, or 1
floor(x)         // Round down
ceil(x)          // Round up
fract(x)         // Fractional part
mod(x, y)        // Modulo
min(x, y)        // Minimum
max(x, y)        // Maximum
clamp(x, min, max) // Constrain value
mix(a, b, t)     // Linear interpolation
step(edge, x)    // 0 if x < edge, else 1
smoothstep(e0, e1, x) // Smooth interpolation

// Trigonometry
sin(x), cos(x), tan(x)
asin(x), acos(x), atan(x, y)
radians(deg), degrees(rad)

// Exponential
pow(x, y)        // x^y
exp(x)           // e^x
log(x)           // Natural log
sqrt(x)          // Square root
```

### Vector Operations

```glsl
length(v)        // Vector length
distance(a, b)   // Distance between points
dot(a, b)        // Dot product
cross(a, b)      // Cross product (vec3)
normalize(v)     // Unit vector
reflect(I, N)    // Reflection vector
refract(I, N, eta) // Refraction vector
```

### Texture Functions

```glsl
// GLSL 1.0 (default)
texture2D(sampler2D, vec2)
textureCube(samplerCube, vec3)

// GLSL 3.0
texture(sampler2D, vec2)
texture(samplerCube, vec3)
```

## Material Properties

```javascript
const material = new THREE.ShaderMaterial({
  uniforms: { /* ... */ },
  vertexShader: `...`,
  fragmentShader: `...`,

  // Transparency
  transparent: true,
  opacity: 0.5,

  // Rendering
  side: THREE.DoubleSide, // FrontSide, BackSide, DoubleSide
  depthTest: true,
  depthWrite: true,

  // Blending
  blending: THREE.NormalBlending, // AdditiveBlending, SubtractiveBlending, MultiplyBlending

  // Extensions
  extensions: {
    derivatives: true,      // #extension GL_OES_standard_derivatives
    fragDepth: false,
    drawBuffers: false,
    shaderTextureLOD: false
  },

  // GLSL version
  glslVersion: THREE.GLSL3 // Use GLSL 3.0
});
```

## Shader Includes

### Using ShaderChunk

```javascript
import { ShaderChunk } from 'three';

const customChunk = `
  float customFunction(float x) {
    return sin(x) * 0.5 + 0.5;
  }
`;

ShaderChunk.customChunk = customChunk;

const shader = `
  #include <customChunk>

  void main() {
    float value = customFunction(vUv.x);
    gl_FragColor = vec4(vec3(value), 1.0);
  }
`;
```

### External .glsl Files (with bundlers)

```javascript
// With Vite or webpack
import vertexShader from './shaders/vertex.glsl';
import fragmentShader from './shaders/fragment.glsl';

const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader
});
```

## Instanced Shaders

```javascript
const geometry = new THREE.PlaneGeometry(1, 1);
const instanceCount = 100;

// Create instanced attributes
const offsets = new Float32Array(instanceCount * 3);
const colors = new Float32Array(instanceCount * 3);

for (let i = 0; i < instanceCount; i++) {
  offsets[i * 3] = Math.random() * 10 - 5;
  offsets[i * 3 + 1] = Math.random() * 10 - 5;
  offsets[i * 3 + 2] = Math.random() * 10 - 5;

  colors[i * 3] = Math.random();
  colors[i * 3 + 1] = Math.random();
  colors[i * 3 + 2] = Math.random();
}

geometry.setAttribute('aOffset', new THREE.InstancedBufferAttribute(offsets, 3));
geometry.setAttribute('aColor', new THREE.InstancedBufferAttribute(colors, 3));

const material = new THREE.ShaderMaterial({
  vertexShader: `
    attribute vec3 aOffset;
    attribute vec3 aColor;
    varying vec3 vColor;

    void main() {
      vColor = aColor;
      vec3 pos = position + aOffset;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
  `,
  fragmentShader: `
    varying vec3 vColor;
    void main() {
      gl_FragColor = vec4(vColor, 1.0);
    }
  `
});

const mesh = new THREE.InstancedMesh(geometry, material, instanceCount);
```

## Debugging

### Log Shader Code

```javascript
material.onBeforeCompile = (shader) => {
  console.log('Vertex Shader:', shader.vertexShader);
  console.log('Fragment Shader:', shader.fragmentShader);
};
```

### Visual Debugging

```glsl
// Output coordinates as color
void main() {
  gl_FragColor = vec4(vUv, 0.0, 1.0); // See UV mapping
  // gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); // See normals
  // gl_FragColor = vec4(vPosition * 0.5 + 0.5, 1.0); // See positions
}
```

### Check Shader Errors

```javascript
renderer.checkShaderErrors = true; // Default is true in dev

// Catch compilation errors
material.onBeforeCompile = (shader) => {
  shader.fragmentShader = shader.fragmentShader.replace(
    'void main()',
    `
    void main() {
      #ifdef GL_ES
      precision mediump float;
      #endif
    `
  );
};
```

## Performance Tips

1. **Minimize uniforms**: Bundle related data into vectors/arrays
2. **Avoid conditionals**: Use `mix()` and `step()` instead of if/else
3. **Precalculate in JavaScript**: Move static calculations to CPU
4. **Lookup textures**: Use textures for complex functions (gradients, noise)
5. **Limit overdraw**: Use `depthTest` and `depthWrite` appropriately
6. **Reduce varying count**: Only pass necessary data to fragment shader

### Key Pattern: Replace Conditionals

```glsl
// BAD: Branching hurts performance
if (value > 0.5) {
  color = colorA;
} else {
  color = colorB;
}

// GOOD: Use step() and mix()
float threshold = step(0.5, value);
color = mix(colorB, colorA, threshold);

// BETTER: Use smoothstep() for smooth transitions
float threshold = smoothstep(0.4, 0.6, value);
color = mix(colorB, colorA, threshold);
```

### Optimize Texture Lookups

```glsl
// BAD: Multiple lookups
vec4 tex1 = texture2D(uTexture, vUv);
vec4 tex2 = texture2D(uTexture, vUv);
float value = tex1.r + tex2.g;

// GOOD: Single lookup
vec4 tex = texture2D(uTexture, vUv);
float value = tex.r + tex.g;
```

### Precalculate Constants

```glsl
// BAD: Calculated every fragment
float pi = 3.14159;
float angle = pi * 2.0 * vUv.x;

// GOOD: Define as constant
const float TWO_PI = 6.28318;
float angle = TWO_PI * vUv.x;
```

## See Also

- [Materials](materials.md) - Built-in materials that shaders can extend
- [Postprocessing](postprocessing.md) - Custom ShaderPass for post-processing effects
- [Textures](textures.md) - Texture sampling and data textures in shaders
- [Animation](animation.md) - Animating shader uniforms over time
textures.md 13.7 KB
# Three.js Textures

## Texture Loading

### TextureLoader with callbacks
```typescript
const loader = new THREE.TextureLoader();
loader.load(
  'texture.jpg',
  (texture) => { /* success */ },
  (progress) => { /* loading */ },
  (error) => { /* error */ }
);
```

### Promise wrapper for async/await
```typescript
function loadTexture(url: string): Promise<THREE.Texture> {
  return new Promise((resolve, reject) => {
    new THREE.TextureLoader().load(url, resolve, undefined, reject);
  });
}

// Usage
const texture = await loadTexture('texture.jpg');
```

### LoadingManager for multiple textures
```typescript
const manager = new THREE.LoadingManager();
manager.onLoad = () => console.log('All loaded');
manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total}`);

const loader = new THREE.TextureLoader(manager);
const tex1 = loader.load('tex1.jpg');
const tex2 = loader.load('tex2.jpg');
```

## Texture Configuration

### Color space - CRITICAL
```typescript
// Color maps (albedo, emissive) - ALWAYS use SRGBColorSpace
texture.colorSpace = THREE.SRGBColorSpace;

// Data maps (normal, roughness, metalness, ao, displacement) - NO color space
normalMap.colorSpace = THREE.NoColorSpace; // default
```

### Wrapping modes
```typescript
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// Options: ClampToEdgeWrapping (default), RepeatWrapping, MirroredRepeatWrapping

texture.repeat.set(4, 4);
texture.offset.set(0.5, 0.5);
texture.rotation = Math.PI / 4; // radians
texture.center.set(0.5, 0.5); // rotation pivot
```

### Filtering
```typescript
// Minification (texture smaller than surface)
texture.minFilter = THREE.LinearMipmapLinearFilter; // default, best quality
// Options: NearestFilter, LinearFilter, NearestMipmapNearestFilter, etc.

// Magnification (texture larger than surface)
texture.magFilter = THREE.LinearFilter; // default
// Options: NearestFilter, LinearFilter

// Anisotropic filtering (better quality at angles)
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
texture.anisotropy = maxAnisotropy; // typically 16
```

### Mipmaps
```typescript
texture.generateMipmaps = true; // default
texture.minFilter = THREE.LinearMipmapLinearFilter;

// Disable for non-power-of-2 or custom mipmaps
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;
```

## Texture Types

### Regular Texture
```typescript
const texture = new THREE.TextureLoader().load('image.jpg');
```

### DataTexture (raw pixel data)
```typescript
const width = 256, height = 256;
const size = width * height;
const data = new Uint8Array(4 * size); // RGBA

for (let i = 0; i < size; i++) {
  const stride = i * 4;
  data[stride] = 255;     // R
  data[stride + 1] = 0;   // G
  data[stride + 2] = 0;   // B
  data[stride + 3] = 255; // A
}

const texture = new THREE.DataTexture(data, width, height);
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true;
```

### CanvasTexture (2D canvas)
```typescript
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 256, 256);

const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true; // call when canvas updates
```

### VideoTexture (auto-updates)
```typescript
const video = document.createElement('video');
video.src = 'video.mp4';
video.loop = true;
video.muted = true;
video.play();

const texture = new THREE.VideoTexture(video);
texture.colorSpace = THREE.SRGBColorSpace;
// Auto-updates each frame while video plays
```

### Compressed textures (KTX2/Basis)
```typescript
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';

const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('basis/');
ktx2Loader.detectSupport(renderer);

const texture = await ktx2Loader.loadAsync('texture.ktx2');
```

## Cube Textures

### CubeTextureLoader for skyboxes/envmaps
```typescript
const loader = new THREE.CubeTextureLoader();
const cubeTexture = loader.load([
  'px.jpg', 'nx.jpg', // +X, -X
  'py.jpg', 'ny.jpg', // +Y, -Y
  'pz.jpg', 'nz.jpg'  // +Z, -Z
]);

scene.background = cubeTexture;
material.envMap = cubeTexture;
```

### Equirectangular to cubemap with PMREMGenerator
```typescript
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';

const rgbeLoader = new RGBELoader();
const hdrTexture = await rgbeLoader.loadAsync('env.hdr');

const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
const envMap = pmremGenerator.fromEquirectangular(hdrTexture).texture;

scene.environment = envMap;
scene.background = envMap;

hdrTexture.dispose();
pmremGenerator.dispose();
```

## HDR Textures

### RGBELoader (.hdr files)
```typescript
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';

const loader = new RGBELoader();
const texture = await loader.loadAsync('environment.hdr');
texture.mapping = THREE.EquirectangularReflectionMapping;
```

### EXRLoader (.exr files)
```typescript
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader';

const loader = new EXRLoader();
const texture = await loader.loadAsync('environment.exr');
texture.mapping = THREE.EquirectangularReflectionMapping;
```

### Background options
```typescript
scene.background = texture; // Direct background
scene.environment = texture; // Environment lighting for all PBR materials
scene.backgroundBlurriness = 0.5; // 0-1, blurs background
scene.backgroundIntensity = 1.0; // Brightness multiplier
```

## Render Targets

### WebGLRenderTarget
```typescript
const renderTarget = new THREE.WebGLRenderTarget(512, 512, {
  minFilter: THREE.LinearFilter,
  magFilter: THREE.LinearFilter,
  format: THREE.RGBAFormat,
  type: THREE.UnsignedByteType
});

// Render to target
renderer.setRenderTarget(renderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);

// Use as texture
material.map = renderTarget.texture;
```

### Depth texture
```typescript
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
renderTarget.depthTexture = new THREE.DepthTexture(512, 512);
renderTarget.depthTexture.type = THREE.FloatType;

// Access depth in material
material.map = renderTarget.depthTexture;
```

### Multi-sample anti-aliasing (MSAA)
```typescript
const renderTarget = new THREE.WebGLRenderTarget(512, 512, {
  samples: 4 // MSAA samples (0 = off, 4 or 8 recommended)
});
```

## CubeCamera

### Dynamic environment maps
```typescript
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);

// Update environment map
function updateEnvMap(reflectiveObject) {
  reflectiveObject.visible = false; // Hide to avoid self-reflection
  cubeCamera.update(renderer, scene);
  reflectiveObject.visible = true;
}

material.envMap = cubeRenderTarget.texture;

// In animation loop
updateEnvMap(sphereMesh);
```

## UV Mapping

### Accessing/modifying UVs
```typescript
const geometry = new THREE.PlaneGeometry(1, 1);
const uvAttribute = geometry.attributes.uv;

for (let i = 0; i < uvAttribute.count; i++) {
  const u = uvAttribute.getX(i);
  const v = uvAttribute.getY(i);

  // Modify UVs
  uvAttribute.setXY(i, u * 2, v * 2);
}

uvAttribute.needsUpdate = true;
```

### Second UV channel for aoMap
```typescript
// Clone UV to UV2 for ambient occlusion
geometry.attributes.uv2 = geometry.attributes.uv.clone();

material.aoMap = aoTexture;
material.aoMapIntensity = 1.0;
```

### UV transform in shaders
```typescript
const material = new THREE.ShaderMaterial({
  uniforms: {
    map: { value: texture },
    uvTransform: { value: new THREE.Matrix3() }
  },
  vertexShader: `
    varying vec2 vUv;
    uniform mat3 uvTransform;
    void main() {
      vUv = (uvTransform * vec3(uv, 1.0)).xy;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D map;
    varying vec2 vUv;
    void main() {
      gl_FragColor = texture2D(map, vUv);
    }
  `
});
```

## Texture Atlas

### Multiple images in one texture
```typescript
// Atlas with 4 sprites in 2x2 grid
const atlas = new THREE.TextureLoader().load('atlas.png');

// Select sprite at (0, 1) in 2x2 grid
const spriteSize = 0.5;
atlas.repeat.set(spriteSize, spriteSize);
atlas.offset.set(0 * spriteSize, 1 * spriteSize);

material.map = atlas;
```

## Material Texture Maps

### Complete PBR material setup
```typescript
const material = new THREE.MeshStandardMaterial({
  // Color maps - SRGBColorSpace
  map: colorTexture,                 // Albedo/diffuse
  emissiveMap: emissiveTexture,

  // Data maps - NoColorSpace (default)
  normalMap: normalTexture,
  normalScale: new THREE.Vector2(1, 1),

  roughnessMap: roughnessTexture,
  roughness: 1.0,

  metalnessMap: metalnessTexture,
  metalness: 1.0,

  aoMap: aoTexture,
  aoMapIntensity: 1.0,

  displacementMap: displacementTexture,
  displacementScale: 0.1,
  displacementBias: 0,

  alphaMap: alphaTexture,
  transparent: true,

  envMap: envMapTexture
});

// Set color space correctly
colorTexture.colorSpace = THREE.SRGBColorSpace;
emissiveTexture.colorSpace = THREE.SRGBColorSpace;
```

## Normal Map Types

### TangentSpace (default, most common)
```typescript
material.normalMap = normalTexture;
material.normalMapType = THREE.TangentSpaceNormalMap; // default
material.normalScale.set(1, 1); // Adjust strength
```

### ObjectSpace (rare, world-aligned)
```typescript
material.normalMap = normalTexture;
material.normalMapType = THREE.ObjectSpaceNormalMap;
```

## Procedural Textures

### Noise generation with DataTexture
```typescript
function generateNoiseTexture(width: number, height: number): THREE.DataTexture {
  const size = width * height;
  const data = new Uint8Array(4 * size);

  for (let i = 0; i < size; i++) {
    const stride = i * 4;
    const value = Math.random() * 255;
    data[stride] = value;
    data[stride + 1] = value;
    data[stride + 2] = value;
    data[stride + 3] = 255;
  }

  const texture = new THREE.DataTexture(data, width, height);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}
```

### Gradient generation
```typescript
function generateGradientTexture(width: number, height: number): THREE.DataTexture {
  const size = width * height;
  const data = new Uint8Array(4 * size);

  for (let i = 0; i < size; i++) {
    const x = i % width;
    const y = Math.floor(i / width);
    const stride = i * 4;

    const gradient = x / width * 255;
    data[stride] = gradient;
    data[stride + 1] = gradient;
    data[stride + 2] = gradient;
    data[stride + 3] = 255;
  }

  const texture = new THREE.DataTexture(data, width, height);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}
```

## Texture Memory Management

### Dispose patterns
```typescript
// Dispose single texture
texture.dispose();

// Dispose material textures
function disposeMaterialTextures(material: THREE.Material) {
  const textures = [
    'map', 'normalMap', 'roughnessMap', 'metalnessMap',
    'aoMap', 'emissiveMap', 'displacementMap', 'alphaMap',
    'envMap', 'lightMap', 'bumpMap', 'specularMap'
  ];

  textures.forEach(key => {
    if (material[key]) {
      material[key].dispose();
    }
  });
}

// Dispose mesh
function disposeMesh(mesh: THREE.Mesh) {
  mesh.geometry.dispose();
  if (Array.isArray(mesh.material)) {
    mesh.material.forEach(mat => {
      disposeMaterialTextures(mat);
      mat.dispose();
    });
  } else {
    disposeMaterialTextures(mesh.material);
    mesh.material.dispose();
  }
}
```

### TexturePool class for reuse
```typescript
class TexturePool {
  private cache = new Map<string, THREE.Texture>();
  private loader = new THREE.TextureLoader();

  load(url: string): THREE.Texture {
    if (this.cache.has(url)) {
      return this.cache.get(url)!;
    }

    const texture = this.loader.load(url);
    this.cache.set(url, texture);
    return texture;
  }

  dispose() {
    this.cache.forEach(texture => texture.dispose());
    this.cache.clear();
  }
}

const texturePool = new TexturePool();
const texture1 = texturePool.load('texture.jpg');
const texture2 = texturePool.load('texture.jpg'); // Reuses same texture
```

## Performance Tips

### Power-of-2 textures
```typescript
// Optimal: 256, 512, 1024, 2048, 4096
// Non-power-of-2 textures have limitations:
// - Cannot use mipmaps
// - Must use ClampToEdgeWrapping
// - Slower on some GPUs

const texture = loader.load('npot-texture.jpg');
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
```

### KTX2/Basis compression (massive savings)
```typescript
// 50-75% smaller file size + GPU compressed format
// Use basis_universal to convert: PNG/JPG -> KTX2
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';

const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('basis/');
ktx2Loader.detectSupport(renderer);

const texture = await ktx2Loader.loadAsync('texture.ktx2');
```

### Texture atlases reduce draw calls
```typescript
// Instead of 100 textures, use 1 atlas with 100 sprites
```

### Mipmaps reduce aliasing and improve performance
```typescript
texture.generateMipmaps = true; // Always enable for power-of-2 textures
```

### Reasonable size limits
```typescript
// Mobile: 1024px max
// Desktop: 2048px typical, 4096px high-end
// Avoid 8192px unless absolutely necessary
```

### Reuse textures across materials
```typescript
const sharedTexture = loader.load('shared.jpg');
material1.map = sharedTexture;
material2.map = sharedTexture;
material3.map = sharedTexture;
```

## See Also

- [Materials](materials.md) - Applying textures to material properties
- [Lighting & Shadows](lighting-and-shadows.md) - HDR/IBL environment lighting
- [Loaders](loaders.md) - Asset loading patterns and caching
- [Shaders](shaders.md) - Custom texture sampling in GLSL

License (Apache-2.0)

View full license text
Licensed under Apache-2.0