第 1 节:Three.js 架构全景与源码阅读方法
🎯 学习目标
- 理解 Three.js 的整体架构与模块划分
- 掌握源码阅读的方法论与工具链
- 建立模块间依赖关系的心智模型
- 追踪一帧渲染的完整调用链
- 搭建一个空壳 MiniRenderer 作为后续课程的基座
❓ 带着问题读源码
- Three.js 的
src/目录下有哪些子目录?它们各自负责什么? - 一次
renderer.render(scene, camera)调用,内部经过了哪些关键步骤? - 模块之间的依赖关系是什么?为什么
Core要放在最底层? - 阅读 10 万行源码,应该从哪里开始?
📐 版本说明
本课程基于 Three.js r160+ 源码,以 WebGL2 为主要目标 API。源码仓库地址:
https://github.com/mrdoob/three.js
Three.js 的源码采用 ES Module 组织,入口文件为 src/Three.js,构建后输出为 build/three.module.js。r160 之后的版本已将 WebGL2 作为默认渲染后端,WebGL1 降级路径仍保留但不再新增功能。
📁 Three.js 源码目录结构
Three.js 的 src/ 目录包含约 400+ 个源文件,按职责划分为以下子目录:
core/
核心基础设施,所有模块都依赖它。
| 文件 | 职责 |
|---|---|
Object3D.js | 场景图节点基类,管理 parent/children 树、局部/世界矩阵 |
BufferGeometry.js | 几何体数据容器,封装 VBO/EBO 的属性描述 |
BufferAttribute.js | 单个顶点属性的包装器(Float32Array + itemSize + version) |
EventDispatcher.js | 事件系统,被 Object3D、Material、Geometry 等继承 |
Raycaster.js | 射线检测,基于 BoundingSphere/BoundingBox 做快速筛选 |
renderers/
渲染器及其子模块,是代码量最大的目录。
| 文件 | 职责 |
|---|---|
WebGLRenderer.js | 主渲染器,~3000 行,包含 render() 入口和所有子模块的协调逻辑 |
webgl/WebGLPrograms.js | Shader Program 的编译、缓存与去重 |
webgl/WebGLState.js | WebGL 状态缓存,避免冗余的 GL 调用 |
webgl/WebGLAttributes.js | BufferAttribute → GPU Buffer 的上传与更新管理 |
webgl/WebGLUniforms.js | Uniform 变量的自动同步与类型分发 |
webgl/WebGLTextures.js | 纹理资源的上传、Mipmap 生成与单元分配 |
shaders/ShaderLib.js | 内置材质的 Shader 源码库 |
shaders/ShaderChunk/ | Shader 片段(Chunk),用于拼接完整的 VS/FS |
materials/
材质系统,30+ 种材质类。
| 文件 | 职责 |
|---|---|
Material.js | 材质基类,定义 needsUpdate、version、side、blending 等通用属性 |
MeshStandardMaterial.js | PBR 标准材质,metalness / roughness 工作流 |
ShaderMaterial.js | 自定义 Shader 材质,允许传入 VS/FS 字符串 |
math/
数学库,纯计算无副作用。
| 文件 | 职责 |
|---|---|
Vector3.js | 三维向量,支持加减乘除、叉积、归一化等 |
Matrix4.js | 4×4 矩阵,支持乘法、逆矩阵、投影矩阵构建 |
Frustum.js | 视锥体,6 个平面组成,用于剔除检测 |
geometries/
内置几何体(BoxGeometry、SphereGeometry 等),都是 BufferGeometry 的子类。
| 文件 | 职责 |
|---|---|
BoxGeometry.js | 立方体几何,生成 position/normal/uv/index |
SphereGeometry.js | 球体几何,基于极坐标参数化 |
PlaneGeometry.js | 平面几何,常用于地面、屏幕四边形 |
cameras/
相机系统。
| 文件 | 职责 |
|---|---|
Camera.js | 相机基类,持有 projectionMatrix 和 matrixWorldInverse |
PerspectiveCamera.js | 透视投影相机,updateProjectionMatrix() 构建透视矩阵 |
OrthographicCamera.js | 正交投影相机 |
lights/
光源系统。
| 文件 | 职责 |
|---|---|
Light.js | 光源基类,继承自 Object3D |
DirectionalLight.js | 平行光,带 shadow 属性 |
PointLight.js | 点光源,带衰减参数 |
objects/
可渲染对象。
| 文件 | 职责 |
|---|---|
Mesh.js | 网格对象,关联 Geometry + Material |
InstancedMesh.js | 实例化网格,一次 Draw Call 绘制多个实例 |
Line.js | 线段对象 |
textures/
纹理类型定义。
| 文件 | 职责 |
|---|---|
Texture.js | 纹理基类,封装 wrap/filter/format 等参数 |
CubeTexture.js | 立方体纹理,用于环境贴图 |
DataTexture.js | 数据纹理,从 TypedArray 直接创建 |
scenes/
场景相关。
| 文件 | 职责 |
|---|---|
Scene.js | 场景根节点,支持 background、environment、fog |
Fog.js | 线性雾效 |
FogExp2.js | 指数雾效 |
🔗 模块依赖图
Three.js 的模块依赖遵循"下层不依赖上层"的原则:
┌─────────────────────────────────────────────────┐
│ Application │
│ renderer.render(scene, camera) │
└───────────┬─────────────────────────┬───────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ Renderers │ │ Objects / Scene │
│ WebGLRenderer │ │ Mesh, Line, Points │
│ WebGLPrograms │ │ Scene, Group │
│ WebGLState │ └───────────┬───────────┘
│ WebGLAttributes │ │
│ WebGLTextures │ ▼
│ WebGLUniforms │ ┌───────────────────────┐
└───────┬───────────┘ │ Cameras / Lights │
│ │ PerspectiveCamera │
│ │ DirectionalLight │
▼ └───────────┬───────────┘
┌───────────────────┐ │
│ Materials │ │
│ MeshStandardMat │ │
│ ShaderMaterial │ │
└───────┬───────────┘ │
│ ┌───────────┘
▼ ▼
┌───────────────────────────────────┐
│ Geometries / Textures │
│ BufferGeometry, BoxGeometry │
│ Texture, CubeTexture │
└───────────────┬───────────────────┘
│
▼
┌───────────────────────────────────┐
│ Core │
│ Object3D, BufferAttribute │
│ EventDispatcher, Raycaster │
└───────────────┬───────────────────┘
│
▼
┌───────────────────────────────────┐
│ Math │
│ Vector3, Matrix4, Quaternion │
│ Frustum, Box3, Sphere │
└───────────────────────────────────┘
关键依赖路径:
- Math 是最底层,不依赖任何其他模块
- Core 依赖 Math(Object3D 需要 Vector3、Quaternion、Matrix4)
- Geometries/Textures 依赖 Core(BufferGeometry 继承 EventDispatcher)
- Materials 依赖 Core + Math(材质参数用到 Color、Vector2 等)
- Renderers 依赖所有上层模块,是最"重"的模块
- Objects 依赖 Core + Geometries + Materials(Mesh = Geometry + Material)
🔄 一帧的调用链
从 renderer.render(scene, camera) 出发,一帧渲染的核心调用链如下:
renderer.render(scene, camera)
│
├── 1. 前置准备
│ ├── _gl.viewport(0, 0, width, height) // 设置视口
│ ├── clear(color, depth, stencil) // 清空缓冲
│ ├── scene.updateMatrixWorld() // 递归更新场景图矩阵
│ └── camera.updateMatrixWorld() // 更新相机矩阵
│ └── camera.matrixWorldInverse.copy(camera.matrixWorld).invert()
│
├── 2. 场景遍历
│ └── projectObject(scene, camera, 0, sortObjects)
│ ├── 递归遍历 scene.children
│ ├── 对每个 Mesh/Line/Points:
│ │ ├── 计算 modelViewMatrix
│ │ ├── Frustum Culling 检测
│ │ └── 加入 currentRenderList (opaque / transparent)
│ └── 对 Light: 加入 currentRenderState.lights
│
├── 3. 排序
│ ├── opaqueSort: renderOrder → material.id → z 从近到远
│ └── transparentSort: renderOrder → z 从远到近
│
├── 4. 渲染不透明队列
│ └── renderObjects(opaqueList, scene, camera)
│ └── for each renderItem:
│ └── renderObject(object, scene, camera, geometry, material, group)
│ ├── 获取/编译 Program (WebGLPrograms)
│ ├── 设置 Uniforms (WebGLUniforms)
│ ├── 绑定属性 (WebGLAttributes)
│ └── renderBufferDirect(camera, scene, geometry, material, object, group)
│ ├── setupVertexAttributes()
│ └── gl.drawArrays() 或 gl.drawElements()
│
├── 5. 渲染透明队列
│ └── renderObjects(transparentList, scene, camera)
│ └── ... (同上)
│
└── 6. 收尾
├── 状态复位
└── info 统计更新 (drawCalls, triangles, points, lines)
这条调用链揭示了 Three.js 渲染器的核心职责:遍历 → 排序 → 渲染。后续每一节课都会深入其中某个环节。
📖 源码阅读策略
策略一:自顶向下(推荐入门)
从 WebGLRenderer.render() 开始,顺着调用链往下追:
render() → projectObject() → renderObjects() → renderObject() → renderBufferDirect()
优点:符合心智模型,知道"为什么"要执行每一步 缺点:容易被中间的分支逻辑淹没(render() 有 3000 行)
策略二:自底向上
从基础数据结构开始:
BufferAttribute → BufferGeometry → Material → WebGLProgram → WebGLRenderer
优点:每个类相对独立,容易理解 缺点:缺乏上下文,不知道"这个类在什么时候被用到"
策略三:组合策略(推荐)
- 先用自顶向下读一遍
render()的主流程,建立全局视图 - 再用自底向上精读每个子模块(BufferGeometry、Material、Program...)
- 最后回到顶层,把细节串联起来
第一遍:render() → 知道有 projectObject、排序、renderBufferDirect 等步骤
第二遍:深入 BufferGeometry → 理解数据如何组织
第三遍:深入 WebGLPrograms → 理解 Shader 如何编译缓存
第四遍:回到 render() → 所有细节串联成完整画面
调试技巧
// 在浏览器中断点调试 Three.js 调用链
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 在 DevTools Sources 面板搜索 "WebGLRenderer.js"
// 找到 this.render = function(scene, camera) { ... }
// 打断点,然后 Step Into 每一个子函数
renderer.render(scene, camera);
🛠️ Mini 实现
搭建一个空壳 MiniRenderer,作为后续课程逐步填充的基座:
class MiniScene {
constructor() {
this.children = [];
}
add(child) {
this.children.push(child);
child.parent = this;
}
}
class MiniCamera {
constructor(fov, aspect, near, far) {
this.fov = fov;
this.aspect = aspect;
this.near = near;
this.far = far;
this.projectionMatrix = new Float32Array(16);
this.viewMatrix = new Float32Array(16);
}
}
class MiniRenderer {
constructor(canvas) {
this.canvas = canvas;
this.gl = canvas.getContext('webgl2');
if (!this.gl) {
throw new Error('WebGL2 not supported');
}
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
this.gl.enable(this.gl.DEPTH_TEST);
}
setSize(width, height) {
this.canvas.width = width;
this.canvas.height = height;
this.gl.viewport(0, 0, width, height);
}
render(scene, camera) {
const gl = this.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// TODO: 第 2 节将实现场景遍历与排序
// TODO: 第 3 节将实现 Geometry 属性绑定
// TODO: 第 4 节将实现 Program 编译与 Uniform 设置
// TODO: 最终调用 gl.drawArrays / gl.drawElements
console.log(`MiniRenderer: rendering ${scene.children.length} objects`);
}
}
// 使用示例
const canvas = document.createElement('canvas');
const renderer = new MiniRenderer(canvas);
const scene = new MiniScene();
const camera = new MiniCamera(75, 1, 0.1, 100);
renderer.setSize(800, 600);
renderer.render(scene, camera);
这个空壳只有约 40 行核心代码,但它已经体现了 Three.js 的基本结构:
- Renderer 持有
gl上下文,提供render(scene, camera)入口 - Scene 是一个树形容器
- Camera 持有投影参数
后续课程将逐步为 render() 填入真实的渲染逻辑。
🔍 OGL 对比
OGL 是一个仅约 3000 行的 WebGL 框架,与 Three.js 的 10 万行形成鲜明对比。
目录结构对比
Three.js src/ OGL src/
├── core/ (15 files) ├── core/
├── renderers/ (30+ files) │ ├── Geometry.js
├── materials/ (30+ files) │ ├── Program.js
├── math/ (20+ files) │ ├── Renderer.js
├── geometries/ (15 files) │ ├── Camera.js
├── cameras/ (5 files) │ ├── Transform.js
├── lights/ (10 files) │ ├── Mesh.js
├── objects/ (10 files) │ └── Texture.js
├── textures/ (5 files) └── math/
├── scenes/ (3 files) ├── Vec3.js
└── ... ├── Mat4.js
└── ...
~400 files ~15 files
核心差异
| 维度 | Three.js | OGL |
|---|---|---|
| 核心文件数 | ~400 | ~15 |
| 内置材质 | 30+ 种(MeshStandardMaterial 等) | 0(需自己写 Shader) |
| Shader 系统 | ShaderChunk 拼接 + 条件编译 | 直接传入 VS/FS 字符串 |
| 光照系统 | 内置完整光照 + 阴影 | 不内置,需手动实现 |
| 状态管理 | 精细的状态缓存(WebGLState) | 基础缓存 |
| 学习曲线 | 功能丰富但源码复杂 | 源码简洁但需更多手动工作 |
OGL 的设计哲学是"只提供薄封装,不做决策",而 Three.js 是"提供完整方案,开箱即用"。读完 Three.js 源码后再看 OGL,你会更加理解这两种设计哲学的取舍。
📝 本节总结
- 目录结构:Three.js src/ 按 core/renderers/materials/math/geometries/cameras/lights/objects/textures/scenes 划分,共约 400 个文件
- 依赖关系:Math → Core → Geometries/Textures → Materials → Renderers,下层不依赖上层
- 一帧调用链:render → projectObject → sort → renderObjects → renderObject → renderBufferDirect → gl.drawArrays/drawElements
- 阅读策略:推荐组合策略——先自顶向下建立全局视图,再自底向上精读子模块
- Mini 实现:搭建了空壳 MiniRenderer/MiniScene/MiniCamera,后续课程逐步填充
- OGL 对比:Three.js 400 文件 vs OGL 15 文件,功能完备 vs 极简封装
➡️ 下一节预告
第 2 节:render() 的完整旅程——一帧内部发生了什么
我们将深入 WebGLRenderer.render() 的每一行代码,理解:
projectObject()如何递归遍历场景图- 不透明/透明两个队列的排序算法
renderBufferDirect()如何最终调用 GL 绘制命令- 实现一个能真正遍历场景图并调用
gl.drawArrays的 MiniRenderer