获取世界变换 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;
}

也可以使用整条 世界矩阵 的最后一行,但是我们仅用 向量/四元数 也能得到同样结果,而且能少几次 4×44\times 4 矩阵相乘,效率上可能更好,需要注意的是变换施加的 顺序:Scale → Rotate → Translate,对应列向量右乘时矩阵链 T * R * S 的逆序

获取世界(有损)缩放 GetGlobalLossyScale

我们在前面章节讨论过,如果在层级中出现 非均匀缩放 再带旋转,其子变换会被 “扭” 出 剪切 Skew;此时单独拿 “纯缩放” 已经带损失(lossy),因此我们叫它 lossyScale

首先,把自己的 scalerotation 合成同一个 3×33\times3 矩阵

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 这么简单,而是要处理整个 3×33\times 3 旋转-缩放 矩阵

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 把它变回父节点的 局部空间,最终去掉旋转(只取主对角线)就得到我们新的 局部缩放

当然这对于用户很不友好,因为手动写这个 3×33\times 3旋转 × 缩放 矩阵很反直觉,我们可能希望直接输入一个想要设置的 世界空间缩放,我们可以再包一层

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 即可