获取世界变换 World Space Getters
整理 transform hierarchy 中全局旋转、位置、lossy scale 的读取和设置方法。
获取世界变换 World Space Getters
目前为止,我们只在前面的章节讨论了如何获取一个 变换 transform 在 世界空间 中的 矩阵 表示,但我们也想获取 变换 在世界空间中的 位置 position,旋转 rotation,缩放 scale;我们可以获取准确的世界空间 位置 和 旋转,但却很难获取准确的世界空间 缩放
根据前面章节的讨论,我们知道一个 变换 只保留指向父节点的指针,以及本地的 position / rotation / scale,真正的世界空间变换,不管是矩阵形式,还是 “世界旋转”,“世界位置”,“世界缩放” 的形式,都不会时时刻刻缓存,所以当我们想获取它们时,需要沿着父链往上把所有局部变换 累积 起来,前面的章节已经介绍了如何累积 矩阵 形式,本章来介绍如何累积 position / rotation / scale 的形式
获取世界旋转 GetGlobalRotation
Quaternion GetGlobalRotation(Transform t) {
Transform iterator = t.parent;
Quaternion rotation = t.rotation;
while(iterator != NULL){
rotation = iterator.rotation * rotation; // 注意乘法顺序
iterator = iterator.parent;
}
return rotation;
}从当前节点开始,把父节点的 Quaternion 按从根到子的顺序相乘,乘法顺序是 父 * 子,因为在 列向量 右乘约定(GLM 默认)下,左边的旋转先生效
获取世界位置 GetGlobalPosition
Vector GetGlobalPosition(Transform t){
Vector worldPos = t.position; // 先拷贝一份
Transform iter = t.parent;
while(iter != NULL){
worldPos = worldPos * iter.scale; // 先缩放
worldPos = iter.rotation * worldPos; // 再旋转
worldPos += iter.position; // 最后平移
iter = iter.parent;
}
return worldPos;
}也可以使用整条 世界矩阵 的最后一行,但是我们仅用 向量/四元数 也能得到同样结果,而且能少几次 矩阵相乘,效率上可能更好,需要注意的是变换施加的 顺序:Scale → Rotate → Translate,对应列向量右乘时矩阵链 T * R * S 的逆序
获取世界(有损)缩放 GetGlobalLossyScale
我们在前面章节讨论过,如果在层级中出现 非均匀缩放 再带旋转,其子变换会被 “扭” 出 剪切 Skew;此时单独拿 “纯缩放” 已经带损失(lossy),因此我们叫它 lossyScale
首先,把自己的 scale 和 rotation 合成同一个 矩阵
Matrix3 worldRS = ToMatrix(t.rotation) * diag(t.scale);然后 递归 与父节点的 RS 矩阵相乘,得到整条链的 R✕S 结果
// Recursivley concatenate with parent
if (t.parent != NULL) {
Matrix3 parentRS = GetGlobalRotationAndScale(t.parent);
worldRS = parentRS * worldRS;
}最终,用 全局旋转 的 逆矩阵 去掉 旋转分量,只留下 对角线(x、y、z 三轴的最终缩放)
invR_global * RS_global = S_with_skew
lossyScale = Vector3( S_with_skew[0], S_with_skew[4], S_with_skew[8] );这样既解决了父节点旋转-缩放耦合带来的剪切,又能在不追踪 4×4 矩阵的情况下得到近似的世界缩放值,以下是完整代码
Matrix3 GetGlobalRotationAndScale(Transform t) {
Matrix3 scaleMat = Matrix3(
t.scale.x, 0, 0,
0, t.scale.y, 0,
0, 0, t.scale.z
);
Matrix3 rotationMat = ToMatrix(t.rotation);
Matrix3 worldRS = rotationMat * scaleMat;
// Recursivley concatenate with parent
if (t.parent != NULL) {
Matrix3 parentRS = GetGlobalRotationAndScale(t.parent);
worldRS = parentRS * worldRS;
}
// Return scale rotation
return worldRS
}
Vector3 GetGlobalLossyScale(Transform t) {
// Find inverse global rotation (rotation only) of transform
Quaternion rotation = GetGlobalRotation(t);
Matrix3 invRotation = ToMatrix(Inverse(rotation));
// Find global rotation and scale of transform
Matrix3 scaleAndRotation = GetGlobalRotationAndScale(t);
// Remove global rotation from rotation & scale
Matrix3 scaleAndSkew = invRotation * scaleAndRotation; // Mat3 * Mat3
// Return the main doagonal of the scale & skew matrix
return Vector3(scaleAndSkew[0], scaleAndSkew[4], scaleAndSkew[8]);
}设置世界空间 World Space Setters
我们已经知道了如何链式遍历从当前的相对 局部空间 计算出最终 世界空间 下的 变换,但有时候,我们可能需要直接设置某个对象 世界空间 中的 变换,其核心问题在于,应该怎么把目标值 “翻译” 成正确的相对局部值?
核心做法是:把父节点的世界变换求出来 → 取其逆 → 用这个逆变换把 “目标的世界值” 映射回父节点局部空间 → 把得到的结果写进自己的局部字段
设定全局旋转 SetGlobalRotation
void SetGlobalRotation(Transform t, Quaternion rotation){
if(!t.parent){ t.rotation = rotation; return; }
Quaternion parentGlobal = GetGlobalRotation(t.parent);
Quaternion invParent = Inverse(parentGlobal);
t.rotation = invParent * rotation; // ← 转回父节点局部
}思路是
- 算出父节点的世界旋转
R_parent R_local = R_parent⁻¹ · R_desiredWorld- 把
R_local填到 t.rotation
如果是最上层(parent == NULL),那 局部=全局,直接赋值即可
设定全局位置 SetGlobalPosition
想要直接设置 变换 的 全局位置,我们需要对想设置的 全局位置 施加当前对象父对象的 世界变换 的 逆变换,我们可以用 矩阵 形式,也可以像之前那样逐分量 (位置,旋转,缩放) 的操作,就和之前一样,这样做可以节省一些矩阵乘法的开销
由于变换一个 位置 的顺序是:施加 缩放,进行 旋转,最后施加 位移;所以当我们进行 逆变换 时,需要按照反顺序进行
- 反施加位移:
position * -1 - 反施加旋转:四元数的逆(也就是其向量部分的相反数)
- 反施加缩放:
1 / scale
也就是说,正着是 S → R → T,而反着是 T⁻¹ → R⁻¹ → S⁻¹
Vector3 InverseTransformPoint(Transform t, Vector3 p){
if(t.parent){ p = InverseTransformPoint(t.parent, p); }
p -= t.position; // 逆平移
p = Inverse(t.rotation) * p;// 逆旋转
p /= t.scale; // 逆缩放(逐分量除)
return p;
}
void SetGlobalPosition(Transform t, Vector3 pos){
if(t.parent){
pos = InverseTransformPoint(t.parent, pos);
}
t.position = pos;
}设定全局缩放 SetGlobalScale / lossyScale
缩放又是最麻烦的一个,因为父节点如果有 “非均匀缩放 + 旋转”,会把子空间弄成 skew 剪切,因此不能只算 scale.x / parentScale.x 这么简单,而是要处理整个 旋转-缩放 矩阵
void SetGlobalScaleFromRotationScaleMatrix(Transform t, Matrix3 rsMat){
t.scale = Vector3(1,1,1); // 先清掉自己的缩放
Matrix3 globalRS = GetGlobalRotationAndScale(t); // 所有父节点的 RS
Matrix3 invGlobal = Inverse(globalRS); // 取逆
Matrix3 localRS = invGlobal * rsMat; // 转回局部
t.scale = Vector3(localRS[0], localRS[4], localRS[8]); // 读对角线
}rsMat 是我们想要设置的 世界空间 中的 旋转 × 缩放 矩阵,通过 invGlobal * rsMat 把它变回父节点的 局部空间,最终去掉旋转(只取主对角线)就得到我们新的 局部缩放
当然这对于用户很不友好,因为手动写这个 的 旋转 × 缩放 矩阵很反直觉,我们可能希望直接输入一个想要设置的 世界空间缩放,我们可以再包一层
void SetGlobalScale(Transform t, Vector3 scale){
Quaternion globalRot = GetGlobalRotation(t); // 当前世界旋转
// 把 (1,0,0)、(0,1,0)、(0,0,1) 三条基向量先各自乘以缩放,再套到全局旋转上
var x = Vector3(scale.x,0,0) * globalRot;
var y = Vector3(0,scale.y,0) * globalRot;
var z = Vector3(0,0,scale.z) * globalRot;
Matrix3 rs = Matrix3( x.x,x.y,x.z,
y.x,y.y,y.z,
z.x,z.y,z.z );
SetGlobalScaleFromRotationScaleMatrix(t, rs);
}此时我们只需要直接输入 Vector3 scale 即可