Three.js 从入门到实战 — 一篇文章吃透 WebGL 的灵魂封装
Three.js 从入门到实战 — 一篇文章吃透 WebGL 的灵魂封装
「WebGL 的门槛在矩阵里,Three.js 的门槛只在一杯咖啡里。」
这是一篇长文,约 1.2 万字 + 12 个可运行 Demo + 1 个完整小项目。从原理 → API → 实战 → 性能优化全部讲透。读完你能独立做出常见 3D 场景:产品展示、数据可视化、3D 游戏原型、Web AR/VR。
目录
- 一、为什么需要 Three.js
- 二、5 分钟跑起来:Hello Cube
- 三、核心三件套:Scene / Camera / Renderer
- 四、坐标系与变换:搞懂这一节就成功一半
- 五、几何体(Geometry)— 物体的"骨架"
- 六、材质(Material)— 物体的"皮肤"
- 七、光照(Light)— 让世界亮起来
- 八、纹理与贴图(Texture)
- 九、相机控制器(OrbitControls 等)
- 十、加载 3D 模型(GLTF / FBX / OBJ)
- 十一、动画:Tween / Clock / Mixer
- 十二、射线检测(Raycaster)— 鼠标拾取
- 十三、后处理(EffectComposer)
- 十四、性能优化清单(生产必备)
- 十五、完整实战:旋转地球 + 星空 + 鼠标交互
- 十六、生态与下一步学习路线
一、为什么需要 Three.js
1.1 WebGL 的痛
WebGL 是浏览器原生 3D API,但它只暴露最底层的 GPU 状态机。画一个三角形要写:
// 原生 WebGL 画三角形(节选,省略错误处理)
const gl = canvas.getContext('webgl');
const vsSource = `
attribute vec4 aPosition;
void main() { gl_Position = aPosition; }
`;
const fsSource = `
void main() { gl_FragColor = vec4(1, 0.5, 0.2, 1); }
`;
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource); gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fsSource); gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs); gl.attachShader(program, fs);
gl.linkProgram(program); gl.useProgram(program);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
70 行代码画了一个三角形 —— 还没考虑相机、光照、纹理、阴影。
1.2 Three.js 的解法
import * as THREE from 'three';
const geo = new THREE.BoxGeometry();
const mat = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
4 行得到一个旋转的彩色立方体。Three.js 把 WebGL 抽象成了面向对象的 3D 引擎:
| 抽象层 | 对应概念 |
|---|---|
| Scene(场景图) | 类似 DOM 树,物体可父子嵌套 |
| Mesh = Geometry + Material | 物体的"形状 + 材质" |
| Camera | 决定"从哪儿看" |
| Light | 决定"光怎么打" |
| Renderer | 把场景画到 canvas |
💡 一句话总结:Three.js 之于 WebGL,相当于 jQuery / React 之于原生 DOM。
1.3 谁在用
- Bruno Simon 个人主页(Three.js 风向标)
- Apple AirPods Pro 产品页(半透明 3D 模型 + 滚动触发动画)
- Figma / 鸿蒙开发者大会 / 阿里淘宝玩一玩
- Stripe / Vercel 官网的 hero 区域
- 特斯拉车机 / 蔚来 NOMI(车端嵌入 H5)
- Google Earth Web / 高德 / 百度地图 3D
二、5 分钟跑起来:Hello Cube
2.1 引入方式
A. CDN(最快,适合 Demo)
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ...
</script>
B. NPM(生产推荐)
npm i three
npm i -D @types/three # TypeScript
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
2.2 第一个程序:旋转的立方体
<!DOCTYPE html>
<html>
<body style="margin:0;overflow:hidden">
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'three';
// 1) 场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f1221);
// 2) 相机:透视投影,FOV 75°,宽高比,近/远裁剪面
const camera = new THREE.PerspectiveCamera(
75, innerWidth / innerHeight, 0.1, 1000
);
camera.position.z = 3;
// 3) 渲染器
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('c'),
antialias: true,
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
// 4) 几何体 + 材质 + 网格
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshNormalMaterial(); // 自带颜色,无需光照
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 5) 渲染循环
function tick() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
tick();
// 6) 自适应窗口
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
打开浏览器,能看到一个彩色立方体在转 —— 你已经入门了。
三、核心三件套:Scene / Camera / Renderer
3.1 Scene(场景)
Scene 本质是一个树形容器,所有要渲染的东西都得 add() 进去。
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222); // 纯色背景
scene.fog = new THREE.Fog(0x222, 5, 50); // 雾效(远处变模糊)
const group = new THREE.Group(); // 分组,可批量平移/旋转
group.add(cube1, cube2);
scene.add(group);
scene.traverse((obj) => console.log(obj.name)); // 深度优先遍历
每个 Object3D 都有:
position/rotation/scale:本地变换(Vector3 / Euler)parent/children:父子关系visible:是否渲染userData:自定义数据挂载点
3.2 Camera(相机)
| 类型 | 用途 | 关键参数 |
|---|---|---|
PerspectiveCamera |
透视(最常用,模拟人眼,近大远小) | fov, aspect, near, far |
OrthographicCamera |
正交(无近大远小,CAD/2.5D 用) | left, right, top, bottom, near, far |
ArrayCamera |
多视口(VR) | — |
CubeCamera |
立方体环境贴图采集 | — |
// 透视相机:90% 场景用它
const camera = new THREE.PerspectiveCamera(
45, // FOV,越小越"长焦"
window.innerWidth / window.innerHeight,
0.1, // near:比这近的物体不显示
1000 // far:比这远的物体不显示
);
camera.position.set(5, 5, 10);
camera.lookAt(0, 0, 0); // 朝原点看
// 正交相机:俯视图、UI、棋盘类游戏
const aspect = innerWidth / innerHeight;
const d = 5;
const orthoCam = new THREE.OrthographicCamera(
-d * aspect, d * aspect, d, -d, 0.1, 100
);
⚠️ 坑:改完
aspect一定要camera.updateProjectionMatrix(),否则不生效。
3.3 Renderer(渲染器)
const renderer = new THREE.WebGLRenderer({
canvas, // 指定 canvas,不传会自动创建
antialias: true, // 抗锯齿(开启会有性能开销)
alpha: true, // 透明背景
powerPreference: 'high-performance', // 强制独显
preserveDrawingBuffer: true, // 截图必备
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // ⚠️ 限到 2
renderer.outputColorSpace = THREE.SRGBColorSpace; // r152+ 必填
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调
renderer.shadowMap.enabled = true; // 开阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
WebGPU 渲染器(实验性):
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
const renderer = new WebGPURenderer();
await renderer.init();
WebGPU 是 WebGL 的下一代,支持 Compute Shader,未来 3 年会逐步替代 WebGL。
四、坐标系与变换:搞懂这一节就成功一半
4.1 右手坐标系
Three.js 用 右手坐标系:
+Y (上)
│
│
└─────── +X (右)
╱
╱
+Z (朝向你)
记忆方法:右手伸出,拇指 X,食指 Y,中指 Z(指向自己)。
4.2 三种空间
| 空间 | 含义 | 例子 |
|---|---|---|
| 本地(local) | 相对父对象 | 子物体 position(1,0,0) 表示在父物体右边 1 米 |
| 世界(world) | 全局坐标 | mesh.getWorldPosition(v) |
| 屏幕(screen) | 像素坐标 | 鼠标事件 event.clientX |
// 本地 → 世界
const v = new THREE.Vector3();
mesh.getWorldPosition(v);
// 世界 → 屏幕
const s = mesh.position.clone().project(camera);
const x = (s.x * 0.5 + 0.5) * width;
const y = (-s.y * 0.5 + 0.5) * height;
// 屏幕 → 世界(反向)
const ndc = new THREE.Vector3(
(mouseX / width) * 2 - 1,
-(mouseY / height) * 2 + 1,
0
);
ndc.unproject(camera);
4.3 旋转的三种姿势
// A. Euler 欧拉角(直观但有"万向锁")
mesh.rotation.x = Math.PI / 4; // 弧度,PI = 180°
mesh.rotation.set(0, Math.PI, 0);
// B. Quaternion 四元数(无万向锁,骨骼动画用它)
const q = new THREE.Quaternion();
q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
mesh.quaternion.copy(q);
// C. lookAt(让物体面朝某点)
mesh.lookAt(0, 0, 0);
4.4 矩阵复合
每个 Object3D 内部维护两个矩阵:
mesh.matrix // 本地变换矩阵(由 position/rotation/scale 自动生成)
mesh.matrixWorld // 世界变换矩阵 = parent.matrixWorld × matrix
修改完 position 后,Three.js 会在每帧渲染前自动重算。如果你手动改 matrix,要:
mesh.matrixAutoUpdate = false;
mesh.matrix.copy(yourMatrix);
mesh.matrixWorldNeedsUpdate = true;
五、几何体(Geometry)— 物体的"骨架"
5.1 内置几何体
new THREE.BoxGeometry(width, height, depth);
new THREE.SphereGeometry(radius, widthSeg, heightSeg);
new THREE.PlaneGeometry(width, height);
new THREE.CylinderGeometry(radiusTop, radiusBottom, height, segs);
new THREE.ConeGeometry(radius, height, segs);
new THREE.TorusGeometry(radius, tubeRadius, radialSeg, tubularSeg);
new THREE.TorusKnotGeometry(...);
new THREE.IcosahedronGeometry(radius, detail); // 二十面体(细分多了变球)
new THREE.TetrahedronGeometry(); // 四面体
new THREE.RingGeometry(innerR, outerR, segs);
new THREE.TextGeometry(text, { font, size }); // 3D 文字(需加载字体)
5.2 自定义几何体(BufferGeometry)
所有内置几何体最终都是 BufferGeometry。手写一个三角形:
const geo = new THREE.BufferGeometry();
// 顶点位置(每 3 个数为一个顶点)
const vertices = new Float32Array([
0, 1, 0,
-1, -1, 0,
1, -1, 0,
]);
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 顶点法线(用于光照计算)
geo.computeVertexNormals();
// 索引(让顶点复用,省内存)
geo.setIndex([0, 1, 2]);
// UV(贴图坐标)
const uvs = new Float32Array([0.5, 1, 0, 0, 1, 0]);
geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
5.3 修改几何体顶点
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const y = pos.getY(i);
pos.setY(i, y + Math.sin(Date.now() * 0.001 + i) * 0.1);
}
pos.needsUpdate = true; // ⚠️ 必加,否则 GPU 不更新
5.4 几何体合并(性能技巧)
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const merged = mergeGeometries([geoA, geoB, geoC]);
const mesh = new THREE.Mesh(merged, material);
// 1 个 mesh = 1 次 draw call,比 3 个 mesh 快得多
六、材质(Material)— 物体的"皮肤"
6.1 选材质的决策树
需要响应光照吗?
├── 否 → MeshBasicMaterial(最便宜)
└── 是
├── 卡通/不在乎物理真实 → MeshLambertMaterial / MeshPhongMaterial
└── 要照片级真实 → MeshStandardMaterial(PBR)
└── 要更高精度 → MeshPhysicalMaterial(车漆/玻璃)
6.2 常用材质
// 1. Basic:纯色 / 贴图,不受光照影响
new THREE.MeshBasicMaterial({ color: 0xff6688, wireframe: true });
// 2. Normal:根据法线显示颜色(debug 神器)
new THREE.MeshNormalMaterial();
// 3. Lambert:漫反射(哑光,磨砂)
new THREE.MeshLambertMaterial({ color: 0x8888ff });
// 4. Phong:带高光(塑料、瓷器)
new THREE.MeshPhongMaterial({
color: 0xff8888,
shininess: 100,
specular: 0xffffff,
});
// 5. Standard(PBR):现代游戏标准
new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.5, // 0=镜面,1=完全粗糙
metalness: 0.8, // 0=塑料,1=金属
map: colorTex,
normalMap: normalTex,
roughnessMap: roughTex,
metalnessMap: metalTex,
aoMap: aoTex,
});
// 6. Physical:Standard 增强(清漆 / 透射 / 虹彩)
new THREE.MeshPhysicalMaterial({
clearcoat: 1.0,
transmission: 0.9, // 透明(水晶/玻璃)
ior: 1.5, // 折射率
thickness: 0.5,
});
// 7. Shader 自定义
new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
vertexShader: `...`,
fragmentShader: `...`,
});
6.3 通用属性
material.transparent = true;
material.opacity = 0.5;
material.side = THREE.DoubleSide; // 双面渲染(默认只渲染正面)
material.depthWrite = false; // 透明物体常配合此项
material.blending = THREE.AdditiveBlending; // 加法混合(火焰/光晕)
material.wireframe = true; // 线框模式
material.flatShading = true; // 低多边形风格
⚠️ 半透明排序坑:Three.js 的透明物体按 z-depth 排序,复杂场景下可能错乱。解决方案:①
material.depthWrite = false② 用renderOrder手动指定 ③ 改用 OIT(Order-Independent Transparency)。
七、光照(Light)— 让世界亮起来
⚠️ 用了
MeshBasicMaterial不需要光照;用Lambert / Phong / Standard必须加灯,否则黑屏。
7.1 6 种光源
// 1. 环境光:均匀照亮全场,无方向、无阴影
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
// 2. 平行光:模拟太阳,方向恒定(可投阴影)
const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 7);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
scene.add(sun);
// 3. 点光源:模拟灯泡,向四周发光(可投阴影)
const bulb = new THREE.PointLight(0xff8844, 1, 20); // 颜色, 强度, 距离
bulb.position.set(0, 5, 0);
scene.add(bulb);
// 4. 聚光灯:手电筒,圆锥范围(可投阴影)
const spot = new THREE.SpotLight(0xffffff, 1, 30, Math.PI / 6, 0.2);
scene.add(spot);
// 5. 半球光:天空 / 地面双色照明(户外场景神器)
scene.add(new THREE.HemisphereLight(0x87ceeb, 0x654321, 0.6));
// 6. 矩形面光:模拟柔光板(IES 物理灯)
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
RectAreaLightUniformsLib.init();
const rect = new THREE.RectAreaLight(0xffffff, 5, 4, 2);
scene.add(rect);
7.2 阴影怎么开
阴影开销大,要配合三步:
// ① 渲染器开
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 软阴影
// ② 光源开
sun.castShadow = true;
sun.shadow.camera.left = -10; // 阴影相机视锥(影响阴影范围)
sun.shadow.camera.right = 10;
sun.shadow.camera.top = 10;
sun.shadow.camera.bottom = -10;
sun.shadow.bias = -0.0005; // 解决"阴影痤疮"
// ③ 物体开
mesh.castShadow = true;
ground.receiveShadow = true;
7.3 调试光照
const helper = new THREE.DirectionalLightHelper(sun, 1);
scene.add(helper);
const shadowHelper = new THREE.CameraHelper(sun.shadow.camera);
scene.add(shadowHelper);
八、纹理与贴图(Texture)
8.1 加载基础纹理
const loader = new THREE.TextureLoader();
const tex = loader.load(
'/wood.jpg',
(t) => console.log('loaded'),
undefined,
(e) => console.error(e),
);
tex.colorSpace = THREE.SRGBColorSpace; // ⚠️ 颜色贴图必加(r152+)
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(4, 4); // 平铺 4×4
tex.anisotropy = 16; // 各向异性过滤(远处不糊)
const mat = new THREE.MeshStandardMaterial({ map: tex });
8.2 PBR 全套贴图
现代 PBR 流程通常需要 5–7 张贴图:
| 贴图 | 作用 | 颜色空间 |
|---|---|---|
| albedo / map | 颜色 | sRGB |
| normalMap | 法线(凹凸细节) | Linear |
| roughnessMap | 粗糙度 | Linear |
| metalnessMap | 金属度 | Linear |
| aoMap | 环境光遮蔽 | Linear |
| displacementMap | 位移(真实位移顶点) | Linear |
| emissiveMap | 自发光 | sRGB |
const mat = new THREE.MeshStandardMaterial({
map: load('color.jpg'),
normalMap: load('normal.jpg'),
roughnessMap: load('rough.jpg'),
metalnessMap: load('metal.jpg'),
aoMap: load('ao.jpg'),
envMap: envTex, // 环境贴图(决定金属表面反射什么)
});
function load(url) {
const t = loader.load(url);
if (url.includes('color') || url.includes('emissive')) {
t.colorSpace = THREE.SRGBColorSpace;
}
return t;
}
8.3 环境贴图(HDRI)— 真实感关键
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
new RGBELoader().load('/sky.hdr', (hdr) => {
hdr.mapping = THREE.EquirectangularReflectionMapping;
scene.background = hdr;
scene.environment = hdr; // 所有 PBR 材质自动反射这张图
});
免费 HDRI 资源:polyhaven.com
8.4 视频纹理
const video = document.createElement('video');
video.src = '/loop.mp4';
video.loop = true;
video.muted = true;
video.play();
const videoTex = new THREE.VideoTexture(video);
const screen = new THREE.Mesh(
new THREE.PlaneGeometry(16, 9),
new THREE.MeshBasicMaterial({ map: videoTex })
);
九、相机控制器(OrbitControls 等)
9.1 OrbitControls — 最常用
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 阻尼,丝滑
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.maxPolarAngle = Math.PI / 2; // 不让相机翻到地下
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
// 渲染循环里调用
function tick() {
controls.update(); // ⚠️ 开启 damping 后必须每帧 update
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
9.2 其他控制器
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
import { FlyControls } from 'three/addons/controls/FlyControls.js';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // FPS
import { TransformControls } from 'three/addons/controls/TransformControls.js'; // 类 Blender
import { DragControls } from 'three/addons/controls/DragControls.js'; // 拖拽
import { ArcballControls } from 'three/addons/controls/ArcballControls.js'; // 自由轨迹球
十、加载 3D 模型(GLTF / FBX / OBJ)
10.1 选哪个格式
| 格式 | 大小 | 动画 | 推荐 |
|---|---|---|---|
| glTF / glb | 小(二进制) | ✅ 骨骼/Morph/PBR | ⭐⭐⭐⭐⭐ Web 标准 |
| FBX | 大 | ✅ | 老项目过渡用 |
| OBJ | 中 | ❌ | 静态模型 |
| STL | 中 | ❌ | 3D 打印 |
| USDZ | — | ✅ | iOS AR Quick Look |
业内共识:优先 glTF,只有 glTF 是 Khronos 官方制定的"3D 界 JPEG"。
10.2 加载 glTF(含 Draco / KTX2 压缩)
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const draco = new DRACOLoader().setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
const ktx2 = new KTX2Loader().setTranscoderPath('https://unpkg.com/three@0.160.0/examples/jsm/libs/basis/');
ktx2.detectSupport(renderer);
const loader = new GLTFLoader().setDRACOLoader(draco).setKTX2Loader(ktx2);
loader.load(
'/models/duck.glb',
(gltf) => {
scene.add(gltf.scene);
console.log('动画', gltf.animations);
console.log('相机', gltf.cameras);
},
(xhr) => console.log((xhr.loaded / xhr.total * 100) + '%'),
(err) => console.error(err),
);
10.3 模型编辑常用操作
gltf.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
// 替换材质
if (child.name === 'Body') child.material.color.set(0xff0000);
}
});
// 居中并缩放到合适大小
const box = new THREE.Box3().setFromObject(gltf.scene);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
gltf.scene.position.sub(center);
gltf.scene.scale.setScalar(2 / Math.max(size.x, size.y, size.z));
十一、动画:Tween / Clock / Mixer
11.1 三种动画方式
A. 手写帧动画(最简单)
function tick() {
cube.rotation.y += 0.01;
cube.position.x = Math.sin(Date.now() * 0.001) * 3;
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
B. Tween.js(补间动画)
import TWEEN from 'three/addons/libs/tween.module.js';
new TWEEN.Tween(cube.position)
.to({ x: 5, y: 2, z: 0 }, 1500)
.easing(TWEEN.Easing.Cubic.InOut)
.onUpdate(() => console.log('animating'))
.start();
// 渲染循环里
TWEEN.update();
C. AnimationMixer(骨骼/Morph 动画,glTF 必用)
const mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
action.setLoop(THREE.LoopRepeat);
action.setEffectiveTimeScale(1);
action.crossFadeTo(otherAction, 0.5); // 平滑切换动作
const clock = new THREE.Clock();
function tick() {
const dt = clock.getDelta();
mixer.update(dt);
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
11.2 Clock — 帧率无关的时间
const clock = new THREE.Clock();
clock.getDelta(); // 上一帧到这一帧的秒数
clock.getElapsedTime(); // 累计运行秒数
// 用 dt 让动画在不同帧率设备上保持一致速度
mesh.rotation.y += 1.0 * dt; // 每秒旋转 1 弧度,60fps/120fps 都一样
十二、射线检测(Raycaster)— 鼠标拾取
12.1 鼠标点击物体
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', (e) => {
// 屏幕坐标 → NDC(归一化设备坐标 -1~1)
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检测哪些物体被命中(按距离排序)
const hits = raycaster.intersectObjects(scene.children, true);
if (hits.length > 0) {
const obj = hits[0].object;
console.log('点中:', obj.name, '距离:', hits[0].distance);
obj.material.color.set(Math.random() * 0xffffff);
}
});
12.2 FPS 射击:以相机正前方发射射线
// 用于 PointerLockControls 模式(FPS 游戏)
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const hits = raycaster.intersectObjects(enemies);
if (hits.length > 0) {
hits[0].object.userData.hp -= 25;
}
12.3 性能优化
// ① 限定层级(不遍历全场景)
const targets = [enemy1, enemy2, ground];
raycaster.intersectObjects(targets, false);
// ② 设置最大距离
raycaster.far = 50;
// ③ Layer 分层
mesh.layers.set(1);
raycaster.layers.set(1); // 只测试 layer 1 的物体
十三、后处理(EffectComposer)
辉光 / 景深 / 抗锯齿 / 描边 / 像素化等"电影感"特效需要后处理。
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 { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
1.5, // strength
0.4, // radius
0.85, // threshold
));
composer.addPass(new OutputPass());
// 渲染循环里改用 composer
function tick() {
composer.render();
requestAnimationFrame(tick);
}
常用 Pass:
| Pass | 作用 |
|---|---|
RenderPass |
必备,输出场景 |
UnrealBloomPass |
辉光(霓虹/魔法效果) |
BokehPass |
景深(专业级) |
OutlinePass |
物体描边(编辑器选中效果) |
SMAAPass / FXAAPass |
后处理抗锯齿 |
GlitchPass |
故障扰动 |
FilmPass |
老电影颗粒 |
ShaderPass |
自定义 GLSL 滤镜 |
十四、性能优化清单(生产必备)
14.1 监控工具
import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats();
document.body.appendChild(stats.dom);
function tick() {
stats.begin();
renderer.render(scene, camera);
stats.end();
requestAnimationFrame(tick);
}
// 关键指标
console.log(renderer.info);
// {
// render: { calls: 23, triangles: 12450, points: 0, lines: 0 },
// memory: { geometries: 5, textures: 8 },
// programs: [...]
// }
生产经验:手机端 draw calls < 100、三角面 < 100K 才能稳 60fps。
14.2 13 条优化清单
- ✅ 合并几何体:
mergeGeometries,10 个 cube → 1 个 mesh - ✅ InstancedMesh:100 万个相同物体也只 1 次 draw call
const instanced = new THREE.InstancedMesh(geo, mat, 10000); const m = new THREE.Matrix4(); for (let i = 0; i < 10000; i++) { m.setPosition(Math.random() * 100, 0, Math.random() * 100); instanced.setMatrixAt(i, m); } - ✅ LOD(Level of Detail):远的用简模
const lod = new THREE.LOD(); lod.addLevel(highPolyMesh, 0); lod.addLevel(midPolyMesh, 50); lod.addLevel(lowPolyMesh, 200); - ✅ frustumCulled:默认开启,相机外的物体不渲染
- ✅ 冻结静态物体:
mesh.matrixAutoUpdate = false - ✅ 像素比限制:
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) - ✅ 纹理压缩:用 KTX2 / Basis Universal,比 PNG 小 6–10 倍
- ✅ Draco 压缩 glTF:模型体积减 80%
- ✅ 关闭不必要的 antialias:用 FXAA/SMAA 后处理代替
- ✅ 阴影分辨率收敛:
shadow.mapSize 1024×1024通常够用 - ✅ 共享材质:相同材质只创建一次
- ✅ OffscreenCanvas + Worker:把渲染挪到 Worker(Chrome 已支持)
- ✅ 节流 resize:
resize事件用 debounce
14.3 内存泄漏防护
function dispose(obj) {
obj.traverse((c) => {
if (c.geometry) c.geometry.dispose();
if (c.material) {
if (Array.isArray(c.material)) c.material.forEach((m) => m.dispose());
else c.material.dispose();
}
if (c.texture) c.texture.dispose();
});
}
scene.remove(model);
dispose(model);
renderer.renderLists.dispose();
十五、完整实战:旋转地球 + 星空 + 鼠标交互
来一个能直接上线的小项目:地球 + 5000 颗星星 + 自动旋转 + 点击爆炸特效。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Three.js Earth Demo</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
#info {
position: fixed; top: 16px; left: 16px;
color: rgba(255,255,255,.7); font: 12px Menlo, monospace;
pointer-events: none;
}
</style>
</head>
<body>
<div id="info">🖱 拖拽旋转 · 滚轮缩放 · 点击地球爆炸</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ========== 基础三件套 ========== */
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 0, 6);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
/* ========== 控制器 ========== */
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 3;
controls.maxDistance = 15;
/* ========== 光照 ========== */
scene.add(new THREE.AmbientLight(0xffffff, 0.15));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(5, 3, 5);
scene.add(sun);
/* ========== 地球 ========== */
const earthGroup = new THREE.Group();
scene.add(earthGroup);
const loader = new THREE.TextureLoader();
const earthTex = loader.load(
'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg'
);
earthTex.colorSpace = THREE.SRGBColorSpace;
const specTex = loader.load(
'https://threejs.org/examples/textures/planets/earth_specular_2048.jpg'
);
const earth = new THREE.Mesh(
new THREE.SphereGeometry(1, 64, 64),
new THREE.MeshPhongMaterial({
map: earthTex,
specularMap: specTex,
shininess: 30,
})
);
earthGroup.add(earth);
/* ========== 大气层(半透明外壳,制造光晕) ========== */
const atmosphere = new THREE.Mesh(
new THREE.SphereGeometry(1.05, 64, 64),
new THREE.ShaderMaterial({
transparent: true,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vNormal;
void main() {
float intensity = pow(0.65 - dot(vNormal, vec3(0,0,1)), 2.0);
gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
}
`,
})
);
earthGroup.add(atmosphere);
/* ========== 星空(5000 个粒子) ========== */
const starGeo = new THREE.BufferGeometry();
const starCount = 5000;
const positions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
// 球面均匀分布
const r = 80 + Math.random() * 20;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = r * Math.cos(phi);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const stars = new THREE.Points(
starGeo,
new THREE.PointsMaterial({
color: 0xffffff,
size: 0.4,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
})
);
scene.add(stars);
/* ========== 点击爆炸特效 ========== */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const explosions = [];
renderer.domElement.addEventListener('click', (e) => {
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObject(earth);
if (hits.length === 0) return;
const point = hits[0].point;
const count = 80;
const pos = new Float32Array(count * 3);
const vel = [];
for (let i = 0; i < count; i++) {
pos[i * 3] = point.x;
pos[i * 3 + 1] = point.y;
pos[i * 3 + 2] = point.z;
const dir = point.clone().normalize().add(
new THREE.Vector3(
(Math.random() - 0.5) * 1.5,
(Math.random() - 0.5) * 1.5,
(Math.random() - 0.5) * 1.5
)
).normalize();
vel.push(dir.multiplyScalar(0.02 + Math.random() * 0.04));
}
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
const m = new THREE.PointsMaterial({
color: 0xffaa33,
size: 0.08,
transparent: true,
});
const p = new THREE.Points(g, m);
scene.add(p);
explosions.push({ p, vel, life: 0 });
});
/* ========== 渲染循环 ========== */
const clock = new THREE.Clock();
function tick() {
const dt = clock.getDelta();
earthGroup.rotation.y += 0.05 * dt; // 地球自转
stars.rotation.y -= 0.005 * dt; // 星空反向缓慢转
// 更新爆炸粒子
for (let i = explosions.length - 1; i >= 0; i--) {
const ex = explosions[i];
ex.life += dt;
const arr = ex.p.geometry.attributes.position.array;
for (let j = 0; j < ex.vel.length; j++) {
arr[j * 3] += ex.vel[j].x;
arr[j * 3 + 1] += ex.vel[j].y;
arr[j * 3 + 2] += ex.vel[j].z;
}
ex.p.geometry.attributes.position.needsUpdate = true;
ex.p.material.opacity = Math.max(0, 1 - ex.life / 1.5);
if (ex.life > 1.5) {
scene.remove(ex.p);
ex.p.geometry.dispose();
ex.p.material.dispose();
explosions.splice(i, 1);
}
}
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
tick();
/* ========== 自适应 ========== */
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
把它保存为 earth.html 双击打开 —— 你已经有一个能展示给老板看的 3D Demo 了 🌍。
十六、生态与下一步学习路线
16.1 上层封装框架
| 框架 | 特色 | 推荐人群 |
|---|---|---|
| React Three Fiber (R3F) | 用 JSX 写 3D | React 用户首选 |
| TresJS | Vue 版 R3F | Vue 用户 |
| Drei | R3F 工具库(控制器、相机、辅助) | 配合 R3F 用 |
| Theatre.js | 时间线动画编辑器 | 做交互动画 |
| Babylon.js | 微软出品,更"游戏引擎"化 | 做 Web 游戏 |
| A-Frame | HTML 标签写 VR | WebXR 入门 |
| PlayCanvas | 商业级 Web 引擎 | 大型在线游戏 |
16.2 R3F 一瞥
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function Box() {
return (
<mesh rotation={[0.4, 0.2, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
export default function App() {
return (
<Canvas camera={{ position: [3, 3, 3] }}>
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} />
<Box />
<OrbitControls />
</Canvas>
);
}
声明式写法 + 组件化 —— 这是未来。
16.3 学习资源
- 📖 官网:threejs.org — 必看 Examples(500+ 案例源码)
- 🎓 Three.js Journey(Bruno Simon)— 业界公认最好的付费课程
- 📺 YouTube:Bruno Simon / Wawa Sensei
- 📚 《WebGL 编程指南》 — 想搞懂底层必读
- 🎨 shadertoy.com — Shader 灵感库
- 🎁 polyhaven.com — 免费高质量 HDRI / 模型 / 贴图
- 🎬 sketchfab.com — 模型素材市场
16.4 进阶路线
入门 ✅(你在这)
↓
熟悉 r3f / drei → 用 React 工作流做交互
↓
学 GLSL Shader → 写自定义视觉效果
↓
学 Blender / 模型优化 → 自己产出资产
↓
WebXR / WebGPU → 下一代 Web 3D
写在最后
Three.js 的强大不在于"能做什么",而在于它把图形学的入门门槛降到了 4 行代码。
但同时,它也是一座冰山 —— 90% 的功能藏在 Examples 文件夹里。遇到问题第一件事不是 Google,而是去 threejs.org/examples 翻 5 分钟,多半能找到答案。
「别担心做不出像 Bruno Simon 那样的作品,每个 Three.js 大佬都是从一个旋转的立方体开始的。」
愿你也能在 Web 里造出自己的星辰大海 🌌。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)