Installation
Install via CLI
npx @vector-labs/skills add threejs Or target a specific tool
npx @vector-labs/skills add threejs --tool cursor 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)
Apache-2.0
Source: vlabsai/skills-hub
View full license text
Licensed under Apache-2.0