Publish threejs skill to marketplace
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace. Tested: Marketplace validation passed.
This commit is contained in:
@@ -75,6 +75,18 @@
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "文档处理"
|
||||
},
|
||||
{
|
||||
"name": "threejs",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/threejs"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "设计"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "threejs",
|
||||
"version": "1.0.0",
|
||||
"description": "一套覆盖 Three.js 完整开发流程的 10 个专业技能,从基础场景搭建到高级视觉效果,助你快速构建专业级 3D 应用:\r\n核心基础\r\n- threejs-fundamentals - 场景、相机、渲染器配置\r\n- threejs-geometry - 几何体创建与自定义网格\r\n- threejs-materials - PBR 材质、ShaderMaterial 与材质优化\r\n视觉表现\r\n- threejs-lighting - 灯光类型、阴影配置与环境光照\r\n- threejs-textures - UV 映射、环境贴图与纹理优化\r\n- threejs-postprocessing - 后期处理、特效与画面调色\r\n交互与动画\r\n- threejs-animation - 关键帧动画、骨骼动画与动画混合\r\n- threejs-interaction - 射线检测、鼠标/触摸交互与相机控制\r\n高级特性\r\n- threejs-shaders - GLSL 着色器、自定义特效与顶点操作\r\n- threejs-loaders - GLTF/GLB 模型加载、资源管理与异步模式\r\n适合人群: WebGL 开发者、3D 可视化工程师、创意技术开发者\r\n使用方式: 在开发过程中按需调用对应技能,获得专业的 Three.js 开发指导与代码建议。",
|
||||
"author": {
|
||||
"name": "EAPIL",
|
||||
"url": "https://git.playones.com/arechen/EapilSkillMarket"
|
||||
},
|
||||
"homepage": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"repository": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"license": "Proprietary",
|
||||
"keywords": [
|
||||
"eapil",
|
||||
"codex-skill",
|
||||
"threejs"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "threejs",
|
||||
"shortDescription": "一套覆盖 Three.js 完整开发流程的 10 个专业技能,从基础场景搭建到高级视觉效果,助你快速构建专业级 3D 应用:\r\n核心基础\r\n- threejs-fundamentals - 场景、相机、渲染器配置\r\n- threejs-geometry - 几何体创建与自定义网格\r\n- threejs-materials - PBR 材质、ShaderMaterial 与材质优化\r\n视觉表现\r\n- threejs-lighting - 灯光类型、阴影配置与环境光照\r\n- threejs-textures - UV 映射、环境贴图与纹理优化\r\n- threejs-postprocessing - 后期处理、特效与画面调色\r\n交互与动画\r\n- threejs-animation - 关键帧动画、骨骼动画与动画混合\r\n- threejs-interaction - 射线检测、鼠标/触摸交互与相机控制\r\n高级特性\r\n- threejs-shaders - GLSL 着色器、自定义特效与顶点操作\r\n- threejs-loaders - GLTF/GLB 模型加载、资源管理与异步模式\r\n适合人群: WebGL 开发者、3D 可视化工程师、创意技术开发者\r\n使用方式: 在开发过程中按需调用对应技能,获得专业的 Three.js 开发指导与代码建议。",
|
||||
"longDescription": "一套覆盖 Three.js 完整开发流程的 10 个专业技能,从基础场景搭建到高级视觉效果,助你快速构建专业级 3D 应用:\r\n核心基础\r\n- threejs-fundamentals - 场景、相机、渲染器配置\r\n- threejs-geometry - 几何体创建与自定义网格\r\n- threejs-materials - PBR 材质、ShaderMaterial 与材质优化\r\n视觉表现\r\n- threejs-lighting - 灯光类型、阴影配置与环境光照\r\n- threejs-textures - UV 映射、环境贴图与纹理优化\r\n- threejs-postprocessing - 后期处理、特效与画面调色\r\n交互与动画\r\n- threejs-animation - 关键帧动画、骨骼动画与动画混合\r\n- threejs-interaction - 射线检测、鼠标/触摸交互与相机控制\r\n高级特性\r\n- threejs-shaders - GLSL 着色器、自定义特效与顶点操作\r\n- threejs-loaders - GLTF/GLB 模型加载、资源管理与异步模式\r\n适合人群: WebGL 开发者、3D 可视化工程师、创意技术开发者\r\n使用方式: 在开发过程中按需调用对应技能,获得专业的 Three.js 开发指导与代码建议。",
|
||||
"developerName": "EAPIL",
|
||||
"category": "设计",
|
||||
"capabilities": [
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"defaultPrompt": [
|
||||
"使用 threejs 帮我完成这个任务。"
|
||||
],
|
||||
"websiteURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"privacyPolicyURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"termsOfServiceURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"brandColor": "#2563EB",
|
||||
"screenshots": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
---
|
||||
name: threejs-animation
|
||||
description: Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
|
||||
---
|
||||
|
||||
# Three.js Animation
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Simple procedural animation
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
function animate() {
|
||||
const delta = clock.getDelta();
|
||||
const elapsed = clock.getElapsedTime();
|
||||
|
||||
mesh.rotation.y += delta;
|
||||
mesh.position.y = Math.sin(elapsed) * 0.5;
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
```
|
||||
|
||||
## Animation System Overview
|
||||
|
||||
Three.js animation system has three main components:
|
||||
|
||||
1. **AnimationClip** - Container for keyframe data
|
||||
2. **AnimationMixer** - Plays animations on a root object
|
||||
3. **AnimationAction** - Controls playback of a clip
|
||||
|
||||
## AnimationClip
|
||||
|
||||
Stores keyframe animation data.
|
||||
|
||||
```javascript
|
||||
// Create animation clip
|
||||
const times = [0, 1, 2]; // Keyframe times (seconds)
|
||||
const values = [0, 1, 0]; // Values at each keyframe
|
||||
|
||||
const track = new THREE.NumberKeyframeTrack(
|
||||
".position[y]", // Property path
|
||||
times,
|
||||
values,
|
||||
);
|
||||
|
||||
const clip = new THREE.AnimationClip("bounce", 2, [track]);
|
||||
```
|
||||
|
||||
### KeyframeTrack Types
|
||||
|
||||
```javascript
|
||||
// Number track (single value)
|
||||
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
|
||||
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);
|
||||
|
||||
// Vector track (position, scale)
|
||||
new THREE.VectorKeyframeTrack(".position", times, [
|
||||
0,
|
||||
0,
|
||||
0, // t=0
|
||||
1,
|
||||
2,
|
||||
0, // t=1
|
||||
0,
|
||||
0,
|
||||
0, // t=2
|
||||
]);
|
||||
|
||||
// Quaternion track (rotation)
|
||||
const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
|
||||
const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));
|
||||
new THREE.QuaternionKeyframeTrack(
|
||||
".quaternion",
|
||||
[0, 1],
|
||||
[q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],
|
||||
);
|
||||
|
||||
// Color track
|
||||
new THREE.ColorKeyframeTrack(".material.color", times, [
|
||||
1,
|
||||
0,
|
||||
0, // red
|
||||
0,
|
||||
1,
|
||||
0, // green
|
||||
0,
|
||||
0,
|
||||
1, // blue
|
||||
]);
|
||||
|
||||
// Boolean track
|
||||
new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);
|
||||
|
||||
// String track (for morph targets)
|
||||
new THREE.StringKeyframeTrack(
|
||||
".morphTargetInfluences[smile]",
|
||||
[0, 1],
|
||||
["0", "1"],
|
||||
);
|
||||
```
|
||||
|
||||
### Interpolation Modes
|
||||
|
||||
```javascript
|
||||
const track = new THREE.VectorKeyframeTrack(".position", times, values);
|
||||
|
||||
// Interpolation
|
||||
track.setInterpolation(THREE.InterpolateLinear); // Default
|
||||
track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline
|
||||
track.setInterpolation(THREE.InterpolateDiscrete); // Step function
|
||||
```
|
||||
|
||||
## AnimationMixer
|
||||
|
||||
Plays animations on an object and its descendants.
|
||||
|
||||
```javascript
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
|
||||
// Create action from clip
|
||||
const action = mixer.clipAction(clip);
|
||||
action.play();
|
||||
|
||||
// Update in animation loop
|
||||
function animate() {
|
||||
const delta = clock.getDelta();
|
||||
mixer.update(delta); // Required!
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
### Mixer Events
|
||||
|
||||
```javascript
|
||||
mixer.addEventListener("finished", (e) => {
|
||||
console.log("Animation finished:", e.action.getClip().name);
|
||||
});
|
||||
|
||||
mixer.addEventListener("loop", (e) => {
|
||||
console.log("Animation looped:", e.action.getClip().name);
|
||||
});
|
||||
```
|
||||
|
||||
## AnimationAction
|
||||
|
||||
Controls playback of an animation clip.
|
||||
|
||||
```javascript
|
||||
const action = mixer.clipAction(clip);
|
||||
|
||||
// Playback control
|
||||
action.play();
|
||||
action.stop();
|
||||
action.reset();
|
||||
action.halt(fadeOutDuration);
|
||||
|
||||
// Playback state
|
||||
action.isRunning();
|
||||
action.isScheduled();
|
||||
|
||||
// Time control
|
||||
action.time = 0.5; // Current time
|
||||
action.timeScale = 1; // Playback speed (negative = reverse)
|
||||
action.paused = false;
|
||||
|
||||
// Weight (for blending)
|
||||
action.weight = 1; // 0-1, contribution to final pose
|
||||
action.setEffectiveWeight(1);
|
||||
|
||||
// Loop modes
|
||||
action.loop = THREE.LoopRepeat; // Default: loop forever
|
||||
action.loop = THREE.LoopOnce; // Play once and stop
|
||||
action.loop = THREE.LoopPingPong; // Alternate forward/backward
|
||||
action.repetitions = 3; // Number of loops (Infinity default)
|
||||
|
||||
// Clamping
|
||||
action.clampWhenFinished = true; // Hold last frame when done
|
||||
|
||||
// Blending
|
||||
action.blendMode = THREE.NormalAnimationBlendMode;
|
||||
action.blendMode = THREE.AdditiveAnimationBlendMode;
|
||||
```
|
||||
|
||||
### Fade In/Out
|
||||
|
||||
```javascript
|
||||
// Fade in
|
||||
action.reset().fadeIn(0.5).play();
|
||||
|
||||
// Fade out
|
||||
action.fadeOut(0.5);
|
||||
|
||||
// Crossfade between animations
|
||||
const action1 = mixer.clipAction(clip1);
|
||||
const action2 = mixer.clipAction(clip2);
|
||||
|
||||
action1.play();
|
||||
|
||||
// Later, crossfade to action2
|
||||
action1.crossFadeTo(action2, 0.5, true);
|
||||
action2.play();
|
||||
```
|
||||
|
||||
## Loading GLTF Animations
|
||||
|
||||
Most common source of skeletal 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);
|
||||
|
||||
// Create mixer
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
|
||||
// Get all clips
|
||||
const clips = gltf.animations;
|
||||
console.log(
|
||||
"Available animations:",
|
||||
clips.map((c) => c.name),
|
||||
);
|
||||
|
||||
// Play first animation
|
||||
if (clips.length > 0) {
|
||||
const action = mixer.clipAction(clips[0]);
|
||||
action.play();
|
||||
}
|
||||
|
||||
// Play specific animation by name
|
||||
const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
|
||||
if (walkClip) {
|
||||
mixer.clipAction(walkClip).play();
|
||||
}
|
||||
|
||||
// Store mixer for update loop
|
||||
window.mixer = mixer;
|
||||
});
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
const delta = clock.getDelta();
|
||||
if (window.mixer) window.mixer.update(delta);
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
## Skeletal Animation
|
||||
|
||||
### Skeleton and Bones
|
||||
|
||||
```javascript
|
||||
// Access skeleton from skinned mesh
|
||||
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
|
||||
const skeleton = skinnedMesh.skeleton;
|
||||
|
||||
// Access bones
|
||||
skeleton.bones.forEach((bone) => {
|
||||
console.log(bone.name, bone.position, bone.rotation);
|
||||
});
|
||||
|
||||
// Find specific bone by name
|
||||
const headBone = skeleton.bones.find((b) => b.name === "Head");
|
||||
if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head
|
||||
|
||||
// Skeleton helper
|
||||
const helper = new THREE.SkeletonHelper(model);
|
||||
scene.add(helper);
|
||||
```
|
||||
|
||||
### Programmatic Bone Animation
|
||||
|
||||
```javascript
|
||||
function animate() {
|
||||
const time = clock.getElapsedTime();
|
||||
|
||||
// Animate bone
|
||||
const headBone = skeleton.bones.find((b) => b.name === "Head");
|
||||
if (headBone) {
|
||||
headBone.rotation.y = Math.sin(time) * 0.3;
|
||||
}
|
||||
|
||||
// Update mixer if also playing clips
|
||||
mixer.update(clock.getDelta());
|
||||
}
|
||||
```
|
||||
|
||||
### Bone Attachments
|
||||
|
||||
```javascript
|
||||
// Attach object to bone
|
||||
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
|
||||
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
|
||||
if (handBone) handBone.add(weapon);
|
||||
|
||||
// Offset attachment
|
||||
weapon.position.set(0, 0, 0.5);
|
||||
weapon.rotation.set(0, Math.PI / 2, 0);
|
||||
```
|
||||
|
||||
## Morph Targets
|
||||
|
||||
Blend between different mesh shapes.
|
||||
|
||||
```javascript
|
||||
// Morph targets are stored in geometry
|
||||
const geometry = mesh.geometry;
|
||||
console.log("Morph attributes:", Object.keys(geometry.morphAttributes));
|
||||
|
||||
// Access morph target influences
|
||||
mesh.morphTargetInfluences; // Array of weights
|
||||
mesh.morphTargetDictionary; // Name -> index mapping
|
||||
|
||||
// Set morph target by index
|
||||
mesh.morphTargetInfluences[0] = 0.5;
|
||||
|
||||
// Set by name
|
||||
const smileIndex = mesh.morphTargetDictionary["smile"];
|
||||
mesh.morphTargetInfluences[smileIndex] = 1;
|
||||
```
|
||||
|
||||
### Animating Morph Targets
|
||||
|
||||
```javascript
|
||||
// Procedural
|
||||
function animate() {
|
||||
const t = clock.getElapsedTime();
|
||||
mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
|
||||
}
|
||||
|
||||
// With keyframe animation
|
||||
const track = new THREE.NumberKeyframeTrack(
|
||||
".morphTargetInfluences[smile]",
|
||||
[0, 0.5, 1],
|
||||
[0, 1, 0],
|
||||
);
|
||||
const clip = new THREE.AnimationClip("smile", 1, [track]);
|
||||
mixer.clipAction(clip).play();
|
||||
```
|
||||
|
||||
## Animation Blending
|
||||
|
||||
Mix multiple animations together.
|
||||
|
||||
```javascript
|
||||
// Setup actions
|
||||
const idleAction = mixer.clipAction(idleClip);
|
||||
const walkAction = mixer.clipAction(walkClip);
|
||||
const runAction = mixer.clipAction(runClip);
|
||||
|
||||
// Play all with different weights
|
||||
idleAction.play();
|
||||
walkAction.play();
|
||||
runAction.play();
|
||||
|
||||
// Set initial weights
|
||||
idleAction.setEffectiveWeight(1);
|
||||
walkAction.setEffectiveWeight(0);
|
||||
runAction.setEffectiveWeight(0);
|
||||
|
||||
// Blend based on speed
|
||||
function updateAnimations(speed) {
|
||||
if (speed < 0.1) {
|
||||
idleAction.setEffectiveWeight(1);
|
||||
walkAction.setEffectiveWeight(0);
|
||||
runAction.setEffectiveWeight(0);
|
||||
} else if (speed < 5) {
|
||||
const t = speed / 5;
|
||||
idleAction.setEffectiveWeight(1 - t);
|
||||
walkAction.setEffectiveWeight(t);
|
||||
runAction.setEffectiveWeight(0);
|
||||
} else {
|
||||
const t = Math.min((speed - 5) / 5, 1);
|
||||
idleAction.setEffectiveWeight(0);
|
||||
walkAction.setEffectiveWeight(1 - t);
|
||||
runAction.setEffectiveWeight(t);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Additive Blending
|
||||
|
||||
```javascript
|
||||
// Base pose
|
||||
const baseAction = mixer.clipAction(baseClip);
|
||||
baseAction.play();
|
||||
|
||||
// Additive layer (e.g., breathing)
|
||||
const additiveAction = mixer.clipAction(additiveClip);
|
||||
additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;
|
||||
additiveAction.play();
|
||||
|
||||
// Convert clip to additive
|
||||
THREE.AnimationUtils.makeClipAdditive(additiveClip);
|
||||
```
|
||||
|
||||
## Animation Utilities
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Find clip by name
|
||||
const clip = THREE.AnimationClip.findByName(clips, "Walk");
|
||||
|
||||
// Create subclip
|
||||
const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);
|
||||
|
||||
// Convert to additive
|
||||
THREE.AnimationUtils.makeClipAdditive(clip);
|
||||
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
|
||||
|
||||
// Clone clip
|
||||
const clone = clip.clone();
|
||||
|
||||
// Get clip duration
|
||||
clip.duration;
|
||||
|
||||
// Optimize clip (remove redundant keyframes)
|
||||
clip.optimize();
|
||||
|
||||
// Reset clip to first frame
|
||||
clip.resetDuration();
|
||||
```
|
||||
|
||||
## Procedural Animation Patterns
|
||||
|
||||
### Smooth Damping
|
||||
|
||||
```javascript
|
||||
// Smooth follow/lerp
|
||||
const target = new THREE.Vector3();
|
||||
const current = new THREE.Vector3();
|
||||
const velocity = new THREE.Vector3();
|
||||
|
||||
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.clone().sub(target);
|
||||
const temp = velocity
|
||||
.clone()
|
||||
.add(change.clone().multiplyScalar(omega))
|
||||
.multiplyScalar(deltaTime);
|
||||
velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);
|
||||
return target.clone().add(change.add(temp).multiplyScalar(exp));
|
||||
}
|
||||
|
||||
function animate() {
|
||||
current.copy(smoothDamp(current, target, velocity, 0.3, delta));
|
||||
mesh.position.copy(current);
|
||||
}
|
||||
```
|
||||
|
||||
### Spring Physics
|
||||
|
||||
```javascript
|
||||
class Spring {
|
||||
constructor(stiffness = 100, damping = 10) {
|
||||
this.stiffness = stiffness;
|
||||
this.damping = damping;
|
||||
this.position = 0;
|
||||
this.velocity = 0;
|
||||
this.target = 0;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
const force = -this.stiffness * (this.position - this.target);
|
||||
const dampingForce = -this.damping * this.velocity;
|
||||
this.velocity += (force + dampingForce) * dt;
|
||||
this.position += this.velocity * dt;
|
||||
return this.position;
|
||||
}
|
||||
}
|
||||
|
||||
const spring = new Spring(100, 10);
|
||||
spring.target = 1;
|
||||
|
||||
function animate() {
|
||||
mesh.position.y = spring.update(delta);
|
||||
}
|
||||
```
|
||||
|
||||
### Oscillation
|
||||
|
||||
```javascript
|
||||
function animate() {
|
||||
const t = clock.getElapsedTime();
|
||||
|
||||
// Sine wave
|
||||
mesh.position.y = Math.sin(t * 2) * 0.5;
|
||||
|
||||
// Bouncing
|
||||
mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;
|
||||
|
||||
// Circular motion
|
||||
mesh.position.x = Math.cos(t) * 2;
|
||||
mesh.position.z = Math.sin(t) * 2;
|
||||
|
||||
// Figure 8
|
||||
mesh.position.x = Math.sin(t) * 2;
|
||||
mesh.position.z = Math.sin(t * 2) * 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Share clips**: Same AnimationClip can be used on multiple mixers
|
||||
2. **Optimize clips**: Call `clip.optimize()` to remove redundant keyframes
|
||||
3. **Disable when off-screen**: Stop mixer updates for invisible objects
|
||||
4. **Use LOD for animations**: Simpler rigs for distant characters
|
||||
5. **Limit active mixers**: Each mixer.update() has a cost
|
||||
|
||||
```javascript
|
||||
// Pause animation when not visible
|
||||
mesh.onBeforeRender = () => {
|
||||
action.paused = false;
|
||||
};
|
||||
|
||||
mesh.onAfterRender = () => {
|
||||
// Check if will be visible next frame
|
||||
if (!isInFrustum(mesh)) {
|
||||
action.paused = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Cache clips
|
||||
const clipCache = new Map();
|
||||
function getClip(name) {
|
||||
if (!clipCache.has(name)) {
|
||||
clipCache.set(name, loadClip(name));
|
||||
}
|
||||
return clipCache.get(name);
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-loaders` - Loading animated GLTF models
|
||||
- `threejs-fundamentals` - Clock and animation loop
|
||||
- `threejs-shaders` - Vertex animation in shaders
|
||||
@@ -0,0 +1,488 @@
|
||||
---
|
||||
name: threejs-fundamentals
|
||||
description: Three.js scene setup, cameras, renderer, Object3D hierarchy, coordinate systems. Use when setting up 3D scenes, creating cameras, configuring renderers, managing object hierarchies, or working with transforms.
|
||||
---
|
||||
|
||||
# Three.js Fundamentals
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Create scene, camera, renderer
|
||||
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));
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Add a 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);
|
||||
|
||||
// Add light
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
dirLight.position.set(5, 5, 5);
|
||||
scene.add(dirLight);
|
||||
|
||||
camera.position.z = 5;
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
cube.rotation.x += 0.01;
|
||||
cube.rotation.y += 0.01;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener("resize", () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
```
|
||||
|
||||
## Core Classes
|
||||
|
||||
### Scene
|
||||
|
||||
Container for all 3D objects, lights, and cameras.
|
||||
|
||||
```javascript
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x000000); // Solid color
|
||||
scene.background = texture; // Skybox texture
|
||||
scene.background = cubeTexture; // Cubemap
|
||||
scene.environment = envMap; // Environment map for PBR
|
||||
scene.fog = new THREE.Fog(0xffffff, 1, 100); // Linear fog
|
||||
scene.fog = new THREE.FogExp2(0xffffff, 0.02); // Exponential fog
|
||||
```
|
||||
|
||||
### Cameras
|
||||
|
||||
**PerspectiveCamera** - Most common, simulates human eye.
|
||||
|
||||
```javascript
|
||||
// PerspectiveCamera(fov, aspect, near, far)
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75, // Field of view (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
|
||||
```
|
||||
|
||||
**OrthographicCamera** - No perspective distortion, good for 2D/isometric.
|
||||
|
||||
```javascript
|
||||
// OrthographicCamera(left, right, top, bottom, near, far)
|
||||
const aspect = window.innerWidth / window.innerHeight;
|
||||
const frustumSize = 10;
|
||||
const camera = new THREE.OrthographicCamera(
|
||||
(frustumSize * aspect) / -2,
|
||||
(frustumSize * aspect) / 2,
|
||||
frustumSize / 2,
|
||||
frustumSize / -2,
|
||||
0.1,
|
||||
1000,
|
||||
);
|
||||
```
|
||||
|
||||
**ArrayCamera** - Multiple viewports with sub-cameras.
|
||||
|
||||
```javascript
|
||||
const cameras = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const subcamera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);
|
||||
subcamera.viewport = new THREE.Vector4(
|
||||
Math.floor(i % 2) * 0.5,
|
||||
Math.floor(i / 2) * 0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
);
|
||||
cameras.push(subcamera);
|
||||
}
|
||||
const arrayCamera = new THREE.ArrayCamera(cameras);
|
||||
```
|
||||
|
||||
**CubeCamera** - Renders environment maps for reflections.
|
||||
|
||||
```javascript
|
||||
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
|
||||
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
|
||||
scene.add(cubeCamera);
|
||||
|
||||
// Use for reflections
|
||||
material.envMap = cubeRenderTarget.texture;
|
||||
|
||||
// Update each frame (expensive!)
|
||||
cubeCamera.position.copy(reflectiveMesh.position);
|
||||
cubeCamera.update(renderer, scene);
|
||||
```
|
||||
|
||||
### WebGLRenderer
|
||||
|
||||
```javascript
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: document.querySelector("#canvas"), // Optional existing canvas
|
||||
antialias: true, // Smooth edges
|
||||
alpha: true, // Transparent background
|
||||
powerPreference: "high-performance", // GPU hint
|
||||
preserveDrawingBuffer: true, // For screenshots
|
||||
});
|
||||
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
|
||||
// Tone mapping
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.0;
|
||||
|
||||
// Color space (Three.js r152+)
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
// Shadows
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
|
||||
// Clear color
|
||||
renderer.setClearColor(0x000000, 1);
|
||||
|
||||
// Render
|
||||
renderer.render(scene, camera);
|
||||
```
|
||||
|
||||
### Object3D
|
||||
|
||||
Base class for all 3D objects. Mesh, Group, Light, Camera all extend Object3D.
|
||||
|
||||
```javascript
|
||||
const obj = new THREE.Object3D();
|
||||
|
||||
// Transform
|
||||
obj.position.set(x, y, z);
|
||||
obj.rotation.set(x, y, z); // Euler angles (radians)
|
||||
obj.quaternion.set(x, y, z, w); // Quaternion rotation
|
||||
obj.scale.set(x, y, z);
|
||||
|
||||
// Local vs World transforms
|
||||
obj.getWorldPosition(targetVector);
|
||||
obj.getWorldQuaternion(targetQuaternion);
|
||||
obj.getWorldDirection(targetVector);
|
||||
|
||||
// Hierarchy
|
||||
obj.add(child);
|
||||
obj.remove(child);
|
||||
obj.parent;
|
||||
obj.children;
|
||||
|
||||
// Visibility
|
||||
obj.visible = false;
|
||||
|
||||
// Layers (for selective rendering/raycasting)
|
||||
obj.layers.set(1);
|
||||
obj.layers.enable(2);
|
||||
obj.layers.disable(0);
|
||||
|
||||
// Traverse hierarchy
|
||||
obj.traverse((child) => {
|
||||
if (child.isMesh) child.material.color.set(0xff0000);
|
||||
});
|
||||
|
||||
// Matrix updates
|
||||
obj.matrixAutoUpdate = true; // Default: auto-update matrices
|
||||
obj.updateMatrix(); // Manual matrix update
|
||||
obj.updateMatrixWorld(true); // Update world matrix recursively
|
||||
```
|
||||
|
||||
### Group
|
||||
|
||||
Empty container for organizing objects.
|
||||
|
||||
```javascript
|
||||
const group = new THREE.Group();
|
||||
group.add(mesh1);
|
||||
group.add(mesh2);
|
||||
scene.add(group);
|
||||
|
||||
// Transform entire group
|
||||
group.position.x = 5;
|
||||
group.rotation.y = Math.PI / 4;
|
||||
```
|
||||
|
||||
### Mesh
|
||||
|
||||
Combines geometry and material.
|
||||
|
||||
```javascript
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
// Multiple materials (one per geometry group)
|
||||
const mesh = new THREE.Mesh(geometry, [material1, material2]);
|
||||
|
||||
// Useful properties
|
||||
mesh.geometry;
|
||||
mesh.material;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
// Frustum culling
|
||||
mesh.frustumCulled = true; // Default: skip if outside camera view
|
||||
|
||||
// Render order
|
||||
mesh.renderOrder = 10; // Higher = rendered later
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
Three.js uses a **right-handed coordinate system**:
|
||||
|
||||
- **+X** points right
|
||||
- **+Y** points up
|
||||
- **+Z** points toward viewer (out of screen)
|
||||
|
||||
```javascript
|
||||
// Axes helper
|
||||
const axesHelper = new THREE.AxesHelper(5);
|
||||
scene.add(axesHelper); // Red=X, Green=Y, Blue=Z
|
||||
```
|
||||
|
||||
## Math Utilities
|
||||
|
||||
### Vector3
|
||||
|
||||
```javascript
|
||||
const v = new THREE.Vector3(x, y, z);
|
||||
v.set(x, y, z);
|
||||
v.copy(otherVector);
|
||||
v.clone();
|
||||
|
||||
// Operations (modify in place)
|
||||
v.add(v2);
|
||||
v.sub(v2);
|
||||
v.multiply(v2);
|
||||
v.multiplyScalar(2);
|
||||
v.divideScalar(2);
|
||||
v.normalize();
|
||||
v.negate();
|
||||
v.clamp(min, max);
|
||||
v.lerp(target, alpha);
|
||||
|
||||
// Calculations (return new value)
|
||||
v.length();
|
||||
v.lengthSq(); // Faster than length()
|
||||
v.distanceTo(v2);
|
||||
v.dot(v2);
|
||||
v.cross(v2); // Modifies v
|
||||
v.angleTo(v2);
|
||||
|
||||
// Transform
|
||||
v.applyMatrix4(matrix);
|
||||
v.applyQuaternion(q);
|
||||
v.project(camera); // World to NDC
|
||||
v.unproject(camera); // NDC to world
|
||||
```
|
||||
|
||||
### Matrix4
|
||||
|
||||
```javascript
|
||||
const m = new THREE.Matrix4();
|
||||
m.identity();
|
||||
m.copy(other);
|
||||
m.clone();
|
||||
|
||||
// Build transforms
|
||||
m.makeTranslation(x, y, z);
|
||||
m.makeRotationX(theta);
|
||||
m.makeRotationY(theta);
|
||||
m.makeRotationZ(theta);
|
||||
m.makeRotationFromQuaternion(q);
|
||||
m.makeScale(x, y, z);
|
||||
|
||||
// Compose/decompose
|
||||
m.compose(position, quaternion, scale);
|
||||
m.decompose(position, quaternion, scale);
|
||||
|
||||
// Operations
|
||||
m.multiply(m2); // m = m * m2
|
||||
m.premultiply(m2); // m = m2 * m
|
||||
m.invert();
|
||||
m.transpose();
|
||||
|
||||
// Camera matrices
|
||||
m.makePerspective(left, right, top, bottom, near, far);
|
||||
m.makeOrthographic(left, right, top, bottom, near, far);
|
||||
m.lookAt(eye, target, up);
|
||||
```
|
||||
|
||||
### Quaternion
|
||||
|
||||
```javascript
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromEuler(euler);
|
||||
q.setFromAxisAngle(axis, angle);
|
||||
q.setFromRotationMatrix(matrix);
|
||||
|
||||
q.multiply(q2);
|
||||
q.slerp(target, t); // Spherical interpolation
|
||||
q.normalize();
|
||||
q.invert();
|
||||
```
|
||||
|
||||
### Euler
|
||||
|
||||
```javascript
|
||||
const euler = new THREE.Euler(x, y, z, "XYZ"); // Order matters!
|
||||
euler.setFromQuaternion(q);
|
||||
euler.setFromRotationMatrix(m);
|
||||
|
||||
// Rotation orders: 'XYZ', 'YXZ', 'ZXY', 'XZY', 'YZX', 'ZYX'
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
```javascript
|
||||
const color = new THREE.Color(0xff0000);
|
||||
const color = new THREE.Color("red");
|
||||
const color = new THREE.Color("rgb(255, 0, 0)");
|
||||
const color = new THREE.Color("#ff0000");
|
||||
|
||||
color.setHex(0x00ff00);
|
||||
color.setRGB(r, g, b); // 0-1 range
|
||||
color.setHSL(h, s, l); // 0-1 range
|
||||
|
||||
color.lerp(otherColor, alpha);
|
||||
color.multiply(otherColor);
|
||||
color.multiplyScalar(2);
|
||||
```
|
||||
|
||||
### MathUtils
|
||||
|
||||
```javascript
|
||||
THREE.MathUtils.clamp(value, min, max);
|
||||
THREE.MathUtils.lerp(start, end, alpha);
|
||||
THREE.MathUtils.mapLinear(value, inMin, inMax, outMin, outMax);
|
||||
THREE.MathUtils.degToRad(degrees);
|
||||
THREE.MathUtils.radToDeg(radians);
|
||||
THREE.MathUtils.randFloat(min, max);
|
||||
THREE.MathUtils.randInt(min, max);
|
||||
THREE.MathUtils.smoothstep(x, min, max);
|
||||
THREE.MathUtils.smootherstep(x, min, max);
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Proper Cleanup
|
||||
|
||||
```javascript
|
||||
function dispose() {
|
||||
// Dispose geometries
|
||||
mesh.geometry.dispose();
|
||||
|
||||
// Dispose materials
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach((m) => m.dispose());
|
||||
} else {
|
||||
mesh.material.dispose();
|
||||
}
|
||||
|
||||
// Dispose textures
|
||||
texture.dispose();
|
||||
|
||||
// Remove from scene
|
||||
scene.remove(mesh);
|
||||
|
||||
// Dispose renderer
|
||||
renderer.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### Clock for Animation
|
||||
|
||||
```javascript
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
function animate() {
|
||||
const delta = clock.getDelta(); // Time since last frame (seconds)
|
||||
const elapsed = clock.getElapsedTime(); // Total time (seconds)
|
||||
|
||||
mesh.rotation.y += delta * 0.5; // Consistent speed regardless of framerate
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive Canvas
|
||||
|
||||
```javascript
|
||||
function onWindowResize() {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
}
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
```
|
||||
|
||||
### Loading Manager
|
||||
|
||||
```javascript
|
||||
const manager = new THREE.LoadingManager();
|
||||
|
||||
manager.onStart = (url, loaded, total) => console.log("Started loading");
|
||||
manager.onLoad = () => console.log("All loaded");
|
||||
manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total}`);
|
||||
manager.onError = (url) => console.error(`Error loading ${url}`);
|
||||
|
||||
const textureLoader = new THREE.TextureLoader(manager);
|
||||
const gltfLoader = new GLTFLoader(manager);
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Limit draw calls**: Merge geometries, use instancing, atlas textures
|
||||
2. **Frustum culling**: Enabled by default, ensure bounding boxes are correct
|
||||
3. **LOD (Level of Detail)**: Use `THREE.LOD` for distance-based mesh switching
|
||||
4. **Object pooling**: Reuse objects instead of creating/destroying
|
||||
5. **Avoid `getWorldPosition` in loops**: Cache results
|
||||
|
||||
```javascript
|
||||
// Merge static geometries
|
||||
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
||||
const merged = mergeGeometries([geo1, geo2, geo3]);
|
||||
|
||||
// LOD
|
||||
const lod = new THREE.LOD();
|
||||
lod.addLevel(highDetailMesh, 0);
|
||||
lod.addLevel(medDetailMesh, 50);
|
||||
lod.addLevel(lowDetailMesh, 100);
|
||||
scene.add(lod);
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-geometry` - Geometry creation and manipulation
|
||||
- `threejs-materials` - Material types and properties
|
||||
- `threejs-lighting` - Light types and shadows
|
||||
@@ -0,0 +1,548 @@
|
||||
---
|
||||
name: threejs-geometry
|
||||
description: Three.js geometry creation - built-in shapes, BufferGeometry, custom geometry, instancing. Use when creating 3D shapes, working with vertices, building custom meshes, or optimizing with instanced rendering.
|
||||
---
|
||||
|
||||
# Three.js Geometry
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Built-in geometry
|
||||
const box = new THREE.BoxGeometry(1, 1, 1);
|
||||
const sphere = new THREE.SphereGeometry(0.5, 32, 32);
|
||||
const plane = new THREE.PlaneGeometry(10, 10);
|
||||
|
||||
// Create mesh
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
|
||||
const mesh = new THREE.Mesh(box, material);
|
||||
scene.add(mesh);
|
||||
```
|
||||
|
||||
## 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, 32);
|
||||
new THREE.SphereGeometry(1, 32, 32, 0, Math.PI * 2, 0, Math.PI); // Full sphere
|
||||
new THREE.SphereGeometry(1, 32, 32, 0, Math.PI); // Hemisphere
|
||||
|
||||
// Plane - width, height, widthSegments, heightSegments
|
||||
new THREE.PlaneGeometry(10, 10, 1, 1);
|
||||
|
||||
// Circle - radius, segments, thetaStart, thetaLength
|
||||
new THREE.CircleGeometry(1, 32);
|
||||
new THREE.CircleGeometry(1, 32, 0, Math.PI); // Semicircle
|
||||
|
||||
// Cylinder - radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded
|
||||
new THREE.CylinderGeometry(1, 1, 2, 32, 1, false);
|
||||
new THREE.CylinderGeometry(0, 1, 2, 32); // Cone
|
||||
new THREE.CylinderGeometry(1, 1, 2, 6); // Hexagonal prism
|
||||
|
||||
// Cone - radius, height, radialSegments, heightSegments, openEnded
|
||||
new THREE.ConeGeometry(1, 2, 32, 1, false);
|
||||
|
||||
// Torus - radius, tube, radialSegments, tubularSegments, arc
|
||||
new THREE.TorusGeometry(1, 0.4, 16, 100);
|
||||
|
||||
// TorusKnot - radius, tube, tubularSegments, radialSegments, p, q
|
||||
new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3);
|
||||
|
||||
// Ring - innerRadius, outerRadius, thetaSegments, phiSegments
|
||||
new THREE.RingGeometry(0.5, 1, 32, 1);
|
||||
```
|
||||
|
||||
### Advanced Shapes
|
||||
|
||||
```javascript
|
||||
// Capsule - radius, length, capSegments, radialSegments
|
||||
new THREE.CapsuleGeometry(0.5, 1, 4, 8);
|
||||
|
||||
// Dodecahedron - radius, detail
|
||||
new THREE.DodecahedronGeometry(1, 0);
|
||||
|
||||
// Icosahedron - radius, detail (0 = 20 faces, higher = smoother)
|
||||
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
|
||||
const vertices = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1];
|
||||
const indices = [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1];
|
||||
new THREE.PolyhedronGeometry(vertices, indices, 1, 0);
|
||||
```
|
||||
|
||||
### Path-Based Shapes
|
||||
|
||||
```javascript
|
||||
// Lathe - points[], segments, phiStart, phiLength
|
||||
const points = [
|
||||
new THREE.Vector2(0, 0),
|
||||
new THREE.Vector2(0.5, 0),
|
||||
new THREE.Vector2(0.5, 1),
|
||||
new THREE.Vector2(0, 1),
|
||||
];
|
||||
new THREE.LatheGeometry(points, 32);
|
||||
|
||||
// Extrude - shape, options
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(1, 0);
|
||||
shape.lineTo(1, 1);
|
||||
shape.lineTo(0, 1);
|
||||
shape.lineTo(0, 0);
|
||||
|
||||
const extrudeSettings = {
|
||||
steps: 2,
|
||||
depth: 1,
|
||||
bevelEnabled: true,
|
||||
bevelThickness: 0.1,
|
||||
bevelSize: 0.1,
|
||||
bevelSegments: 3,
|
||||
};
|
||||
new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
|
||||
// Tube - path, tubularSegments, radius, radialSegments, closed
|
||||
const curve = new THREE.CatmullRomCurve3([
|
||||
new THREE.Vector3(-1, 0, 0),
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
new THREE.Vector3(1, 0, 0),
|
||||
]);
|
||||
new THREE.TubeGeometry(curve, 64, 0.2, 8, false);
|
||||
```
|
||||
|
||||
### Text Geometry
|
||||
|
||||
```javascript
|
||||
import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
|
||||
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
|
||||
|
||||
const loader = new FontLoader();
|
||||
loader.load("fonts/helvetiker_regular.typeface.json", (font) => {
|
||||
const geometry = new TextGeometry("Hello", {
|
||||
font: font,
|
||||
size: 1,
|
||||
depth: 0.2, // Was 'height' in older versions
|
||||
curveSegments: 12,
|
||||
bevelEnabled: true,
|
||||
bevelThickness: 0.03,
|
||||
bevelSize: 0.02,
|
||||
bevelSegments: 5,
|
||||
});
|
||||
|
||||
// Center text
|
||||
geometry.computeBoundingBox();
|
||||
geometry.center();
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
});
|
||||
```
|
||||
|
||||
## BufferGeometry
|
||||
|
||||
The base class for all geometries. Stores data as typed arrays for GPU efficiency.
|
||||
|
||||
### Custom BufferGeometry
|
||||
|
||||
```javascript
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
// Vertices (3 floats per vertex: x, y, z)
|
||||
const vertices = new Float32Array([
|
||||
-1,
|
||||
-1,
|
||||
0, // vertex 0
|
||||
1,
|
||||
-1,
|
||||
0, // vertex 1
|
||||
1,
|
||||
1,
|
||||
0, // vertex 2
|
||||
-1,
|
||||
1,
|
||||
0, // vertex 3
|
||||
]);
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
|
||||
|
||||
// Indices (for indexed geometry - reuse vertices)
|
||||
const indices = new Uint16Array([
|
||||
0,
|
||||
1,
|
||||
2, // triangle 1
|
||||
0,
|
||||
2,
|
||||
3, // triangle 2
|
||||
]);
|
||||
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||
|
||||
// Normals (required for lighting)
|
||||
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 (for texturing)
|
||||
const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
||||
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
||||
|
||||
// Colors (per-vertex colors)
|
||||
const colors = new Float32Array([
|
||||
1,
|
||||
0,
|
||||
0, // red
|
||||
0,
|
||||
1,
|
||||
0, // green
|
||||
0,
|
||||
0,
|
||||
1, // blue
|
||||
1,
|
||||
1,
|
||||
0, // yellow
|
||||
]);
|
||||
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
||||
// Use with: material.vertexColors = true
|
||||
```
|
||||
|
||||
### BufferAttribute Types
|
||||
|
||||
```javascript
|
||||
// Common attribute types
|
||||
new THREE.BufferAttribute(array, itemSize);
|
||||
|
||||
// Typed array options
|
||||
new Float32Array(count * itemSize); // Positions, normals, UVs
|
||||
new Uint16Array(count); // Indices (up to 65535 vertices)
|
||||
new Uint32Array(count); // Indices (larger meshes)
|
||||
new Uint8Array(count * itemSize); // Colors (0-255 range)
|
||||
|
||||
// Item sizes
|
||||
// Position: 3 (x, y, z)
|
||||
// Normal: 3 (x, y, z)
|
||||
// UV: 2 (u, v)
|
||||
// Color: 3 (r, g, b) or 4 (r, g, b, a)
|
||||
// Index: 1
|
||||
```
|
||||
|
||||
### Modifying BufferGeometry
|
||||
|
||||
```javascript
|
||||
const positions = geometry.attributes.position;
|
||||
|
||||
// Modify vertex
|
||||
positions.setXYZ(index, x, y, z);
|
||||
|
||||
// Access vertex
|
||||
const x = positions.getX(index);
|
||||
const y = positions.getY(index);
|
||||
const z = positions.getZ(index);
|
||||
|
||||
// Flag for GPU update
|
||||
positions.needsUpdate = true;
|
||||
|
||||
// Recompute normals after position changes
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
// Recompute bounding box/sphere after changes
|
||||
geometry.computeBoundingBox();
|
||||
geometry.computeBoundingSphere();
|
||||
```
|
||||
|
||||
### Interleaved Buffers (Advanced)
|
||||
|
||||
```javascript
|
||||
// More efficient memory layout for large meshes
|
||||
const interleavedBuffer = new THREE.InterleavedBuffer(
|
||||
new Float32Array([
|
||||
// pos.x, pos.y, pos.z, uv.u, uv.v (repeated per vertex)
|
||||
-1, -1, 0, 0, 0, 1, -1, 0, 1, 0, 1, 1, 0, 1, 1, -1, 1, 0, 0, 1,
|
||||
]),
|
||||
5, // stride (floats per vertex)
|
||||
);
|
||||
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0),
|
||||
); // size 3, offset 0
|
||||
geometry.setAttribute(
|
||||
"uv",
|
||||
new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 3),
|
||||
); // size 2, offset 3
|
||||
```
|
||||
|
||||
## EdgesGeometry & WireframeGeometry
|
||||
|
||||
```javascript
|
||||
// Edge lines (only hard edges)
|
||||
const edges = new THREE.EdgesGeometry(boxGeometry, 15); // 15 = threshold angle
|
||||
const edgeMesh = new THREE.LineSegments(
|
||||
edges,
|
||||
new THREE.LineBasicMaterial({ color: 0xffffff }),
|
||||
);
|
||||
|
||||
// Wireframe (all triangles)
|
||||
const wireframe = new THREE.WireframeGeometry(boxGeometry);
|
||||
const wireMesh = new THREE.LineSegments(
|
||||
wireframe,
|
||||
new THREE.LineBasicMaterial({ color: 0xffffff }),
|
||||
);
|
||||
```
|
||||
|
||||
## Points
|
||||
|
||||
```javascript
|
||||
// Create point cloud
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(1000 * 3);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
positions[i * 3] = (Math.random() - 0.5) * 10;
|
||||
positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
|
||||
}
|
||||
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
|
||||
const material = new THREE.PointsMaterial({
|
||||
size: 0.1,
|
||||
sizeAttenuation: true, // Size decreases with distance
|
||||
color: 0xffffff,
|
||||
});
|
||||
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
```
|
||||
|
||||
## Lines
|
||||
|
||||
```javascript
|
||||
// Line (connected 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, material);
|
||||
|
||||
// LineSegments (pairs of points)
|
||||
const segmentsGeometry = new THREE.BufferGeometry();
|
||||
segmentsGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(
|
||||
new Float32Array([
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0, // segment 1
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0, // segment 2
|
||||
]),
|
||||
3,
|
||||
),
|
||||
);
|
||||
const segments = new THREE.LineSegments(segmentsGeometry, material);
|
||||
```
|
||||
|
||||
## InstancedMesh
|
||||
|
||||
Efficiently render many copies of the same geometry.
|
||||
|
||||
```javascript
|
||||
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 for each instance
|
||||
const dummy = new THREE.Object3D();
|
||||
const matrix = new THREE.Matrix4();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
dummy.position.set(
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 20,
|
||||
);
|
||||
dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
|
||||
dummy.scale.setScalar(0.5 + Math.random());
|
||||
dummy.updateMatrix();
|
||||
|
||||
instancedMesh.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
|
||||
// Flag for GPU update
|
||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||
|
||||
// Optional: 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()),
|
||||
);
|
||||
}
|
||||
instancedMesh.instanceColor.needsUpdate = true;
|
||||
|
||||
scene.add(instancedMesh);
|
||||
```
|
||||
|
||||
### Update Instance at Runtime
|
||||
|
||||
```javascript
|
||||
// Update single instance
|
||||
const matrix = new THREE.Matrix4();
|
||||
instancedMesh.getMatrixAt(index, matrix);
|
||||
// Modify matrix...
|
||||
instancedMesh.setMatrixAt(index, matrix);
|
||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||
|
||||
// Raycasting with instanced mesh
|
||||
const intersects = raycaster.intersectObject(instancedMesh);
|
||||
if (intersects.length > 0) {
|
||||
const instanceId = intersects[0].instanceId;
|
||||
}
|
||||
```
|
||||
|
||||
## InstancedBufferGeometry (Advanced)
|
||||
|
||||
For custom per-instance attributes beyond transform/color.
|
||||
|
||||
```javascript
|
||||
const geometry = new THREE.InstancedBufferGeometry();
|
||||
geometry.copy(new THREE.BoxGeometry(1, 1, 1));
|
||||
|
||||
// Add per-instance attribute
|
||||
const offsets = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
offsets[i * 3] = Math.random() * 10;
|
||||
offsets[i * 3 + 1] = Math.random() * 10;
|
||||
offsets[i * 3 + 2] = Math.random() * 10;
|
||||
}
|
||||
geometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));
|
||||
|
||||
// Use in shader
|
||||
// attribute vec3 offset;
|
||||
// vec3 transformed = position + offset;
|
||||
```
|
||||
|
||||
## Geometry Utilities
|
||||
|
||||
```javascript
|
||||
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
||||
|
||||
// Merge geometries (must have same attributes)
|
||||
const merged = BufferGeometryUtils.mergeGeometries([geo1, geo2, geo3]);
|
||||
|
||||
// Merge with groups (for multi-material)
|
||||
const merged = BufferGeometryUtils.mergeGeometries([geo1, geo2], true);
|
||||
|
||||
// Compute tangents (required for normal maps)
|
||||
BufferGeometryUtils.computeTangents(geometry);
|
||||
|
||||
// Interleave attributes for better performance
|
||||
const interleaved = BufferGeometryUtils.interleaveAttributes([
|
||||
geometry.attributes.position,
|
||||
geometry.attributes.normal,
|
||||
geometry.attributes.uv,
|
||||
]);
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Center Geometry
|
||||
|
||||
```javascript
|
||||
geometry.computeBoundingBox();
|
||||
geometry.center(); // Move vertices so center is at origin
|
||||
```
|
||||
|
||||
### Scale to Fit
|
||||
|
||||
```javascript
|
||||
geometry.computeBoundingBox();
|
||||
const size = new THREE.Vector3();
|
||||
geometry.boundingBox.getSize(size);
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
geometry.scale(1 / maxDim, 1 / maxDim, 1 / maxDim);
|
||||
```
|
||||
|
||||
### Clone and Transform
|
||||
|
||||
```javascript
|
||||
const clone = geometry.clone();
|
||||
clone.rotateX(Math.PI / 2);
|
||||
clone.translate(0, 1, 0);
|
||||
clone.scale(2, 2, 2);
|
||||
```
|
||||
|
||||
### Morph Targets
|
||||
|
||||
```javascript
|
||||
// Base geometry
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1, 4, 4, 4);
|
||||
|
||||
// Create morph target
|
||||
const morphPositions = geometry.attributes.position.array.slice();
|
||||
for (let i = 0; i < morphPositions.length; i += 3) {
|
||||
morphPositions[i] *= 2; // Scale X
|
||||
morphPositions[i + 1] *= 0.5; // Squash Y
|
||||
}
|
||||
|
||||
geometry.morphAttributes.position = [
|
||||
new THREE.BufferAttribute(new Float32Array(morphPositions), 3),
|
||||
];
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.morphTargetInfluences[0] = 0.5; // 50% blend
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use indexed geometry**: Reuse vertices with indices
|
||||
2. **Merge static meshes**: Reduce draw calls with `mergeGeometries`
|
||||
3. **Use InstancedMesh**: For many identical objects
|
||||
4. **Choose appropriate segment counts**: More segments = smoother but slower
|
||||
5. **Dispose unused geometry**: `geometry.dispose()`
|
||||
|
||||
```javascript
|
||||
// Good segment counts for common uses
|
||||
new THREE.SphereGeometry(1, 32, 32); // Good quality
|
||||
new THREE.SphereGeometry(1, 64, 64); // High quality
|
||||
new THREE.SphereGeometry(1, 16, 16); // Performance mode
|
||||
|
||||
// Dispose when done
|
||||
geometry.dispose();
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-fundamentals` - Scene setup and Object3D
|
||||
- `threejs-materials` - Material types for meshes
|
||||
- `threejs-shaders` - Custom vertex manipulation
|
||||
@@ -0,0 +1,660 @@
|
||||
---
|
||||
name: threejs-interaction
|
||||
description: Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.
|
||||
---
|
||||
|
||||
# Three.js Interaction
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
// Camera controls
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
// Raycasting for click detection
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
function onClick(event) {
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
console.log("Clicked:", intersects[0].object);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("click", onClick);
|
||||
```
|
||||
|
||||
## Raycaster
|
||||
|
||||
### Basic Raycasting
|
||||
|
||||
```javascript
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
// From camera (mouse picking)
|
||||
raycaster.setFromCamera(mousePosition, camera);
|
||||
|
||||
// From any origin and direction
|
||||
raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3
|
||||
|
||||
// Get intersections
|
||||
const intersects = raycaster.intersectObjects(objects, recursive);
|
||||
|
||||
// intersects array contains:
|
||||
// {
|
||||
// distance: number, // Distance from ray origin
|
||||
// point: Vector3, // Intersection point in world coords
|
||||
// face: Face3, // Intersected face
|
||||
// faceIndex: number, // Face index
|
||||
// object: Object3D, // Intersected object
|
||||
// uv: Vector2, // UV coordinates at intersection
|
||||
// uv1: Vector2, // Second UV channel
|
||||
// normal: Vector3, // Interpolated face normal
|
||||
// instanceId: number // For InstancedMesh
|
||||
// }
|
||||
```
|
||||
|
||||
### Mouse Position Conversion
|
||||
|
||||
```javascript
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
function updateMouse(event) {
|
||||
// For full window
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
}
|
||||
|
||||
// For specific canvas element
|
||||
function updateMouseCanvas(event, canvas) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Touch Support
|
||||
|
||||
```javascript
|
||||
function onTouchStart(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.touches.length === 1) {
|
||||
const touch = event.touches[0];
|
||||
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(clickableObjects);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
handleSelection(intersects[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderer.domElement.addEventListener("touchstart", onTouchStart);
|
||||
```
|
||||
|
||||
### Raycaster Options
|
||||
|
||||
```javascript
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
// Near/far clipping (default: 0, Infinity)
|
||||
raycaster.near = 0;
|
||||
raycaster.far = 100;
|
||||
|
||||
// Line/Points precision
|
||||
raycaster.params.Line.threshold = 0.1;
|
||||
raycaster.params.Points.threshold = 0.1;
|
||||
|
||||
// Layers (only intersect objects on specific layers)
|
||||
raycaster.layers.set(1);
|
||||
```
|
||||
|
||||
### Efficient Raycasting
|
||||
|
||||
```javascript
|
||||
// Only check specific objects
|
||||
const clickables = [mesh1, mesh2, mesh3];
|
||||
const intersects = raycaster.intersectObjects(clickables, false);
|
||||
|
||||
// Use layers for filtering
|
||||
mesh1.layers.set(1); // Clickable layer
|
||||
raycaster.layers.set(1);
|
||||
|
||||
// Throttle raycast for hover effects
|
||||
let lastRaycast = 0;
|
||||
function onMouseMove(event) {
|
||||
const now = Date.now();
|
||||
if (now - lastRaycast < 50) return; // 20fps max
|
||||
lastRaycast = now;
|
||||
|
||||
// Raycast here
|
||||
}
|
||||
```
|
||||
|
||||
## Camera Controls
|
||||
|
||||
### OrbitControls
|
||||
|
||||
```javascript
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
|
||||
// Damping (smooth movement)
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// Rotation limits
|
||||
controls.minPolarAngle = 0; // Top
|
||||
controls.maxPolarAngle = Math.PI / 2; // Horizon
|
||||
controls.minAzimuthAngle = -Math.PI / 4; // Left
|
||||
controls.maxAzimuthAngle = Math.PI / 4; // Right
|
||||
|
||||
// Zoom limits
|
||||
controls.minDistance = 2;
|
||||
controls.maxDistance = 50;
|
||||
|
||||
// Enable/disable features
|
||||
controls.enableRotate = true;
|
||||
controls.enableZoom = true;
|
||||
controls.enablePan = true;
|
||||
|
||||
// Auto-rotate
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 2.0;
|
||||
|
||||
// Target (orbit point)
|
||||
controls.target.set(0, 1, 0);
|
||||
|
||||
// Update in animation loop
|
||||
function animate() {
|
||||
controls.update(); // Required for damping and auto-rotate
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
### FlyControls
|
||||
|
||||
```javascript
|
||||
import { FlyControls } from "three/addons/controls/FlyControls.js";
|
||||
|
||||
const controls = new FlyControls(camera, renderer.domElement);
|
||||
controls.movementSpeed = 10;
|
||||
controls.rollSpeed = Math.PI / 24;
|
||||
controls.dragToLook = true;
|
||||
|
||||
// Update with delta
|
||||
function animate() {
|
||||
controls.update(clock.getDelta());
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
### FirstPersonControls
|
||||
|
||||
```javascript
|
||||
import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js";
|
||||
|
||||
const controls = new FirstPersonControls(camera, renderer.domElement);
|
||||
controls.movementSpeed = 10;
|
||||
controls.lookSpeed = 0.1;
|
||||
controls.lookVertical = true;
|
||||
controls.constrainVertical = true;
|
||||
controls.verticalMin = Math.PI / 4;
|
||||
controls.verticalMax = (Math.PI * 3) / 4;
|
||||
|
||||
function animate() {
|
||||
controls.update(clock.getDelta());
|
||||
}
|
||||
```
|
||||
|
||||
### PointerLockControls
|
||||
|
||||
```javascript
|
||||
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
|
||||
|
||||
const controls = new PointerLockControls(camera, document.body);
|
||||
|
||||
// Lock pointer on click
|
||||
document.addEventListener("click", () => {
|
||||
controls.lock();
|
||||
});
|
||||
|
||||
controls.addEventListener("lock", () => {
|
||||
console.log("Pointer locked");
|
||||
});
|
||||
|
||||
controls.addEventListener("unlock", () => {
|
||||
console.log("Pointer unlocked");
|
||||
});
|
||||
|
||||
// Movement
|
||||
const velocity = new THREE.Vector3();
|
||||
const direction = new THREE.Vector3();
|
||||
const moveForward = false;
|
||||
const moveBackward = false;
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
switch (event.code) {
|
||||
case "KeyW":
|
||||
moveForward = true;
|
||||
break;
|
||||
case "KeyS":
|
||||
moveBackward = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function animate() {
|
||||
if (controls.isLocked) {
|
||||
direction.z = Number(moveForward) - Number(moveBackward);
|
||||
direction.normalize();
|
||||
|
||||
velocity.z -= direction.z * 0.1;
|
||||
velocity.z *= 0.9; // Friction
|
||||
|
||||
controls.moveForward(-velocity.z);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TrackballControls
|
||||
|
||||
```javascript
|
||||
import { TrackballControls } from "three/addons/controls/TrackballControls.js";
|
||||
|
||||
const controls = new TrackballControls(camera, renderer.domElement);
|
||||
controls.rotateSpeed = 2.0;
|
||||
controls.zoomSpeed = 1.2;
|
||||
controls.panSpeed = 0.8;
|
||||
controls.staticMoving = true;
|
||||
|
||||
function animate() {
|
||||
controls.update();
|
||||
}
|
||||
```
|
||||
|
||||
### MapControls
|
||||
|
||||
```javascript
|
||||
import { MapControls } from "three/addons/controls/MapControls.js";
|
||||
|
||||
const controls = new MapControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = false;
|
||||
controls.maxPolarAngle = Math.PI / 2;
|
||||
```
|
||||
|
||||
## TransformControls
|
||||
|
||||
Gizmo for moving/rotating/scaling objects.
|
||||
|
||||
```javascript
|
||||
import { TransformControls } from "three/addons/controls/TransformControls.js";
|
||||
|
||||
const transformControls = new TransformControls(camera, renderer.domElement);
|
||||
scene.add(transformControls);
|
||||
|
||||
// Attach to object
|
||||
transformControls.attach(selectedMesh);
|
||||
|
||||
// Switch modes
|
||||
transformControls.setMode("translate"); // 'translate', 'rotate', 'scale'
|
||||
|
||||
// Change space
|
||||
transformControls.setSpace("local"); // 'local', 'world'
|
||||
|
||||
// Size
|
||||
transformControls.setSize(1);
|
||||
|
||||
// Events
|
||||
transformControls.addEventListener("dragging-changed", (event) => {
|
||||
// Disable orbit controls while dragging
|
||||
orbitControls.enabled = !event.value;
|
||||
});
|
||||
|
||||
transformControls.addEventListener("change", () => {
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (event.key) {
|
||||
case "g":
|
||||
transformControls.setMode("translate");
|
||||
break;
|
||||
case "r":
|
||||
transformControls.setMode("rotate");
|
||||
break;
|
||||
case "s":
|
||||
transformControls.setMode("scale");
|
||||
break;
|
||||
case "Escape":
|
||||
transformControls.detach();
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## DragControls
|
||||
|
||||
Drag objects directly.
|
||||
|
||||
```javascript
|
||||
import { DragControls } from "three/addons/controls/DragControls.js";
|
||||
|
||||
const draggableObjects = [mesh1, mesh2, mesh3];
|
||||
const dragControls = new DragControls(
|
||||
draggableObjects,
|
||||
camera,
|
||||
renderer.domElement,
|
||||
);
|
||||
|
||||
dragControls.addEventListener("dragstart", (event) => {
|
||||
orbitControls.enabled = false;
|
||||
event.object.material.emissive.set(0xaaaaaa);
|
||||
});
|
||||
|
||||
dragControls.addEventListener("drag", (event) => {
|
||||
// Constrain to ground plane
|
||||
event.object.position.y = 0;
|
||||
});
|
||||
|
||||
dragControls.addEventListener("dragend", (event) => {
|
||||
orbitControls.enabled = true;
|
||||
event.object.material.emissive.set(0x000000);
|
||||
});
|
||||
```
|
||||
|
||||
## Selection System
|
||||
|
||||
### Click to Select
|
||||
|
||||
```javascript
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
let selectedObject = null;
|
||||
|
||||
function onMouseDown(event) {
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(selectableObjects);
|
||||
|
||||
// Deselect previous
|
||||
if (selectedObject) {
|
||||
selectedObject.material.emissive.set(0x000000);
|
||||
}
|
||||
|
||||
// Select new
|
||||
if (intersects.length > 0) {
|
||||
selectedObject = intersects[0].object;
|
||||
selectedObject.material.emissive.set(0x444444);
|
||||
} else {
|
||||
selectedObject = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Box Selection
|
||||
|
||||
```javascript
|
||||
import { SelectionBox } from "three/addons/interactive/SelectionBox.js";
|
||||
import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js";
|
||||
|
||||
const selectionBox = new SelectionBox(camera, scene);
|
||||
const selectionHelper = new SelectionHelper(renderer, "selectBox"); // CSS class
|
||||
|
||||
document.addEventListener("pointerdown", (event) => {
|
||||
selectionBox.startPoint.set(
|
||||
(event.clientX / window.innerWidth) * 2 - 1,
|
||||
-(event.clientY / window.innerHeight) * 2 + 1,
|
||||
0.5,
|
||||
);
|
||||
});
|
||||
|
||||
document.addEventListener("pointermove", (event) => {
|
||||
if (selectionHelper.isDown) {
|
||||
selectionBox.endPoint.set(
|
||||
(event.clientX / window.innerWidth) * 2 - 1,
|
||||
-(event.clientY / window.innerHeight) * 2 + 1,
|
||||
0.5,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("pointerup", (event) => {
|
||||
selectionBox.endPoint.set(
|
||||
(event.clientX / window.innerWidth) * 2 - 1,
|
||||
-(event.clientY / window.innerHeight) * 2 + 1,
|
||||
0.5,
|
||||
);
|
||||
|
||||
const selected = selectionBox.select();
|
||||
console.log("Selected objects:", selected);
|
||||
});
|
||||
```
|
||||
|
||||
### Hover Effects
|
||||
|
||||
```javascript
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
let hoveredObject = null;
|
||||
|
||||
function onMouseMove(event) {
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(hoverableObjects);
|
||||
|
||||
// Reset previous hover
|
||||
if (hoveredObject) {
|
||||
hoveredObject.material.color.set(hoveredObject.userData.originalColor);
|
||||
document.body.style.cursor = "default";
|
||||
}
|
||||
|
||||
// Apply new hover
|
||||
if (intersects.length > 0) {
|
||||
hoveredObject = intersects[0].object;
|
||||
if (!hoveredObject.userData.originalColor) {
|
||||
hoveredObject.userData.originalColor =
|
||||
hoveredObject.material.color.getHex();
|
||||
}
|
||||
hoveredObject.material.color.set(0xff6600);
|
||||
document.body.style.cursor = "pointer";
|
||||
} else {
|
||||
hoveredObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
```
|
||||
|
||||
## Keyboard Input
|
||||
|
||||
```javascript
|
||||
const keys = {};
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
keys[event.code] = true;
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (event) => {
|
||||
keys[event.code] = false;
|
||||
});
|
||||
|
||||
function update() {
|
||||
const speed = 0.1;
|
||||
|
||||
if (keys["KeyW"]) player.position.z -= speed;
|
||||
if (keys["KeyS"]) player.position.z += speed;
|
||||
if (keys["KeyA"]) player.position.x -= speed;
|
||||
if (keys["KeyD"]) player.position.x += speed;
|
||||
if (keys["Space"]) player.position.y += speed;
|
||||
if (keys["ShiftLeft"]) player.position.y -= speed;
|
||||
}
|
||||
```
|
||||
|
||||
## World-Screen Coordinate Conversion
|
||||
|
||||
### World to Screen
|
||||
|
||||
```javascript
|
||||
function worldToScreen(position, camera) {
|
||||
const vector = position.clone();
|
||||
vector.project(camera);
|
||||
|
||||
return {
|
||||
x: ((vector.x + 1) / 2) * window.innerWidth,
|
||||
y: (-(vector.y - 1) / 2) * window.innerHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// Position HTML element over 3D object
|
||||
const screenPos = worldToScreen(mesh.position, camera);
|
||||
element.style.left = screenPos.x + "px";
|
||||
element.style.top = screenPos.y + "px";
|
||||
```
|
||||
|
||||
### Screen to World
|
||||
|
||||
```javascript
|
||||
function screenToWorld(screenX, screenY, camera, targetZ = 0) {
|
||||
const vector = new THREE.Vector3(
|
||||
(screenX / window.innerWidth) * 2 - 1,
|
||||
-(screenY / window.innerHeight) * 2 + 1,
|
||||
0.5,
|
||||
);
|
||||
|
||||
vector.unproject(camera);
|
||||
|
||||
const dir = vector.sub(camera.position).normalize();
|
||||
const distance = (targetZ - camera.position.z) / dir.z;
|
||||
|
||||
return camera.position.clone().add(dir.multiplyScalar(distance));
|
||||
}
|
||||
```
|
||||
|
||||
### Ray-Plane Intersection
|
||||
|
||||
```javascript
|
||||
function getRayPlaneIntersection(mouse, camera, plane) {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersection = new THREE.Vector3();
|
||||
raycaster.ray.intersectPlane(plane, intersection);
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
// Ground plane
|
||||
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||
const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);
|
||||
```
|
||||
|
||||
## Event Handling Best Practices
|
||||
|
||||
```javascript
|
||||
class InteractionManager {
|
||||
constructor(camera, renderer, scene) {
|
||||
this.camera = camera;
|
||||
this.renderer = renderer;
|
||||
this.scene = scene;
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.mouse = new THREE.Vector2();
|
||||
this.clickables = [];
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const canvas = this.renderer.domElement;
|
||||
|
||||
canvas.addEventListener("click", (e) => this.onClick(e));
|
||||
canvas.addEventListener("mousemove", (e) => this.onMouseMove(e));
|
||||
canvas.addEventListener("touchstart", (e) => this.onTouchStart(e));
|
||||
}
|
||||
|
||||
updateMouse(event) {
|
||||
const rect = this.renderer.domElement.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
}
|
||||
|
||||
getIntersects() {
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
return this.raycaster.intersectObjects(this.clickables, true);
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
this.updateMouse(event);
|
||||
const intersects = this.getIntersects();
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const object = intersects[0].object;
|
||||
if (object.userData.onClick) {
|
||||
object.userData.onClick(intersects[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addClickable(object, callback) {
|
||||
this.clickables.push(object);
|
||||
object.userData.onClick = callback;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Remove event listeners
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const interaction = new InteractionManager(camera, renderer, scene);
|
||||
interaction.addClickable(mesh, (intersect) => {
|
||||
console.log("Clicked at:", intersect.point);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Limit raycasts**: Throttle mousemove handlers
|
||||
2. **Use layers**: Filter raycast targets
|
||||
3. **Simple collision meshes**: Use invisible simpler geometry for raycasting
|
||||
4. **Disable controls when not needed**: `controls.enabled = false`
|
||||
5. **Batch updates**: Group interaction checks
|
||||
|
||||
```javascript
|
||||
// Use simpler geometry for raycasting
|
||||
const complexMesh = loadedModel;
|
||||
const collisionMesh = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshBasicMaterial({ visible: false }),
|
||||
);
|
||||
collisionMesh.userData.target = complexMesh;
|
||||
clickables.push(collisionMesh);
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-fundamentals` - Camera and scene setup
|
||||
- `threejs-animation` - Animating interactions
|
||||
- `threejs-shaders` - Visual feedback effects
|
||||
@@ -0,0 +1,481 @@
|
||||
---
|
||||
name: threejs-lighting
|
||||
description: Three.js lighting - light types, shadows, environment lighting. Use when adding lights, configuring shadows, setting up IBL, or optimizing lighting performance.
|
||||
---
|
||||
|
||||
# Three.js Lighting
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Basic lighting setup
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(5, 5, 5);
|
||||
scene.add(directionalLight);
|
||||
```
|
||||
|
||||
## Light Types Overview
|
||||
|
||||
| Light | Description | Shadow Support | Cost |
|
||||
| ---------------- | ---------------------- | -------------- | -------- |
|
||||
| AmbientLight | Uniform everywhere | No | Very Low |
|
||||
| HemisphereLight | Sky/ground gradient | No | Very Low |
|
||||
| DirectionalLight | Parallel rays (sun) | Yes | Low |
|
||||
| PointLight | Omnidirectional (bulb) | Yes | Medium |
|
||||
| SpotLight | Cone-shaped | Yes | Medium |
|
||||
| RectAreaLight | Area light (window) | No\* | High |
|
||||
|
||||
\*RectAreaLight shadows require custom solutions
|
||||
|
||||
## AmbientLight
|
||||
|
||||
Illuminates all objects equally. No direction, no shadows.
|
||||
|
||||
```javascript
|
||||
// AmbientLight(color, intensity)
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
scene.add(ambient);
|
||||
|
||||
// Modify at runtime
|
||||
ambient.color.set(0xffffcc);
|
||||
ambient.intensity = 0.3;
|
||||
```
|
||||
|
||||
## HemisphereLight
|
||||
|
||||
Gradient from sky to ground color. Good for outdoor scenes.
|
||||
|
||||
```javascript
|
||||
// HemisphereLight(skyColor, groundColor, intensity)
|
||||
const hemi = new THREE.HemisphereLight(0x87ceeb, 0x8b4513, 0.6);
|
||||
hemi.position.set(0, 50, 0);
|
||||
scene.add(hemi);
|
||||
|
||||
// Properties
|
||||
hemi.color; // Sky color
|
||||
hemi.groundColor; // Ground color
|
||||
hemi.intensity;
|
||||
```
|
||||
|
||||
## DirectionalLight
|
||||
|
||||
Parallel light rays. Simulates distant light source (sun).
|
||||
|
||||
```javascript
|
||||
// DirectionalLight(color, intensity)
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
dirLight.position.set(5, 10, 5);
|
||||
|
||||
// Light points at target (default: 0, 0, 0)
|
||||
dirLight.target.position.set(0, 0, 0);
|
||||
scene.add(dirLight.target);
|
||||
|
||||
scene.add(dirLight);
|
||||
```
|
||||
|
||||
### DirectionalLight Shadows
|
||||
|
||||
```javascript
|
||||
dirLight.castShadow = true;
|
||||
|
||||
// Shadow map size (higher = sharper, more expensive)
|
||||
dirLight.shadow.mapSize.width = 2048;
|
||||
dirLight.shadow.mapSize.height = 2048;
|
||||
|
||||
// Shadow camera (orthographic)
|
||||
dirLight.shadow.camera.near = 0.5;
|
||||
dirLight.shadow.camera.far = 50;
|
||||
dirLight.shadow.camera.left = -10;
|
||||
dirLight.shadow.camera.right = 10;
|
||||
dirLight.shadow.camera.top = 10;
|
||||
dirLight.shadow.camera.bottom = -10;
|
||||
|
||||
// Shadow softness
|
||||
dirLight.shadow.radius = 4; // Blur radius (PCFSoftShadowMap only)
|
||||
|
||||
// Shadow bias (fixes shadow acne)
|
||||
dirLight.shadow.bias = -0.0001;
|
||||
dirLight.shadow.normalBias = 0.02;
|
||||
|
||||
// Helper to visualize shadow camera
|
||||
const helper = new THREE.CameraHelper(dirLight.shadow.camera);
|
||||
scene.add(helper);
|
||||
```
|
||||
|
||||
## PointLight
|
||||
|
||||
Emits light in all directions from a point. Like a light bulb.
|
||||
|
||||
```javascript
|
||||
// PointLight(color, intensity, distance, decay)
|
||||
const pointLight = new THREE.PointLight(0xffffff, 1, 100, 2);
|
||||
pointLight.position.set(0, 5, 0);
|
||||
scene.add(pointLight);
|
||||
|
||||
// Properties
|
||||
pointLight.distance; // Maximum range (0 = infinite)
|
||||
pointLight.decay; // Light falloff (physically correct = 2)
|
||||
```
|
||||
|
||||
### PointLight Shadows
|
||||
|
||||
```javascript
|
||||
pointLight.castShadow = true;
|
||||
pointLight.shadow.mapSize.width = 1024;
|
||||
pointLight.shadow.mapSize.height = 1024;
|
||||
|
||||
// Shadow camera (perspective - 6 directions for cube map)
|
||||
pointLight.shadow.camera.near = 0.5;
|
||||
pointLight.shadow.camera.far = 50;
|
||||
|
||||
pointLight.shadow.bias = -0.005;
|
||||
```
|
||||
|
||||
## SpotLight
|
||||
|
||||
Cone-shaped light. Like a flashlight or stage light.
|
||||
|
||||
```javascript
|
||||
// SpotLight(color, intensity, distance, angle, penumbra, decay)
|
||||
const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 6, 0.5, 2);
|
||||
spotLight.position.set(0, 10, 0);
|
||||
|
||||
// Target (light points at this)
|
||||
spotLight.target.position.set(0, 0, 0);
|
||||
scene.add(spotLight.target);
|
||||
|
||||
scene.add(spotLight);
|
||||
|
||||
// Properties
|
||||
spotLight.angle; // Cone angle (radians, max Math.PI/2)
|
||||
spotLight.penumbra; // Soft edge (0-1)
|
||||
spotLight.distance; // Range
|
||||
spotLight.decay; // Falloff
|
||||
```
|
||||
|
||||
### SpotLight Shadows
|
||||
|
||||
```javascript
|
||||
spotLight.castShadow = true;
|
||||
spotLight.shadow.mapSize.width = 1024;
|
||||
spotLight.shadow.mapSize.height = 1024;
|
||||
|
||||
// Shadow camera (perspective)
|
||||
spotLight.shadow.camera.near = 0.5;
|
||||
spotLight.shadow.camera.far = 50;
|
||||
spotLight.shadow.camera.fov = 30;
|
||||
|
||||
spotLight.shadow.bias = -0.0001;
|
||||
|
||||
// Focus (affects shadow projection)
|
||||
spotLight.shadow.focus = 1;
|
||||
```
|
||||
|
||||
## RectAreaLight
|
||||
|
||||
Rectangular area light. Great for soft, realistic lighting.
|
||||
|
||||
```javascript
|
||||
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper.js";
|
||||
import { RectAreaLightUniformsLib } from "three/examples/jsm/lights/RectAreaLightUniformsLib.js";
|
||||
|
||||
// Must initialize uniforms first
|
||||
RectAreaLightUniformsLib.init();
|
||||
|
||||
// RectAreaLight(color, intensity, width, height)
|
||||
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);
|
||||
|
||||
// Note: Only works with MeshStandardMaterial and MeshPhysicalMaterial
|
||||
// Does not cast shadows natively
|
||||
```
|
||||
|
||||
## Shadow Setup
|
||||
|
||||
### Enable Shadows
|
||||
|
||||
```javascript
|
||||
// 1. Enable on renderer
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
|
||||
// Shadow map types:
|
||||
// THREE.BasicShadowMap - fastest, low quality
|
||||
// THREE.PCFShadowMap - default, filtered
|
||||
// THREE.PCFSoftShadowMap - softer edges
|
||||
// THREE.VSMShadowMap - variance shadow map
|
||||
|
||||
// 2. Enable on light
|
||||
light.castShadow = true;
|
||||
|
||||
// 3. Enable on objects
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
// Ground plane
|
||||
floor.receiveShadow = true;
|
||||
floor.castShadow = false; // Usually false for floors
|
||||
```
|
||||
|
||||
### Optimizing Shadows
|
||||
|
||||
```javascript
|
||||
// Tight shadow camera frustum
|
||||
const d = 10;
|
||||
dirLight.shadow.camera.left = -d;
|
||||
dirLight.shadow.camera.right = d;
|
||||
dirLight.shadow.camera.top = d;
|
||||
dirLight.shadow.camera.bottom = -d;
|
||||
dirLight.shadow.camera.near = 0.5;
|
||||
dirLight.shadow.camera.far = 30;
|
||||
|
||||
// Fix shadow acne
|
||||
dirLight.shadow.bias = -0.0001; // Depth bias
|
||||
dirLight.shadow.normalBias = 0.02; // Bias along normal
|
||||
|
||||
// Shadow map size (balance quality vs performance)
|
||||
// 512 - low quality
|
||||
// 1024 - medium quality
|
||||
// 2048 - high quality
|
||||
// 4096 - very high quality (expensive)
|
||||
```
|
||||
|
||||
### Contact Shadows (Fake, Fast)
|
||||
|
||||
```javascript
|
||||
import { ContactShadows } from "three/examples/jsm/objects/ContactShadows.js";
|
||||
|
||||
const contactShadows = new ContactShadows({
|
||||
resolution: 512,
|
||||
blur: 2,
|
||||
opacity: 0.5,
|
||||
scale: 10,
|
||||
position: [0, 0, 0],
|
||||
});
|
||||
scene.add(contactShadows);
|
||||
```
|
||||
|
||||
## Light Helpers
|
||||
|
||||
```javascript
|
||||
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper.js";
|
||||
|
||||
// DirectionalLight helper
|
||||
const dirHelper = new THREE.DirectionalLightHelper(dirLight, 5);
|
||||
scene.add(dirHelper);
|
||||
|
||||
// PointLight helper
|
||||
const pointHelper = new THREE.PointLightHelper(pointLight, 1);
|
||||
scene.add(pointHelper);
|
||||
|
||||
// SpotLight helper
|
||||
const spotHelper = new THREE.SpotLightHelper(spotLight);
|
||||
scene.add(spotHelper);
|
||||
|
||||
// Hemisphere helper
|
||||
const hemiHelper = new THREE.HemisphereLightHelper(hemiLight, 5);
|
||||
scene.add(hemiHelper);
|
||||
|
||||
// RectAreaLight helper
|
||||
const rectHelper = new RectAreaLightHelper(rectLight);
|
||||
rectLight.add(rectHelper);
|
||||
|
||||
// Update helpers when light changes
|
||||
dirHelper.update();
|
||||
spotHelper.update();
|
||||
```
|
||||
|
||||
## Environment Lighting (IBL)
|
||||
|
||||
Image-Based Lighting using HDR environment maps.
|
||||
|
||||
```javascript
|
||||
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
||||
|
||||
const rgbeLoader = new RGBELoader();
|
||||
rgbeLoader.load("environment.hdr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
|
||||
// Set as scene environment (affects all PBR materials)
|
||||
scene.environment = texture;
|
||||
|
||||
// Optional: also use as background
|
||||
scene.background = texture;
|
||||
scene.backgroundBlurriness = 0; // 0-1, blur the background
|
||||
scene.backgroundIntensity = 1;
|
||||
});
|
||||
|
||||
// PMREMGenerator for better reflections
|
||||
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||||
pmremGenerator.compileEquirectangularShader();
|
||||
|
||||
rgbeLoader.load("environment.hdr", (texture) => {
|
||||
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||
scene.environment = envMap;
|
||||
texture.dispose();
|
||||
pmremGenerator.dispose();
|
||||
});
|
||||
```
|
||||
|
||||
### Cube Texture Environment
|
||||
|
||||
```javascript
|
||||
const cubeLoader = new THREE.CubeTextureLoader();
|
||||
const envMap = cubeLoader.load([
|
||||
"px.jpg",
|
||||
"nx.jpg",
|
||||
"py.jpg",
|
||||
"ny.jpg",
|
||||
"pz.jpg",
|
||||
"nz.jpg",
|
||||
]);
|
||||
|
||||
scene.environment = envMap;
|
||||
scene.background = envMap;
|
||||
```
|
||||
|
||||
## Light Probes (Advanced)
|
||||
|
||||
Capture lighting from a point in space for ambient lighting.
|
||||
|
||||
```javascript
|
||||
import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js";
|
||||
|
||||
// Generate from cube texture
|
||||
const lightProbe = new THREE.LightProbe();
|
||||
scene.add(lightProbe);
|
||||
|
||||
lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture));
|
||||
|
||||
// Or from render target
|
||||
const cubeCamera = new THREE.CubeCamera(
|
||||
0.1,
|
||||
100,
|
||||
new THREE.WebGLCubeRenderTarget(256),
|
||||
);
|
||||
cubeCamera.update(renderer, scene);
|
||||
lightProbe.copy(
|
||||
LightProbeGenerator.fromCubeRenderTarget(renderer, cubeCamera.renderTarget),
|
||||
);
|
||||
```
|
||||
|
||||
## Common Lighting Setups
|
||||
|
||||
### Three-Point Lighting
|
||||
|
||||
```javascript
|
||||
// Key light (main light)
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
keyLight.position.set(5, 5, 5);
|
||||
scene.add(keyLight);
|
||||
|
||||
// Fill light (softer, opposite side)
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
fillLight.position.set(-5, 3, 5);
|
||||
scene.add(fillLight);
|
||||
|
||||
// Back light (rim lighting)
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
||||
backLight.position.set(0, 5, -5);
|
||||
scene.add(backLight);
|
||||
|
||||
// Ambient fill
|
||||
const ambient = new THREE.AmbientLight(0x404040, 0.3);
|
||||
scene.add(ambient);
|
||||
```
|
||||
|
||||
### Outdoor Daylight
|
||||
|
||||
```javascript
|
||||
// Sun
|
||||
const sun = new THREE.DirectionalLight(0xffffcc, 1.5);
|
||||
sun.position.set(50, 100, 50);
|
||||
sun.castShadow = true;
|
||||
scene.add(sun);
|
||||
|
||||
// Sky ambient
|
||||
const hemi = new THREE.HemisphereLight(0x87ceeb, 0x8b4513, 0.6);
|
||||
scene.add(hemi);
|
||||
```
|
||||
|
||||
### Indoor Studio
|
||||
|
||||
```javascript
|
||||
// Multiple area lights
|
||||
RectAreaLightUniformsLib.init();
|
||||
|
||||
const light1 = new THREE.RectAreaLight(0xffffff, 5, 2, 2);
|
||||
light1.position.set(3, 3, 3);
|
||||
light1.lookAt(0, 0, 0);
|
||||
scene.add(light1);
|
||||
|
||||
const light2 = new THREE.RectAreaLight(0xffffff, 3, 2, 2);
|
||||
light2.position.set(-3, 3, 3);
|
||||
light2.lookAt(0, 0, 0);
|
||||
scene.add(light2);
|
||||
|
||||
// Ambient fill
|
||||
const ambient = new THREE.AmbientLight(0x404040, 0.2);
|
||||
scene.add(ambient);
|
||||
```
|
||||
|
||||
## Light Animation
|
||||
|
||||
```javascript
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
function animate() {
|
||||
const time = clock.getElapsedTime();
|
||||
|
||||
// Orbit light around scene
|
||||
light.position.x = Math.cos(time) * 5;
|
||||
light.position.z = Math.sin(time) * 5;
|
||||
|
||||
// Pulsing intensity
|
||||
light.intensity = 1 + Math.sin(time * 2) * 0.5;
|
||||
|
||||
// Color cycling
|
||||
light.color.setHSL((time * 0.1) % 1, 1, 0.5);
|
||||
|
||||
// Update helpers if using
|
||||
lightHelper.update();
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Limit light count**: Each light adds shader complexity
|
||||
2. **Use baked lighting**: For static scenes, bake to textures
|
||||
3. **Smaller shadow maps**: 512-1024 often sufficient
|
||||
4. **Tight shadow frustums**: Only cover needed area
|
||||
5. **Disable unused shadows**: Not all lights need shadows
|
||||
6. **Use light layers**: Exclude objects from certain lights
|
||||
|
||||
```javascript
|
||||
// Light layers
|
||||
light.layers.set(1); // Light only affects layer 1
|
||||
mesh.layers.enable(1); // Mesh is on layer 1
|
||||
otherMesh.layers.disable(1); // Other mesh not affected
|
||||
|
||||
// Selective shadows
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
decorMesh.castShadow = false; // Small objects often don't need to cast
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-materials` - Material light response
|
||||
- `threejs-textures` - Lightmaps and environment maps
|
||||
- `threejs-postprocessing` - Bloom and other light effects
|
||||
@@ -0,0 +1,623 @@
|
||||
---
|
||||
name: threejs-loaders
|
||||
description: Three.js asset loading - GLTF, textures, images, models, async patterns. Use when loading 3D models, textures, HDR environments, or managing loading progress.
|
||||
---
|
||||
|
||||
# Three.js Loaders
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
|
||||
// Load GLTF model
|
||||
const loader = new GLTFLoader();
|
||||
loader.load("model.glb", (gltf) => {
|
||||
scene.add(gltf.scene);
|
||||
});
|
||||
|
||||
// Load texture
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const texture = textureLoader.load("texture.jpg");
|
||||
```
|
||||
|
||||
## LoadingManager
|
||||
|
||||
Coordinate multiple loaders and track progress.
|
||||
|
||||
```javascript
|
||||
const manager = new THREE.LoadingManager();
|
||||
|
||||
// Callbacks
|
||||
manager.onStart = (url, loaded, total) => {
|
||||
console.log(`Started loading: ${url}`);
|
||||
};
|
||||
|
||||
manager.onLoad = () => {
|
||||
console.log("All assets loaded!");
|
||||
startGame();
|
||||
};
|
||||
|
||||
manager.onProgress = (url, loaded, total) => {
|
||||
const progress = (loaded / total) * 100;
|
||||
console.log(`Loading: ${progress.toFixed(1)}%`);
|
||||
updateProgressBar(progress);
|
||||
};
|
||||
|
||||
manager.onError = (url) => {
|
||||
console.error(`Error loading: ${url}`);
|
||||
};
|
||||
|
||||
// Use manager with loaders
|
||||
const textureLoader = new THREE.TextureLoader(manager);
|
||||
const gltfLoader = new GLTFLoader(manager);
|
||||
|
||||
// Load assets
|
||||
textureLoader.load("texture1.jpg");
|
||||
textureLoader.load("texture2.jpg");
|
||||
gltfLoader.load("model.glb");
|
||||
// onLoad fires when ALL are complete
|
||||
```
|
||||
|
||||
## Texture Loading
|
||||
|
||||
### TextureLoader
|
||||
|
||||
```javascript
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
// Callback style
|
||||
loader.load(
|
||||
"texture.jpg",
|
||||
(texture) => {
|
||||
// onLoad
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
},
|
||||
undefined, // onProgress - not supported for image loading
|
||||
(error) => {
|
||||
// onError
|
||||
console.error("Error loading texture", error);
|
||||
},
|
||||
);
|
||||
|
||||
// Synchronous (returns texture, loads async)
|
||||
const texture = loader.load("texture.jpg");
|
||||
material.map = texture;
|
||||
```
|
||||
|
||||
### Texture Configuration
|
||||
|
||||
```javascript
|
||||
const texture = loader.load("texture.jpg", (tex) => {
|
||||
// Color space (important for color accuracy)
|
||||
tex.colorSpace = THREE.SRGBColorSpace; // For color/albedo maps
|
||||
// tex.colorSpace = THREE.LinearSRGBColorSpace; // For data maps (normal, roughness)
|
||||
|
||||
// Wrapping
|
||||
tex.wrapS = THREE.RepeatWrapping;
|
||||
tex.wrapT = THREE.RepeatWrapping;
|
||||
// ClampToEdgeWrapping, RepeatWrapping, MirroredRepeatWrapping
|
||||
|
||||
// Repeat/offset
|
||||
tex.repeat.set(2, 2);
|
||||
tex.offset.set(0.5, 0.5);
|
||||
tex.rotation = Math.PI / 4;
|
||||
tex.center.set(0.5, 0.5);
|
||||
|
||||
// Filtering
|
||||
tex.minFilter = THREE.LinearMipmapLinearFilter; // Default
|
||||
tex.magFilter = THREE.LinearFilter; // Default
|
||||
// NearestFilter - pixelated
|
||||
// LinearFilter - smooth
|
||||
// LinearMipmapLinearFilter - smooth with mipmaps
|
||||
|
||||
// Anisotropic filtering (sharper at angles)
|
||||
tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
|
||||
// Flip Y (usually true for standard textures)
|
||||
tex.flipY = true;
|
||||
|
||||
tex.needsUpdate = true;
|
||||
});
|
||||
```
|
||||
|
||||
### CubeTextureLoader
|
||||
|
||||
For environment maps and skyboxes.
|
||||
|
||||
```javascript
|
||||
const loader = new THREE.CubeTextureLoader();
|
||||
|
||||
// Load 6 faces
|
||||
const cubeTexture = loader.load([
|
||||
"px.jpg",
|
||||
"nx.jpg", // positive/negative X
|
||||
"py.jpg",
|
||||
"ny.jpg", // positive/negative Y
|
||||
"pz.jpg",
|
||||
"nz.jpg", // positive/negative Z
|
||||
]);
|
||||
|
||||
// Use as background
|
||||
scene.background = cubeTexture;
|
||||
|
||||
// Use as environment map
|
||||
scene.environment = cubeTexture;
|
||||
material.envMap = cubeTexture;
|
||||
```
|
||||
|
||||
### HDR/EXR Loading
|
||||
|
||||
```javascript
|
||||
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
|
||||
import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
|
||||
|
||||
// HDR
|
||||
const rgbeLoader = new RGBELoader();
|
||||
rgbeLoader.load("environment.hdr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
scene.background = texture;
|
||||
});
|
||||
|
||||
// EXR
|
||||
const exrLoader = new EXRLoader();
|
||||
exrLoader.load("environment.exr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
});
|
||||
```
|
||||
|
||||
### PMREMGenerator
|
||||
|
||||
Generate prefiltered environment maps for PBR.
|
||||
|
||||
```javascript
|
||||
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
|
||||
|
||||
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||||
pmremGenerator.compileEquirectangularShader();
|
||||
|
||||
new RGBELoader().load("environment.hdr", (texture) => {
|
||||
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||
|
||||
scene.environment = envMap;
|
||||
scene.background = envMap;
|
||||
|
||||
texture.dispose();
|
||||
pmremGenerator.dispose();
|
||||
});
|
||||
```
|
||||
|
||||
## GLTF/GLB Loading
|
||||
|
||||
The most common 3D format for web.
|
||||
|
||||
```javascript
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
loader.load("model.glb", (gltf) => {
|
||||
// The loaded scene
|
||||
const model = gltf.scene;
|
||||
scene.add(model);
|
||||
|
||||
// Animations
|
||||
const animations = gltf.animations;
|
||||
if (animations.length > 0) {
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
animations.forEach((clip) => {
|
||||
mixer.clipAction(clip).play();
|
||||
});
|
||||
}
|
||||
|
||||
// Cameras (if any)
|
||||
const cameras = gltf.cameras;
|
||||
|
||||
// Asset info
|
||||
console.log(gltf.asset); // Version, generator, etc.
|
||||
|
||||
// User data from Blender/etc
|
||||
console.log(gltf.userData);
|
||||
});
|
||||
```
|
||||
|
||||
### GLTF with Draco Compression
|
||||
|
||||
```javascript
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
|
||||
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath(
|
||||
"https://www.gstatic.com/draco/versioned/decoders/1.5.6/",
|
||||
);
|
||||
dracoLoader.preload();
|
||||
|
||||
const gltfLoader = new GLTFLoader();
|
||||
gltfLoader.setDRACOLoader(dracoLoader);
|
||||
|
||||
gltfLoader.load("compressed-model.glb", (gltf) => {
|
||||
scene.add(gltf.scene);
|
||||
});
|
||||
```
|
||||
|
||||
### GLTF with KTX2 Textures
|
||||
|
||||
```javascript
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
|
||||
|
||||
const ktx2Loader = new KTX2Loader();
|
||||
ktx2Loader.setTranscoderPath(
|
||||
"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/",
|
||||
);
|
||||
ktx2Loader.detectSupport(renderer);
|
||||
|
||||
const gltfLoader = new GLTFLoader();
|
||||
gltfLoader.setKTX2Loader(ktx2Loader);
|
||||
|
||||
gltfLoader.load("model-with-ktx2.glb", (gltf) => {
|
||||
scene.add(gltf.scene);
|
||||
});
|
||||
```
|
||||
|
||||
### Process GLTF Content
|
||||
|
||||
```javascript
|
||||
loader.load("model.glb", (gltf) => {
|
||||
const model = gltf.scene;
|
||||
|
||||
// Enable shadows
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Find specific mesh
|
||||
const head = model.getObjectByName("Head");
|
||||
|
||||
// Adjust materials
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
child.material.envMapIntensity = 0.5;
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
model.scale.setScalar(1 / maxDim);
|
||||
|
||||
scene.add(model);
|
||||
});
|
||||
```
|
||||
|
||||
## Other Model Formats
|
||||
|
||||
### OBJ + MTL
|
||||
|
||||
```javascript
|
||||
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
||||
import { MTLLoader } from "three/addons/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/addons/loaders/FBXLoader.js";
|
||||
|
||||
const loader = new FBXLoader();
|
||||
loader.load("model.fbx", (object) => {
|
||||
// FBX often has large scale
|
||||
object.scale.setScalar(0.01);
|
||||
|
||||
// Animations
|
||||
const mixer = new THREE.AnimationMixer(object);
|
||||
object.animations.forEach((clip) => {
|
||||
mixer.clipAction(clip).play();
|
||||
});
|
||||
|
||||
scene.add(object);
|
||||
});
|
||||
```
|
||||
|
||||
### STL
|
||||
|
||||
```javascript
|
||||
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
||||
|
||||
const loader = new STLLoader();
|
||||
loader.load("model.stl", (geometry) => {
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x888888 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
});
|
||||
```
|
||||
|
||||
### PLY
|
||||
|
||||
```javascript
|
||||
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
|
||||
|
||||
const loader = new PLYLoader();
|
||||
loader.load("model.ply", (geometry) => {
|
||||
geometry.computeVertexNormals();
|
||||
const material = new THREE.MeshStandardMaterial({ vertexColors: true });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
});
|
||||
```
|
||||
|
||||
## Async/Promise Loading
|
||||
|
||||
### Promisified Loader
|
||||
|
||||
```javascript
|
||||
function loadModel(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
loader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
async function init() {
|
||||
try {
|
||||
const gltf = await loadModel("model.glb");
|
||||
scene.add(gltf.scene);
|
||||
} catch (error) {
|
||||
console.error("Failed to load model:", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Load Multiple Assets
|
||||
|
||||
```javascript
|
||||
async function loadAssets() {
|
||||
const [modelGltf, envTexture, colorTexture] = await Promise.all([
|
||||
loadGLTF("model.glb"),
|
||||
loadRGBE("environment.hdr"),
|
||||
loadTexture("color.jpg"),
|
||||
]);
|
||||
|
||||
scene.add(modelGltf.scene);
|
||||
scene.environment = envTexture;
|
||||
material.map = colorTexture;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function loadGLTF(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new GLTFLoader().load(url, resolve, undefined, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function loadRGBE(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new RGBELoader().load(
|
||||
url,
|
||||
(texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
resolve(texture);
|
||||
},
|
||||
undefined,
|
||||
reject,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function loadTexture(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new THREE.TextureLoader().load(url, resolve, undefined, reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
### Built-in Cache
|
||||
|
||||
```javascript
|
||||
// Enable cache
|
||||
THREE.Cache.enabled = true;
|
||||
|
||||
// Clear cache
|
||||
THREE.Cache.clear();
|
||||
|
||||
// Manual cache management
|
||||
THREE.Cache.add("key", data);
|
||||
THREE.Cache.get("key");
|
||||
THREE.Cache.remove("key");
|
||||
```
|
||||
|
||||
### Custom Asset Manager
|
||||
|
||||
```javascript
|
||||
class AssetManager {
|
||||
constructor() {
|
||||
this.textures = new Map();
|
||||
this.models = new Map();
|
||||
this.gltfLoader = new GLTFLoader();
|
||||
this.textureLoader = new THREE.TextureLoader();
|
||||
}
|
||||
|
||||
async loadTexture(key, url) {
|
||||
if (this.textures.has(key)) {
|
||||
return this.textures.get(key);
|
||||
}
|
||||
|
||||
const texture = await new Promise((resolve, reject) => {
|
||||
this.textureLoader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
|
||||
this.textures.set(key, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
async loadModel(key, url) {
|
||||
if (this.models.has(key)) {
|
||||
return this.models.get(key).clone();
|
||||
}
|
||||
|
||||
const gltf = await new Promise((resolve, reject) => {
|
||||
this.gltfLoader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
|
||||
this.models.set(key, gltf.scene);
|
||||
return gltf.scene.clone();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.textures.forEach((t) => t.dispose());
|
||||
this.textures.clear();
|
||||
this.models.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const assets = new AssetManager();
|
||||
const texture = await assets.loadTexture("brick", "brick.jpg");
|
||||
const model = await assets.loadModel("tree", "tree.glb");
|
||||
```
|
||||
|
||||
## Loading from Different Sources
|
||||
|
||||
### Data URL / Base64
|
||||
|
||||
```javascript
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load("data:image/png;base64,iVBORw0KGgo...");
|
||||
```
|
||||
|
||||
### Blob URL
|
||||
|
||||
```javascript
|
||||
async function loadFromBlob(blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const texture = await loadTexture(url);
|
||||
URL.revokeObjectURL(url);
|
||||
return texture;
|
||||
}
|
||||
```
|
||||
|
||||
### ArrayBuffer
|
||||
|
||||
```javascript
|
||||
// From fetch
|
||||
const response = await fetch("model.glb");
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
// Parse with loader
|
||||
const loader = new GLTFLoader();
|
||||
loader.parse(buffer, "", (gltf) => {
|
||||
scene.add(gltf.scene);
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Path/URL
|
||||
|
||||
```javascript
|
||||
// Set base path
|
||||
loader.setPath("assets/models/");
|
||||
loader.load("model.glb"); // Loads from assets/models/model.glb
|
||||
|
||||
// Set resource path (for textures referenced in model)
|
||||
loader.setResourcePath("assets/textures/");
|
||||
|
||||
// Custom URL modifier
|
||||
manager.setURLModifier((url) => {
|
||||
return `https://cdn.example.com/${url}`;
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```javascript
|
||||
// Graceful fallback
|
||||
async function loadWithFallback(primaryUrl, fallbackUrl) {
|
||||
try {
|
||||
return await loadModel(primaryUrl);
|
||||
} catch (error) {
|
||||
console.warn(`Primary failed, trying fallback: ${error}`);
|
||||
return await loadModel(fallbackUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Retry logic
|
||||
async function loadWithRetry(url, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await loadModel(url);
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout
|
||||
async function loadWithTimeout(url, timeout = 30000) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw new Error("Loading timed out");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use compressed formats**: DRACO for geometry, KTX2/Basis for textures
|
||||
2. **Load progressively**: Show placeholders while loading
|
||||
3. **Lazy load**: Only load what's needed
|
||||
4. **Use CDN**: Faster asset delivery
|
||||
5. **Enable cache**: `THREE.Cache.enabled = true`
|
||||
|
||||
```javascript
|
||||
// Progressive loading with placeholder
|
||||
const placeholder = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshBasicMaterial({ wireframe: true }),
|
||||
);
|
||||
scene.add(placeholder);
|
||||
|
||||
loadModel("model.glb").then((gltf) => {
|
||||
scene.remove(placeholder);
|
||||
scene.add(gltf.scene);
|
||||
});
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-textures` - Texture configuration
|
||||
- `threejs-animation` - Playing loaded animations
|
||||
- `threejs-materials` - Material from loaded models
|
||||
@@ -0,0 +1,520 @@
|
||||
---
|
||||
name: threejs-materials
|
||||
description: Three.js materials - PBR, basic, phong, shader materials, material properties. Use when styling meshes, working with textures, creating custom shaders, or optimizing material performance.
|
||||
---
|
||||
|
||||
# Three.js Materials
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// PBR material (recommended for realistic rendering)
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0x00ff00,
|
||||
roughness: 0.5,
|
||||
metalness: 0.5,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
```
|
||||
|
||||
## Material Types Overview
|
||||
|
||||
| Material | Use Case | Lighting |
|
||||
| -------------------- | ------------------------------------- | ------------------ |
|
||||
| MeshBasicMaterial | Unlit, flat colors, wireframes | No |
|
||||
| MeshLambertMaterial | Matte surfaces, performance | Yes (diffuse only) |
|
||||
| MeshPhongMaterial | Shiny surfaces, specular highlights | Yes |
|
||||
| MeshStandardMaterial | PBR, realistic materials | Yes (PBR) |
|
||||
| MeshPhysicalMaterial | Advanced PBR, clearcoat, transmission | Yes (PBR+) |
|
||||
| MeshToonMaterial | Cel-shaded, cartoon look | Yes (toon) |
|
||||
| MeshNormalMaterial | Debug normals | No |
|
||||
| MeshDepthMaterial | Depth visualization | No |
|
||||
| ShaderMaterial | Custom GLSL shaders | Custom |
|
||||
| RawShaderMaterial | Full shader control | Custom |
|
||||
|
||||
## MeshBasicMaterial
|
||||
|
||||
No lighting calculations. Fast, always visible.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide, // FrontSide, BackSide, DoubleSide
|
||||
wireframe: false,
|
||||
map: texture, // Color/diffuse texture
|
||||
alphaMap: alphaTexture, // Transparency texture
|
||||
envMap: envTexture, // Reflection texture
|
||||
reflectivity: 1, // Env map intensity
|
||||
fog: true, // Affected by scene fog
|
||||
});
|
||||
```
|
||||
|
||||
## MeshLambertMaterial
|
||||
|
||||
Diffuse-only lighting. Fast, no specular highlights.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshLambertMaterial({
|
||||
color: 0x00ff00,
|
||||
emissive: 0x111111, // Self-illumination color
|
||||
emissiveIntensity: 1,
|
||||
map: texture,
|
||||
emissiveMap: emissiveTexture,
|
||||
envMap: envTexture,
|
||||
reflectivity: 0.5,
|
||||
});
|
||||
```
|
||||
|
||||
## MeshPhongMaterial
|
||||
|
||||
Specular highlights. Good for shiny, plastic-like surfaces.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: 0x0000ff,
|
||||
specular: 0xffffff, // Highlight color
|
||||
shininess: 100, // Highlight sharpness (0-1000)
|
||||
emissive: 0x000000,
|
||||
flatShading: false, // Flat vs smooth shading
|
||||
map: texture,
|
||||
specularMap: specTexture, // Per-pixel shininess
|
||||
normalMap: normalTexture,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
bumpMap: bumpTexture,
|
||||
bumpScale: 1,
|
||||
displacementMap: dispTexture,
|
||||
displacementScale: 1,
|
||||
});
|
||||
```
|
||||
|
||||
## MeshStandardMaterial (PBR)
|
||||
|
||||
Physically-based rendering. Recommended for realistic results.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0xffffff,
|
||||
roughness: 0.5, // 0 = mirror, 1 = diffuse
|
||||
metalness: 0.0, // 0 = dielectric, 1 = metal
|
||||
|
||||
// Textures
|
||||
map: colorTexture, // Albedo/base color
|
||||
roughnessMap: roughTexture, // Per-pixel roughness
|
||||
metalnessMap: metalTexture, // Per-pixel metalness
|
||||
normalMap: normalTexture, // Surface detail
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
aoMap: aoTexture, // Ambient occlusion (uses uv2!)
|
||||
aoMapIntensity: 1,
|
||||
displacementMap: dispTexture, // Vertex displacement
|
||||
displacementScale: 0.1,
|
||||
displacementBias: 0,
|
||||
|
||||
// Emissive
|
||||
emissive: 0x000000,
|
||||
emissiveIntensity: 1,
|
||||
emissiveMap: emissiveTexture,
|
||||
|
||||
// Environment
|
||||
envMap: envTexture,
|
||||
envMapIntensity: 1,
|
||||
|
||||
// Other
|
||||
flatShading: false,
|
||||
wireframe: false,
|
||||
fog: true,
|
||||
});
|
||||
|
||||
// Note: aoMap requires second UV channel
|
||||
geometry.setAttribute("uv2", geometry.attributes.uv);
|
||||
```
|
||||
|
||||
## MeshPhysicalMaterial (Advanced PBR)
|
||||
|
||||
Extends MeshStandardMaterial with advanced features.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshPhysicalMaterial({
|
||||
// All MeshStandardMaterial properties plus:
|
||||
|
||||
// Clearcoat (car paint, lacquer)
|
||||
clearcoat: 1.0, // 0-1 clearcoat layer strength
|
||||
clearcoatRoughness: 0.1,
|
||||
clearcoatMap: ccTexture,
|
||||
clearcoatRoughnessMap: ccrTexture,
|
||||
clearcoatNormalMap: ccnTexture,
|
||||
clearcoatNormalScale: new THREE.Vector2(1, 1),
|
||||
|
||||
// Transmission (glass, water)
|
||||
transmission: 1.0, // 0 = opaque, 1 = fully transparent
|
||||
transmissionMap: transTexture,
|
||||
thickness: 0.5, // Volume thickness for refraction
|
||||
thicknessMap: thickTexture,
|
||||
attenuationDistance: 1, // Absorption distance
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
|
||||
// Refraction
|
||||
ior: 1.5, // Index of refraction (1-2.333)
|
||||
|
||||
// Sheen (fabric, velvet)
|
||||
sheen: 1.0,
|
||||
sheenRoughness: 0.5,
|
||||
sheenColor: new THREE.Color(0xffffff),
|
||||
sheenColorMap: sheenTexture,
|
||||
sheenRoughnessMap: sheenRoughTexture,
|
||||
|
||||
// Iridescence (soap bubbles, oil slicks)
|
||||
iridescence: 1.0,
|
||||
iridescenceIOR: 1.3,
|
||||
iridescenceThicknessRange: [100, 400],
|
||||
iridescenceMap: iridTexture,
|
||||
iridescenceThicknessMap: iridThickTexture,
|
||||
|
||||
// Anisotropy (brushed metal)
|
||||
anisotropy: 1.0,
|
||||
anisotropyRotation: 0,
|
||||
anisotropyMap: anisoTexture,
|
||||
|
||||
// Specular
|
||||
specularIntensity: 1,
|
||||
specularColor: new THREE.Color(0xffffff),
|
||||
specularIntensityMap: specIntTexture,
|
||||
specularColorMap: specColorTexture,
|
||||
});
|
||||
```
|
||||
|
||||
### Glass Material Example
|
||||
|
||||
```javascript
|
||||
const glass = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0,
|
||||
roughness: 0,
|
||||
transmission: 1,
|
||||
thickness: 0.5,
|
||||
ior: 1.5,
|
||||
envMapIntensity: 1,
|
||||
});
|
||||
```
|
||||
|
||||
### Car Paint Example
|
||||
|
||||
```javascript
|
||||
const carPaint = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xff0000,
|
||||
metalness: 0.9,
|
||||
roughness: 0.5,
|
||||
clearcoat: 1,
|
||||
clearcoatRoughness: 0.1,
|
||||
});
|
||||
```
|
||||
|
||||
## MeshToonMaterial
|
||||
|
||||
Cel-shaded cartoon look.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshToonMaterial({
|
||||
color: 0x00ff00,
|
||||
gradientMap: gradientTexture, // Optional: custom shading gradient
|
||||
});
|
||||
|
||||
// Create step gradient texture
|
||||
const colors = new Uint8Array([0, 128, 255]);
|
||||
const gradientMap = new THREE.DataTexture(colors, 3, 1, THREE.RedFormat);
|
||||
gradientMap.minFilter = THREE.NearestFilter;
|
||||
gradientMap.magFilter = THREE.NearestFilter;
|
||||
gradientMap.needsUpdate = true;
|
||||
```
|
||||
|
||||
## MeshNormalMaterial
|
||||
|
||||
Visualize surface normals. Useful for debugging.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
wireframe: false,
|
||||
});
|
||||
```
|
||||
|
||||
## MeshDepthMaterial
|
||||
|
||||
Render depth values. Used for shadow maps, DOF effects.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.RGBADepthPacking,
|
||||
});
|
||||
```
|
||||
|
||||
## PointsMaterial
|
||||
|
||||
For point clouds.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.PointsMaterial({
|
||||
color: 0xffffff,
|
||||
size: 0.1,
|
||||
sizeAttenuation: true, // Scale with distance
|
||||
map: pointTexture,
|
||||
alphaMap: alphaTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.5, // Discard pixels below threshold
|
||||
vertexColors: true, // Use per-vertex colors
|
||||
});
|
||||
|
||||
const points = new THREE.Points(geometry, material);
|
||||
```
|
||||
|
||||
## LineBasicMaterial & LineDashedMaterial
|
||||
|
||||
```javascript
|
||||
// Solid lines
|
||||
const lineMaterial = new THREE.LineBasicMaterial({
|
||||
color: 0xffffff,
|
||||
linewidth: 1, // Note: >1 only works on some systems
|
||||
linecap: "round",
|
||||
linejoin: "round",
|
||||
});
|
||||
|
||||
// Dashed lines
|
||||
const dashedMaterial = new THREE.LineDashedMaterial({
|
||||
color: 0xffffff,
|
||||
dashSize: 0.5,
|
||||
gapSize: 0.25,
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
// Required for dashed lines
|
||||
const line = new THREE.Line(geometry, dashedMaterial);
|
||||
line.computeLineDistances();
|
||||
```
|
||||
|
||||
## ShaderMaterial
|
||||
|
||||
Custom GLSL shaders with Three.js uniforms.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
time: { value: 0 },
|
||||
color: { value: new THREE.Color(0xff0000) },
|
||||
texture1: { value: texture },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
uniform float time;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vec3 pos = position;
|
||||
pos.z += sin(pos.x * 10.0 + time) * 0.1;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 color;
|
||||
uniform sampler2D texture1;
|
||||
|
||||
void main() {
|
||||
// Use texture2D() for GLSL 1.0, texture() for GLSL 3.0 (glslVersion: THREE.GLSL3)
|
||||
vec4 texColor = texture2D(texture1, vUv);
|
||||
gl_FragColor = vec4(color * texColor.rgb, 1.0);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
|
||||
// Update uniform in animation loop
|
||||
material.uniforms.time.value = clock.getElapsedTime();
|
||||
```
|
||||
|
||||
### Built-in Uniforms (auto-provided)
|
||||
|
||||
```glsl
|
||||
// Vertex shader
|
||||
uniform mat4 modelMatrix; // Object to world
|
||||
uniform mat4 modelViewMatrix; // Object to camera
|
||||
uniform mat4 projectionMatrix; // Camera projection
|
||||
uniform mat4 viewMatrix; // World to camera
|
||||
uniform mat3 normalMatrix; // For transforming normals
|
||||
uniform vec3 cameraPosition; // Camera world position
|
||||
|
||||
// Attributes
|
||||
attribute vec3 position;
|
||||
attribute vec3 normal;
|
||||
attribute vec2 uv;
|
||||
```
|
||||
|
||||
## RawShaderMaterial
|
||||
|
||||
Full control - no built-in uniforms/attributes.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.RawShaderMaterial({
|
||||
uniforms: {
|
||||
projectionMatrix: { value: camera.projectionMatrix },
|
||||
modelViewMatrix: { value: new THREE.Matrix4() },
|
||||
},
|
||||
vertexShader: `
|
||||
precision highp float;
|
||||
attribute vec3 position;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform mat4 modelViewMatrix;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
precision highp float;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
## Common Material Properties
|
||||
|
||||
All materials share these base properties:
|
||||
|
||||
```javascript
|
||||
// Visibility
|
||||
material.visible = true;
|
||||
material.transparent = false;
|
||||
material.opacity = 1.0;
|
||||
material.alphaTest = 0; // Discard pixels with alpha < value
|
||||
|
||||
// Rendering
|
||||
material.side = THREE.FrontSide; // FrontSide, BackSide, DoubleSide
|
||||
material.depthTest = true;
|
||||
material.depthWrite = true;
|
||||
material.colorWrite = true;
|
||||
|
||||
// Blending
|
||||
material.blending = THREE.NormalBlending;
|
||||
// NormalBlending, AdditiveBlending, SubtractiveBlending, MultiplyBlending, CustomBlending
|
||||
|
||||
// Stencil
|
||||
material.stencilWrite = false;
|
||||
material.stencilFunc = THREE.AlwaysStencilFunc;
|
||||
material.stencilRef = 0;
|
||||
material.stencilMask = 0xff;
|
||||
|
||||
// Polygon offset (z-fighting fix)
|
||||
material.polygonOffset = false;
|
||||
material.polygonOffsetFactor = 0;
|
||||
material.polygonOffsetUnits = 0;
|
||||
|
||||
// Misc
|
||||
material.dithering = false;
|
||||
material.toneMapped = true;
|
||||
```
|
||||
|
||||
## Multiple Materials
|
||||
|
||||
```javascript
|
||||
// Assign different materials to geometry groups
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||
const materials = [
|
||||
new THREE.MeshBasicMaterial({ color: 0xff0000 }), // right
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ff00 }), // left
|
||||
new THREE.MeshBasicMaterial({ color: 0x0000ff }), // top
|
||||
new THREE.MeshBasicMaterial({ color: 0xffff00 }), // bottom
|
||||
new THREE.MeshBasicMaterial({ color: 0xff00ff }), // front
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ffff }), // back
|
||||
];
|
||||
const mesh = new THREE.Mesh(geometry, materials);
|
||||
|
||||
// Custom groups
|
||||
geometry.clearGroups();
|
||||
geometry.addGroup(0, 6, 0); // start, count, materialIndex
|
||||
geometry.addGroup(6, 6, 1);
|
||||
```
|
||||
|
||||
## Environment Maps
|
||||
|
||||
```javascript
|
||||
// Load cube texture
|
||||
const cubeLoader = new THREE.CubeTextureLoader();
|
||||
const envMap = cubeLoader.load([
|
||||
"px.jpg",
|
||||
"nx.jpg", // positive/negative X
|
||||
"py.jpg",
|
||||
"ny.jpg", // positive/negative Y
|
||||
"pz.jpg",
|
||||
"nz.jpg", // positive/negative Z
|
||||
]);
|
||||
|
||||
// Apply to material
|
||||
material.envMap = envMap;
|
||||
material.envMapIntensity = 1;
|
||||
|
||||
// Or set as scene environment (affects all PBR materials)
|
||||
scene.environment = envMap;
|
||||
|
||||
// HDR environment (recommended)
|
||||
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
||||
const rgbeLoader = new RGBELoader();
|
||||
rgbeLoader.load("environment.hdr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
scene.background = texture;
|
||||
});
|
||||
```
|
||||
|
||||
## Material Cloning and Modification
|
||||
|
||||
```javascript
|
||||
// Clone material
|
||||
const clone = material.clone();
|
||||
clone.color.set(0x00ff00);
|
||||
|
||||
// Modify at runtime
|
||||
material.color.set(0xff0000);
|
||||
material.needsUpdate = true; // Only needed for some changes
|
||||
|
||||
// When needsUpdate is required:
|
||||
// - Changing flat shading
|
||||
// - Changing texture
|
||||
// - Changing transparent
|
||||
// - Custom shader code changes
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse materials**: Same material = batched draw calls
|
||||
2. **Avoid transparent when possible**: Transparent materials require sorting
|
||||
3. **Use alphaTest instead of transparency**: When applicable, faster
|
||||
4. **Choose simpler materials**: Basic > Lambert > Phong > Standard > Physical
|
||||
5. **Limit active lights**: Each light adds shader complexity
|
||||
|
||||
```javascript
|
||||
// Material pooling
|
||||
const materialCache = new Map();
|
||||
function getMaterial(color) {
|
||||
const key = color.toString(16);
|
||||
if (!materialCache.has(key)) {
|
||||
materialCache.set(key, new THREE.MeshStandardMaterial({ color }));
|
||||
}
|
||||
return materialCache.get(key);
|
||||
}
|
||||
|
||||
// Dispose when done
|
||||
material.dispose();
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-textures` - Texture loading and configuration
|
||||
- `threejs-shaders` - Custom shader development
|
||||
- `threejs-lighting` - Light interaction with materials
|
||||
@@ -0,0 +1,602 @@
|
||||
---
|
||||
name: threejs-postprocessing
|
||||
description: Three.js post-processing - EffectComposer, bloom, DOF, screen effects. Use when adding visual effects, color grading, blur, glow, or creating custom screen-space shaders.
|
||||
---
|
||||
|
||||
# Three.js Post-Processing
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
||||
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
||||
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
||||
|
||||
// Setup composer
|
||||
const composer = new EffectComposer(renderer);
|
||||
|
||||
// Render scene
|
||||
const renderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(renderPass);
|
||||
|
||||
// Add bloom
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
1.5, // strength
|
||||
0.4, // radius
|
||||
0.85, // threshold
|
||||
);
|
||||
composer.addPass(bloomPass);
|
||||
|
||||
// Animation loop - use composer instead of renderer
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
composer.render(); // NOT renderer.render()
|
||||
}
|
||||
```
|
||||
|
||||
## EffectComposer Setup
|
||||
|
||||
```javascript
|
||||
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
||||
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
||||
|
||||
const composer = new EffectComposer(renderer);
|
||||
|
||||
// First pass: render scene
|
||||
const renderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(renderPass);
|
||||
|
||||
// Add more passes...
|
||||
composer.addPass(effectPass);
|
||||
|
||||
// Last pass should render to screen
|
||||
effectPass.renderToScreen = true; // Default for last pass
|
||||
|
||||
// Handle resize
|
||||
function onResize() {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer.setSize(width, height);
|
||||
composer.setSize(width, height);
|
||||
}
|
||||
```
|
||||
|
||||
## Common Effects
|
||||
|
||||
### Bloom (Glow)
|
||||
|
||||
```javascript
|
||||
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
||||
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
1.5, // strength - intensity of glow
|
||||
0.4, // radius - spread of glow
|
||||
0.85, // threshold - brightness threshold
|
||||
);
|
||||
|
||||
composer.addPass(bloomPass);
|
||||
|
||||
// Adjust at runtime
|
||||
bloomPass.strength = 2.0;
|
||||
bloomPass.threshold = 0.5;
|
||||
bloomPass.radius = 0.8;
|
||||
```
|
||||
|
||||
### Selective Bloom
|
||||
|
||||
Apply bloom only to specific objects.
|
||||
|
||||
```javascript
|
||||
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
|
||||
// Layer setup
|
||||
const BLOOM_LAYER = 1;
|
||||
const bloomLayer = new THREE.Layers();
|
||||
bloomLayer.set(BLOOM_LAYER);
|
||||
|
||||
// Mark objects to bloom
|
||||
glowingMesh.layers.enable(BLOOM_LAYER);
|
||||
|
||||
// Dark material for non-blooming objects
|
||||
const darkMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
||||
const materials = {};
|
||||
|
||||
function darkenNonBloomed(obj) {
|
||||
if (obj.isMesh && !bloomLayer.test(obj.layers)) {
|
||||
materials[obj.uuid] = obj.material;
|
||||
obj.material = darkMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreMaterial(obj) {
|
||||
if (materials[obj.uuid]) {
|
||||
obj.material = materials[obj.uuid];
|
||||
delete materials[obj.uuid];
|
||||
}
|
||||
}
|
||||
|
||||
// Custom render loop
|
||||
function render() {
|
||||
// Render bloom pass
|
||||
scene.traverse(darkenNonBloomed);
|
||||
composer.render();
|
||||
scene.traverse(restoreMaterial);
|
||||
|
||||
// Render final scene over bloom
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
```
|
||||
|
||||
### FXAA (Anti-Aliasing)
|
||||
|
||||
```javascript
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
|
||||
|
||||
const fxaaPass = new ShaderPass(FXAAShader);
|
||||
fxaaPass.material.uniforms["resolution"].value.set(
|
||||
1 / window.innerWidth,
|
||||
1 / window.innerHeight,
|
||||
);
|
||||
|
||||
composer.addPass(fxaaPass);
|
||||
|
||||
// Update on resize
|
||||
function onResize() {
|
||||
fxaaPass.material.uniforms["resolution"].value.set(
|
||||
1 / window.innerWidth,
|
||||
1 / window.innerHeight,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### SMAA (Better Anti-Aliasing)
|
||||
|
||||
```javascript
|
||||
import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
|
||||
|
||||
const smaaPass = new SMAAPass(
|
||||
window.innerWidth * renderer.getPixelRatio(),
|
||||
window.innerHeight * renderer.getPixelRatio(),
|
||||
);
|
||||
|
||||
composer.addPass(smaaPass);
|
||||
```
|
||||
|
||||
### SSAO (Ambient Occlusion)
|
||||
|
||||
```javascript
|
||||
import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js";
|
||||
|
||||
const ssaoPass = new SSAOPass(
|
||||
scene,
|
||||
camera,
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
);
|
||||
ssaoPass.kernelRadius = 16;
|
||||
ssaoPass.minDistance = 0.005;
|
||||
ssaoPass.maxDistance = 0.1;
|
||||
|
||||
composer.addPass(ssaoPass);
|
||||
|
||||
// Output modes
|
||||
ssaoPass.output = SSAOPass.OUTPUT.Default;
|
||||
// SSAOPass.OUTPUT.Default - Final composited output
|
||||
// SSAOPass.OUTPUT.SSAO - Just the AO
|
||||
// SSAOPass.OUTPUT.Blur - Blurred AO
|
||||
// SSAOPass.OUTPUT.Depth - Depth buffer
|
||||
// SSAOPass.OUTPUT.Normal - Normal buffer
|
||||
```
|
||||
|
||||
### Depth of Field (DOF)
|
||||
|
||||
```javascript
|
||||
import { BokehPass } from "three/addons/postprocessing/BokehPass.js";
|
||||
|
||||
const bokehPass = new BokehPass(scene, camera, {
|
||||
focus: 10.0, // Focus distance
|
||||
aperture: 0.025, // Aperture (smaller = more DOF)
|
||||
maxblur: 0.01, // Max blur amount
|
||||
});
|
||||
|
||||
composer.addPass(bokehPass);
|
||||
|
||||
// Update focus dynamically
|
||||
bokehPass.uniforms["focus"].value = distanceToTarget;
|
||||
```
|
||||
|
||||
### Film Grain
|
||||
|
||||
```javascript
|
||||
import { FilmPass } from "three/addons/postprocessing/FilmPass.js";
|
||||
|
||||
const filmPass = new FilmPass(
|
||||
0.35, // noise intensity
|
||||
0.5, // scanline intensity
|
||||
648, // scanline count
|
||||
false, // grayscale
|
||||
);
|
||||
|
||||
composer.addPass(filmPass);
|
||||
```
|
||||
|
||||
### Vignette
|
||||
|
||||
```javascript
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
import { VignetteShader } from "three/addons/shaders/VignetteShader.js";
|
||||
|
||||
const vignettePass = new ShaderPass(VignetteShader);
|
||||
vignettePass.uniforms["offset"].value = 1.0; // Vignette size
|
||||
vignettePass.uniforms["darkness"].value = 1.0; // Vignette intensity
|
||||
|
||||
composer.addPass(vignettePass);
|
||||
```
|
||||
|
||||
### Color Correction
|
||||
|
||||
```javascript
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
import { ColorCorrectionShader } from "three/addons/shaders/ColorCorrectionShader.js";
|
||||
|
||||
const colorPass = new ShaderPass(ColorCorrectionShader);
|
||||
colorPass.uniforms["powRGB"].value = new THREE.Vector3(1.2, 1.2, 1.2); // Power
|
||||
colorPass.uniforms["mulRGB"].value = new THREE.Vector3(1.0, 1.0, 1.0); // Multiply
|
||||
|
||||
composer.addPass(colorPass);
|
||||
```
|
||||
|
||||
### Gamma Correction
|
||||
|
||||
```javascript
|
||||
import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";
|
||||
|
||||
const gammaPass = new ShaderPass(GammaCorrectionShader);
|
||||
composer.addPass(gammaPass);
|
||||
```
|
||||
|
||||
### Pixelation
|
||||
|
||||
```javascript
|
||||
import { RenderPixelatedPass } from "three/addons/postprocessing/RenderPixelatedPass.js";
|
||||
|
||||
const pixelPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size
|
||||
|
||||
composer.addPass(pixelPass);
|
||||
```
|
||||
|
||||
### Glitch Effect
|
||||
|
||||
```javascript
|
||||
import { GlitchPass } from "three/addons/postprocessing/GlitchPass.js";
|
||||
|
||||
const glitchPass = new GlitchPass();
|
||||
glitchPass.goWild = false; // Continuous glitching
|
||||
|
||||
composer.addPass(glitchPass);
|
||||
```
|
||||
|
||||
### Halftone
|
||||
|
||||
```javascript
|
||||
import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js";
|
||||
|
||||
const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, {
|
||||
shape: 1, // 1 = dot, 2 = ellipse, 3 = line, 4 = square
|
||||
radius: 4, // Dot size
|
||||
rotateR: Math.PI / 12,
|
||||
rotateB: (Math.PI / 12) * 2,
|
||||
rotateG: (Math.PI / 12) * 3,
|
||||
scatter: 0,
|
||||
blending: 1,
|
||||
blendingMode: 1,
|
||||
greyscale: false,
|
||||
});
|
||||
|
||||
composer.addPass(halftonePass);
|
||||
```
|
||||
|
||||
### Outline
|
||||
|
||||
```javascript
|
||||
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
|
||||
|
||||
const outlinePass = new OutlinePass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
scene,
|
||||
camera,
|
||||
);
|
||||
|
||||
outlinePass.edgeStrength = 3;
|
||||
outlinePass.edgeGlow = 0;
|
||||
outlinePass.edgeThickness = 1;
|
||||
outlinePass.pulsePeriod = 0;
|
||||
outlinePass.visibleEdgeColor.set(0xffffff);
|
||||
outlinePass.hiddenEdgeColor.set(0x190a05);
|
||||
|
||||
// Select objects to outline
|
||||
outlinePass.selectedObjects = [mesh1, mesh2];
|
||||
|
||||
composer.addPass(outlinePass);
|
||||
```
|
||||
|
||||
## Custom ShaderPass
|
||||
|
||||
Create your own post-processing effects.
|
||||
|
||||
```javascript
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
|
||||
const CustomShader = {
|
||||
uniforms: {
|
||||
tDiffuse: { value: null }, // Required: input texture
|
||||
time: { value: 0 },
|
||||
intensity: { value: 1.0 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
|
||||
// Wave distortion
|
||||
uv.x += sin(uv.y * 10.0 + time) * 0.01 * intensity;
|
||||
|
||||
vec4 color = texture2D(tDiffuse, uv);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const customPass = new ShaderPass(CustomShader);
|
||||
composer.addPass(customPass);
|
||||
|
||||
// Update in animation loop
|
||||
customPass.uniforms.time.value = clock.getElapsedTime();
|
||||
```
|
||||
|
||||
### Invert Colors Shader
|
||||
|
||||
```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);
|
||||
}
|
||||
`,
|
||||
};
|
||||
```
|
||||
|
||||
### Chromatic Aberration
|
||||
|
||||
```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 dir = vUv - 0.5;
|
||||
float dist = length(dir);
|
||||
|
||||
float r = texture2D(tDiffuse, vUv - dir * amount * dist).r;
|
||||
float g = texture2D(tDiffuse, vUv).g;
|
||||
float b = texture2D(tDiffuse, vUv + dir * amount * dist).b;
|
||||
|
||||
gl_FragColor = vec4(r, g, b, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
```
|
||||
|
||||
## Combining Multiple Effects
|
||||
|
||||
```javascript
|
||||
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
||||
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
||||
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
||||
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
||||
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
|
||||
import { VignetteShader } from "three/addons/shaders/VignetteShader.js";
|
||||
import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";
|
||||
|
||||
const composer = new EffectComposer(renderer);
|
||||
|
||||
// 1. Render scene
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
|
||||
// 2. Bloom
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
0.5,
|
||||
0.4,
|
||||
0.85,
|
||||
);
|
||||
composer.addPass(bloomPass);
|
||||
|
||||
// 3. Vignette
|
||||
const vignettePass = new ShaderPass(VignetteShader);
|
||||
vignettePass.uniforms["offset"].value = 0.95;
|
||||
vignettePass.uniforms["darkness"].value = 1.0;
|
||||
composer.addPass(vignettePass);
|
||||
|
||||
// 4. Gamma correction
|
||||
composer.addPass(new ShaderPass(GammaCorrectionShader));
|
||||
|
||||
// 5. Anti-aliasing (always last before output)
|
||||
const fxaaPass = new ShaderPass(FXAAShader);
|
||||
fxaaPass.uniforms["resolution"].value.set(
|
||||
1 / window.innerWidth,
|
||||
1 / window.innerHeight,
|
||||
);
|
||||
composer.addPass(fxaaPass);
|
||||
```
|
||||
|
||||
## Render to Texture
|
||||
|
||||
```javascript
|
||||
// Create render target
|
||||
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
|
||||
|
||||
// Render scene to target
|
||||
renderer.setRenderTarget(renderTarget);
|
||||
renderer.render(scene, camera);
|
||||
renderer.setRenderTarget(null);
|
||||
|
||||
// Use texture
|
||||
const texture = renderTarget.texture;
|
||||
otherMaterial.map = texture;
|
||||
```
|
||||
|
||||
## Multi-Pass Rendering
|
||||
|
||||
```javascript
|
||||
// Multiple composers for different scenes/layers
|
||||
const bgComposer = new EffectComposer(renderer);
|
||||
bgComposer.addPass(new RenderPass(bgScene, camera));
|
||||
|
||||
const fgComposer = new EffectComposer(renderer);
|
||||
fgComposer.addPass(new RenderPass(fgScene, camera));
|
||||
fgComposer.addPass(bloomPass);
|
||||
|
||||
// Combine in render loop
|
||||
function animate() {
|
||||
// Render background without clearing
|
||||
renderer.autoClear = false;
|
||||
renderer.clear();
|
||||
|
||||
bgComposer.render();
|
||||
|
||||
// Render foreground over it
|
||||
renderer.clearDepth();
|
||||
fgComposer.render();
|
||||
}
|
||||
```
|
||||
|
||||
## WebGPU Post-Processing (Three.js r150+)
|
||||
|
||||
```javascript
|
||||
import { postProcessing } from "three/addons/nodes/Nodes.js";
|
||||
import { pass, bloom, dof } from "three/addons/nodes/Nodes.js";
|
||||
|
||||
// Using node-based system
|
||||
const scenePass = pass(scene, camera);
|
||||
const bloomNode = bloom(scenePass, 0.5, 0.4, 0.85);
|
||||
|
||||
const postProcessing = new THREE.PostProcessing(renderer);
|
||||
postProcessing.outputNode = bloomNode;
|
||||
|
||||
// Render
|
||||
function animate() {
|
||||
postProcessing.render();
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Limit passes**: Each pass adds a full-screen render
|
||||
2. **Lower resolution**: Use smaller render targets for blur passes
|
||||
3. **Disable unused effects**: Toggle passes on/off
|
||||
4. **Use FXAA over MSAA**: Less expensive anti-aliasing
|
||||
5. **Profile with DevTools**: Check GPU usage
|
||||
|
||||
```javascript
|
||||
// Disable pass
|
||||
bloomPass.enabled = false;
|
||||
|
||||
// Reduce bloom resolution
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2),
|
||||
strength,
|
||||
radius,
|
||||
threshold,
|
||||
);
|
||||
|
||||
// Only apply effects in high-performance scenarios
|
||||
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
|
||||
if (!isMobile) {
|
||||
composer.addPass(expensivePass);
|
||||
}
|
||||
```
|
||||
|
||||
## Handle Resize
|
||||
|
||||
```javascript
|
||||
function onWindowResize() {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const pixelRatio = renderer.getPixelRatio();
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer.setSize(width, height);
|
||||
composer.setSize(width, height);
|
||||
|
||||
// Update pass-specific resolutions
|
||||
if (fxaaPass) {
|
||||
fxaaPass.material.uniforms["resolution"].value.set(
|
||||
1 / (width * pixelRatio),
|
||||
1 / (height * pixelRatio),
|
||||
);
|
||||
}
|
||||
|
||||
if (bloomPass) {
|
||||
bloomPass.resolution.set(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-shaders` - Custom shader development
|
||||
- `threejs-textures` - Render targets
|
||||
- `threejs-fundamentals` - Renderer setup
|
||||
@@ -0,0 +1,642 @@
|
||||
---
|
||||
name: threejs-shaders
|
||||
description: Three.js shaders - GLSL, ShaderMaterial, uniforms, custom effects. Use when creating custom visual effects, modifying vertices, writing fragment shaders, or extending built-in materials.
|
||||
---
|
||||
|
||||
# Three.js Shaders
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
time: { value: 0 },
|
||||
color: { value: new THREE.Color(0xff0000) },
|
||||
},
|
||||
vertexShader: `
|
||||
void main() {
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 color;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Update in animation loop
|
||||
material.uniforms.time.value = clock.getElapsedTime();
|
||||
```
|
||||
|
||||
## ShaderMaterial vs RawShaderMaterial
|
||||
|
||||
### ShaderMaterial
|
||||
|
||||
Three.js provides built-in uniforms and attributes.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
// Built-in uniforms available:
|
||||
// uniform mat4 modelMatrix;
|
||||
// uniform mat4 modelViewMatrix;
|
||||
// uniform mat4 projectionMatrix;
|
||||
// uniform mat4 viewMatrix;
|
||||
// uniform mat3 normalMatrix;
|
||||
// uniform vec3 cameraPosition;
|
||||
|
||||
// Built-in attributes available:
|
||||
// attribute vec3 position;
|
||||
// attribute vec3 normal;
|
||||
// attribute vec2 uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
void main() {
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
### RawShaderMaterial
|
||||
|
||||
Full control - you define everything.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.RawShaderMaterial({
|
||||
uniforms: {
|
||||
projectionMatrix: { value: camera.projectionMatrix },
|
||||
modelViewMatrix: { value: new THREE.Matrix4() },
|
||||
},
|
||||
vertexShader: `
|
||||
precision highp float;
|
||||
|
||||
attribute vec3 position;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform mat4 modelViewMatrix;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
precision highp float;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
## Uniforms
|
||||
|
||||
### Uniform Types
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
// Numbers
|
||||
floatValue: { value: 1.5 },
|
||||
intValue: { value: 1 },
|
||||
|
||||
// Vectors
|
||||
vec2Value: { value: new THREE.Vector2(1, 2) },
|
||||
vec3Value: { value: new THREE.Vector3(1, 2, 3) },
|
||||
vec4Value: { value: new THREE.Vector4(1, 2, 3, 4) },
|
||||
|
||||
// Colors (converted to vec3)
|
||||
colorValue: { value: new THREE.Color(0xff0000) },
|
||||
|
||||
// Matrices
|
||||
mat3Value: { value: new THREE.Matrix3() },
|
||||
mat4Value: { value: new THREE.Matrix4() },
|
||||
|
||||
// Textures
|
||||
textureValue: { value: texture },
|
||||
cubeTextureValue: { value: cubeTexture },
|
||||
|
||||
// Arrays
|
||||
floatArray: { value: [1.0, 2.0, 3.0] },
|
||||
vec3Array: {
|
||||
value: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### GLSL Declarations
|
||||
|
||||
```glsl
|
||||
// In shader
|
||||
uniform float floatValue;
|
||||
uniform int intValue;
|
||||
uniform vec2 vec2Value;
|
||||
uniform vec3 vec3Value;
|
||||
uniform vec3 colorValue; // Color becomes vec3
|
||||
uniform vec4 vec4Value;
|
||||
uniform mat3 mat3Value;
|
||||
uniform mat4 mat4Value;
|
||||
uniform sampler2D textureValue;
|
||||
uniform samplerCube cubeTextureValue;
|
||||
uniform float floatArray[3];
|
||||
uniform vec3 vec3Array[2];
|
||||
```
|
||||
|
||||
### Updating Uniforms
|
||||
|
||||
```javascript
|
||||
// Direct assignment
|
||||
material.uniforms.time.value = clock.getElapsedTime();
|
||||
|
||||
// Vector/Color updates
|
||||
material.uniforms.position.value.set(x, y, z);
|
||||
material.uniforms.color.value.setHSL(hue, 1, 0.5);
|
||||
|
||||
// Matrix updates
|
||||
material.uniforms.matrix.value.copy(mesh.matrixWorld);
|
||||
```
|
||||
|
||||
## Varyings
|
||||
|
||||
Pass data from vertex to fragment shader.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
|
||||
void main() {
|
||||
// Use interpolated values
|
||||
gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
## Common Shader Patterns
|
||||
|
||||
### Texture Sampling
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
map: { value: texture },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D map;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec4 texColor = texture2D(map, vUv);
|
||||
gl_FragColor = texColor;
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
### Vertex Displacement
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
time: { value: 0 },
|
||||
amplitude: { value: 0.5 },
|
||||
},
|
||||
vertexShader: `
|
||||
uniform float time;
|
||||
uniform float amplitude;
|
||||
|
||||
void main() {
|
||||
vec3 pos = position;
|
||||
|
||||
// Wave displacement
|
||||
pos.z += sin(pos.x * 5.0 + time) * amplitude;
|
||||
pos.z += sin(pos.y * 5.0 + time) * amplitude;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
void main() {
|
||||
gl_FragColor = vec4(0.5, 0.8, 1.0, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
### Fresnel Effect
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorldPosition;
|
||||
|
||||
void main() {
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorldPosition;
|
||||
|
||||
void main() {
|
||||
// cameraPosition is auto-provided by ShaderMaterial
|
||||
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
|
||||
float fresnel = pow(1.0 - dot(viewDirection, vNormal), 3.0);
|
||||
|
||||
vec3 baseColor = vec3(0.0, 0.0, 0.5);
|
||||
vec3 fresnelColor = vec3(0.5, 0.8, 1.0);
|
||||
|
||||
gl_FragColor = vec4(mix(baseColor, fresnelColor, fresnel), 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
### Noise-Based Effects
|
||||
|
||||
```glsl
|
||||
// Simple noise function
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
// 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 = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
// Usage
|
||||
float n = noise(vUv * 10.0 + time);
|
||||
```
|
||||
|
||||
### Gradient
|
||||
|
||||
```glsl
|
||||
// Linear gradient
|
||||
vec3 color = mix(colorA, colorB, vUv.y);
|
||||
|
||||
// Radial gradient
|
||||
float dist = distance(vUv, vec2(0.5));
|
||||
vec3 color = mix(centerColor, edgeColor, dist * 2.0);
|
||||
|
||||
// Smooth gradient with custom curve
|
||||
float t = smoothstep(0.0, 1.0, vUv.y);
|
||||
vec3 color = mix(colorA, colorB, t);
|
||||
```
|
||||
|
||||
### Rim Lighting
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
void main() {
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
vViewPosition = mvPosition.xyz;
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
void main() {
|
||||
vec3 viewDir = normalize(-vViewPosition);
|
||||
float rim = 1.0 - max(0.0, dot(viewDir, vNormal));
|
||||
rim = pow(rim, 4.0);
|
||||
|
||||
vec3 baseColor = vec3(0.2, 0.2, 0.8);
|
||||
vec3 rimColor = vec3(1.0, 0.5, 0.0);
|
||||
|
||||
gl_FragColor = vec4(baseColor + rimColor * rim, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
### Dissolve Effect
|
||||
|
||||
```glsl
|
||||
uniform float progress;
|
||||
uniform sampler2D noiseMap;
|
||||
|
||||
void main() {
|
||||
float noise = texture2D(noiseMap, vUv).r;
|
||||
|
||||
if (noise < progress) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// Edge glow
|
||||
float edge = smoothstep(progress, progress + 0.1, noise);
|
||||
vec3 edgeColor = vec3(1.0, 0.5, 0.0);
|
||||
vec3 baseColor = vec3(0.5);
|
||||
|
||||
gl_FragColor = vec4(mix(edgeColor, baseColor, edge), 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
## Extending Built-in Materials
|
||||
|
||||
### onBeforeCompile
|
||||
|
||||
Modify existing material shaders.
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
|
||||
|
||||
material.onBeforeCompile = (shader) => {
|
||||
// Add custom uniform
|
||||
shader.uniforms.time = { value: 0 };
|
||||
|
||||
// Store reference for updates
|
||||
material.userData.shader = shader;
|
||||
|
||||
// Modify vertex shader
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <begin_vertex>",
|
||||
`
|
||||
#include <begin_vertex>
|
||||
transformed.y += sin(position.x * 10.0 + time) * 0.1;
|
||||
`,
|
||||
);
|
||||
|
||||
// Add uniform declaration
|
||||
shader.vertexShader = "uniform float time;\n" + shader.vertexShader;
|
||||
};
|
||||
|
||||
// Update in animation loop
|
||||
if (material.userData.shader) {
|
||||
material.userData.shader.uniforms.time.value = clock.getElapsedTime();
|
||||
}
|
||||
```
|
||||
|
||||
### Common Injection Points
|
||||
|
||||
```javascript
|
||||
// Vertex shader chunks
|
||||
"#include <begin_vertex>"; // After position is calculated
|
||||
"#include <project_vertex>"; // After gl_Position
|
||||
"#include <beginnormal_vertex>"; // Normal calculation start
|
||||
|
||||
// Fragment shader chunks
|
||||
"#include <color_fragment>"; // After diffuse color
|
||||
"#include <output_fragment>"; // Final output
|
||||
"#include <fog_fragment>"; // After fog applied
|
||||
```
|
||||
|
||||
## GLSL Built-in Functions
|
||||
|
||||
### Math Functions
|
||||
|
||||
```glsl
|
||||
// Basic
|
||||
abs(x), sign(x), floor(x), ceil(x), fract(x)
|
||||
mod(x, y), min(x, y), max(x, y), clamp(x, min, max)
|
||||
mix(a, b, t), step(edge, x), smoothstep(edge0, edge1, x)
|
||||
|
||||
// Trigonometry
|
||||
sin(x), cos(x), tan(x)
|
||||
asin(x), acos(x), atan(y, x), atan(x)
|
||||
radians(degrees), degrees(radians)
|
||||
|
||||
// Exponential
|
||||
pow(x, y), exp(x), log(x), exp2(x), log2(x)
|
||||
sqrt(x), inversesqrt(x)
|
||||
```
|
||||
|
||||
### Vector Functions
|
||||
|
||||
```glsl
|
||||
// Length and distance
|
||||
length(v), distance(p0, p1), dot(x, y), cross(x, y)
|
||||
|
||||
// Normalization
|
||||
normalize(v)
|
||||
|
||||
// Reflection and refraction
|
||||
reflect(I, N), refract(I, N, eta)
|
||||
|
||||
// Component-wise
|
||||
lessThan(x, y), lessThanEqual(x, y)
|
||||
greaterThan(x, y), greaterThanEqual(x, y)
|
||||
equal(x, y), notEqual(x, y)
|
||||
any(bvec), all(bvec)
|
||||
```
|
||||
|
||||
### Texture Functions
|
||||
|
||||
```glsl
|
||||
// GLSL 1.0 (default) - use texture2D/textureCube
|
||||
texture2D(sampler, coord)
|
||||
texture2D(sampler, coord, bias)
|
||||
textureCube(sampler, coord)
|
||||
|
||||
// GLSL 3.0 (glslVersion: THREE.GLSL3) - use texture()
|
||||
// texture(sampler, coord) replaces texture2D/textureCube
|
||||
// Also use: out vec4 fragColor instead of gl_FragColor
|
||||
|
||||
// Texture size (GLSL 1.30+)
|
||||
textureSize(sampler, lod)
|
||||
```
|
||||
|
||||
## Common Material Properties
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
/* ... */
|
||||
},
|
||||
vertexShader: "/* ... */",
|
||||
fragmentShader: "/* ... */",
|
||||
|
||||
// Rendering
|
||||
transparent: true,
|
||||
opacity: 1.0,
|
||||
side: THREE.DoubleSide,
|
||||
depthTest: true,
|
||||
depthWrite: true,
|
||||
|
||||
// Blending
|
||||
blending: THREE.NormalBlending,
|
||||
// AdditiveBlending, SubtractiveBlending, MultiplyBlending
|
||||
|
||||
// Wireframe
|
||||
wireframe: false,
|
||||
wireframeLinewidth: 1, // Note: >1 has no effect on most platforms (WebGL limitation)
|
||||
|
||||
// Extensions
|
||||
extensions: {
|
||||
derivatives: true, // For fwidth, dFdx, dFdy
|
||||
fragDepth: true, // gl_FragDepth
|
||||
drawBuffers: true, // Multiple render targets
|
||||
shaderTextureLOD: true, // texture2DLod
|
||||
},
|
||||
|
||||
// GLSL version
|
||||
glslVersion: THREE.GLSL3, // For WebGL2 features
|
||||
});
|
||||
```
|
||||
|
||||
## Shader Includes
|
||||
|
||||
### Using Three.js Shader Chunks
|
||||
|
||||
```javascript
|
||||
import { ShaderChunk } from "three";
|
||||
|
||||
const fragmentShader = `
|
||||
${ShaderChunk.common}
|
||||
${ShaderChunk.packing}
|
||||
|
||||
uniform sampler2D depthTexture;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float depth = texture2D(depthTexture, vUv).r;
|
||||
float linearDepth = perspectiveDepthToViewZ(depth, 0.1, 1000.0);
|
||||
gl_FragColor = vec4(vec3(-linearDepth / 100.0), 1.0);
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
### External Shader Files
|
||||
|
||||
```javascript
|
||||
// With vite/webpack
|
||||
import vertexShader from "./shaders/vertex.glsl";
|
||||
import fragmentShader from "./shaders/fragment.glsl";
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
});
|
||||
```
|
||||
|
||||
## Instanced Shaders
|
||||
|
||||
```javascript
|
||||
// Instanced attribute
|
||||
const offsets = new Float32Array(instanceCount * 3);
|
||||
// Fill offsets...
|
||||
geometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
attribute vec3 offset;
|
||||
|
||||
void main() {
|
||||
vec3 pos = position + offset;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
void main() {
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Shaders
|
||||
|
||||
```javascript
|
||||
// Check for compile errors
|
||||
material.onBeforeCompile = (shader) => {
|
||||
console.log("Vertex Shader:", shader.vertexShader);
|
||||
console.log("Fragment Shader:", shader.fragmentShader);
|
||||
};
|
||||
|
||||
// Visual debugging
|
||||
fragmentShader: `
|
||||
void main() {
|
||||
// Debug UV
|
||||
gl_FragColor = vec4(vUv, 0.0, 1.0);
|
||||
|
||||
// Debug normals
|
||||
gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);
|
||||
|
||||
// Debug position
|
||||
gl_FragColor = vec4(vPosition * 0.1 + 0.5, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// Check WebGL errors
|
||||
renderer.debug.checkShaderErrors = true;
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Minimize uniforms**: Group related values into vectors
|
||||
2. **Avoid conditionals**: Use mix/step instead of if/else
|
||||
3. **Precalculate**: Move calculations to JS when possible
|
||||
4. **Use textures**: For complex functions, use lookup tables
|
||||
5. **Limit overdraw**: Avoid transparent objects when possible
|
||||
|
||||
```glsl
|
||||
// Instead of:
|
||||
if (value > 0.5) {
|
||||
color = colorA;
|
||||
} else {
|
||||
color = colorB;
|
||||
}
|
||||
|
||||
// Use:
|
||||
color = mix(colorB, colorA, step(0.5, value));
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-materials` - Built-in material types
|
||||
- `threejs-postprocessing` - Full-screen shader effects
|
||||
- `threejs-textures` - Texture sampling in shaders
|
||||
@@ -0,0 +1,628 @@
|
||||
---
|
||||
name: threejs-textures
|
||||
description: Three.js textures - texture types, UV mapping, environment maps, texture settings. Use when working with images, UV coordinates, cubemaps, HDR environments, or texture optimization.
|
||||
---
|
||||
|
||||
# Three.js Textures
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
// Load texture
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load("texture.jpg");
|
||||
|
||||
// Apply to material
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
map: texture,
|
||||
});
|
||||
```
|
||||
|
||||
## Texture Loading
|
||||
|
||||
### Basic Loading
|
||||
|
||||
```javascript
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
// Async with callbacks
|
||||
loader.load(
|
||||
"texture.jpg",
|
||||
(texture) => console.log("Loaded"),
|
||||
(progress) => console.log("Progress"),
|
||||
(error) => console.error("Error"),
|
||||
);
|
||||
|
||||
// Synchronous style (loads async internally)
|
||||
const texture = loader.load("texture.jpg");
|
||||
material.map = texture;
|
||||
```
|
||||
|
||||
### Promise Wrapper
|
||||
|
||||
```javascript
|
||||
function loadTexture(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new THREE.TextureLoader().load(url, resolve, undefined, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [colorMap, normalMap, roughnessMap] = await Promise.all([
|
||||
loadTexture("color.jpg"),
|
||||
loadTexture("normal.jpg"),
|
||||
loadTexture("roughness.jpg"),
|
||||
]);
|
||||
```
|
||||
|
||||
## Texture Configuration
|
||||
|
||||
### Color Space
|
||||
|
||||
Critical for accurate color reproduction.
|
||||
|
||||
```javascript
|
||||
// Color/albedo textures - use sRGB
|
||||
colorTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
// Data textures (normal, roughness, metalness, AO) - leave as default
|
||||
// Do NOT set colorSpace for data textures (NoColorSpace is default)
|
||||
```
|
||||
|
||||
### Wrapping Modes
|
||||
|
||||
```javascript
|
||||
texture.wrapS = THREE.RepeatWrapping; // Horizontal
|
||||
texture.wrapT = THREE.RepeatWrapping; // Vertical
|
||||
|
||||
// Options:
|
||||
// THREE.ClampToEdgeWrapping - Stretches edge pixels (default)
|
||||
// THREE.RepeatWrapping - Tiles the texture
|
||||
// THREE.MirroredRepeatWrapping - Tiles with mirror flip
|
||||
```
|
||||
|
||||
### Repeat, Offset, Rotation
|
||||
|
||||
```javascript
|
||||
// Tile texture 4x4
|
||||
texture.repeat.set(4, 4);
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
|
||||
// Offset (0-1 range)
|
||||
texture.offset.set(0.5, 0.5);
|
||||
|
||||
// Rotation (radians, around center)
|
||||
texture.rotation = Math.PI / 4;
|
||||
texture.center.set(0.5, 0.5); // Rotation pivot
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
```javascript
|
||||
// Minification (texture larger than screen pixels)
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter; // Default, smooth
|
||||
texture.minFilter = THREE.NearestFilter; // Pixelated
|
||||
texture.minFilter = THREE.LinearFilter; // Smooth, no mipmaps
|
||||
|
||||
// Magnification (texture smaller than screen pixels)
|
||||
texture.magFilter = THREE.LinearFilter; // Smooth (default)
|
||||
texture.magFilter = THREE.NearestFilter; // Pixelated (retro games)
|
||||
|
||||
// Anisotropic filtering (sharper at angles)
|
||||
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
```
|
||||
|
||||
### Generate Mipmaps
|
||||
|
||||
```javascript
|
||||
// Usually true by default
|
||||
texture.generateMipmaps = true;
|
||||
|
||||
// Disable for non-power-of-2 textures or data textures
|
||||
texture.generateMipmaps = false;
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
```
|
||||
|
||||
## Texture Types
|
||||
|
||||
### Regular Texture
|
||||
|
||||
```javascript
|
||||
const texture = new THREE.Texture(image);
|
||||
texture.needsUpdate = true;
|
||||
```
|
||||
|
||||
### Data Texture
|
||||
|
||||
Create texture from raw data.
|
||||
|
||||
```javascript
|
||||
// Create gradient texture
|
||||
const size = 256;
|
||||
const data = new Uint8Array(size * size * 4);
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
for (let j = 0; j < size; j++) {
|
||||
const index = (i * size + j) * 4;
|
||||
data[index] = i; // R
|
||||
data[index + 1] = j; // G
|
||||
data[index + 2] = 128; // B
|
||||
data[index + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
|
||||
const texture = new THREE.DataTexture(data, size, size);
|
||||
texture.needsUpdate = true;
|
||||
```
|
||||
|
||||
### Canvas Texture
|
||||
|
||||
```javascript
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Draw on canvas
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(0, 0, 256, 256);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "48px Arial";
|
||||
ctx.fillText("Hello", 50, 150);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
|
||||
// Update when canvas changes
|
||||
texture.needsUpdate = true;
|
||||
```
|
||||
|
||||
### Video Texture
|
||||
|
||||
```javascript
|
||||
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;
|
||||
|
||||
// No need to set needsUpdate - auto-updates
|
||||
```
|
||||
|
||||
### Compressed Textures
|
||||
|
||||
```javascript
|
||||
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
|
||||
|
||||
const ktx2Loader = new KTX2Loader();
|
||||
ktx2Loader.setTranscoderPath("path/to/basis/");
|
||||
ktx2Loader.detectSupport(renderer);
|
||||
|
||||
ktx2Loader.load("texture.ktx2", (texture) => {
|
||||
material.map = texture;
|
||||
});
|
||||
```
|
||||
|
||||
## Cube Textures
|
||||
|
||||
For environment maps and skyboxes.
|
||||
|
||||
### CubeTextureLoader
|
||||
|
||||
```javascript
|
||||
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
|
||||
]);
|
||||
|
||||
// As background
|
||||
scene.background = cubeTexture;
|
||||
|
||||
// As environment map
|
||||
scene.environment = cubeTexture;
|
||||
material.envMap = cubeTexture;
|
||||
```
|
||||
|
||||
### Equirectangular to Cubemap
|
||||
|
||||
```javascript
|
||||
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
||||
|
||||
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||||
pmremGenerator.compileEquirectangularShader();
|
||||
|
||||
new RGBELoader().load("environment.hdr", (texture) => {
|
||||
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||
scene.environment = envMap;
|
||||
scene.background = envMap;
|
||||
|
||||
texture.dispose();
|
||||
pmremGenerator.dispose();
|
||||
});
|
||||
```
|
||||
|
||||
## HDR Textures
|
||||
|
||||
### RGBELoader
|
||||
|
||||
```javascript
|
||||
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
||||
|
||||
const loader = new RGBELoader();
|
||||
loader.load("environment.hdr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
scene.background = texture;
|
||||
});
|
||||
```
|
||||
|
||||
### EXRLoader
|
||||
|
||||
```javascript
|
||||
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
||||
|
||||
const loader = new EXRLoader();
|
||||
loader.load("environment.exr", (texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
});
|
||||
```
|
||||
|
||||
### Background Options
|
||||
|
||||
```javascript
|
||||
scene.background = texture;
|
||||
scene.backgroundBlurriness = 0.5; // 0-1, blur background
|
||||
scene.backgroundIntensity = 1.0; // Brightness
|
||||
scene.backgroundRotation.y = Math.PI; // Rotate background
|
||||
```
|
||||
|
||||
## Render Targets
|
||||
|
||||
Render to texture for effects.
|
||||
|
||||
```javascript
|
||||
// Create render target
|
||||
const renderTarget = new THREE.WebGLRenderTarget(512, 512, {
|
||||
minFilter: THREE.LinearFilter,
|
||||
magFilter: THREE.LinearFilter,
|
||||
format: THREE.RGBAFormat,
|
||||
});
|
||||
|
||||
// Render scene to target
|
||||
renderer.setRenderTarget(renderTarget);
|
||||
renderer.render(scene, camera);
|
||||
renderer.setRenderTarget(null); // Back to screen
|
||||
|
||||
// Use as texture
|
||||
material.map = renderTarget.texture;
|
||||
```
|
||||
|
||||
### Depth Texture
|
||||
|
||||
```javascript
|
||||
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
|
||||
renderTarget.depthTexture = new THREE.DepthTexture(
|
||||
512,
|
||||
512,
|
||||
THREE.UnsignedShortType,
|
||||
);
|
||||
|
||||
// Access depth
|
||||
const depthTexture = renderTarget.depthTexture;
|
||||
```
|
||||
|
||||
### Multi-Sample Render Target
|
||||
|
||||
```javascript
|
||||
const renderTarget = new THREE.WebGLRenderTarget(512, 512, {
|
||||
samples: 4, // MSAA
|
||||
});
|
||||
```
|
||||
|
||||
## CubeCamera
|
||||
|
||||
Dynamic environment maps for reflections.
|
||||
|
||||
```javascript
|
||||
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
|
||||
generateMipmaps: true,
|
||||
minFilter: THREE.LinearMipmapLinearFilter,
|
||||
});
|
||||
|
||||
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
|
||||
scene.add(cubeCamera);
|
||||
|
||||
// Apply to reflective material
|
||||
reflectiveMaterial.envMap = cubeRenderTarget.texture;
|
||||
|
||||
// Update in animation loop (expensive!)
|
||||
function animate() {
|
||||
// Hide reflective object, update env map, show again
|
||||
reflectiveObject.visible = false;
|
||||
cubeCamera.position.copy(reflectiveObject.position);
|
||||
cubeCamera.update(renderer, scene);
|
||||
reflectiveObject.visible = true;
|
||||
}
|
||||
```
|
||||
|
||||
## UV Mapping
|
||||
|
||||
### Accessing UVs
|
||||
|
||||
```javascript
|
||||
const uvs = geometry.attributes.uv;
|
||||
|
||||
// Read UV
|
||||
const u = uvs.getX(vertexIndex);
|
||||
const v = uvs.getY(vertexIndex);
|
||||
|
||||
// Modify UV
|
||||
uvs.setXY(vertexIndex, newU, newV);
|
||||
uvs.needsUpdate = true;
|
||||
```
|
||||
|
||||
### Second UV Channel (for AO maps)
|
||||
|
||||
```javascript
|
||||
// Required for aoMap
|
||||
geometry.setAttribute("uv2", geometry.attributes.uv);
|
||||
|
||||
// Or create custom second UV
|
||||
const uv2 = new Float32Array(vertexCount * 2);
|
||||
// ... fill uv2 data
|
||||
geometry.setAttribute("uv2", new THREE.BufferAttribute(uv2, 2));
|
||||
```
|
||||
|
||||
### UV Transform in Shader
|
||||
|
||||
```javascript
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
map: { value: texture },
|
||||
uvOffset: { value: new THREE.Vector2(0, 0) },
|
||||
uvScale: { value: new THREE.Vector2(1, 1) },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec2 uvOffset;
|
||||
uniform vec2 uvScale;
|
||||
|
||||
void main() {
|
||||
vUv = uv * uvScale + uvOffset;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D map;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = texture2D(map, vUv);
|
||||
}
|
||||
`,
|
||||
});
|
||||
```
|
||||
|
||||
## Texture Atlas
|
||||
|
||||
Multiple images in one texture.
|
||||
|
||||
```javascript
|
||||
// Atlas with 4 sprites (2x2 grid)
|
||||
const atlas = loader.load("atlas.png");
|
||||
atlas.wrapS = THREE.ClampToEdgeWrapping;
|
||||
atlas.wrapT = THREE.ClampToEdgeWrapping;
|
||||
|
||||
// Select sprite by UV offset/scale
|
||||
function selectSprite(row, col, gridSize = 2) {
|
||||
atlas.offset.set(col / gridSize, 1 - (row + 1) / gridSize);
|
||||
atlas.repeat.set(1 / gridSize, 1 / gridSize);
|
||||
}
|
||||
|
||||
// Select top-left sprite
|
||||
selectSprite(0, 0);
|
||||
```
|
||||
|
||||
## Material Texture Maps
|
||||
|
||||
### PBR Texture Set
|
||||
|
||||
```javascript
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
// Base color (sRGB)
|
||||
map: colorTexture,
|
||||
|
||||
// Surface detail (Linear)
|
||||
normalMap: normalTexture,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
|
||||
// Roughness (Linear, grayscale)
|
||||
roughnessMap: roughnessTexture,
|
||||
roughness: 1, // Multiplier
|
||||
|
||||
// Metalness (Linear, grayscale)
|
||||
metalnessMap: metalnessTexture,
|
||||
metalness: 1, // Multiplier
|
||||
|
||||
// Ambient occlusion (Linear, uses uv2)
|
||||
aoMap: aoTexture,
|
||||
aoMapIntensity: 1,
|
||||
|
||||
// Self-illumination (sRGB)
|
||||
emissiveMap: emissiveTexture,
|
||||
emissive: 0xffffff,
|
||||
emissiveIntensity: 1,
|
||||
|
||||
// Vertex displacement (Linear)
|
||||
displacementMap: displacementTexture,
|
||||
displacementScale: 0.1,
|
||||
displacementBias: 0,
|
||||
|
||||
// Alpha (Linear)
|
||||
alphaMap: alphaTexture,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
// Don't forget UV2 for AO
|
||||
geometry.setAttribute("uv2", geometry.attributes.uv);
|
||||
```
|
||||
|
||||
### Normal Map Types
|
||||
|
||||
```javascript
|
||||
// OpenGL style normals (default)
|
||||
material.normalMapType = THREE.TangentSpaceNormalMap;
|
||||
|
||||
// Object space normals
|
||||
material.normalMapType = THREE.ObjectSpaceNormalMap;
|
||||
```
|
||||
|
||||
## Procedural Textures
|
||||
|
||||
### Noise Texture
|
||||
|
||||
```javascript
|
||||
function generateNoiseTexture(size = 256) {
|
||||
const data = new Uint8Array(size * size * 4);
|
||||
|
||||
for (let i = 0; i < size * size; i++) {
|
||||
const value = Math.random() * 255;
|
||||
data[i * 4] = value;
|
||||
data[i * 4 + 1] = value;
|
||||
data[i * 4 + 2] = value;
|
||||
data[i * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
const texture = new THREE.DataTexture(data, size, size);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
```
|
||||
|
||||
### Gradient Texture
|
||||
|
||||
```javascript
|
||||
function generateGradientTexture(color1, color2, size = 256) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, size, 0);
|
||||
gradient.addColorStop(0, color1);
|
||||
gradient.addColorStop(1, color2);
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, size, 1);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
}
|
||||
```
|
||||
|
||||
## Texture Memory Management
|
||||
|
||||
### Dispose Textures
|
||||
|
||||
```javascript
|
||||
// Single texture
|
||||
texture.dispose();
|
||||
|
||||
// Material textures
|
||||
function disposeMaterial(material) {
|
||||
const maps = [
|
||||
"map",
|
||||
"normalMap",
|
||||
"roughnessMap",
|
||||
"metalnessMap",
|
||||
"aoMap",
|
||||
"emissiveMap",
|
||||
"displacementMap",
|
||||
"alphaMap",
|
||||
"envMap",
|
||||
"lightMap",
|
||||
"bumpMap",
|
||||
"specularMap",
|
||||
];
|
||||
|
||||
maps.forEach((mapName) => {
|
||||
if (material[mapName]) {
|
||||
material[mapName].dispose();
|
||||
}
|
||||
});
|
||||
|
||||
material.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### Texture Pooling
|
||||
|
||||
```javascript
|
||||
class TexturePool {
|
||||
constructor() {
|
||||
this.textures = new Map();
|
||||
this.loader = new THREE.TextureLoader();
|
||||
}
|
||||
|
||||
async get(url) {
|
||||
if (this.textures.has(url)) {
|
||||
return this.textures.get(url);
|
||||
}
|
||||
|
||||
const texture = await new Promise((resolve, reject) => {
|
||||
this.loader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
|
||||
this.textures.set(url, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
dispose(url) {
|
||||
const texture = this.textures.get(url);
|
||||
if (texture) {
|
||||
texture.dispose();
|
||||
this.textures.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
disposeAll() {
|
||||
this.textures.forEach((t) => t.dispose());
|
||||
this.textures.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use power-of-2 dimensions**: 256, 512, 1024, 2048
|
||||
2. **Compress textures**: KTX2/Basis for web delivery
|
||||
3. **Use texture atlases**: Reduce texture switches
|
||||
4. **Enable mipmaps**: For distant objects
|
||||
5. **Limit texture size**: 2048 usually sufficient for web
|
||||
6. **Reuse textures**: Same texture = better batching
|
||||
|
||||
```javascript
|
||||
// Check texture memory
|
||||
console.log(renderer.info.memory.textures);
|
||||
|
||||
// Optimize for mobile
|
||||
const maxSize = renderer.capabilities.maxTextureSize;
|
||||
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
|
||||
const textureSize = isMobile ? 1024 : 2048;
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `threejs-materials` - Applying textures to materials
|
||||
- `threejs-loaders` - Loading texture files
|
||||
- `threejs-shaders` - Custom texture sampling
|
||||
Reference in New Issue
Block a user