Skip to main content

第 12 节:设计启示——从 Three.js 到自研引擎

经过前 11 节对 Three.js 源码的精读,我们已经看到了一个工业级 WebGL 框架的"全貌"。最后一节,我们要把这些代码层面的细节升华为设计哲学,回答一个核心问题:

"如果让你从零开始写一个 WebGL 引擎,你会从 Three.js 学到什么?"


🎯 Three.js 的设计哲学

Three.js 不是一个"完美"的引擎,但它是最成功的 WebGL 框架。它的成功可以归结为以下几条设计哲学:

1. 通用性优先(General-Purpose First)

Three.js 不假设你的使用场景——3D 编辑器、可视化大屏、游戏、AR/VR、建筑漫游……它试图覆盖所有

代价

  • 30+ 种 Material 类,每种都有自己的 Shader 拼接规则
  • 兼容 WebGL 1.0(已淘汰)和 WebGL 2.0
  • 大量向后兼容的 API 难以重构

收益

  • 用户基数大、社区活跃
  • 任何项目都能找到"开箱即用"的方案
  • 长期稳定(2010 年至今仍在更新)

2. 渐进式复杂度(Progressive Complexity)

Three.js 的 API 设计遵循"入门简单、深入可控"原则:

// 入门:3 行画一个立方体
new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({ color: 0xff0000 }));

// 进阶:自定义 Shader
material.onBeforeCompile = (shader) => { /* ... */ };

// 极客:完全接管渲染
renderer.shadowMap.enabled = false;
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;

3. 状态机思想贯穿始终

正如我们在前几节反复强调的:Three.js 是一个 WebGL 状态的"代理层"。所有的"封装"本质上都是"状态缓存 + 状态切换的合理化"。


🏆 值得借鉴的 4 大模式

1. 状态缓存模式(State Cache Pattern)

源码WebGLState.jsWebGLAttributes.jsWebGLTextures.js

核心思想:GPU 调用是昂贵的。在 JS 端维护"当前状态"副本,跳过冗余的 GL 调用。

自研引擎应用

  • 任何需要"反复设置状态"的场景
  • 不仅是 WebGL,Vulkan、Metal 等现代 API 同样适用
  • 注意:缓存与实际状态必须严格同步(上下文丢失时要 reset)

2. Program 缓存与去重(Program Cache Pattern)

源码WebGLPrograms.js

核心思想:Shader 编译是昂贵的。用 cacheKey 把"等价"的 Shader 去重,避免重复编译。

自研引擎应用

  • 配合 Material 抽象层使用
  • cacheKey 设计要稳定(参数顺序、字段命名)
  • LRU 策略防止内存无限增长

3. 事件驱动的资源释放(Dispose Pattern)

源码BufferGeometry.dispose()Material.dispose()Texture.dispose()

核心思想:GPU 资源不会随 JS 对象 GC 自动释放,需要手动管理。Three.js 用事件总线让上层"声明式释放",底层"实际清理"。

自研引擎应用

  • 对所有"占用 GPU 资源"的对象实现 dispose() 方法
  • dispatchEvent 通知 GPU 层
  • 避免循环引用:dispose 监听器要可移除

4. Uniform 自动同步(Uniform Sync Pattern)

源码WebGLUniforms.js

核心思想:开发者只需要更新 JS 数据(material.color.setHex(0xff0000)),引擎自动决定何时如何上传到 GPU。

自研引擎应用

  • 在 Material 层维护"脏标记"(version 字段)
  • 在 Renderer 层用统一的 uniforms.upload(gl) 接口
  • 缓存已上传的值,跳过未变化的上传

⚠️ 需要精简的部分

Three.js 的"大而全"也带来了一些可改进的设计:

1. 过度抽象的 Material 层级

Material (基类)
├── MeshBasicMaterial (无光照)
├── MeshLambertMaterial (漫反射)
├── MeshPhongMaterial (高光)
├── MeshStandardMaterial (PBR)
├── MeshPhysicalMaterial (扩展 PBR)
├── MeshToonMaterial (卡通)
├── LineBasicMaterial
├── LineDashedMaterial
├── PointsMaterial
├── SpriteMaterial
├── ShadowMaterial
└── ... (30+ 种)

问题:每种都对应一个 ShaderLib、一组 ShaderChunk、一套参数映射。扩展新材质要修改 10+ 个文件

精简思路

  • 配置对象代替继承层级
  • 暴露 ShaderChunk 拼接规则,允许用户自定义
  • ShaderMaterial 直接接管,绕过 Material 抽象

2. 全局单例依赖

Three.js 的 WebGLRenderer 内部维护了大量全局状态:

  • _currentRenderTarget
  • _currentActiveCubeFace
  • _currentActiveMipmapLevel
  • _gl

问题:导致单例污染——多个 Renderer 实例难以共存。

精简思路

  • 状态用参数传递而不是全局变量
  • 或者渲染上下文(Context)对象显式管理

3. 遗留兼容代码

// 兼容旧版 API
if (this.useLegacyLights === true) {
// 老的光照计算路径
}

问题:约 20% 的代码是"为了兼容旧版本而存在"。

精简思路

  • 新引擎从一开始就只支持 WebGL 2.0+
  • 设定明确的 deprecation 策略
  • 不为"未来兼容"过度设计

📐 自研引擎设计清单

基于 Three.js 的经验,自研 WebGL 引擎应当遵循以下 10 条原则:

#原则核心思想
1状态机先于面向对象用显式状态转换,不要把状态隐藏在对象中
2资源管理独立VBO / Texture / Program 都是"长生命周期"对象,独立管理
3脏标记优于轮询version / needsUpdate 显式标记,避免每帧重算
4场景图与渲染分离场景图只描述"什么对象在哪里",不关心"怎么画"
5材质即 Shader 描述不要让 Material 类过度膨胀,配置对象 + Shader 字符串即可
6统一 Uniform 接口所有 Shader 共用 uniforms.upload(gl),自动比对缓存
7缓存去重Program、Geometry、Texture 全部缓存,cacheKey 必须稳定
8事件驱动释放每个 GPU 资源有 dispose(),通过事件通知清理
9API 渐进式暴露入门简单(高阶 API),深入可控(暴露 ShaderChunk、状态)
10可观测性提供 renderer.info 统计信息:Draw Call、Triangle、Texture Memory

🆚 Three.js vs OGL:全模块对比

模块Three.jsOGL行数差异
渲染器WebGLRendererRenderer8000 vs 400
场景图Object3DTransform1500 vs 300
几何体BufferGeometryGeometry700 vs 150
材质30+ 类1 个 Material10000+ vs 200
着色器ShaderChunk 拼积木完整 VS/FS 传入20000 vs 0
纹理20+ 压缩格式支持仅 2D/Cube1500 vs 100
光照内置 6 种光源不内置2000 vs 0
后处理EffectComposer无(用户自写)1500 vs 0
合计~50000 行~3000 行~16x 差距

核心差异

  • Three.js:通用性优先,开箱即用,学习成本高但功能完备
  • OGL:极简哲学,开发者自驱,灵活但所有功能都要自己实现

🚀 桥接下一步:OGL 源码解析 → TinyDrive 引擎

学完 Three.js 源码解析后,你已经掌握了"功能完备引擎"的设计模式。下一步的 OGL 课程会带你换一个角度——极简设计

  • 砍掉所有"过度抽象"
  • 暴露所有"内部状态"
  • 让开发者完全控制渲染流程

最终目标:结合 Three.js 的工程经验 + OGL 的极简理念,实现一个 1000 行的 TinyDrive 引擎,覆盖:

  • 自定义场景图
  • 基础 PBR 渲染
  • 简单的后处理
  • 性能监控

🎓 最后一课:阅读源码的方法论

11 节下来,我们看了 50000+ 行代码。但真正重要的是方法

1. 从"调用入口"读起

不要从 WebGLRenderer.js 第 1 行开始读——从 renderer.render(scene, camera) 这一个调用入手,跟踪它的完整调用链

2. 带着问题读

每节开头先问"这一节要回答的问题是什么",然后只读与问题相关的代码。

3. 善用调试器

// 在代码中插入断点
debugger;
renderer.render(scene, camera);

然后在 Chrome DevTools 里 Step Into,跟着调用栈走。

4. 写 Mini 实现

只看不写等于没学。每节末尾的 50-100 行 Mini 实现是最有效的理解验证

5. 多方对比

  • 原生 WebGL(看底层)
  • Three.js(看工业级封装)
  • OGL(看极简设计)

三方对比才能看到设计取舍的全貌


📚 延伸阅读


恭喜你完成 Three.js 源码解析课程!

🎓 你现在拥有:

  • 理解工业级 WebGL 框架的能力
  • 阅读任何图形引擎源码的自信
  • 构建自研引擎的架构蓝图

下一步:开始你的 TinyDrive 引擎项目。🚀