Three.js 矩阵变换:position/quaternion/scale 和 matrix 到底怎么配合
原文出处:Three.js Manual -- Matrix transformations
本文基于官方手册「Matrix transformations」章节整理,用通俗方式带你看懂:Three.js 怎么用一个 4×4 矩阵存下位置/旋转/缩放、两种改变换的方式分别怎么用、对象矩阵和世界矩阵差在哪,以及为什么旋转永远用四元数存。
写在前面
你在 Three.js 里让一个物体移动、旋转、缩放,本质上都是在操作它的矩阵(matrix) 。每个 Object3D 身上都有一块矩阵,把「位置、旋转、缩放」这三件事编码成一个 4×4 数学对象。这篇就讲清楚:这矩阵从哪来、怎么改、对象矩阵和世界矩阵有什么区别、以及旋转为什么老跟 quaternion 绑在一起。
顺带呼应一下前面「运行时更新」那篇:
matrixAutoUpdate这套开关,本篇会讲得更底层。
一、矩阵是什么:一个 4×4 存下三大变换
Three.js 用矩阵来编码三维变换------平移(position)、旋转、缩放。每个 Object3D 实例都有个 matrix,存的就是这个对象的位置、旋转和缩放。
更新一个对象的变换,有两条路可走。
二、方式一:改便捷属性,让引擎重算矩阵(最常用)
绝大多数情况你都用这套:直接改 position、quaternion、scale 这几个「便捷属性」,让 three.js 自己从它们重算出矩阵。
js
object.position.copy( start_position );
object.quaternion.copy( quaternion );
默认情况下,matrixAutoUpdate 是 true,矩阵会被自动重新计算。
但如果你确定对象是静态的,或者想自己掌控「什么时候重算」,把它关掉能获得更好的性能:
js
object.matrixAutoUpdate = false;
关掉之后,改完属性要手动刷新矩阵:
js
object.updateMatrix();
这正是「运行时更新」那篇提过的套路:静态对象关掉自动更新,只在需要时
updateMatrix()一次,省掉每帧的矩阵运算。
三、方式二:直接改 matrix(进阶)
如果你更习惯直接操作矩阵本身,Matrix4 类提供了一堆方法。比如从四元数造旋转、再设位置:
js
object.matrix.makeRotationFromQuaternion( quaternion );
object.matrix.setPosition( start_position );
object.matrixAutoUpdate = false;
matrixAutoUpdate必须 设为false------否则引擎每帧又从position/scale重算,把你手改的矩阵覆盖掉;- 千万别调
updateMatrix()。一旦调用,它会从position、scale等属性重新算矩阵,把你手动做的修改全冲掉。
换句话说:方式一和方式二互斥------要么让引擎从属性算矩阵,要么你自己改矩阵并关掉自动重算,二选一。
四、对象矩阵 vs 世界矩阵
这是新手最容易混的概念:
- 对象的
matrix:存的是相对于父级 的变换。如果一个物体挂在scene下,它的父级是场景,那 matrix 基本就是「世界里的样子」;但如果它挂在另一个会动的物体(比如一个 Group)下,它的 matrix 只是「相对那个父物体」的偏移。 - 世界矩阵(world matrix) :要拿到对象在世界坐标下的真正变换,得访问它的世界矩阵。
当父级或子级的变换发生变化时,可以调用下面这个方法,让子对象的世界矩阵刷新:
js
object.updateMatrixWorld();
另外,可以通过 applyMatrix4() 直接用一个 Matrix4 去变换对象。但要注意一个坑:这个方法底层依赖 Matrix4.decompose()(把矩阵分解回 position/quaternion/scale),并不是所有矩阵都能这么分解 。例如:当对象有一个非均匀缩放(non-uniform scale)的父级 时,它的世界矩阵可能无法分解,applyMatrix4() 就不适用了。
划重点:非均匀缩放(
scale三个分量不相等,比如 x=2、y=1、z=1)会"污染"子节点的世界矩阵分解,做矩阵运算时要特别小心。
五、Rotation 与 Quaternion:为什么旋转永远用四元数
Three.js 表示三维旋转有两套:
- 欧拉角(Euler angles) :直观,但有个著名毛病叫万向节死锁(gimbal lock)------某些角度配置下会丢失一个旋转自由度,导致物体绕某一轴转不动。
- 四元数(Quaternion):没有万向节死锁问题。
所以 Three.js 的规则是:对象的旋转永远存在它的 quaternion 里,欧拉角只是个方便你输入的界面。
早版本里有个 useQuaternion 属性:设成 false 就让矩阵从欧拉角算。这个做法已经被废弃。现在想从欧拉角设旋转,用这个方法,它会同步更新四元数:
js
object.setRotationFromEuler( euler );
记忆法:你可以用欧拉角「下达指令」,但引擎内部始终以四元数为准存储和执行。
总结
矩阵变换速查:
| 主题 | 要点 | 代码/开关 |
|---|---|---|
| 存储 | 每个 Object3D 用 4×4 matrix 存 position/rotation/scale |
--- |
| 方式一 | 改 position/quaternion/scale,引擎重算矩阵 |
matrixAutoUpdate=true(默认),静态可设 false 后手动 updateMatrix() |
| 方式二 | 直接改 matrix |
matrixAutoUpdate=false 且绝不调 updateMatrix() |
| 对象 vs 世界 | matrix 相对父级;世界变换要刷世界矩阵 |
updateMatrixWorld() |
| 旋转 | 永远存于 quaternion,欧拉角只是输入界面 |
用 setRotationFromEuler() 替代废弃的 useQuaternion |