Skip to main content

第 5 节:场景图与矩阵系统——Object3D 的树形世界

Object3D 是 Three.js 一切可变换对象的"基类"——MeshLightCamera 都继承它。理解它,就理解了 Three.js 的"树形世界"是如何运转的。


🌳 Object3D 的核心结构

Object3D
├── position: Vector3
├── rotation: Euler
├── quaternion: Quaternion
├── scale: Vector3
├── matrix: Matrix4 // 本地变换 (position + rotation + scale)
├── matrixWorld: Matrix4 // 世界变换 = parent.matrixWorld * matrix
├── children: Object3D[]
├── parent: Object3D
├── layers: Layers // 32 位掩码,可见性分层
├── visible: boolean
└── matrixAutoUpdate: boolean // 是否每帧自动重算

1. 为什么同时有 rotationquaternion

因为双重同步——开发者可以用任何一种方式表达旋转:

// 简化源码:setFromEuler / setFromQuaternion
onRotationChange() {
this.quaternion.setFromEuler(this.rotation, false);
}
onQuaternionChange() {
this.rotation.setFromQuaternion(this.quaternion, undefined, false);
}

设置一个会自动同步另一个,避免数据不一致。


🔗 矩阵传播链

local 变换 (JS 侧设置)
↓ updateMatrix()
matrix (本地矩阵)
↓ updateMatrixWorld(force)
matrixWorld = parent.matrixWorld * matrix

1. updateMatrix():JS 数据 → 本地矩阵

// 简化源码
updateMatrix() {
this.matrix.compose(this.position, this.quaternion, this.scale);
this.matrixWorldNeedsUpdate = true;
}

compose 把 position / quaternion / scale 拼成 4x4 矩阵。

2. updateMatrixWorld(force):递归传播

// 简化源码
updateMatrixWorld(force) {
if (this.matrixAutoUpdate) this.updateMatrix();

if (this.matrixWorldNeedsUpdate || force) {
if (this.parent === null) {
this.matrixWorld.copy(this.matrix);
} else {
this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix);
}
this.matrixWorldNeedsUpdate = false;
force = true;
}

for (const child of this.children) {
child.updateMatrixWorld(force); // 子节点强制重算
}
}
性能优化点

matrixWorldNeedsUpdate 是个脏标记——只有当 position / rotation / scale 真的变了,才重算 matrixWorld。这避免了"每帧遍历整个场景树"的浪费。

3. matrixAutoUpdate = false:手动控制

mesh.matrixAutoUpdate = false;  // 引擎不再每帧重算
mesh.updateMatrix(); // 只在 transform 变化时手动调用

适用场景:大量静态物体(如地形块、背景),省下遍历开销。


🔍 遍历方法

方法行为用途
traverse(cb)深度优先遍历所有后代整体遍历
traverseVisible(cb)只遍历 visible = true渲染可见对象
traverseAncestors(cb)向上遍历父级链查找祖级
// 简化源码
traverse(callback) {
callback(this);
for (const child of this.children) {
child.traverse(callback);
}
}

🎭 Layers 系统:32 位可见性掩码

Layers 用一个 32 位整数表示"我属于哪些层"和"我想看哪些层":

// 简化源码
class Layers {
constructor() {
this.mask = 1; // 默认在第 0 层
}
set(channel) { this.mask = (1 << channel) | 0; }
enable(channel) { this.mask |= (1 << channel) | 0; }
test(layers) { return (this.mask & layers.mask) !== 0; }
}

// 用法
camera.layers.enable(1); // 相机看第 0 和第 1 层
mesh.layers.set(1); // mesh 只属于第 1 层
// mesh 会被 camera 渲染

工业应用

  • 多相机分屏(左屏/右屏各一个 camera)
  • 调试可视化(只显示碰撞体/灯光范围)
  • 选择性渲染(编辑器只显示选中对象的高亮层)

🎯 Mini 实现

class MiniObject3D {
constructor() {
this.position = { x: 0, y: 0, z: 0 };
this.rotation = { x: 0, y: 0, z: 0 };
this.scale = { x: 1, y: 1, z: 1 };
this.children = [];
this.parent = null;
this.matrix = identity();
this.matrixWorld = identity();
}

add(child) {
this.children.push(child);
child.parent = this;
}

updateMatrixWorld(force = false) {
// 1. 算本地矩阵
this.matrix = compose(this.position, this.rotation, this.scale);
// 2. 与父级合成
this.matrixWorld = this.parent
? multiply(this.parent.matrixWorld, this.matrix)
: this.matrix;
// 3. 递归子节点
for (const child of this.children) {
child.updateMatrixWorld(force);
}
}
}

🆚 OGL 对比

OGL 把 Object3D 重命名为 Transform,但核心概念一致

// OGL: src/core/Transform.js
class Transform extends Mat4 {
constructor() {
super(); // 矩阵本身
this.parent = null;
this.children = [];
this.position = new Vec3();
this.scale = new Vec3(1);
this.rotation = new Quat();
}
// 关键差异:OGL 让 Transform 直接继承 Mat4,省去一层 .matrix
// OGL 没有 matrixWorldNeedsUpdate 脏标记,每帧强制重算
}

设计取舍:Three.js 重视"零冗余"(脏标记优化),OGL 重视"API 简洁"(矩阵即变换)。


🎓 深度思考

  1. 为什么用四元数而不是欧拉角?
    四元数避免万向锁(Gimbal Lock),且插值平滑。Three.js 同时保留两者是为了开发体验——大部分人理解欧拉角更直观。

  2. matrixWorldrenderer.render() 的哪个时机更新?
    场景准备阶段。render() 内部第一步就是 scene.updateMatrixWorld(),确保所有矩阵都是最新的。

  3. 子节点能否绕过父级变换?
    不能。子节点的世界矩阵必然受父级影响——这是树形结构的核心语义。


📚 延伸阅读