threejs源码解析:从框架封装中学习设计
🎯 教程特色
本教程采用源码驱动学习法,每一节都包含:
- 源码精读:定位 Three.js 关键模块的核心代码,逐层剖析设计意图
- 原生 WebGL 对照:回顾底层 API,理解 Three.js 为什么要这样封装
- 设计决策分析:讨论 Three.js 在通用性、性能、易用性之间的取舍
- Mini 实现:每节末尾用 50-100 行代码实现一个"极简版"同等模块,加深理解
- OGL 对比:预览 OGL 对同一问题的极简解法,为后续课程铺路
📚 为什么需要读 Three.js 源码?
| 学完 WebGL 教程后你知道的 | 读完 Three.js 源码后你还会知道的 |
|---|---|
gl.bindBuffer / gl.bindTexture | Three.js 如何用状态缓存避免冗余绑定 |
| 手动编译 Shader | ShaderChunk 拼接 + Program 缓存 + 宏定义条件编译 |
| 手动传 Uniform | WebGLUniforms 如何自动同步整个材质树 |
| 手动排序绘制 | 不透明/透明两队列 + painterSort + reversePainterSort |
| 手动管理 FBO | WebGLRenderTarget 如何复用 FBO 和自动管理 viewport |
| 知道性能瓶颈在哪 | Three.js 的 Frustum Culling、Instancing、LOD 如何实现 |
一句话:WebGL 教程让你"看见了骨骼",本课程让你"看见了肌肉是如何包裹骨骼的"。
📐 课程定位
数学基础 → Three.js 进阶 → GLSL → WebGL 底层
↓
★ Three.js 源码解析 ★ ← 你在这里
↓
OGL 源码解析 → TinyDrive 引擎
前置要求:
- 已完成 WebGL 教程(熟悉原生 API)
- 已完成 GLSL 教程(熟悉 ShaderChunk 和 onBeforeCompile)
学习产出:
- 理解工业级 WebGL 框架的封装模式
- 能独立阅读 Three.js 任意模块的源码
- 为构建自研引擎积累设计经验
🗺️ 课程大纲
本教程共 12 节,从架构全景到设计启示,完整拆解 Three.js 的封装艺术。
第一阶段:架构全景(第 1-2 节)
第 1 节:Three.js 架构全景与源码阅读方法
- 模块依赖图:Core / Math / Renderers / Materials / Geometries / Lights / Cameras 的关系
- 源码目录结构:
src/下各子目录的职责划分 - 阅读路径:从
renderer.render(scene, camera)出发,跟踪一帧的完整调用链 - 版本说明:基于 Three.js r160+ 源码,兼顾 WebGL1/2 差异
- Mini 实现:搭建一个"空壳 Renderer",定义
render(scene, camera)入口
第 2 节:render() 的完整旅程——一帧内部发生了什么
- 入口分析:
WebGLRenderer.render()的完整调用链 - 场景准备:
projectObject()递归遍历场景图 → 收集可渲染对象 - 排序策略:不透明队列(前→后)vs 透明队列(后→前)的排序实现
- 渲染循环:
renderObjects()→renderObject()→renderBufferDirect() - 清理收尾:状态复位、统计信息更新
- Mini 实现:实现一个能遍历场景图并按顺序调用
gl.drawArrays的极简渲染循环
第二阶段:核心封装三件套(第 3-5 节)
第 3 节:Geometry 系统——BufferGeometry 如何驯服 VBO
- BufferAttribute:如何把
Float32Array包装成带itemSize/normalized/needsUpdate的属性描述 - BufferGeometry:attributes 字典 + index + groups 的数据结构设计
- 上传机制:
WebGLAttributes.update()如何决定gl.bufferDatavsgl.bufferSubData - Interleaved 优化:
InterleavedBuffer/InterleavedBufferAttribute的 stride 共享 - Dispose 机制:
geometry.dispose()如何触发 GPU 资源释放 - Mini 实现:实现一个带
setAttribute/setIndex/update的极简 Geometry 类 - OGL 对比:OGL 的
Geometry类如何用更少的代码达到同样目的
第 4 节:Material 与 Program 系统——Shader 的编译、缓存与管理
- Material 基类:
needsUpdate、version、onBeforeCompile的协作机制 - ShaderChunk 拼接:
ShaderLib如何把几十个 Chunk 拼成完整的 VS/FS - 宏定义系统:
#define条件编译如何实现"一套 Shader 适配多种配置" - Program 缓存:
WebGLPrograms.getParameters()如何生成唯一 key,避免重复编译 - Uniform 同步:
WebGLUniforms如何用setValueXxx系列方法自动传递数据 - Mini 实现:实现一个支持 Shader 编译 + Uniform 管理 + 简易缓存的 Program 类
- OGL 对比:OGL 的
Program类——无 Chunk 拼接,直接传入 VS/FS 字符串
第 5 节:场景图与矩阵系统——Object3D 的树形世界
- Object3D 设计:
position/rotation/quaternion/scale的"双向同步"机制 - 矩阵传播:
updateMatrix()→updateMatrixWorld()→updateWorldMatrix()的递归链 - 脏标记优化:
matrixWorldNeedsUpdate如何避免每帧都重算所有矩阵 - 遍历方法:
traverse()/traverseVisible()/traverseAncestors()的实现 - Layers 系统:位掩码实现的可见性分层(用于多相机 / 选择性渲染)
- Mini 实现:实现一个带 parent/children + 矩阵自动传播的极简场景图
- OGL 对比:OGL 的
Transform类——相同的树结构,更精简的矩阵更新
第三阶段:GPU 资源管理(第 6-7 节)
第 6 节:纹理系统——从 Image 到 GPU 纹理对象
- Texture 类设计:
wrapS/T、magFilter/minFilter、format、type的参数映射 - WebGLTextures 模块:
uploadTexture()如何处理 Image / Canvas / Video / TypedArray 等不同来源 - Mipmap 策略:自动生成 vs 手动指定、
needsPowerOfTwo的兼容处理 - 压缩纹理:
CompressedTexture如何对接 ASTC / S3TC / ETC2 等 GPU 格式 - 纹理单元管理:
WebGLTextures.allocateTextureUnit()的分配与复用 - 内存估算:Three.js 内部如何统计纹理内存使用量
- Mini 实现:实现一个支持 2D 纹理上传 + 参数设置 + 纹理单元分配的极简类
- OGL 对比:OGL 的
Texture类——直接暴露gl.texImage2D,零抽象开销
第 7 节:WebGL 状态管理——减少冗余 GL 调用的艺术
- WebGLState 模块:Three.js 如何缓存
blendFunc/depthFunc/cullFace等状态 - 绑定缓存:
bindTexture()/bindBuffer()如何跳过"已经绑定"的重复调用 - 状态栈:
colorBuffer/depthBuffer/stencilBuffer的setMask/setTest/setFunc设计 - Reset 机制:
reset()在上下文丢失 / 外部库干扰时如何恢复 - 性能影响:实测对比——有/无状态缓存时的 Draw Call 开销差异
- Mini 实现:实现一个
GLState类,缓存 5 种常见 WebGL 状态 - OGL 对比:OGL 的状态管理——在
Renderer内部用极简对象缓存
第四阶段:高级渲染系统(第 8-10 节)
第 8 节:Camera 与投影系统
- Camera 基类:
matrixWorldInverse(View 矩阵)和projectionMatrix的更新时机 - PerspectiveCamera:
updateProjectionMatrix()内部的透视矩阵推导 - OrthographicCamera:正交矩阵的参数映射
- Frustum Culling:
Frustum.intersectsObject()如何用 6 个平面做快速剔除 - Viewport 与 Scissor:
setViewport()/setScissor()对多视口渲染的支持 - Mini 实现:实现一个带 View/Projection 矩阵 + 基础 Frustum Culling 的 Camera 类
- OGL 对比:OGL 的
Camera类——继承自Transform,矩阵更新与场景图融为一体
第 9 节:光照与阴影系统
- Light 体系:
AmbientLight/DirectionalLight/PointLight/SpotLight的 Uniform 结构 - 光照数据打包:
WebGLLights.setup()如何把场景中所有光源打包成 Uniform 数组 - 阴影渲染管线:
WebGLShadowMap.render()的完整流程——从光源视角渲染深度图 - Shadow Map 类型:Basic / PCF / PCF Soft / VSM 的实现差异
- 级联阴影(CSM):
CSMShadowMap的分割策略与混合 - Mini 实现:实现一个支持单个平行光 + 基础阴影贴图的光照系统
- OGL 对比:OGL 不内置光照——理解"引擎不做光照"的设计取舍
第 10 节:RenderTarget 与后处理管线
- WebGLRenderTarget:如何封装
gl.createFramebuffer+ 颜色/深度附件 - MRT(Multiple Render Targets):
WebGLMultipleRenderTargets的绑定策略 - EffectComposer 内部:
readBuffer/writeBuffer的 ping-pong 交换机制 - Pass 系统:
RenderPass→ShaderPass→OutputPass的链式执行 - 深度纹理:
depthTexture的创建与在后处理中的使用 - Mini 实现:实现一个支持离屏渲染 + 单个后处理 Pass 的极简管线
- OGL 对比:OGL 的
RenderTarget——仅 30 行代码的 FBO 封装
第五阶段:性能与设计总结(第 11-12 节)
第 11 节:性能优化机制深度解析
- Instanced Rendering:
InstancedMesh/InstancedBufferGeometry的源码实现 - Geometry Merging:
BufferGeometryUtils.mergeGeometries()的合并策略 - LOD 系统:
LOD.update()如何根据距离切换几何体 - Frustum Culling 优化:
BoundingSpherevsBoundingBox的选择与自动计算 - 资源释放:
dispose()事件系统如何通知 WebGL 层释放 GPU 资源 - 内存追踪:
WebGLInfo如何统计 Draw Call、三角面数、纹理内存 - Mini 实现:为前面的极简引擎添加 Instancing 支持
- OGL 对比:OGL 中的 Instancing——直接在 Geometry 上设置
instanced属性
第 12 节:设计启示——从 Three.js 到自研引擎
- Three.js 的设计哲学:通用性优先、向后兼容、功能完备——以及由此带来的体积与性能代价
- 值得借鉴的模式:
- 状态缓存模式(WebGLState)
- Program 缓存与去重(WebGLPrograms)
- 事件驱动的资源释放(dispose 模式)
- Uniform 自动同步(WebGLUniforms)
- 需要精简的部分:
- 过度抽象的 Material 层级(30+ 种材质类)
- 全局单例依赖(如
_canvas、_gl) - 遗留兼容代码(WebGL1 降级路径)
- 自研引擎设计清单:从 Three.js 源码中提炼的 10 条架构原则
- OGL 全面对比:逐模块对比 Three.js(大而全)vs OGL(小而美)的代码量与设计取舍
- 桥接下一步:如何把 Three.js 的工程经验带入 OGL 源码解析课程
📊 源码版本与阅读工具
推荐版本
- Three.js r160+(本教程基于此版本,最新 API 风格)
- 源码仓库:https://github.com/mrdoob/three.js
阅读工具推荐
- VS Code + Go to Definition:直接跳转源码定义
- Chrome DevTools Source Panel:断点调试运行时调用栈
- GitHub Code Search:快速全局搜索
WebGLRenderer等关键词
核心源码文件索引
| 模块 | 源码路径 | 对应章节 |
|---|---|---|
| 渲染器入口 | src/renderers/WebGLRenderer.js | 第 1-2 节 |
| Geometry 系统 | src/core/BufferGeometry.js | 第 3 节 |
| 属性上传 | src/renderers/webgl/WebGLAttributes.js | 第 3 节 |
| Material 基类 | src/materials/Material.js | 第 4 节 |
| Shader 拼接 | src/renderers/shaders/ShaderLib.js | 第 4 节 |
| Program 缓存 | src/renderers/webgl/WebGLPrograms.js | 第 4 节 |
| Uniform 管理 | src/renderers/webgl/WebGLUniforms.js | 第 4 节 |
| 场景图 | src/core/Object3D.js | 第 5 节 |
| 纹理管理 | src/renderers/webgl/WebGLTextures.js | 第 6 节 |
| 状态管理 | src/renderers/webgl/WebGLState.js | 第 7 节 |
| 相机 | src/cameras/PerspectiveCamera.js | 第 8 节 |
| 视锥体剔除 | src/math/Frustum.js | 第 8 节 |
| 光照 | src/renderers/webgl/WebGLLights.js | 第 9 节 |
| 阴影 | src/renderers/webgl/WebGLShadowMap.js | 第 9 节 |
| 渲染目标 | src/renderers/WebGLRenderTarget.js | 第 10 节 |
| 后处理 | examples/jsm/postprocessing/EffectComposer.js | 第 10 节 |
| 实例化渲染 | src/objects/InstancedMesh.js | 第 11 节 |
📝 学习方式建议
每节学习流程
- 先读导论:理解本节要解决的问题和 Three.js 的设计思路
- 跟读源码:对照教程,在 IDE 中打开对应源文件,逐函数阅读
- 断点调试:写一个最小 Three.js demo,在 Chrome DevTools 中断点跟踪调用链
- 完成 Mini 实现:用 50-100 行代码实现本节的核心封装
- 对比 OGL:翻阅 OGL 对应模块的源码,体会"极简设计"与"功能完备"的差异
调试技巧
// 在 Chrome DevTools 中断点调试 Three.js 源码
// 方法 1:在 renderer.render() 入口打断点
debugger; // 加在你的代码里
renderer.render(scene, camera); // 然后 Step Into
// 方法 2:直接在 Three.js 源码中搜索关键函数
// Sources → webpack:// → three → src/renderers/WebGLRenderer.js
// 找到 render() 函数,打断点
🎓 学习建议
- 不要试图读完所有源码:Three.js 有 10 万行代码,跟着教程读核心模块即可
- 带着问题读:每节开头都有"这一节要回答的问题",带着问题读效率更高
- 动手实现 Mini 版:只看不写等于没学,Mini 实现是最好的理解验证
- 善用对比:原生 WebGL / Three.js / OGL 三方对照,理解不同抽象层级的设计取舍
- 记录设计笔记:为后续构建自研引擎积累设计决策参考
🚀 开始学习
从第 1 节开始:Three.js 架构全景与源码阅读方法
学习路径:
- 按顺序学习第 1-11 节,每节完成 Mini 实现
- 第 12 节整合所有知识,提炼设计启示
预计学习时间:
- 每节:3-4 小时(包括源码阅读 + Mini 实现)
- 完整课程:36-48 小时
- 适合:已完成 WebGL 教程和 GLSL 教程的开发者
前置课程:
- WebGL 教程(必修)
- GLSL 教程(必修)
- Three.js 进阶开发(推荐)