第 5 节:场景图与矩阵系统——Object3D 的树形世界
Object3D 是 Three.js 一切可变换对象的"基类"——Mesh、Light、Camera 都继承它。理解它,就理解了 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. 为什么同时有 rotation 和 quaternion?
因为双重同步——开发者可以用任何一种方式表达旋转:
// 简化源码: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 简洁"(矩阵即变换)。
🎓 深度思考
为什么用四元数而不是欧拉角?
四元数避免万向锁(Gimbal Lock),且插值平滑。Three.js 同时保留两者是为了开发体验——大部分人理解欧拉角更直观。matrixWorld在renderer.render()的哪个时机更新?
场景准备阶段。render()内部第一步就是scene.updateMatrixWorld(),确保所有矩阵都是最新的。子节点能否绕过父级变换?
不能。子节点的世界矩阵必然受父级影响——这是树形结构的核心语义。