Skip to main content

第 1 节:Three.js 架构全景与源码阅读方法

🎯 学习目标

  • 理解 Three.js 的整体架构与模块划分
  • 掌握源码阅读的方法论与工具链
  • 建立模块间依赖关系的心智模型
  • 追踪一帧渲染的完整调用链
  • 搭建一个空壳 MiniRenderer 作为后续课程的基座

❓ 带着问题读源码

  1. Three.js 的 src/ 目录下有哪些子目录?它们各自负责什么?
  2. 一次 renderer.render(scene, camera) 调用,内部经过了哪些关键步骤?
  3. 模块之间的依赖关系是什么?为什么 Core 要放在最底层?
  4. 阅读 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.jsShader Program 的编译、缓存与去重
webgl/WebGLState.jsWebGL 状态缓存,避免冗余的 GL 调用
webgl/WebGLAttributes.jsBufferAttribute → GPU Buffer 的上传与更新管理
webgl/WebGLUniforms.jsUniform 变量的自动同步与类型分发
webgl/WebGLTextures.js纹理资源的上传、Mipmap 生成与单元分配
shaders/ShaderLib.js内置材质的 Shader 源码库
shaders/ShaderChunk/Shader 片段(Chunk),用于拼接完整的 VS/FS

materials/

材质系统,30+ 种材质类。

文件职责
Material.js材质基类,定义 needsUpdateversionsideblending 等通用属性
MeshStandardMaterial.jsPBR 标准材质,metalness / roughness 工作流
ShaderMaterial.js自定义 Shader 材质,允许传入 VS/FS 字符串

math/

数学库,纯计算无副作用。

文件职责
Vector3.js三维向量,支持加减乘除、叉积、归一化等
Matrix4.js4×4 矩阵,支持乘法、逆矩阵、投影矩阵构建
Frustum.js视锥体,6 个平面组成,用于剔除检测

geometries/

内置几何体(BoxGeometry、SphereGeometry 等),都是 BufferGeometry 的子类。

文件职责
BoxGeometry.js立方体几何,生成 position/normal/uv/index
SphereGeometry.js球体几何,基于极坐标参数化
PlaneGeometry.js平面几何,常用于地面、屏幕四边形

cameras/

相机系统。

文件职责
Camera.js相机基类,持有 projectionMatrixmatrixWorldInverse
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

优点:每个类相对独立,容易理解 缺点:缺乏上下文,不知道"这个类在什么时候被用到"

策略三:组合策略(推荐)

  1. 先用自顶向下读一遍 render() 的主流程,建立全局视图
  2. 再用自底向上精读每个子模块(BufferGeometry、Material、Program...)
  3. 最后回到顶层,把细节串联起来
第一遍: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.jsOGL
核心文件数~400~15
内置材质30+ 种(MeshStandardMaterial 等)0(需自己写 Shader)
Shader 系统ShaderChunk 拼接 + 条件编译直接传入 VS/FS 字符串
光照系统内置完整光照 + 阴影不内置,需手动实现
状态管理精细的状态缓存(WebGLState)基础缓存
学习曲线功能丰富但源码复杂源码简洁但需更多手动工作

OGL 的设计哲学是"只提供薄封装,不做决策",而 Three.js 是"提供完整方案,开箱即用"。读完 Three.js 源码后再看 OGL,你会更加理解这两种设计哲学的取舍。


📝 本节总结

  1. 目录结构:Three.js src/ 按 core/renderers/materials/math/geometries/cameras/lights/objects/textures/scenes 划分,共约 400 个文件
  2. 依赖关系:Math → Core → Geometries/Textures → Materials → Renderers,下层不依赖上层
  3. 一帧调用链:render → projectObject → sort → renderObjects → renderObject → renderBufferDirect → gl.drawArrays/drawElements
  4. 阅读策略:推荐组合策略——先自顶向下建立全局视图,再自底向上精读子模块
  5. Mini 实现:搭建了空壳 MiniRenderer/MiniScene/MiniCamera,后续课程逐步填充
  6. OGL 对比:Three.js 400 文件 vs OGL 15 文件,功能完备 vs 极简封装

➡️ 下一节预告

第 2 节:render() 的完整旅程——一帧内部发生了什么

我们将深入 WebGLRenderer.render() 的每一行代码,理解:

  • projectObject() 如何递归遍历场景图
  • 不透明/透明两个队列的排序算法
  • renderBufferDirect() 如何最终调用 GL 绘制命令
  • 实现一个能真正遍历场景图并调用 gl.drawArrays 的 MiniRenderer