双渲染器架构:WebGLRenderer 与新 Backend 系统
前置知识
- ›第 1-2 篇文章
- ›对图形渲染管线概念的基本了解(顶点/片元着色器、渲染目标、深度缓冲区)
- ›熟悉 WebGL 或 WebGPU API 的基本概念
双渲染器架构:WebGLRenderer 与新 Backend 系统
Three.js 正处于一场历史性的架构变革之中。十余年来,WebGLRenderer 始终是一个单体式的巨型类,通过 30 余个内部辅助模块直接管理 WebGL 状态。如今,一套全新的三层架构将职责彻底分离:负责统筹调度的 API 无关层 Renderer、抽象的 Backend 接口,以及分别面向 WebGPU 和 WebGL 2 的具体 backend 实现。理解这两套架构至关重要——旧版渲染器仍被广泛使用,而新系统才是当前所有活跃开发的主战场。
架构之变:单体式 vs. 模块化
WebGLRenderer 直接引入了 30 余个 WebGL 专属内部模块:WebGLAttributes、WebGLBindingStates、WebGLBufferRenderer、WebGLCapabilities、WebGLClipping、WebGLExtensions、WebGLGeometries、WebGLInfo、WebGLMorphtargets、WebGLObjects、WebGLPrograms、WebGLProperties、WebGLRenderLists、WebGLRenderStates、WebGLShadowMap、WebGLState、WebGLTextures、WebGLUniforms 等等。这些模块无一例外地直接调用 gl.* 方法。这种高度耦合的设计使得添加 WebGPU 支持几乎不可能,除非对整个系统进行彻底重写。
新架构从根本上反转了这种耦合关系:
flowchart TB
subgraph "Legacy Architecture"
WGL["WebGLRenderer<br/>(3,600+ lines)"]
WGL --> WGLA["WebGLAttributes"]
WGL --> WGLB["WebGLBindingStates"]
WGL --> WGLP["WebGLPrograms"]
WGL --> WGLS["WebGLState"]
WGL --> WGLT["WebGLTextures"]
WGL --> WGLMore["...25+ more modules"]
end
subgraph "New Architecture"
R["Renderer<br/>(3,680+ lines)<br/>API-agnostic"]
R --> RL["RenderLists"]
R --> RO["RenderObjects"]
R --> PP["Pipelines"]
R --> TX["Textures"]
R --> NM["NodeManager"]
R --> More["...10+ more"]
R -->|"delegates GPU calls"| BE["Backend (abstract)"]
BE --> WGPU["WebGPUBackend<br/>(~2,600 lines)"]
BE --> WGLBE["WebGLBackend<br/>(~2,800 lines)"]
end
核心思路在于:Renderer 负责处理所有渲染逻辑(场景遍历、排序、裁剪、管线管理),而 Backend 负责所有 GPU API 调用(创建缓冲区、编译着色器、发出绘制命令)。这种分离意味着,若要添加新的 backend(比如面向 WebNN 或未来某个 API),只需实现 Backend 接口,而无需重写整个渲染循环。
新版 Renderer 基类
Renderer 是这套架构的核心调度器,代码超过 3680 行。其构造函数位于 第 83-107 行,接收一个 Backend 实例和一个参数对象,并初始化一系列管理组件:
| 组件 | 职责 |
|---|---|
RenderLists |
管理不透明/透明对象的有序渲染列表 |
RenderObjects |
缓存并提供 RenderObject 实例 |
Pipelines |
管理 GPU 管线状态(着色器程序 + 渲染状态) |
Bindings |
管理 uniform 缓冲区和绑定组 |
Attributes |
管理 GPU attribute 缓冲区 |
Geometries |
管理几何体状态和线框生成 |
Textures |
管理 GPU 纹理的创建与更新 |
NodeManager |
管理节点图的编译与缓存 |
Background |
处理场景背景渲染 |
Lighting |
管理光照节点的配置 |
RenderContexts |
为渲染 pass 提供渲染上下文对象 |
RenderBundles |
管理命令包的录制 |
Animation |
管理动画/渲染循环 |
XRManager |
管理 WebXR 会话集成 |
Renderer.js 顶部的 import 列表直观地展示了其规模——来自 common/ 目录的 20 余个引用,加上用于内部着色器构建的 TSL 节点引用。
提示: 在新系统中调试渲染问题时,可以直接定位到负责对应功能域的管理组件。纹理异常?查
Textures。着色器编译失败?查NodeManager和Pipelines。绘制顺序有误?查RenderList。
Backend:抽象 GPU 接口
Backend 定义了 GPU 操作的契约。其构造函数相当精简——存储参数、创建用于保存各对象 GPU 数据的 WeakMap,并持有渲染器和 DOM 元素的引用(在初始化期间赋值)。
各 backend 必须实现的抽象方法覆盖了完整的 GPU 生命周期:
flowchart LR
subgraph "Backend Interface"
Init["init()"]
Create["createTexture()<br/>createAttribute()<br/>createBindings()"]
Update["updateTexture()<br/>updateAttribute()<br/>updateBindings()"]
Render["beginRender()<br/>draw()<br/>endRender()"]
Compute["beginCompute()<br/>compute()<br/>endCompute()"]
Destroy["destroyTexture()<br/>destroyAttribute()"]
end
Init --> Create --> Update --> Render --> Destroy
Create --> Compute
WebGPUBackend(约 2600 行)使用原生 WebGPU API(GPUDevice、GPUCommandEncoder、GPURenderPassEncoder)实现这些方法;WebGLBackend(约 2800 行)则使用 WebGL 2 调用(gl.bindTexture、gl.drawElements 等)实现同一套接口,为尚不支持 WebGPU 的浏览器提供无缝降级方案。
data WeakMap 是一个关键的设计细节。各 backend 不会将自身专属的属性直接挂载到 Three.js 对象上(那样会破坏抽象边界),而是以源对象为键,将 GPU 句柄(WebGL 纹理、WebGPU 缓冲区、管线对象等)统一存入这个 WeakMap。当 Three.js 对象被垃圾回收时,对应的 GPU 数据也会自动变得可回收。
WebGPURenderer:107 行的调度层
WebGPURenderer 出人意料地简洁——全文仅 107 行,其中大部分还是 JSDoc 注释。构造函数(第 53-103 行)只做三件事:
- 选择 backend:若
parameters.forceWebGL为 true,则直接使用WebGLBackend;否则创建WebGPUBackend,并配置一个降级函数:
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
-
将 backend 传入 Renderer:
super( backend, parameters ) -
挂载 StandardNodeLibrary:
this.library = new StandardNodeLibrary()—— 将 Three.js 经典材质和灯光映射到对应的基于节点的实现,从而保证向后兼容性(详见第 4 篇)。
这套降级机制设计得相当优雅:若主 backend 在 init() 阶段初始化失败,Renderer 会自动调用 getFallback()。也就是说,只需写一行 new WebGPURenderer(),即可在支持 WebGPU 和仅支持 WebGL 2 的浏览器上都正常运行。
渲染管线:_renderScene() 执行流程
渲染循环的核心是位于 第 1424 行 的 _renderScene()。让我们逐步追踪一帧的完整执行过程:
sequenceDiagram
participant App
participant Renderer
participant RenderList
participant Background
participant Backend
App->>Renderer: render(scene, camera)
Renderer->>Renderer: _renderScene(scene, camera)
Renderer->>Renderer: Update projection matrix
Renderer->>Renderer: _projectObject(scene) → build RenderList
Renderer->>RenderList: sort opaque (front-to-back)
Renderer->>RenderList: sort transparent (back-to-front)
Renderer->>Backend: beginRender(renderContext)
Renderer->>Background: render background
Renderer->>Renderer: _renderObjects(opaqueList)
Renderer->>Renderer: _renderTransparents(transparentList)
Renderer->>Backend: endRender(renderContext)
该方法首先保存当前渲染状态(render ID、render context、render object 函数),以支持嵌套渲染调用——阴影贴图和透射效果都会触发递归渲染。随后,它配置渲染上下文、设置相机坐标系以及反转深度缓冲区。
投影阶段会遍历场景图,对每个对象做视锥体裁剪测试,并将可见对象分类存入 RenderList 的不透明、透明和 bundle 三个分组。排序完成后(不透明对象从前到后以利用 early-z 剔除,透明对象从后到前以保证正确混合),依次渲染背景、不透明对象和透明对象。
RenderObject 与 RenderList
RenderObject 是场景图与 GPU 绘制命令之间的桥梁。它在构建时持有 3D 对象、材质、场景、相机、灯光节点、渲染上下文和裁剪上下文的引用,并缓存 backend 发出绘制调用所需的一切数据:已编译的节点图、管线对象、绑定组以及绘制参数。
缓存策略是性能的关键所在。RenderObject 不会在每帧都重新编译着色器程序,而是通过对当前材质属性、几何体 attribute 和光照配置进行哈希计算来验证缓存是否仍然有效。只有在发生变化时,才会触发重新编译。
RenderList 实现了两种排序策略。对于不透明对象,painterSortStable 依次按 groupOrder、renderOrder、z(从前到后,近者优先)排序;对于透明对象,reversePainterSortStable 反转 z 的比较方向(从后到前,远者优先):
// Opaque: front-to-back (smaller z first → early-z rejection)
return a.z - b.z;
// Transparent: back-to-front (larger z first → correct blending)
return b.z - a.z;
两个排序函数均以 a.id - b.id 作为最终的决胜条件,确保排序结果的确定性。
flowchart TD
PO["_projectObject()"] -->|"Categorize"| Opaque["Opaque List"]
PO -->|"Categorize"| Trans["Transparent List"]
Opaque -->|"painterSortStable<br/>front-to-back"| SortO["Sorted Opaques"]
Trans -->|"reversePainterSortStable<br/>back-to-front"| SortT["Sorted Transparents"]
SortO --> RenderO["_renderObjects()"]
SortT --> RenderT["_renderTransparents()"]
RenderO -->|"For each item"| RO["Get/Create RenderObject"]
RenderT -->|"For each item"| RO
RO -->|"Backend.draw()"| GPU["GPU Draw Call"]
下篇预告
新版渲染器架构与节点系统密不可分——整个着色器生成管线已从静态的 GLSL 模板片段转向动态节点图,并最终编译为 WGSL 或 GLSL。下一篇文章将深入探讨 Node 基类的工作机制、TSL 如何提供流畅的 JavaScript API 来编写着色器,以及 NodeBuilder 如何遍历 DAG 并生成实际的着色器代码。