Read OSS

资产管线:加载器、插件与 Three.js 生态系统

中级

前置知识

  • 第 1–3 篇文章
  • 熟悉常见 3D 资产格式(glTF、FBX、OBJ)

资产管线:加载器、插件与 Three.js 生态系统

一个 3D 引擎的价值,很大程度上取决于它能加载哪些内容。Three.js 以 Loader 基类和 LoadingManager 为核心,构建了一套结构清晰的加载系统——库本身内置了针对基础文件格式的核心加载器,而插件生态中则提供了 50 余种专用加载器。本系列的最后一篇文章将完整介绍资产管线从 HTTP 请求到场景图的全过程,并深入探讨 GLTFLoader、后处理架构、控制器系统,以及测试与贡献相关的基础设施。

Loader 基类与 LoadingManager

Loader 是所有加载器的抽象基类。其构造函数(第 15–70 行)定义了每个加载器都需要的基础配置:

  • managerLoadingManager 实例(默认使用 DefaultLoadingManager
  • crossOrigin:跨域资源的 CORS 设置(默认为 'anonymous'
  • path:拼接到 URL 前面的基础路径
  • resourcePath:用于依赖资源(如模型引用的贴图)的独立基础路径
  • requestHeader:用于鉴权接口的自定义 HTTP 请求头

核心抽象方法是 load(url, onLoad, onProgress, onError),所有具体的加载器都必须实现这个方法。此外,基类还提供了 loadAsync(url),它将 load() 封装为 Promise,配合 async/await 使用时代码更加简洁。

LoadingManager 用于协调多个并发加载任务。它追踪当前正在加载的资源总数,提供 onStartonLoadonProgressonError 等回调,并通过 setURLModifier() 实现 URL 重写——这在不同部署环境下重定向资产路径时非常实用。此外,它还与 Cache 模块集成,支持 HTTP 层面的响应缓存。

classDiagram
    class Loader {
        +manager: LoadingManager
        +crossOrigin: string
        +path: string
        +resourcePath: string
        +load(url, onLoad, onProgress, onError)*
        +loadAsync(url): Promise
        +setPath(path): this
    }

    class FileLoader {
        +responseType: string
        +mimeType: string
        +load(): XMLHttpRequest
    }

    class ImageLoader {
        +load(): HTMLImageElement
    }

    class TextureLoader {
        +load(): Texture
    }

    class ObjectLoader {
        +load(): Object3D
    }

    Loader <|-- FileLoader
    Loader <|-- ImageLoader
    Loader <|-- TextureLoader
    Loader <|-- ObjectLoader
    FileLoader <-- ImageLoader : uses
    ImageLoader <-- TextureLoader : uses

核心加载器:从 FileLoader 到 ObjectLoader

核心加载器之间形成了一条依赖链:

FileLoader 是整个链条的基础——它通过 XMLHttpRequest 处理 HTTP 请求,支持多种响应类型(text、arraybuffer、blob、json),与 LoadingManager 集成以追踪加载进度,并借助 Cache 模块避免重复的网络请求。

ImageLoader 间接基于 FileLoader,将图像加载为 HTMLImageElementImageBitmap 对象,并处理 CORS 和 data URL。

TextureLoader 封装了 ImageLoader,返回一个包含已加载图像数据的 Texture 对象,是基础贴图映射场景中最常用的加载器。

ObjectLoader 负责将 Three.js 的 JSON 格式反序列化为完整的场景图,包含几何体、材质、贴图和动画片段,是 Object3D 及其子类上 toJSON() 方法的逆操作。

flowchart LR
    URL["Asset URL"] --> FL["FileLoader<br/>(HTTP request + caching)"]
    FL --> IL["ImageLoader<br/>(Image element)"]
    IL --> TL["TextureLoader<br/>(Texture object)"]

    FL -->|"JSON"| OL["ObjectLoader<br/>(Scene graph)"]

    FL -->|"ArrayBuffer"| Addons["Addon Loaders<br/>(GLTFLoader, FBXLoader, etc.)"]

提示: 同时加载多个资产时,始终使用 LoadingManager 来统一追踪整体进度。建议通过 new LoadingManager(onAllLoaded, onProgress, onError) 创建自定义 manager,而不是依赖各个加载器的独立回调。

插件生态系统:examples/jsm/

正如第 1 篇文章所介绍的,examples/jsm/ 是官方插件系统,以 three/addons 的形式发布。Addons.js 这个桶文件(barrel file)从各有组织的子目录中重新导出内容:

目录 内容 代表性模块
loaders/ 特定格式的资产加载器 GLTFLoader, FBXLoader, DRACOLoader, KTX2Loader, OBJLoader
controls/ 用户交互处理器 OrbitControls, FlyControls, MapControls, TransformControls
effects/ 视觉特效 AnaglyphEffect, AsciiEffect, OutlineEffect
geometries/ 专用几何体类型 TextGeometry, DecalGeometry, ParametricGeometry
csm/ 级联阴影贴图 CSM, CSMHelper
curves/ 额外的曲线类型 NURBSCurve, NURBSSurface
animation/ 动画工具 CCDIKSolver, AnimationClipCreator

插件目录的设计支持按需导入。每个模块都是独立的 ES module,从 'three'(核心库)导入依赖,因此按需导入单个插件时,tree-shaking 可以正常工作:

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

深入解析 GLTFLoader

GLTFLoader 是最重要的插件加载器——glTF 已是 Web 3D 内容事实上的标准格式。该加载器采用插件架构来处理 glTF 扩展:

flowchart TD
    Input["glTF file<br/>(.gltf JSON or .glb binary)"] --> Parse["Parse JSON + binary chunk"]
    Parse --> Extensions["Register Extension Plugins"]

    Extensions --> DRC["DRACOLoader<br/>(mesh compression)"]
    Extensions --> KTX["KTX2Loader<br/>(texture compression)"]
    Extensions --> Mat["Material Extensions<br/>(transmission, sheen, etc.)"]

    Parse --> Build["Build Scene Graph"]
    Build --> Meshes["Create Meshes"]
    Build --> Mats["Create Materials"]
    Build --> Anim["Create AnimationClips"]
    Build --> Skins["Create Skeletons"]

    Meshes --> Scene["Complete Scene"]
    Mats --> Scene
    Anim --> Scene
    Skins --> Scene

插件系统允许第三方扩展介入解析流程。例如,DRACOLoader 负责解压几何体数据,KTX2Loader 负责解压 GPU 纹理格式,材质扩展插件则处理 KHR_materials_transmissionKHR_materials_sheen 等 PBR 扩展。

GLTFLoader 的输出是一个结果对象,包含 scene(根 Object3D)、scenes(文件中的所有场景)、camerasanimations(AnimationClip 数组)以及 asset 元数据。加载器完整支持 glTF 规范:多 primitive 网格(创建 Group)、有索引和无索引的几何体、sparse accessor、morph target、骨骼动画,以及完整的 PBR 材质模型。

后处理:两种范式

Three.js 目前支持两种后处理方式,这也反映了整体架构正在经历的转型。

传统方式(WebGLRenderer)使用插件中的 EffectComposer,它管理着一条渲染 pass 链。每个 pass 渲染到一个帧缓冲区,后续 pass 读取上一个 pass 的结果:

Scene → EffectComposer → RenderPass → BloomPass → FXAAPass → Screen

新方式(WebGPURenderer)使用 RenderPipeline(原名 PostProcessing,已在 r183 中重命名)。它利用节点系统,以 TSL 节点图的形式表达后处理效果:

const renderPipeline = new RenderPipeline( renderer );
const scenePass = pass( scene, camera );
const bloomPass = bloom( scenePass, { strength: 1.5 } );
renderPipeline.outputNode = bloomPass;
flowchart LR
    subgraph "Legacy (WebGL)"
        EC["EffectComposer"] --> RP["RenderPass"]
        RP --> BP["BloomPass"]
        BP --> FP["FXAAPass"]
        FP --> Screen1["Canvas"]
    end

    subgraph "New (WebGPU/Node)"
        PP["RenderPipeline"] --> SP["pass(scene, camera)"]
        SP --> BN["bloom(pass, options)"]
        BN -->|"outputNode"| Screen2["Canvas"]
    end

基于节点的方式组合性更强——由于效果本身就是节点,可以通过 TSL 的流式 API 自由组合、分支和条件应用。RenderPipeline 类还通过 outputColorTransform 属性自动处理色调映射和色彩空间转换。

请注意 src/renderers/common/PostProcessing.js 中的废弃说明:PostProcessing 现在只是一个包装器,在继承 RenderPipeline 的同时会输出重命名警告。

控制器与编辑器

Controls 基类位于核心库(而非插件)中,因为它定义了所有控制器实现共享的接口。它继承自 EventDispatcher,并提供以下能力:

  • object:被控制的 Object3D(通常是摄像机)
  • domElement:接收输入事件的 HTML 元素
  • enabled:总开关
  • connect(element) / disconnect():挂载/卸载 DOM 事件监听器
  • update(delta):每帧调用的状态更新方法
  • dispose():清理资源

OrbitControls 是使用最广泛的控制器实现,位于 examples/jsm/controls/OrbitControls.js。它支持围绕 target 点进行轨道旋转(左键拖拽)、缩放(滚轮)和平移(右键拖拽)。其他常用控制器还包括 FlyControls(第一人称飞行)、MapControls(屏幕空间平移的轨道控制)和 TransformControls(用于平移/旋转/缩放的 3D Gizmo)。

Three.js 还在 editor/ 目录下内置了一个完整的浏览器端编辑器应用。它完全基于库本身及其插件构建,提供可视化场景编辑、视口渲染、对象操作、材质编辑和导出功能。虽然不及商业工具精细,但它既是一个有力的示例应用,也是一个实用的创作工具。

测试与贡献

测试基础设施使用两套框架:

QUnit 单元测试通过 test/unit/three.source.unit.js 组织,该文件汇聚了各源码模块的测试套件。目录结构与 src/ 保持一致——每个主要类都有对应的测试文件,覆盖构造函数行为、方法正确性和边界情况。运行命令:npm run test-unit

Puppeteer E2E 测试使用无头 Chrome 渲染示例页面,并将截图与基准图像进行对比。这类测试能捕获单元测试发现不了的视觉回归——例如着色器编译差异、排序顺序错误或几何体生成问题。运行命令:npm run test-e2e,或通过 npm run test-e2e-webgpu 测试 WebGPU 路径。

flowchart TD
    PR["Pull Request"] --> Lint["npm run lint<br/>(ESLint on src/)"]
    Lint --> Unit["npm run test-unit<br/>(QUnit tests)"]
    Unit --> UnitAddons["npm run test-unit-addons<br/>(Addon tests)"]
    UnitAddons --> E2E["npm run test-e2e<br/>(Puppeteer screenshots)"]
    E2E --> TreeShake["npm run test-treeshake<br/>(Bundle size check)"]
    TreeShake --> Review["Code Review"]

贡献者需要注意以下规范:

  • 代码风格:使用 tab 缩进,遵循 Three.js 特定的空格规范(括号内加空格),并符合 eslint-config-mdcs 规则
  • Tree-shaking:避免在 src/nodes/ 之外产生顶层副作用;模块级别的内存分配请使用 /*@__PURE__*/ 注释
  • is* 标志:新类必须添加类型检测布尔标志(this.isMyClass = true
  • ID 与 UUID:对于需要标识的新类,遵循双 ID 模式
  • 临时对象:在方法内部应使用模块级 /*@__PURE__*/ 临时变量,而不是在方法内动态分配

提示: 贡献新功能之前,请先确认它应该归属于核心库还是插件。核心库的改动需要更严格的审查,并且要保证向后兼容性。大多数新功能(加载器、控制器、特效、几何体)都应放在 examples/jsm/ 中。

系列回顾

在这六篇文章中,我们从构建系统到着色器生成,完整梳理了 Three.js 的整体架构:

  1. 架构与导航 — 四个入口点、目录结构、基础设计模式
  2. 场景图 — Object3D 变换、父子树结构、几何体与材质的契约关系
  3. 双渲染器 — WebGLRenderer 单体架构 vs. 新的 Renderer + Backend 架构
  4. 节点系统与 TSL — 基于 DAG 的着色器构建与流式着色器编写
  5. 数学、摄像机与光照 — 线性代数库、投影矩阵、光照到节点的处理链路
  6. 资产管线 — 加载器、插件、后处理、控制器及贡献指南

Three.js 正在经历其诞生以来最深刻的架构变革——从 WebGL 专属的单体架构,向多后端、基于节点的渲染系统演进。深入理解这两套架构,将帮助你在当下游刃有余,也让你做好准备,迎接 WebGPU 时代的到来。