Read OSS

Three.js 内部机制:架构概览与代码导航指南

中级

前置知识

  • JavaScript 基础知识及 ES 模块语法
  • 熟悉 npm 包结构

Three.js 内部机制:架构概览与代码导航指南

Three.js 是 Web 端使用最广泛的 3D 图形库,拥有超过十年的历史和数百位贡献者。但要真正读懂它的代码库——400 多个源文件、数以千计的导出项——并非易事。本文将在你正式深入之前,为你提供一张全局地图:构建系统的工作方式、代码的目录布局,以及随处可见的设计模式。

我们以提交 582a03b74d843d2c99082aa84589a228b480d262(版本 0.183.1)为基准,所有引用均锁定在这个快照上。

多入口点系统

Three.js 并不以单一的整体 bundle 形式发布,而是提供四个独立的入口点,分别面向不同的使用场景。它们之间是层层叠加的关系:每个更高层的入口点都会重新导出其下层的所有内容。

flowchart TB
    Core["Three.Core.js<br/>Renderer-agnostic types<br/>(~165 re-exports)"]
    Classic["Three.js<br/>Core + WebGLRenderer<br/>(+8 WebGL exports)"]
    WebGPU["Three.WebGPU.js<br/>Core + Node system + WebGPU/WebGL backends<br/>(+35 exports)"]
    TSL["Three.TSL.js<br/>Standalone TSL re-exports<br/>(600+ symbols)"]

    Core --> Classic
    Core --> WebGPU
    WebGPU --> TSL

    style Core fill:#e8f4f8
    style Classic fill:#fff3cd
    style WebGPU fill:#d4edda
    style TSL fill:#f8d7da

经典入口点 src/Three.js 极为简洁——它重新导出 Core 的全部内容,并额外添加了 WebGLRenderer 以及少量 WebGL 专用工具,如 ShaderLibUniformsLib

核心入口点 src/Three.Core.js 是整个库的核心所在,重新导出了约 165 个类型,涵盖场景、对象、材质、几何体、相机、光源、加载器、数学工具、动画、音频、辅助工具及扩展——除渲染器外,你所需要的一切都在这里。

WebGPU 入口点 src/Three.WebGPU.js 在此基础上叠加了新的渲染器架构:WebGPURendererWebGPUBackendWebGLBackend(回退方案)、完整的节点材质系统,以及 TSL(Three Shading Language)。这是面向未来的入口点。

最后,src/Three.TSL.js 是一个独立文件,重新导出了 600 多个 TSL 符号。它的存在让开发者可以通过 import { float, vec3, mul } from 'three/tsl' 单独引入 TSL,而无需加载整个 WebGPU bundle。

package.json 导出映射

package.json 中的 exports 字段是一张路由表,将各个 import 路径映射到对应的构建产物:

Import 路径 解析目标
import * from 'three' build/three.module.js
require('three') build/three.cjs
import * from 'three/webgpu' build/three.webgpu.js
import * from 'three/tsl' build/three.tsl.js
import * from 'three/addons' examples/jsm/Addons.js
import * from 'three/addons/*' examples/jsm/*
import * from 'three/src/*' src/*(直接访问源码)

提示: 新建项目时,建议将 'three/webgpu' 作为入口点。它内置了 WebGL 2 回退支持,让你在享受现代节点材质系统的同时,无需牺牲浏览器兼容性。

注意 package.json#L25-L27 中的 sideEffects 字段:只有 src/nodes/**/* 被标记为有副作用,其余所有内容都是可以安全 tree-shake 的。考虑到库的体积,这一点至关重要。

目录结构与模块组织

src/ 目录采用清晰的功能划分方式,每个子目录对应一个概念领域:

目录 用途 核心类型
core/ 基础类与数据结构 Object3D, BufferGeometry, BufferAttribute, EventDispatcher, Raycaster
math/ 线性代数基础类型 Vector2/3/4, Matrix3/4, Quaternion, Euler, Color, Frustum
scenes/ 场景容器 Scene, Fog, FogExp2
cameras/ 视图投影 Camera, PerspectiveCamera, OrthographicCamera
objects/ 可渲染的场景对象 Mesh, InstancedMesh, BatchedMesh, Line, Points, Sprite
materials/ 表面外观 Material, MeshStandardMaterial, NodeMaterial(位于 materials/nodes/
geometries/ 程序化形状 BoxGeometry, SphereGeometry 等(共 22 种)
lights/ 光源 Light, DirectionalLight, PointLight, SpotLight
loaders/ 资源加载 Loader, FileLoader, TextureLoader, ObjectLoader
textures/ 图像数据封装 Texture, DataTexture, CubeTexture
animation/ 关键帧动画系统 AnimationMixer, AnimationClip, KeyframeTrack
renderers/ GPU 渲染后端 WebGLRenderer, Renderer, WebGPUBackend, WebGLBackend
nodes/ 基于节点的着色器图系统 Node, NodeBuilder, TSL, MaterialNode
extras/ 工具与辅助类 Controls, Curves, PMREMGenerator

src/ 根目录下的两个文件承担着共享基础设施的职责。src/constants.js 定义了库中所有枚举常量,从剔除模式、混合类型到纹理格式和色彩空间,一应俱全。文件开头的 REVISION 字符串(在当前快照中为 '184dev')会被嵌入到构建产物中。

graph TD
    subgraph "src/"
        Constants["constants.js"]
        Utils["utils.js"]
        subgraph "core/"
            ED[EventDispatcher]
            O3D[Object3D]
            BG[BufferGeometry]
        end
        subgraph "math/"
            V3[Vector3]
            M4[Matrix4]
            Q[Quaternion]
        end
        subgraph "renderers/"
            WGL[WebGLRenderer]
            R[Renderer]
            BE[Backend]
        end
        subgraph "nodes/"
            N[Node]
            NB[NodeBuilder]
            TSL[TSLCore]
        end
    end

    Constants --> O3D
    Constants --> WGL
    Utils --> O3D
    ED --> O3D
    ED --> BG
    ED --> N
    V3 --> O3D
    M4 --> O3D
    Q --> O3D

EventDispatcher:根基类

Three.js 中几乎所有重要的类都继承自 EventDispatcher——Object3D、BufferGeometry、Material、Texture 和 Node 都扩展了它。其实现位于 src/core/EventDispatcher.js,仅有 106 行实质代码,提供了 addEventListenerhasEventListenerremoveEventListenerdispatchEvent 四个方法。

其中一个关键设计决策是延迟初始化_listeners 映射表只在第一次调用 addEventListener 时才会创建(第 33 行)。由于很多对象在创建后从未绑定过监听器,这样做避免了在每个构造函数调用时都分配一个空对象。在包含成千上万个几何体的场景中,这种优化效果相当显著。

classDiagram
    class EventDispatcher {
        +addEventListener(type, listener)
        +hasEventListener(type, listener)
        +removeEventListener(type, listener)
        +dispatchEvent(event)
        -_listeners: Object
    }

    class Object3D {
        +isObject3D: boolean
        +id: number
        +uuid: string
    }

    class BufferGeometry {
        +isBufferGeometry: boolean
    }

    class Material {
        +isMaterial: boolean
    }

    class Node {
        +isNode: boolean
    }

    EventDispatcher <|-- Object3D
    EventDispatcher <|-- BufferGeometry
    EventDispatcher <|-- Material
    EventDispatcher <|-- Node

还有一个细节值得留意:dispatchEvent 在遍历之前会先复制一份监听器数组(第 114 行)。这样做是为了防止在派发事件过程中修改数组导致回调被跳过或重复执行——这是一种经典的迭代安全模式。

普遍存在的设计模式

以下三种模式在代码库中随处可见,需要优先掌握。

用布尔值 is* 标志进行类型判断

Three.js 使用 isObject3DisMeshisBufferGeometry 这样的布尔标志来判断类型,而非 instanceof。在 src/core/Object3D.js#L80 中可以看到这样的写法:this.isObject3D = true。这种设计规避了跨 realm 的 instanceof 失效问题(例如同时加载了多个版本的 Three.js,或对象跨 iframe 边界传递的情况),在热路径上也略有性能优势。

自增 ID + UUID

每个 Object3D、BufferGeometry 和 Material 都会通过模块作用域计数器获得一个数字 id,同时还有一个字符串 uuidsrc/core/Object3D.js#L11-L89 中展示了两者的实现:_object3DId 是闭包作用域内的计数器,而 src/math/MathUtils.js#L17-L33 中的 generateUUID() 则利用预计算的十六进制查找表生成 v4 UUID 以提升性能。数字 ID 用于运行时的快速排序与比较,UUID 则用于序列化和资源标识。

模块级 /*@__PURE__*/ 暂存对象

在 Object3D.js、Mesh.js 以及数十个其他文件的顶部,你会发现这样的声明:

const _v1 = /*@__PURE__*/ new Vector3();
const _q1 = /*@__PURE__*/ new Quaternion();
const _m1 = /*@__PURE__*/ new Matrix4();

这些是复用的暂存对象,在方法调用之间共享,从而避免在紧密循环中频繁分配临时对象。/*@__PURE__*/ 注释告诉 tree-shaking 工具:如果该模块未被使用,这些分配可以被移除。可以在 src/core/Object3D.js#L13-L24 看到一个典型示例。

提示: 向 Three.js 贡献代码时,切勿在每帧都会执行的方法内部调用 new Vector3()。请使用以下划线为前缀的模块级暂存对象代替。

构建系统与 GLSL 压缩

构建流程由 Rollup 驱动,配置文件位于 utils/build/rollup.config.js。该文件定义了七套构建配置,分别生成普通版和压缩版的 ESM bundle,以及一个 CJS bundle。

构建过程依赖两个自定义插件:

glsl()第 4-36 行)针对以 .glsl.js 结尾的文件,查找以 /* glsl */`...` 包裹的标签模板字面量,并通过去除注释、折叠空白来压缩其中的 GLSL 代码。这样一来,着色器源码在开发阶段可以保持带注释的可读形式,而构建产物中则会是压缩后的版本。

header()第 38-61 行)在每个输出 chunk 的开头添加 MIT 许可证声明。

flowchart LR
    A["Source Entry<br/>src/Three.*.js"] --> B["glsl() Plugin<br/>Minify .glsl.js"]
    B --> C["Rollup Tree-shake<br/>& Bundle"]
    C --> D["header() Plugin<br/>Add license"]
    D --> E["build/*.js<br/>ESM Output"]
    C --> F["terser() Plugin<br/>(minified builds)"]
    F --> D

TSL 的构建方式值得特别关注:在第 122 行three/webgpu 被声明为 external,即 TSL bundle 不会将 WebGPU 入口点打包进来,而是将其作为 peer dependency 处理。这使得 three.tsl.js 保持极小的体积(仅包含重新导出)。

核心与插件:examples/jsm 的故事

examples/jsm/ 目录是 Three.js 官方的插件生态,以 three/addons 路径发布。虽然名字里带着"examples",但这个目录里存放的是生产级质量的代码:GLTFLoader、DRACOLoader 等加载器,OrbitControls 等交互控制器,后处理特效,以及各类专用几何体。

examples/jsm/Addons.js 这个桶文件统一导出所有内容,但为了充分利用 tree-shaking,建议按需引入:

// ✅ 推荐——支持 tree-shaking
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// ❌ 不推荐——会引入全部内容
import { OrbitControls } from 'three/addons';

核心与插件之间的划分遵循一个清晰的原则:核心包含渲染器所需的内容(场景图、材质、几何体、相机、光源、数学工具、内置格式加载器),而插件扩展的是用户所需的内容(特定格式加载器、交互控制器、视觉特效、专用几何体)。

flowchart TB
    subgraph "Core (src/)"
        direction TB
        SceneGraph["Scene Graph<br/>Object3D, Scene, Camera"]
        Rendering["Renderers<br/>WebGLRenderer, WebGPURenderer"]
        DataTypes["Data Types<br/>Materials, Geometry, Textures"]
        Math["Math<br/>Vector3, Matrix4, Color"]
    end

    subgraph "Addons (examples/jsm/)"
        direction TB
        Loaders["Loaders<br/>GLTFLoader, FBXLoader, DRACOLoader"]
        Controls["Controls<br/>OrbitControls, FlyControls"]
        Effects["Effects<br/>EffectComposer, Bloom, SSAO"]
        Extra["Extras<br/>CSM, NURBS, CSG"]
    end

    Core --> Addons

接下来

有了这张全局地图,我们就可以深入探索场景图了——它是每个 Three.js 应用的核心数据结构。在下一篇文章中,我们将剖析 Object3D 的双向 Euler/Quaternion 旋转同步机制、矩阵更新的传播流程,以及 BufferGeometry 与 Material 如何在 Mesh 中协同工作,共同构成最基本的渲染契约。