这篇文章面向已经会 JavaScript、想系统学习 Web 3D 的前端开发者。它不是 Three.js API 的逐条翻译,而是一份从底层原理、核心对象、渲染机制、资源加载、动画交互到工程优化的完整学习笔记。
一、为什么前端需要 Three.js
前端可视化不是只有 ECharts、SVG 和 Canvas 2D。当业务开始进入空间表达时,2D 方案会越来越吃力:
电商产品 3D 展示
汽车、家居、工业设备配置器
机房、工厂、园区数字孪生
3D 地图、楼层导航、空间编辑器
Web 游戏、互动营销页
数据大屏中的三维关系、地球、粒子系统
VR / AR / WebXR 场景
这些需求的共同点是:它们不只是“画图”,而是在浏览器里构建一个可观察、可交互、可动画、可加载复杂资源的三维世界。
Three.js 的价值在于,它把 WebGL 这种底层图形接口包装成更接近应用开发的对象模型。你可以用前端熟悉的方式组织场景、相机、模型、材质、事件和动画,而不必一上来就手写顶点着色器、片元着色器和矩阵乘法。
二、WebGL 与 Three.js 的关系
先把几个概念摆正:
flowchart LR
A[OpenGL] --> B[OpenGL ES]
B --> C[WebGL / WebGL2]
C --> D[Three.js WebGLRenderer]
D --> E[浏览器 Canvas]
E --> F[用户屏幕]
G[WebGPU] -.新一代图形 API.-> H[Three.js WebGPURenderer]
1. OpenGL
OpenGL 是一套跨平台图形 API 标准,历史很长,主要用于渲染 2D / 3D 图形。
2. OpenGL ES
OpenGL ES 是 OpenGL 的嵌入式版本,更适合移动端、浏览器等资源受限环境。WebGL 的 API 设计与 OpenGL ES 有很深关系。
3. WebGL
WebGL 是浏览器提供的 JavaScript 图形 API,可以在 <canvas> 中进行 GPU 加速渲染。MDN 对 WebGL 的描述要点是:它可以在兼容浏览器中无需插件渲染高性能交互式 2D 和 3D 图形,并利用设备的硬件图形加速能力。
直接写 WebGL,你需要处理:
flowchart TD
A[创建 canvas] --> B[获取 WebGL context]
B --> C[编写 shader]
C --> D[创建 program]
D --> E[传入顶点 buffer]
E --> F[传入 uniform / attribute]
F --> G[设置纹理 / 深度 / 混合状态]
G --> H[drawArrays / drawElements]
H --> I[浏览器提交 GPU 渲染]
4. Three.js
Three.js 是一个 JavaScript 3D 库。它可以通过 WebGLRenderer 把三维场景渲染到 canvas,也提供 WebGPU、SVG、CSS3D 等相关能力。对绝大多数生产项目来说,WebGLRenderer 仍然是成熟、兼容性强、资料最多的默认选择。
底层 WebGL 关注点
Three.js 中的抽象
shader program
Material / ShaderMaterial
顶点数据 buffer
BufferGeometry
uniform / attribute
材质参数、几何体 attribute
draw call
renderer.render(scene, camera)
纹理对象
Texture
矩阵变换
Object3D.position/rotation/scale
相机矩阵
PerspectiveCamera / OrthographicCamera
场景组织
Scene / Group
一句话:WebGL 是浏览器通向 GPU 的底层接口,Three.js 是更适合应用开发的 3D 引擎式封装。
三、Three.js 知识地图
学习 Three.js 最容易迷路,因为它不是单一知识点,而是一组互相咬合的系统。
mindmap
root((Three.js))
基础对象
Scene
Object3D
Mesh
Camera
Renderer
形状
BufferGeometry
Built-in Geometry
Attribute
Index
Normal
UV
外观
Material
Texture
PBR
ColorSpace
EnvironmentMap
光照
AmbientLight
HemisphereLight
DirectionalLight
PointLight
SpotLight
ShadowMap
资源
GLTFLoader
TextureLoader
DRACOLoader
KTX2Loader
LoadingManager
动画
setAnimationLoop
Clock
AnimationMixer
KeyframeTrack
交互
OrbitControls
Pointer Events
Raycaster
TransformControls
高级
Postprocessing
ShaderMaterial
InstancedMesh
LOD
WebXR
工程
资源压缩
内存释放
性能监控
SPA 生命周期
模块拆分
从应用角度看,它可以被拆成四层:
flowchart TB
A[业务层<br/>产品展示 / 数字孪生 / 游戏 / 地图] --> B[交互与状态层<br/>选中 / 动画 / 数据驱动 / UI 面板]
B --> C[Three.js 对象层<br/>Scene / Mesh / Material / Camera / Loader]
C --> D[渲染与 GPU 层<br/>WebGLRenderer / Shader / Buffer / Texture]
这张图很重要。大部分项目失败,不是因为 API 不会用,而是把这几层混在一起:UI 状态和 3D 对象互相污染,资源生命周期没人管,动画循环到处散落,最后页面越跑越慢。
四、渲染一帧到底发生了什么
Three.js 里最常见的一句代码是:
js 复制代码 1 renderer.render (scene, camera); renderer.render(scene, camera);
这句代码背后做了很多事情。
sequenceDiagram
participant JS as JavaScript 主线程
participant Three as Three.js
participant WebGL as WebGLRenderer
participant GPU as GPU
participant Screen as 屏幕
JS->>Three: 更新 Object3D 位置/旋转/缩放
JS->>Three: controls.update / mixer.update
JS->>WebGL: renderer.render(scene, camera)
WebGL->>Three: 遍历 scene graph
WebGL->>Three: 更新世界矩阵与相机矩阵
WebGL->>Three: 视锥裁剪 / 排序 / 分组
WebGL->>GPU: 上传 buffer / texture / uniform
WebGL->>GPU: 发起 draw call
GPU->>Screen: 光栅化、片元处理、输出像素
一帧里的主要步骤
阶段
发生了什么
常见性能风险
JS 更新
更新动画、交互、业务状态
每帧大量计算、创建临时对象
场景遍历
遍历 Scene 中的对象树
对象数量太多
矩阵更新
计算世界矩阵、相机矩阵
深层嵌套、频繁变换
裁剪排序
剔除不可见物体,透明物排序
透明对象多、排序复杂
状态切换
切换材质、纹理、shader
材质数量过多
Draw Call
GPU 绘制几何体
网格太碎
后处理
Bloom、SSAO、描边等
多 Pass、高分辨率
所以性能优化不是一句“少放模型”就完了,而是要从对象数、draw call、材质纹理、像素比、后处理、内存生命周期一起看。
五、核心对象:Scene、Object3D、Camera、Renderer
Three.js 的基本结构是:
classDiagram
Object3D <|-- Scene
Object3D <|-- Camera
Camera <|-- PerspectiveCamera
Camera <|-- OrthographicCamera
Object3D <|-- Mesh
Object3D <|-- Light
Object3D <|-- Group
Mesh --> BufferGeometry
Mesh --> Material
WebGLRenderer --> Scene
WebGLRenderer --> Camera
1. Scene:三维世界
Scene 是对象容器,本身也继承自 Object3D。
js 复制代码 1 const scene = new THREE .Scene ();2 scene.background = new THREE .Color (0xf6f7fb ); 3 scene.add (mesh, light, camera); const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf6f7fb);
scene.add(mesh, light, camera);
常用属性:
属性
作用
background
设置背景颜色、纹理或环境
environment
给 PBR 材质提供环境反射
fog
添加雾效
children
子对象列表
2. Object3D:一切三维对象的基类
Mesh、Camera、Light、Group 都继承自 Object3D。
js 复制代码 1 object.position .set (1 , 2 , 3 ); 2 object.rotation .set (0 , Math .PI / 4 , 0 ); 3 object.scale .set (2 , 2 , 2 ); 4 5 object.visible = true ; 6 object.name = 'product-shell' ; 7 object.userData = { id : 'sku-001' }; object.position.set(1, 2, 3);
object.rotation.set(0, Math.PI / 4, 0);
object.scale.set(2, 2, 2);
object.visible = true;
object.name = 'product-shell';
object.userData = { id: 'sku-001' };
对象变换关系:
flowchart LR
A[position] --> D[local matrix]
B[rotation / quaternion] --> D
C[scale] --> D
D --> E[parent matrixWorld]
E --> F[object matrixWorld]
F --> G[renderer 使用世界矩阵渲染]
3. Group:组织对象
Group 适合把一组对象作为整体移动、旋转、显示隐藏。
js 复制代码 1 const car = new THREE .Group ();2 car.add (body, wheelFL, wheelFR, wheelBL, wheelBR); 3 scene.add (car); 4 5 car.position .set (0 , 0 , 3 ); 6 car.rotation .y = Math .PI / 2 ; const car = new THREE.Group();
car.add(body, wheelFL, wheelFR, wheelBL, wheelBR);
scene.add(car);
car.position.set(0, 0, 3);
car.rotation.y = Math.PI / 2;
4. Camera:观察场景的眼睛
PerspectiveCamera
透视相机符合人眼直觉,近大远小。
js 复制代码 1 const camera = new THREE .PerspectiveCamera (2 60 , 3 window .innerWidth / window .innerHeight , 4 0.1 , 5 1000 6 ); 7 8 camera.position .set (3 , 2 , 6 ); 9 camera.lookAt (0 , 0 , 0 ); const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(3, 2, 6);
camera.lookAt(0, 0, 0);
参数
含义
建议
fov
垂直视野角
产品展示常用 35-60,游戏可更大
aspect
宽高比
resize 时必须更新
near
近裁剪面
不要过小,常用 0.1 或 1
far
远裁剪面
不要无限大,按场景规模设置
OrthographicCamera
正交相机没有透视缩放,适合工程图、地图、等距视图、2.5D 编辑器。
js 复制代码 1 const size = 10 ;2 const aspect = window .innerWidth / window .innerHeight ;3 4 const camera = new THREE .OrthographicCamera (5 -size * aspect, 6 size * aspect, 7 size, 8 -size, 9 0.1 , 10 100 11 ); const size = 10;
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.OrthographicCamera(
-size * aspect,
size * aspect,
size,
-size,
0.1,
100
);
5. Renderer:渲染器
基础写法:
js 复制代码 1 const renderer = new THREE .WebGLRenderer ({2 canvas : document .querySelector ('#app' ), 3 antialias : true , 4 }); 5 6 renderer.setSize (window .innerWidth , window .innerHeight ); 7 renderer.setPixelRatio (Math .min (window .devicePixelRatio , 2 )); 8 renderer.setAnimationLoop (animate); const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#app'),
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setAnimationLoop(animate);
生产项目常见配置:
js 复制代码 1 renderer.toneMapping = THREE .ACESFilmicToneMapping ; 2 renderer.toneMappingExposure = 1 ; 3 renderer.shadowMap .enabled = true ; 4 renderer.shadowMap .type = THREE .PCFSoftShadowMap ; renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
6. resize 处理
js 复制代码 1 function resize ( ) {2 const width = window .innerWidth ; 3 const height = window .innerHeight ; 4 5 camera.aspect = width / height; 6 camera.updateProjectionMatrix (); 7 8 renderer.setSize (width, height); 9 renderer.setPixelRatio (Math .min (window .devicePixelRatio , 2 )); 10 } 11 12 window .addEventListener ('resize' , resize);function resize() {
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', resize);
如果使用后处理,还要同步更新 composer:
js 复制代码 1 composer.setSize (width, height); composer.setSize(width, height);
六、几何体:从 Geometry 到 BufferGeometry
几何体决定物体“长什么形状”。
1. 常见内置几何体
几何体
说明
常见用途
BoxGeometry
立方体
建筑块、盒子、体素
SphereGeometry
球体
星球、球形节点
PlaneGeometry
平面
地面、贴图面板、视频
CylinderGeometry
圆柱
管道、柱子
ConeGeometry
圆锥
指示器、箭头
TorusGeometry
圆环
装饰、路径节点
ExtrudeGeometry
挤出几何
2D 图形拉伸
ShapeGeometry
形状面
自定义平面形状
TubeGeometry
沿路径生成管道
流线、路线
示例:
js 复制代码 1 const geometry = new THREE .BoxGeometry (1 , 1 , 1 , 4 , 4 , 4 );const geometry = new THREE.BoxGeometry(1, 1, 1, 4, 4, 4);
后面的分段参数会增加顶点数量。分段越多,形状越细腻,但性能成本越高。
2. BufferGeometry 的本质
Three.js 现代几何体底层基本都是 BufferGeometry。它用 TypedArray 存储顶点属性,适合传给 GPU。
flowchart TD
A[BufferGeometry] --> B[position<br/>顶点坐标]
A --> C[normal<br/>法线]
A --> D[uv<br/>纹理坐标]
A --> E[color<br/>顶点颜色]
A --> F[index<br/>顶点索引]
B --> G[GPU Vertex Buffer]
C --> G
D --> G
E --> G
F --> H[GPU Index Buffer]
最小自定义三角形:
js 复制代码 1 const geometry = new THREE .BufferGeometry ();2 3 const vertices = new Float32Array ([4 0 , 1 , 0 , 5 -1 , -1 , 0 , 6 1 , -1 , 0 , 7 ]); 8 9 geometry.setAttribute ('position' , new THREE .BufferAttribute (vertices, 3 )); 10 geometry.computeVertexNormals (); 11 12 const mesh = new THREE .Mesh (13 geometry, 14 new THREE .MeshStandardMaterial ({ color : 0x4e8cff }) 15 ); 16 17 scene.add (mesh); const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 1, 0,
-1, -1, 0,
1, -1, 0,
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshStandardMaterial({ color: 0x4e8cff })
);
scene.add(mesh);
3. 法线 normal 为什么重要
法线用于计算光照。如果模型没有正确法线,常见现象是:
表面黑一块亮一块
高光不对
模型像被压扁
PBR 材质效果很奇怪
自定义几何体时可以调用:
js 复制代码 1 geometry.computeVertexNormals (); geometry.computeVertexNormals();
4. UV 为什么重要
UV 是二维纹理如何贴到三维模型表面的坐标。没有 UV,贴图不知道该怎么映射。
text 复制代码 1 3D 顶点坐标 position:物体在空间里哪里 2 2D 纹理坐标 uv:图片上的哪个点贴到这个顶点 3D 顶点坐标 position:物体在空间里哪里
2D 纹理坐标 uv:图片上的哪个点贴到这个顶点
5. 几何体优化要点
问题
优化方式
顶点太多
降面、LOD、远处简化模型
网格太碎
合并静态网格
重复物体多
使用 InstancedMesh
法线错误
重新导出模型或计算法线
缺 UV
在 Blender 等工具中展开 UV
七、材质系统:物体为什么看起来像它自己
材质决定表面如何响应光照、纹理、环境反射和透明度。
1. 常见材质选择表
材质
是否受光
典型用途
MeshBasicMaterial
否
UI 面板、贴图展示、调试
MeshLambertMaterial
是
性能友好的漫反射物体
MeshPhongMaterial
是
有传统高光的物体
MeshStandardMaterial
是
默认推荐,PBR 标准材质
MeshPhysicalMaterial
是
玻璃、清漆、透射、车漆
MeshNormalMaterial
否
调试法线方向
MeshDepthMaterial
否
深度效果、阴影相关
ShaderMaterial
自定义
高级 shader 效果
2. 材质决策图
flowchart TD
A[我要创建一个物体材质] --> B{是否需要受灯光影响?}
B -- 否 --> C[MeshBasicMaterial]
B -- 是 --> D{是否追求真实 PBR?}
D -- 是 --> E[MeshStandardMaterial]
D -- 更高级玻璃/车漆 --> F[MeshPhysicalMaterial]
D -- 传统简单高光 --> G[MeshPhongMaterial]
D -- 只要漫反射 --> H[MeshLambertMaterial]
A --> I{是否需要完全自定义 GPU 效果?}
I -- 是 --> J[ShaderMaterial / RawShaderMaterial]
3. PBR 材质核心参数
MeshStandardMaterial 是实际项目里最常用的默认材质。
js 复制代码 1 const material = new THREE .MeshStandardMaterial ({2 color : 0xcccccc , 3 metalness : 0.2 , 4 roughness : 0.55 , 5 envMapIntensity : 1 , 6 }); const material = new THREE.MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.2,
roughness: 0.55,
envMapIntensity: 1,
});
参数
作用
直觉理解
color
基础颜色
物体底色
metalness
金属度
0 是非金属,1 是金属
roughness
粗糙度
0 很光滑,1 很粗糙
map
颜色贴图
物体表面图片
normalMap
法线贴图
伪造凹凸细节
roughnessMap
粗糙度贴图
不同区域粗糙度不同
metalnessMap
金属度贴图
不同区域金属感不同
aoMap
环境遮蔽
缝隙更暗
emissive
自发光颜色
霓虹、屏幕
envMapIntensity
环境反射强度
调整 HDR 反射影响
4. 透明材质
js 复制代码 1 const material = new THREE .MeshStandardMaterial ({2 color : 0xffffff , 3 transparent : true , 4 opacity : 0.5 , 5 }); const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5,
});
透明物体常见坑:
透明排序不稳定
多个透明面互相穿插时显示异常
深度写入导致后面的透明物体看不到
可以根据情况尝试:
js 复制代码 1 material.depthWrite = false ; 2 material.side = THREE .DoubleSide ; material.depthWrite = false;
material.side = THREE.DoubleSide;
5. 材质更新
有些材质属性修改后需要:
js 复制代码 1 material.needsUpdate = true ; material.needsUpdate = true;
尤其是影响 shader 编译分支的属性,比如是否透明、是否有 map、side、skinning、morphTargets 等。
八、纹理、色彩空间与环境贴图
纹理系统是 Three.js 中非常容易被低估的一块。很多“模型看起来不真实”的问题,不是模型不行,而是贴图、色彩空间、环境贴图和 tone mapping 没处理好。
1. 纹理加载
js 复制代码 1 const texture = new THREE .TextureLoader ().load ('/textures/base-color.jpg' );2 texture.colorSpace = THREE .SRGBColorSpace ; 3 4 const material = new THREE .MeshStandardMaterial ({5 map : texture, 6 }); const texture = new THREE.TextureLoader().load('/textures/base-color.jpg');
texture.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map: texture,
});
颜色贴图通常需要设置 SRGBColorSpace。非颜色数据贴图,比如 normal、roughness、metalness,一般不要设置成 sRGB。
2. 常见贴图类型
贴图
材质属性
是否颜色数据
作用
Base Color
map
是
表面颜色
Normal
normalMap
否
表面凹凸方向
Roughness
roughnessMap
否
粗糙度
Metalness
metalnessMap
否
金属度
AO
aoMap
否
环境遮蔽
Emissive
emissiveMap
是
自发光
Alpha
alphaMap
否
透明度
Displacement
displacementMap
否
真实移动顶点
3. 贴图重复
js 复制代码 1 texture.wrapS = THREE .RepeatWrapping ; 2 texture.wrapT = THREE .RepeatWrapping ; 3 texture.repeat .set (4 , 4 ); texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
4. 环境贴图 IBL
真实感的关键之一是 IBL,也就是 Image Based Lighting。它用环境图片为 PBR 材质提供反射和间接光照。
js 复制代码 1 import { RGBELoader } from 'three/addons/loaders/RGBELoader.js' ;2 3 const envMap = await new RGBELoader ().loadAsync ('/hdr/studio.hdr' );4 envMap.mapping = THREE .EquirectangularReflectionMapping ; 5 6 scene.environment = envMap; 7 scene.background = envMap; import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const envMap = await new RGBELoader().loadAsync('/hdr/studio.hdr');
envMap.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = envMap;
scene.background = envMap;
环境贴图工作流:
flowchart LR
A[HDR / EXR / PNG 环境图] --> B[Loader 加载]
B --> C[EquirectangularReflectionMapping]
C --> D[scene.environment]
D --> E[PBR 材质反射 / 高光]
C --> F[scene.background 可选]
5. Tone Mapping
HDR 环境和高亮材质通常需要 tone mapping。
js 复制代码 1 renderer.toneMapping = THREE .ACESFilmicToneMapping ; 2 renderer.toneMappingExposure = 1 ; renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
如果你发现画面过曝或太灰,可以调 toneMappingExposure,也可以调整环境贴图强度或灯光强度。
九、灯光与阴影
灯光系统决定受光材质如何显示。没有灯光时,MeshStandardMaterial 可能看起来一片黑。
1. 常见灯光对比
灯光
特点
是否可投影
典型用途
AmbientLight
全局均匀补光
否
避免暗部死黑
HemisphereLight
天空色 + 地面色
否
户外场景基础光
DirectionalLight
平行光
是
太阳光、主光
PointLight
点状发光
是
灯泡、能量球
SpotLight
锥形聚光
是
舞台灯、手电
RectAreaLight
矩形面光
否或有限
产品、室内柔光
2. 基础布光公式
js 复制代码 1 scene.add (new THREE .HemisphereLight (0xb1e1ff , 0x444444 , 1.5 )); 2 3 const keyLight = new THREE .DirectionalLight (0xffffff , 2.5 );4 keyLight.position .set (5 , 8 , 5 ); 5 scene.add (keyLight); scene.add(new THREE.HemisphereLight(0xb1e1ff, 0x444444, 1.5));
const keyLight = new THREE.DirectionalLight(0xffffff, 2.5);
keyLight.position.set(5, 8, 5);
scene.add(keyLight);
3. 阴影配置
js 复制代码 1 renderer.shadowMap .enabled = true ; 2 renderer.shadowMap .type = THREE .PCFSoftShadowMap ; 3 4 keyLight.castShadow = true ; 5 mesh.castShadow = true ; 6 ground.receiveShadow = true ; renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
keyLight.castShadow = true;
mesh.castShadow = true;
ground.receiveShadow = true;
方向光阴影还要注意 shadow camera 的范围:
js 复制代码 1 const shadowCamera = keyLight.shadow .camera ;2 shadowCamera.left = -10 ; 3 shadowCamera.right = 10 ; 4 shadowCamera.top = 10 ; 5 shadowCamera.bottom = -10 ; 6 shadowCamera.near = 0.5 ; 7 shadowCamera.far = 50 ; 8 keyLight.shadow .mapSize .set (1024 , 1024 ); const shadowCamera = keyLight.shadow.camera;
shadowCamera.left = -10;
shadowCamera.right = 10;
shadowCamera.top = 10;
shadowCamera.bottom = -10;
shadowCamera.near = 0.5;
shadowCamera.far = 50;
keyLight.shadow.mapSize.set(1024, 1024);
阴影渲染流程:
flowchart TD
A[开启 renderer.shadowMap] --> B[光源 castShadow]
B --> C[物体 castShadow]
C --> D[接收面 receiveShadow]
D --> E[先从光源视角生成 shadow map]
E --> F[正常渲染时采样 shadow map]
F --> G[得到明暗与投影]
4. 阴影性能建议
不要所有物体都投影
只给主光开启阴影
控制 shadow map 分辨率
缩小 shadow camera 范围
静态场景优先考虑烘焙阴影
十、模型加载:glTF / GLB 是主线
虽然 Three.js 支持 OBJ、FBX、STL 等很多格式,但现代 Web 3D 项目优先使用 glTF / GLB。
1. 为什么是 glTF / GLB
特性
意义
面向实时渲染
更适合 Web 和游戏引擎
支持 PBR
材质还原更稳定
支持动画
骨骼动画、关键帧动画
.glb 单文件
便于部署和缓存
支持 Draco / Meshopt
可以显著减小模型体积
Three.js 官方支持好
GLTFLoader 是常用主力
2. 基础加载
js 复制代码 1 import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' ;2 3 const loader = new GLTFLoader ();4 const gltf = await loader.loadAsync ('/models/product.glb' );5 6 scene.add (gltf.scene ); import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('/models/product.glb');
scene.add(gltf.scene);
3. Draco 压缩模型
js 复制代码 1 import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' ;2 import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js' ;3 4 const dracoLoader = new DRACOLoader ();5 dracoLoader.setDecoderPath ('/draco/' ); 6 7 const gltfLoader = new GLTFLoader ();8 gltfLoader.setDRACOLoader (dracoLoader); 9 10 const gltf = await gltfLoader.loadAsync ('/models/product-draco.glb' );11 scene.add (gltf.scene ); import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
const gltf = await gltfLoader.loadAsync('/models/product-draco.glb');
scene.add(gltf.scene);
4. LoadingManager
复杂项目通常有多个资源:模型、贴图、HDR、音频、配置。可以用 LoadingManager 统一管理加载进度。
js 复制代码 1 const manager = new THREE .LoadingManager ();2 3 manager.onProgress = (url, loaded, total ) => { 4 console .log (`loading ${url} : ${loaded} /${total} ` ); 5 }; 6 7 manager.onLoad = () => { 8 console .log ('all resources loaded' ); 9 }; 10 11 const gltfLoader = new GLTFLoader (manager);12 const textureLoader = new THREE .TextureLoader (manager);const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
console.log(`loading ${url}: ${loaded}/${total}`);
};
manager.onLoad = () => {
console.log('all resources loaded');
};
const gltfLoader = new GLTFLoader(manager);
const textureLoader = new THREE.TextureLoader(manager);
5. 加载后自动居中和适配相机
js 复制代码 1 function frameObject (camera, object, controls ) {2 const box = new THREE .Box3 ().setFromObject (object); 3 const size = box.getSize (new THREE .Vector3 ()).length (); 4 const center = box.getCenter (new THREE .Vector3 ()); 5 6 const halfFov = THREE .MathUtils .degToRad (camera.fov * 0.5 ); 7 const distance = size / (2 * Math .tan (halfFov)); 8 9 camera.position .set (center.x , center.y + size * 0.2 , center.z + distance * 1.4 ); 10 camera.near = size / 100 ; 11 camera.far = size * 100 ; 12 camera.updateProjectionMatrix (); 13 camera.lookAt (center); 14 15 if (controls) { 16 controls.target .copy (center); 17 controls.update (); 18 } 19 } function frameObject(camera, object, controls) {
const box = new THREE.Box3().setFromObject(object);
const size = box.getSize(new THREE.Vector3()).length();
const center = box.getCenter(new THREE.Vector3());
const halfFov = THREE.MathUtils.degToRad(camera.fov * 0.5);
const distance = size / (2 * Math.tan(halfFov));
camera.position.set(center.x, center.y + size * 0.2, center.z + distance * 1.4);
camera.near = size / 100;
camera.far = size * 100;
camera.updateProjectionMatrix();
camera.lookAt(center);
if (controls) {
controls.target.copy(center);
controls.update();
}
}
6. 模型加载链路
flowchart LR
A[Blender / C4D / Maya 建模] --> B[导出 glTF / GLB]
B --> C{是否压缩?}
C -- Draco --> D[DRACOLoader 解码]
C -- Meshopt --> E[MeshoptDecoder 解码]
C -- 不压缩 --> F[GLTFLoader 直接解析]
D --> G[gltf.scene]
E --> G
F --> G
G --> H[遍历 Mesh 设置阴影/材质/名称]
H --> I[加入 Scene]
I --> J[Box3 适配相机]
7. 模型导出建议
模型单位统一,避免导入后巨大或极小
原点尽量合理,便于旋转和定位
删除看不见的面和无用对象
合并静态网格,减少 draw call
贴图尺寸按实际展示距离控制
优先 .glb,部署更省心
动画命名清晰,比如 idle、run、open
十一、动画系统
Three.js 动画分为三层:
flowchart TD
A[动画循环] --> B[手动更新 Object3D]
A --> C[AnimationMixer 播放模型动画]
A --> D[Shader / GPU 动画]
B --> E[position / rotation / scale / material]
C --> F[AnimationClip / KeyframeTrack]
D --> G[uniform time / vertex shader]
1. 动画循环
官方示例中常见写法:
js 复制代码 1 function animate ( ) {2 renderer.render (scene, camera); 3 } 4 5 renderer.setAnimationLoop (animate); function animate() {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
setAnimationLoop 更适合与 WebXR 等场景兼容。
2. Clock 与 delta
js 复制代码 1 const clock = new THREE .Clock ();2 3 function animate ( ) {4 const delta = clock.getDelta (); 5 const elapsed = clock.getElapsedTime (); 6 7 cube.rotation .y = elapsed; 8 mixer?.update (delta); 9 10 renderer.render (scene, camera); 11 } const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
cube.rotation.y = elapsed;
mixer?.update(delta);
renderer.render(scene, camera);
}
值
含义
用途
delta
距离上一帧的时间
物理、mixer、速度积分
elapsed
从开始到现在的总时间
周期动画、sin/cos
3. 模型动画 AnimationMixer
js 复制代码 1 const gltf = await loader.loadAsync ('/models/character.glb' );2 scene.add (gltf.scene ); 3 4 const mixer = new THREE .AnimationMixer (gltf.scene );5 const action = mixer.clipAction (gltf.animations [0 ]);6 action.play (); 7 8 function animate ( ) {9 const delta = clock.getDelta (); 10 mixer.update (delta); 11 renderer.render (scene, camera); 12 } const gltf = await loader.loadAsync('/models/character.glb');
scene.add(gltf.scene);
const mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
}
4. 动画状态切换
js 复制代码 1 const actions = {};2 3 for (const clip of gltf.animations ) {4 actions[clip.name ] = mixer.clipAction (clip); 5 } 6 7 function play (name ) {8 for (const action of Object .values (actions)) { 9 action.fadeOut (0.2 ); 10 } 11 12 actions[name].reset ().fadeIn (0.2 ).play (); 13 } const actions = {};
for (const clip of gltf.animations) {
actions[clip.name] = mixer.clipAction(clip);
}
function play(name) {
for (const action of Object.values(actions)) {
action.fadeOut(0.2);
}
actions[name].reset().fadeIn(0.2).play();
}
十二、交互系统:Controls 与 Raycaster
1. OrbitControls
js 复制代码 1 import { OrbitControls } from 'three/addons/controls/OrbitControls.js' ;2 3 const controls = new OrbitControls (camera, renderer.domElement );4 controls.enableDamping = true ; 5 controls.target .set (0 , 0 , 0 ); 6 controls.update (); import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);
controls.update();
常用配置:
js 复制代码 1 controls.enablePan = true ; 2 controls.enableZoom = true ; 3 controls.minDistance = 2 ; 4 controls.maxDistance = 20 ; 5 controls.maxPolarAngle = Math .PI * 0.5 ; controls.enablePan = true;
controls.enableZoom = true;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.maxPolarAngle = Math.PI * 0.5;
2. Raycaster
Raycaster 用于把屏幕上的二维点击转换成三维空间中的选中结果。
flowchart LR
A[PointerEvent clientX/clientY] --> B[归一化设备坐标 NDC]
B --> C[raycaster.setFromCamera]
C --> D[从相机发出射线]
D --> E[intersectObjects]
E --> F[命中 Mesh]
F --> G[高亮 / 弹窗 / 业务操作]
基础代码:
js 复制代码 1 const raycaster = new THREE .Raycaster ();2 const pointer = new THREE .Vector2 ();3 4 renderer.domElement .addEventListener ('pointerdown' , (event ) => { 5 const rect = renderer.domElement .getBoundingClientRect (); 6 7 pointer.x = ((event.clientX - rect.left ) / rect.width ) * 2 - 1 ; 8 pointer.y = -((event.clientY - rect.top ) / rect.height ) * 2 + 1 ; 9 10 raycaster.setFromCamera (pointer, camera); 11 12 const intersects = raycaster.intersectObjects (scene.children , true ); 13 if (!intersects.length ) return ; 14 15 const object = intersects[0 ].object ; 16 console .log (object.name , object.userData ); 17 }); const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
renderer.domElement.addEventListener('pointerdown', (event) => {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (!intersects.length) return;
const object = intersects[0].object;
console.log(object.name, object.userData);
});
注意:如果 canvas 不是全屏,必须用 canvas 的 getBoundingClientRect() 计算坐标,不能直接用 window.innerWidth。
十三、后处理与 Shader
1. 后处理是什么
后处理是在正常渲染之后,对整张画面再做图像处理。
flowchart LR
A[Scene + Camera] --> B[RenderPass]
B --> C[BloomPass]
C --> D[SSAOPass / OutlinePass / FXAA]
D --> E[OutputPass]
E --> F[Screen]
基础结构:
js 复制代码 1 import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js' ;2 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js' ;3 import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js' ;4 import { OutputPass } from 'three/addons/postprocessing/OutputPass.js' ;5 6 const composer = new EffectComposer (renderer);7 composer.addPass (new RenderPass (scene, camera)); 8 composer.addPass (new UnrealBloomPass (new THREE .Vector2 (width, height), 0.8 , 0.4 , 0.85 )); 9 composer.addPass (new OutputPass ()); 10 11 function animate ( ) {12 controls.update (); 13 composer.render (); 14 } 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(width, height), 0.8, 0.4, 0.85));
composer.addPass(new OutputPass());
function animate() {
controls.update();
composer.render();
}
后处理注意点:
Bloom、SSAO、景深都可能明显增加 GPU 压力
resize 时要调用 composer.setSize(width, height)
移动端要降低分辨率或关闭部分 Pass
后处理链越长,渲染成本越高
2. ShaderMaterial 入门
普通材质不够时,可以写 shader。
js 复制代码 1 const material = new THREE .ShaderMaterial ({2 uniforms : { 3 uTime : { value : 0 }, 4 uColor : { value : new THREE .Color (0x4e8cff ) }, 5 }, 6 vertexShader : ` 7 varying vec2 vUv; 8 9 void main ( ) { 10 vUv = uv; 11 gl_Position = projectionMatrix * modelViewMatrix * vec4 (position, 1.0 ); 12 } 13 `, 14 fragmentShader : ` 15 uniform float uTime; 16 uniform vec3 uColor; 17 varying vec2 vUv; 18 19 void main ( ) { 20 float wave = 0.5 + 0.5 * sin (vUv.x * 20.0 + uTime); 21 gl_FragColor = vec4 (uColor * wave, 1.0 ); 22 } 23 `, 24 }); 25 26 function animate ( ) {27 material.uniforms .uTime .value = clock.getElapsedTime (); 28 renderer.render (scene, camera); 29 } const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x4e8cff) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float wave = 0.5 + 0.5 * sin(vUv.x * 20.0 + uTime);
gl_FragColor = vec4(uColor * wave, 1.0);
}
`,
});
function animate() {
material.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
Shader 的学习主线:
flowchart TD
A[顶点 position / uv / normal] --> B[Vertex Shader]
B --> C[裁剪空间 gl_Position]
B --> D[varying 传值]
D --> E[Fragment Shader]
F[uniform 参数] --> B
F --> E
E --> G[像素颜色 gl_FragColor]
十四、性能优化与内存释放
这部分是 Three.js 从“能跑”到“能上线”的分水岭。
1. 性能指标看什么
指标
含义
目标
FPS
每秒帧数
60 理想,30 可接受
Draw Calls
绘制调用次数
越少越好
Triangles
三角面数量
与设备能力匹配
Textures
纹理数量和尺寸
控制显存
JS Heap
JS 内存
避免持续上涨
GPU Memory
GPU 资源
dispose 后应释放
可以通过:
js 复制代码 1 console .log (renderer.info );console.log(renderer.info);
观察渲染信息。
2. 优化总览图
flowchart TD
A[Three.js 性能优化] --> B[减少 CPU 压力]
A --> C[减少 GPU 压力]
A --> D[减少内存压力]
A --> E[减少网络压力]
B --> B1[少创建临时对象]
B --> B2[减少 scene 遍历规模]
B --> B3[事件与状态节流]
C --> C1[降低 draw call]
C --> C2[控制像素比]
C --> C3[减少后处理]
C --> C4[优化阴影]
D --> D1[dispose geometry]
D --> D2[dispose material]
D --> D3[dispose texture]
D --> D4[释放 render target]
E --> E1[glb 压缩]
E --> E2[贴图压缩]
E --> E3[懒加载]
E --> E4[CDN 缓存]
3. 控制 draw call
如果场景中有 2000 个独立 Mesh,即使每个面数很低,也可能卡。
优化方法:
合并静态网格
复用材质
大量重复对象用 InstancedMesh
减少透明材质
减少状态切换
InstancedMesh 示例:
js 复制代码 1 const geometry = new THREE .BoxGeometry (1 , 1 , 1 );2 const material = new THREE .MeshStandardMaterial ({ color : 0x4e8cff });3 const count = 1000 ;4 5 const mesh = new THREE .InstancedMesh (geometry, material, count);6 const matrix = new THREE .Matrix4 ();7 8 for (let i = 0 ; i < count; i += 1 ) {9 matrix.makeTranslation ( 10 (Math .random () - 0.5 ) * 50 , 11 (Math .random () - 0.5 ) * 10 , 12 (Math .random () - 0.5 ) * 50 13 ); 14 mesh.setMatrixAt (i, matrix); 15 } 16 17 scene.add (mesh); const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x4e8cff });
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i += 1) {
matrix.makeTranslation(
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 50
);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
4. 控制像素比
高分屏像素比很高,渲染分辨率会成倍增长。
js 复制代码 1 renderer.setPixelRatio (Math .min (window .devicePixelRatio , 2 )); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
更激进的移动端策略:
js 复制代码 1 const isMobile = /Android|iPhone|iPad/i .test (navigator.userAgent );2 renderer.setPixelRatio (isMobile ? 1.5 : Math .min (window .devicePixelRatio , 2 )); const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
renderer.setPixelRatio(isMobile ? 1.5 : Math.min(window.devicePixelRatio, 2));
5. 控制贴图体积
场景
建议
小图标、标识
256 / 512
普通模型
1K
主视觉模型
2K
极近距离特写
4K,谨慎使用
移动端
尽量不超过 2K
6. 资源释放
Three.js 的 GPU 资源不会因为 JS 对象不可达就立刻自动释放。切换页面、移除模型、重新加载场景时要手动 dispose。
js 复制代码 1 function disposeMaterial (material ) {2 for (const key in material) { 3 const value = material[key]; 4 if (value && value.isTexture ) { 5 value.dispose (); 6 } 7 } 8 material.dispose (); 9 } 10 11 function disposeObject (object ) {12 object.traverse ((child ) => { 13 if (child.geometry ) { 14 child.geometry .dispose (); 15 } 16 17 if (child.material ) { 18 if (Array .isArray (child.material )) { 19 child.material .forEach (disposeMaterial); 20 } else { 21 disposeMaterial (child.material ); 22 } 23 } 24 }); 25 } function disposeMaterial(material) {
for (const key in material) {
const value = material[key];
if (value && value.isTexture) {
value.dispose();
}
}
material.dispose();
}
function disposeObject(object) {
object.traverse((child) => {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(disposeMaterial);
} else {
disposeMaterial(child.material);
}
}
});
}
页面卸载时:
js 复制代码 1 renderer.setAnimationLoop (null ); 2 window .removeEventListener ('resize' , resize);3 4 disposeObject (scene);5 renderer.dispose (); 6 renderer.domElement .remove (); renderer.setAnimationLoop(null);
window.removeEventListener('resize', resize);
disposeObject(scene);
renderer.dispose();
renderer.domElement.remove();
资源生命周期:
stateDiagram-v2
[*] --> Create
Create --> UploadGPU: 首次渲染或加载
UploadGPU --> InUse: 场景中使用
InUse --> Removed: scene.remove
Removed --> Disposed: geometry/material/texture.dispose
Disposed --> [*]
Removed --> Leak: 忘记 dispose
Leak --> MemoryGrowth: 路由切换后显存上涨
十五、工程化架构
一个可维护的 Three.js 项目,最好从一开始就拆分职责。
1. 推荐模块结构
text 复制代码 1 src/ 2 three/ 3 core/ 4 createRenderer.ts 5 createScene.ts 6 createCamera.ts 7 createControls.ts 8 lights/ 9 createStudioLights.ts 10 createOutdoorLights.ts 11 loaders/ 12 loadGLTF.ts 13 loadEnvironment.ts 14 loadingManager.ts 15 utils/ 16 frameObject.ts 17 disposeObject.ts 18 raycast.ts 19 resizeRenderer.ts 20 systems/ 21 AnimationSystem.ts 22 InteractionSystem.ts 23 LabelSystem.ts 24 PostprocessingSystem.ts 25 components/ 26 ModelViewer.tsx 27 InspectorPanel.tsx 28 assets/ 29 models/ 30 textures/ 31 hdr/ src/
three/
core/
createRenderer.ts
createScene.ts
createCamera.ts
createControls.ts
lights/
createStudioLights.ts
createOutdoorLights.ts
loaders/
loadGLTF.ts
loadEnvironment.ts
loadingManager.ts
utils/
frameObject.ts
disposeObject.ts
raycast.ts
resizeRenderer.ts
systems/
AnimationSystem.ts
InteractionSystem.ts
LabelSystem.ts
PostprocessingSystem.ts
components/
ModelViewer.tsx
InspectorPanel.tsx
assets/
models/
textures/
hdr/
2. 生命周期设计
flowchart TD
A[组件挂载] --> B[创建 renderer / scene / camera]
B --> C[加载环境贴图]
C --> D[加载模型]
D --> E[绑定 controls / raycaster / resize]
E --> F[启动 animation loop]
F --> G{组件是否卸载?}
G -- 否 --> F
G -- 是 --> H[停止 loop]
H --> I[移除事件]
I --> J[dispose 资源]
J --> K[移除 canvas]
3. 不建议的写法
在 React / Vue render 函数里创建 Three.js 对象
每次状态变化都重建 renderer
把 mesh、material、texture 全塞进响应式对象
路由离开时只删除 DOM,不释放 GPU 资源
每个业务对象一个材质,导致材质爆炸
不区分初始化、加载、渲染、销毁逻辑
4. 推荐的 Viewer 类封装
js 复制代码 1 class ModelViewer {2 constructor (canvas ) { 3 this .canvas = canvas; 4 this .scene = new THREE .Scene (); 5 this .camera = new THREE .PerspectiveCamera (60 , 1 , 0.1 , 1000 ); 6 this .renderer = new THREE .WebGLRenderer ({ canvas, antialias : true }); 7 this .clock = new THREE .Clock (); 8 9 this .animate = this .animate .bind (this ); 10 this .resize = this .resize .bind (this ); 11 } 12 13 init ( ) { 14 window .addEventListener ('resize' , this .resize ); 15 this .resize (); 16 this .renderer .setAnimationLoop (this .animate ); 17 } 18 19 animate ( ) { 20 const delta = this .clock .getDelta (); 21 this .mixer ?.update (delta); 22 this .controls ?.update (); 23 this .renderer .render (this .scene , this .camera ); 24 } 25 26 resize ( ) { 27 const { clientWidth, clientHeight } = this .canvas ; 28 this .camera .aspect = clientWidth / clientHeight; 29 this .camera .updateProjectionMatrix (); 30 this .renderer .setSize (clientWidth, clientHeight, false ); 31 this .renderer .setPixelRatio (Math .min (window .devicePixelRatio , 2 )); 32 } 33 34 destroy ( ) { 35 this .renderer .setAnimationLoop (null ); 36 window .removeEventListener ('resize' , this .resize ); 37 disposeObject (this .scene ); 38 this .renderer .dispose (); 39 } 40 } class ModelViewer {
constructor(canvas) {
this.canvas = canvas;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
this.clock = new THREE.Clock();
this.animate = this.animate.bind(this);
this.resize = this.resize.bind(this);
}
init() {
window.addEventListener('resize', this.resize);
this.resize();
this.renderer.setAnimationLoop(this.animate);
}
animate() {
const delta = this.clock.getDelta();
this.mixer?.update(delta);
this.controls?.update();
this.renderer.render(this.scene, this.camera);
}
resize() {
const { clientWidth, clientHeight } = this.canvas;
this.camera.aspect = clientWidth / clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(clientWidth, clientHeight, false);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
destroy() {
this.renderer.setAnimationLoop(null);
window.removeEventListener('resize', this.resize);
disposeObject(this.scene);
this.renderer.dispose();
}
}
十六、常见问题排查
1. 黑屏
flowchart TD
A[黑屏] --> B{控制台有报错?}
B -- 有 --> C[先修 JS / 资源 / CORS 错误]
B -- 无 --> D{renderer 是否 render?}
D -- 否 --> E[检查动画循环]
D -- 是 --> F{相机是否对准?}
F -- 否 --> G[camera.lookAt / frameObject]
F -- 是 --> H{材质是否受光?}
H -- 是 --> I[检查灯光 / environment]
H -- 否 --> J[检查物体尺寸 / near far / visible]
2. 模型看不到
检查:
模型是否加载成功
scene.add(gltf.scene) 是否执行
模型尺寸是否合理
相机 near / far 是否覆盖模型
相机是否看向模型中心
模型材质是否透明或黑色
3. 模型颜色不对
检查:
颜色贴图是否设置 texture.colorSpace = THREE.SRGBColorSpace
HDR / tone mapping 是否过强
灯光强度是否过大或过小
模型导出时贴图是否丢失
材质 metalness / roughness 是否被错误设置
4. 阴影不显示
必须同时满足:
text 复制代码 1 renderer.shadowMap.enabled = true 2 light.castShadow = true 3 mesh.castShadow = true 4 ground.receiveShadow = true 5 物体在光源 shadow camera 范围内 renderer.shadowMap.enabled = true
light.castShadow = true
mesh.castShadow = true
ground.receiveShadow = true
物体在光源 shadow camera 范围内
5. 页面越来越卡
优先检查:
路由切换后旧的 animation loop 是否还在跑
事件监听是否重复绑定
几何体、材质、纹理是否 dispose
是否每帧创建大量 Vector3 / Matrix4
是否重复加载同一模型和贴图
十七、学习路线
建议按下面顺序学习,而不是一开始就冲 Shader:
flowchart TD
A[跑通旋转方块] --> B[理解 Scene / Camera / Renderer / Mesh]
B --> C[几何体 / 材质 / 灯光]
C --> D[纹理 / 色彩空间 / 环境贴图]
D --> E[glTF / GLB 模型加载]
E --> F[OrbitControls / Raycaster]
F --> G[AnimationMixer]
G --> H[阴影 / 后处理]
H --> I[性能优化 / 内存释放]
I --> J[Shader / 粒子 / Instancing / LOD]
J --> K[数字孪生 / 产品展示 / 游戏项目]
每个阶段建议做一个小作品:
阶段
小作品
基础闭环
旋转方块 + 拖拽观察
材质灯光
产品展示台
模型加载
GLB 模型查看器
交互
点击模型部件显示信息
动画
人物动作切换或机械开合
性能
1000 个实例化对象
后处理
Bloom 科技风场景
工程化
可复用 ModelViewer 组件
最后总结
Three.js 的核心不是背 API,而是理解它的系统结构:
text 复制代码 1 Scene 管对象 2 Object3D 管层级和变换 3 Geometry 管形状 4 Material 管外观 5 Texture 管表面细节 6 Light / Environment 管光照 7 Camera 管观察 8 Renderer 管输出 9 Loader 管资源 10 AnimationMixer 管模型动画 11 Raycaster 管交互 12 EffectComposer 管后处理 13 dispose 管生命周期 Scene 管对象
Object3D 管层级和变换
Geometry 管形状
Material 管外观
Texture 管表面细节
Light / Environment 管光照
Camera 管观察
Renderer 管输出
Loader 管资源
AnimationMixer 管模型动画
Raycaster 管交互
EffectComposer 管后处理
dispose 管生命周期
如果你只想做一个 Demo,掌握 Scene、Camera、Renderer、Mesh 就够了。
如果你要做真实业务项目,就必须继续掌握模型资源、PBR、贴图色彩空间、阴影、交互、性能和内存释放。
Three.js 是前端进入 3D 世界最实用的一条路。它不替代图形学基础,但能让我们更快把图形学能力转化成真实可用的 Web 产品。
参考资料