Read OSS

双渲染器架构: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 专属内部模块:WebGLAttributesWebGLBindingStatesWebGLBufferRendererWebGLCapabilitiesWebGLClippingWebGLExtensionsWebGLGeometriesWebGLInfoWebGLMorphtargetsWebGLObjectsWebGLProgramsWebGLPropertiesWebGLRenderListsWebGLRenderStatesWebGLShadowMapWebGLStateWebGLTexturesWebGLUniforms 等等。这些模块无一例外地直接调用 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。着色器编译失败?查 NodeManagerPipelines。绘制顺序有误?查 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(GPUDeviceGPUCommandEncoderGPURenderPassEncoder)实现这些方法;WebGLBackend(约 2800 行)则使用 WebGL 2 调用(gl.bindTexturegl.drawElements 等)实现同一套接口,为尚不支持 WebGPU 的浏览器提供无缝降级方案。

data WeakMap 是一个关键的设计细节。各 backend 不会将自身专属的属性直接挂载到 Three.js 对象上(那样会破坏抽象边界),而是以源对象为键,将 GPU 句柄(WebGL 纹理、WebGPU 缓冲区、管线对象等)统一存入这个 WeakMap。当 Three.js 对象被垃圾回收时,对应的 GPU 数据也会自动变得可回收。

WebGPURenderer:107 行的调度层

WebGPURenderer 出人意料地简洁——全文仅 107 行,其中大部分还是 JSDoc 注释。构造函数(第 53-103 行)只做三件事:

  1. 选择 backend:若 parameters.forceWebGL 为 true,则直接使用 WebGLBackend;否则创建 WebGPUBackend,并配置一个降级函数:
parameters.getFallback = () => {
    warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
    return new WebGLBackend( parameters );
};
  1. 将 backend 传入 Renderersuper( backend, parameters )

  2. 挂载 StandardNodeLibrarythis.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 依次按 groupOrderrenderOrderz(从前到后,近者优先)排序;对于透明对象,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 并生成实际的着色器代码。