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"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "文档处理"
|
"category": "文档处理"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "threejs",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/codex/plugins/threejs"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "设计"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,18 @@
|
|||||||
"authentication": "ON_INSTALL"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "文档处理"
|
"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