积累变换 Accumulating Transforms

整理用 position、rotation、scale 分量累积世界变换,并实现 world space getter/setter。

积累变换 Accumulating Transforms

在之前的章节讨论中,我们知道传统的变换组合做法是逐级相乘 4×44\times 4 变换矩阵,也就是将每一级节点局部的 Position / Rotation / Scale 转换成 4×44\times 4TRS 矩阵,然后组合

MWorld=MparentMchild\mathbf{M}_{\text{World}} = \mathbf{M}_{\text{parent}}\mathbf{M}_{\text{child}}

如果父节点带 非均匀缩放,再叠加了 旋转,那么矩阵乘法会在结果里混入 剪切 skew 成分,子物体会被 “拉歪”

本章讨论另一种做法:直接按照各 分量 累积变换

struct Transform {
    Vec3  position;   // 平移
    Quat  rotation;   // 四元数
    Vec3  scale;      // 缩放
};

然后我们可以像下面这样合并两个变换 (替代 矩阵相乘)

// a = parent, b = child
out.scale    = a.scale * b.scale;                     // 各分量相乘
out.rotation = b.rotation * a.rotation;               // 注意顺序: child * parent
Vec3 p = a.scale * b.position;        // 先按父缩放缩放子位置
p     = a.rotation * p;               // 再用父旋转旋转它
out.position = a.position + p;        // 最后加到父位置

把这段逻辑 递归 到根节点,就能得到任何节点的 世界变换,最后如果还需要 矩阵,再把总的 position/rotation/scale 组合一次转 矩阵 即可,注意这里使用矩阵乘法顺序

q_total = q_parent * q_child;   // column-vector 写法

因为 UE4 使用 行向量,所以是 child * parent,如果使用 列向量,则需要反过来

这样做的 优点 是:直到完全展开完层级才把 RS 放到同一个矩阵里,因此不会产生 剪切,解决了 “斜扭曲 (skew artifact)” 问题

但是它当然也有 缺点:由于所有缩放都发生在同一空间 world space,所以 子节点 再怎么 旋转,缩放始终沿 世界坐标轴,而不是沿着 “被旋转后” 的 本地轴,结果是模型不会被 剪切,但看起来会 “缩放” 在世界 X/Y/Z 方向,而不是期望的本地方向

// a = parent transform, b = child (or current) transform
Transform CombineTransforms(Transform a, Transform b) {
    Transform out;
 
    out.scale = a.scale * b.scale; // vec3 * vec3, parent scale times child scale
    out.rotation = b.rotation *  a.rotation; // quat * quat, Quaternions multiply in reverse, this is parent times child
 
    // parent scale times child position, rotated by parent rotation:
    out.position = (a.scale * b.position) * a.rotation; // (vec3 * vec3) * quat, quaternions multiply in reverse
    out.position = a.position + out.position; // ve3 + vec3, combine positions
 
    return out;
}
 
Transform GetWorldTransform(Transform transform) {
    Transform worldTransform = transform; // This is acopy, not a reference
 
    if (transform.parent != NULL) {
        Transform worldParent = GetWorldTransform(transform.parent);
 
        // Accumulate scale, Vector * Vector
        worldTransform.scale = worldParent.scale * worldTransform.scale;
 
        // Accumulate rotation, Quaternion * Quaternion
        // Remember, quaternions multiply in reverse order! So:
        // parent times child is written as: child * parent
        worldTransform.rotation = worldTransform.rotation * worldParent.rotation;
 
        // Accumulate position: scale first, Vector * vector
        worldTransform.position = worldParent.scale * worldTransform.position;
        // Accumulate position: rotate next, vector * Quaternion (quats rotate right to left)
        worldTransform.position = worldTransform.position * worldParent.rotation;
        // Accumulate position: transform last, Vector + Vector
        worldTransform.position = worldParent.position + worldTransform.position;
    }
 
    return worldTransform;
}
 
Matrix GetWorldMatrix(Transform transform) {
    Transform worldSpaceTransform = GetWorldTransform(transform);
    return ToMatrix(worldSpaceTransform);
}

获取世界空间 World Space Getters

当选择 积累变换 而不是 积累矩阵 时,获取 世界空间 下的变换变得容易,只需要像上一章介绍的那样逐变换分量的累积即可

Transform GetWorldTransform(Transform transform) {
    Transform worldTransform = transform; // This is acopy, not a reference
 
    if (transform.parent != NULL) {
        Transform worldParent = GetWorldTransform(transform.parent);
 
        // Accumulate scale, Vector * Vector
        worldTransform.scale = worldParent.scale * worldTransform.scale;
 
        // Accumulate rotation, Quaternion * Quaternion
        // Remember, quaternions multiply in reverse order! So:
        // parent times child is written as: child * parent
        worldTransform.rotation = worldTransform.rotation * worldParent.rotation;
 
        // Accumulate position: scale first, Vector * vector
        worldTransform.position = worldParent.scale * worldTransform.position;
        // Accumulate position: rotate next, vector * Quaternion (quats rotate right to left)
        worldTransform.position = worldTransform.position * worldParent.rotation;
        // Accumulate position: transform last, Vector + Vector
        worldTransform.position = worldParent.position + worldTransform.position;
    }
 
    return worldTransform;
}
 
Quaternion Transform_GetGlobalRotation(Transform t){
    return GetWorldTransform(t).rotation;
}
 
Vector3 Transform_GetGlobalPosition(Transform t){
    return GetWorldTransform(t).position;
}
 
Vector3 Transform_GetGlobalScale(Transform t){
    return GetWorldTransform(t).scale;
}
 

设置世界空间 World Space Setters

而此时要设置 世界空间 下的变换,我们必须首先将 父变换世界空间 表示求出,然后对其求逆,并施加给我们想要设置的 目标世界空间变换,这样做会将其转换到对应的 局部空间

父节点世界变换

worldParent = GetWorldTransform(t.parent);

父节点世界变换(LocalInverse)

invScale       = 1.0 / worldParent.scale;
invRotation    = Conjugate(worldParent.rotation);
invTranslation = invRotation * (invScale * (-worldParent.position));
  • 逆旋转 = 共轭
  • 逆缩放 = 1 / scale
  • 逆位移 = 先把 -translation 进行 逆缩放、再进行 逆旋转

最终,把目标世界 SRT 乘到 invParent

worldXForm.position = p;  // 期望的世界位置
worldXForm.rotation = r;  // 期望的世界旋转
worldXForm.scale    = s;  // 期望的世界缩放
 
localXForm = CombineTransforms(invParent, worldXForm);
 
// 把结果写回子节点
t.position = localXForm.position;
t.rotation = localXForm.rotation;
t.scale    = localXForm.scale;

得到最终结果

Transform LocalInverse(Transform t) {
    Quaternion invRotation = Inverse(t.rotation);
 
    Vector3 invScale = Vector3(0, 0, 0);
    if (t.scale.x != 0) { // Do epsilon comparison here
        invScale.x = 1.0 / t.scale.x
    }
    if (t.scale.y != 0) { // Do epsilon comparison here
        invScale.y = 1.0 / t.scale.y
    }
    if (t.scale.z != 0) { // Do epsilon comparison here
        invScale.z = 1.0 / t.scale.z
    }
 
    Vector3 invTranslation = invRotation * (invScale * (-1 * t.translation));
 
    Transform result;
    result.position = invTranslation;
    result.rotation = invRotation;
    result.scale = invScale;
 
    return result;
}
 
void SetGlobalSRT(Transform t, Vector3 s, Quaternion r, Vector3 p) {
    if (t.parent == NULL) {
        t.rotation = r;
        t.position = p;
        t.scale = s;
        return;
    }
 
    var worldParent = GetWorldTransform(t.parent);
    var invParent = LocalInverse(worldParent);
 
    Transform worldXForm;
    worldXForm.position = p;
    worldXForm.rotation = r;
    worldXForm.scale = s;
 
    worldXForm = CombineTransforms(invParent, worldXForm);
 
    t.position = worldXForm.position;
    t.rotation = worldXForm.rotation;
    t.scale = worldXForm.scale;
}
 
void SetGlobalRotation (Transform t, Quaternion rotation) {
    Transform worldXForm = GetWorldTransform(t);
    SetGlobalSRT(t, worldXForm.scale, rotation, worldXForm.position);
}
 
void SetGlobalPosition (Transform t, Vector3 position) {
    Transform worldXForm = GetWorldTransform(t);
    SetGlobalSRT(t, worldXForm.scale, worldXForm.rotation, position);
}
 
void SetGlobalScale(Transform t, Vector3 scale) {
    Transform worldXForm = GetWorldTransform(t);
    SetGlobalSRT(t, scale, worldXForm.rotation, worldXForm.position);
}