three.js Object3D 深度解析:3D 场景图中一切对象的基类、变换矩阵与父子层级机制
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
Object3D是 three.js 中几乎所有 3D 对象的基类,Mesh、Camera、Light、Group、Scene都从它继承而来。本文基于仓库中的官方文档 docs/pages/Object3D.html.md 与源码实现 src/core/Object3D.js,系统梳理它的继承关系、全部属性与方法的语义、位置/旋转/缩放与变换矩阵的双向同步机制,以及add/attach/traverse/toJSON等关键 API 的底层原理与适用限制,帮助你在构建场景图、做拾取检测、动态重挂对象和序列化资源时做出正确判断。
一、类定位与继承关系
Object3D的继承链为EventDispatcher → Object3D。它自身不持有任何几何体或材质,只负责三件事:在 3D 空间中定位一个对象(变换状态)、组织对象的父子层级(场景图)、提供遍历与查找工具。可渲染对象Mesh、Line、Points在其之上补充了几何与材质;Camera、Light则补充了各自的观察/发光属性。
源码中类声明位于 src/core/Object3D.js#L64,构造函数new Object3D()无参数,创建一个新的 3D 对象。构造函数内部完成的工作比文档罗列的更细(src/core/Object3D.js#L69-L390):
- 模块级自增计数器
_object3DId为每个实例分配只读整数id(Object.defineProperty保证不可重写); uuid由generateUUID()生成,全局唯一,是序列化与资源缓存的键;type固定为字符串'Object3D',只读,子类各自覆盖为'Mesh'、'Group'等,供序列化/反序列化时识别类型;rotation(Euler)与quaternion之间注册了_onChange回调实现双向联动——修改任一方都会自动同步另一方(src/core/Object3D.js#L145-L158)。这是理解“为什么改了rotation后quaternion也变了”的关键。
二、实例标识与命名
| 属性 | 类型 | 说明 |
|---|---|---|
id | number(只读) | 对象 ID,进程内自增,稳定可用于getObjectById |
uuid | string(只读) | 全局唯一标识,序列化、Raycaster去重等场景使用 |
name | string | 名称,默认空字符串,用于getObjectByName |
type | string(只读) | 对象类型,用于序列化/反序列化识别 |
isObject3D | boolean(只读) | 恒为true,是 three.js 惯例的类型测试标志 |
从add()的实现可以看到这些标志的实际用途:添加子对象前先做object && object.isObject3D判断,不是Object3D实例会打印错误提示并静默忽略(src/core/Object3D.js#L767-L783)。
三、变换状态:position / rotation / quaternion / scale
本地变换由四个属性共同描述,构造函数中以Object.defineProperties定义(src/core/Object3D.js#L160-L226):
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
position | Vector3 | (0,0,0) | 本地位置 |
rotation | Euler | (0,0,0) | 本地旋转,欧拉角(弧度) |
quaternion | Quaternion | 单位四元数 | 本地旋转,四元数表示 |
scale | Vector3 | (1,1,1) | 本地缩放 |
rotation与quaternion是同一旋转状态的两种表示,源码通过_onChange机制自动互相同步,因此你可以任选其一操作,无需手动换算。选择建议:需要插值(球面线性插值slerp)或避免万向锁时用四元数;需要直观调试单个轴角时用欧拉角。
此外还有两个矩阵相关属性与变换状态配套:
modelViewMatrix(Matrix4):模型视图矩阵,由渲染器在使用时填充;normalMatrix(Matrix3):法线矩阵,供着色器中法线变换使用。
pivot:绕指定点旋转/缩放
.pivot : Vector3属性(默认null)是较新的能力:设置后,旋转与缩放将围绕该点而非对象原点施加。其实现藏在updateMatrix()中——先用position/quaternion/scale合成矩阵,再直接对矩阵平移动量(elements[12..14])做补偿,使变换绕pivot生效(src/core/Object3D.js#L1144-L1163)。toJSON()与copy()也已覆盖该字段,说明它已进入序列化契约。
四、矩阵系统与自动更新标志
这是Object3D最核心的性能与正确性机制。
| 属性 | 默认值 | 说明 |
|---|---|---|
matrix | Matrix4 | 本地空间变换矩阵 |
matrixWorld | Matrix4 | 世界空间变换矩阵;无父对象时与matrix相同 |
matrixAutoUpdate | true(由DEFAULT_MATRIX_AUTO_UPDATE决定) | true时每帧由引擎从 position/rotation/scale 自动计算matrix;false时需手动调用updateMatrix() |
matrixWorldAutoUpdate | true(由DEFAULT_MATRIX_WORLD_AUTO_UPDATE决定) | true时引擎自动根据父级matrixWorld与本级matrix计算matrixWorld;false时由应用直接维护 |
matrixWorldNeedsUpdate | false | 置true后,本帧会重算世界矩阵并自动复位为false |
三个静态默认值定义在文件末尾(src/core/Object3D.js#L1687-L1707):
Object3D.DEFAULT_UP = new Vector3( 0, 1, 0 ); // 也用于 DirectionalLight / HemisphereLight 默认位置 Object3D.DEFAULT_MATRIX_AUTO_UPDATE = true; Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true;updateMatrix()
从当前position / quaternion / scale重新合成matrix(Matrix4.compose),处理pivot补偿,并置matrixWorldNeedsUpdate = true标记脏状态(src/core/Object3D.js#L1144-L1163)。
updateMatrixWorld( force )
更新自身及所有后代的世界矩阵:
- 若
matrixAutoUpdate为true,先调updateMatrix(); - 若
matrixWorldNeedsUpdate || force且matrixWorldAutoUpdate === true:无父级时matrixWorld直接拷贝matrix,有父级时matrixWorld = parent.matrixWorld * matrix; - 关键点:一旦本节点需要重算,
force会被内部提升为true并逐层传给子节点,保证脏状态沿子树向下传播(src/core/Object3D.js#L1176-L1214)。
updateWorldMatrix( updateParents, updateChildren, force )
updateMatrixWorld的受控版本,可精确控制更新范围(src/core/Object3D.js#L1225-L1275):
updateParents(默认false):是否向上递归更新祖先链;updateChildren(默认false):是否向下递归更新后代;force(默认false):即使matrixWorldNeedsUpdate为false也强制重算。
localToWorld、worldToLocal、getWorldPosition、lookAt等方法的内部实现统一采用this.updateWorldMatrix( true, false )这一模式——先确保祖先链最新,再取matrixWorld,避免整棵子树无谓重算。这也是官方示例(如 examples/jsm/utils/SceneUtils.js#L174 的detach辅助流程)遵循的惯例。
五、父子层级操作
add / remove / removeFromParent / clear
add( ...objects ) : Object3D:将任意多个对象作为子对象加入。一个对象最多只有一个父对象,源码会先对传入对象执行removeFromParent()再挂入,因此对象若已有父级会被自动摘走。同时触发added(挂在子对象上)与childadded(挂在父对象上,事件携带child)两个事件。把对象加到自身会触发error提示并中止(src/core/Object3D.js#L746-L787)。remove( ...objects ) : Object3D:移除子对象,将parent置null,触发removed/childremoved。removeFromParent() : Object3D:等价于parent?.remove(this),是重挂对象前的标准清理步骤。clear() : Object3D:移除全部子对象,实现为一行return this.remove( ... this.children )(src/core/Object3D.js#L859-L863)。
单元测试 test/unit/src/core/Object3D.tests.js 对add/clear/removeFromParent的父子指针一致性做了断言验证。
attach:保持世界变换的重挂
attach( object ) : Object3D是最具实战价值的方法:把对象挂到当前对象之下,同时保持其世界变换(matrixWorld)不变(src/core/Object3D.js#L874-L908)。源码流程:
this.updateWorldMatrix( true, false )更新自身世界矩阵,求逆得_m1;- 若对象原本有父级,乘上旧父级的
matrixWorld,得到“从新父系到旧父系”的补偿矩阵; object.applyMatrix4( _m1 )把补偿直接写入对象的 position/rotation/scale;- 摘除旧父级、挂入新父级,再
updateWorldMatrix( false, true )向下刷新。
限制:文档与源码注释均明确不支持场景图中存在非均匀缩放的节点。测试用例中专门验证了“attach 前后 world matrix 逐元素相等”(test/unit/src/core/Object3D.tests.js#L456-L511)。仓库内真实使用案例可见 examples/jsm/misc/ProgressiveLightMap.js 将对象在多个光照容器场景间attach,以及 examples/jsm/csm/CSMHelper.js#L185。
六、查找与世界空间查询
属性检索
| 方法 | 说明 |
|---|---|
getObjectById( id ) | 从自身开始深度优先查找,返回第一个 ID 匹配的对象,未找到返回undefined |
getObjectByName( name ) | 同上,按name匹配 |
getObjectByProperty( name, value ) | 按任意属性值匹配,是前两者的底层实现 |
getObjectsByProperty( name, value, result = [] ) | 返回所有匹配对象,结果写入传入数组 |
从源码看(src/core/Object3D.js#L944-L988),查找是严格的“自身优先、子节点递归”的 DFS,getObjectByProperty用===做全等比较。
世界空间分解查询
四个方法都要求传入target复用内存(three.js 零分配惯例),且内部都会先updateWorldMatrix( true, false )保证数据新鲜:
getWorldPosition( target : Vector3 ) : Vector3:从matrixWorld提取平移分量;getWorldQuaternion( target : Quaternion ) : Quaternion:对matrixWorld做decompose提取旋转;getWorldScale( target : Vector3 ) : Vector3:同上提取缩放;getWorldDirection( target : Vector3 ) : Vector3:直接取矩阵第 3 列elements[8..10]并归一化,即对象的“朝向”(注意:对非相机/灯光对象,这是其局部 Z 轴在世界的方向)。
七、坐标转换与朝向
localToWorld( vector ) : Vector3:matrixWorld作用于向量,本地 → 世界;worldToLocal( vector ) : Vector3:对matrixWorld求逆后作用于向量,世界 → 本地(src/core/Object3D.js#L663-L683)。两者都原地修改并返回传入的向量;lookAt( x, y, z ):旋转对象以面向世界空间中的目标点,也接受一个Vector3参数。源码中有两个易踩的细节(src/core/Object3D.js#L694-L734):- 相机与灯光面向目标时是“正轴指向目标”(
lookAt( position, target, up )),而普通对象是“负 Z 轴指向目标”(lookAt( target, position, up )),因此普通网格用lookAt后其正面(-Z)朝向目标; - 若对象有父级,会从
parent.matrixWorld提取旋转并左乘其逆四元数做抵消,最终只改变对象在父坐标系中的朝向; - 限制:不支持父级存在非均匀缩放的场景。
- 相机与灯光面向目标时是“正轴指向目标”(
八、增量旋转与平移 API
所有方法都以四元数/向量运算实现并返回this,可链式调用。旋转类方法(局部空间)内部均为“构造轴角四元数后quaternion.multiply(_q1)”,世界空间版则是premultiply(src/core/Object3D.js#L531-L599)。
| 方法 | 语义 |
|---|---|
rotateOnAxis( axis, angle ) | 沿局部空间指定轴旋转(axis需归一化) |
rotateOnWorldAxis( axis, angle ) | 沿世界空间指定轴旋转;源码注释说明假定父级无旋转 |
rotateX / rotateY / rotateZ( angle ) | 沿局部 X/Y/Z 轴旋转,分别复用静态_xAxis/_yAxis/_zAxis |
setRotationFromAxisAngle( axis, angle ) | 用轴角覆盖当前旋转(写入 quaternion) |
setRotationFromEuler( euler ) | 用欧拉角覆盖当前旋转 |
setRotationFromQuaternion( q ) | 用四元数覆盖当前旋转 |
setRotationFromMatrix( m ) | 从 4x4 矩阵提取旋转(要求上 3x3 为纯旋转、无缩放) |
translateOnAxis( axis, distance ) | 沿局部轴平移:先将轴向量应用对象自身四元数再累加到position |
translateX / translateY / translateZ( distance ) | 沿局部轴平移的快捷形式 |
applyQuaternion( q ) | quaternion.premultiply(q),把一次旋转叠加到对象 |
applyMatrix4( matrix ) | 将矩阵左乘进matrix,再decompose回写 position/quaternion/scale(src/core/Object3D.js#L448-L456) |
“set”系列是赋值语义,rotate*/translate*/apply*是增量语义,二者不要混用。
九、渲染相关属性与回调
| 属性 | 默认值 | 作用 |
|---|---|---|
visible | true | 为true时对象参与渲染;traverseVisible也以它为剪枝条件 |
frustumCulled | true | 为true时对象受视锥剔除;边界情况(如粒子系统跨越视锥)可置false |
renderOrder | 0 | 覆盖默认渲染排序;不透明与透明对象仍各自独立排序,从低到高渲染。设置在Group上时,其所有后代会被聚合到一起排序渲染 |
castShadow | false | 为true时对象被渲染进阴影贴图 |
receiveShadow | false | 为true时对象受场景阴影影响 |
layers | Layers | 层级成员;对象与相机至少共享一个 layer 才可见,也可用于Raycaster拾取过滤 |
customDepthMaterial | undefined | 写深度缓冲时使用的自定义深度材质(仅 Mesh 相关);用平行光/聚光灯投影且顶点着色器修改了顶点位置时必须提供,否则阴影不正确。仅 WebGLRenderer 相关 |
customDistanceMaterial | undefined | 同customDepthMaterial,用于点光源(PointLight)距离阴影 |
static | false | 声明对象在首次渲染后不再变化(含几何与材质设置),渲染器可跳过部分状态检查获得小幅加速。仅 WebGPURenderer 相关 |
animations | Array<AnimationClip> | 对象持有的动画片段数组,AnimationMixer常用 |
userData | Object | 存放自定义业务数据;不要放函数引用,克隆时不会保留 |
up | (0,1,0) | 对象的上方向,影响lookAt姿态;全局默认值即Object3D.DEFAULT_UP |
渲染回调钩子(默认空实现,子类或实例可覆盖):
onBeforeRender( renderer, object, camera, geometry, material, group ):对象渲染前回调;onAfterRender( renderer, object, camera, geometry, material, group ):渲染后回调;onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, group ):写入阴影贴图前回调;onAfterShadow( ... ):写入阴影贴图后回调。
典型用途:RTT 特效、在渲染单对象前切换纹理、逐对象调试。注意它们只对被实际渲染的对象触发。
十、遍历:traverse / traverseVisible / traverseAncestors
traverse( callback ):对对象自身及所有后代逐个执行回调;traverseVisible( callback ):仅对visible === true的对象执行回调,且不可见对象的后代整棵子树直接跳过(src/core/Object3D.js#L1103-L1117);traverseAncestors( callback ):反向,只沿parent链向上执行。
三个实现均为递归且简单直观。文档与源码注释一致强调:不推荐在回调中修改场景图(增删子对象会改变正在遍历的children数组)。
十一、克隆与序列化
clone / copy
clone( recursive = true ) : Object3D:返回new this.constructor().copy( this, recursive )——注意它用当前构造函数实例化,因此Group.clone()得到的是Group;copy( source, recursive = true ) : Object3D:从source拷贝 name、up、position、rotation.order、quaternion、scale、pivot(克隆或置 null)、matrix/matrixWorld、两个 auto-update 标志、matrixWorldNeedsUpdate、layers.mask、visible、castShadow/receiveShadow、frustumCulled、renderOrder、static、animations(浅拷贝数组),并用JSON.parse(JSON.stringify(...))深拷贝userData;recursive为true时对每个子对象clone()后重新add(src/core/Object3D.js#L1605-L1654)。
dispose()用于释放 GPU 相关资源并触发dispose事件;几何体、材质、纹理可能被共享,需要分别释放。
toJSON( meta )
toJSON( meta = undefined ) : Object将对象序列化为 JSON(src/core/Object3D.js#L1284-L1584)。机制要点:
meta为undefined或字符串(JSON.stringify调用时即字符串)时视为根对象:初始化meta = { geometries, materials, textures, images, shapes, skeletons, animations, nodes }去重缓存,并写入metadata = { version: 4.7, type: 'Object', generator: 'Object3D.toJSON' };- 标准字段包括 uuid、type、name、castShadow、receiveShadow、visible、frustumCulled、renderOrder、static、matrixAutoUpdate、layers.mask、matrix(16 元素数组)、up、pivot(非 null 时)、非空时的 userData;
- 子类扩展分支:
InstancedMesh输出 count/instanceMatrix/instanceColor;isScene输出 background/environment;Mesh/Line/Points 通过serialize()缓存 geometry;SkinnedMesh输出 bindMode/bindMatrix/skeleton uuid;材质(单个或数组)同样按 uuid 入缓存;children 递归输出。
反序列化端对应 docs/pages/ObjectLoader.html.md 中的ObjectLoader#parse,两者共同构成 three.js 的对象 JSON 生态。
十二、事件
Object3D继承EventDispatcher,除通用的addEventListener/removeEventListener/dispatchEvent外,层级操作触发以下四类事件(事件对象为模块级单例,复用内存):
| 事件 | 触发者 | 说明 |
|---|---|---|
added | 被添加的对象自身 | 对象被加入其父对象后触发 |
childadded | 父对象 | 新的子对象被加入时触发,事件携带child属性 |
removed | 被移除的对象自身 | 对象从父对象中移除后触发 |
childremoved | 父对象 | 子对象被移除时触发,事件携带child |
典型应用:在父对象上监听childadded/childremoved维护拾取列表或 LOD 注册表。
十二、实战要点小结
- 改完变换要等世界矩阵:
position修改后matrixWorld在下一次updateMatrixWorld前是旧的;getWorldPosition等 API 已自动处理,但手写渲染循环取matrixWorld前需自行保证更新; - 批量静态场景:对不移动的对象可设
matrixAutoUpdate = false并在初始化时手动updateMatrix(),减少每帧 compose 开销; - 跨父级移动对象用
attach而非手动改parent,但需确认链路无非均匀缩放; - 拾取前过滤:
visible控制渲染、layers控制渲染与 Raycaster 双重过滤、frustumCulled只影响渲染剔除,三者职责不同; - 序列化:
toJSON的meta参数在递归序列化多个根对象时可手动传入以共享几何/材质缓存,避免重复输出。
本文全部内容以当前仓库 src/core/Object3D.js 的实现、test/unit/src/core/Object3D.tests.js 的测试断言及 examples/jsm 中的真实调用为证据;文档与源码行为存在差异时,以源码为准。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考