Transform Hierarchies 变换的层次结构

介绍 local space、world space 以及层级变换累积的基本概念。

Transform Hierarchies 变换的层次结构

在图形引擎里,如果想让物体正确地跟随父级节点做平移、旋转、缩放,就需要把每个节点的 位置 position旋转 rotation,和 缩放 scale 组织为一颗层级树,并在渲染前将它们积累成 4×44\times 4 矩阵

struct Transform {
    Transform parent;   // 父节点(引用或指针)
    Vector   position;  // 平移
    Quaternion rotation;// 旋转
    Vector   scale;     // 缩放
};

每个节点记录自己的 位移旋转四元数缩放向量,并持有一个到 父节点的引用 这样层层向上就能追溯到根节点

由于 GPU / 图形 API 最终只认 4×4 矩阵,所以要将 Transform 转成矩阵

  • 先用 旋转四元数 把三个单位轴 (1,0,0), (0,1,0), (0,0,1) 旋转出物体在世界空间中的 基向量,这一步也可以直接使用我们在 四元数 笔记中介绍过的 公式展开法 直接展开四元数为对应的 旋转矩阵

  • 然后分别对 基向量 执行 scale.x / scale.y / scale.z 的缩放,这一步可以构造缩放矩阵

  • 最后把平移 position 放到矩阵最后一列,得到

    | x.x  y.x  z.x  0 |
    | x.y  y.y  z.y  0 |
    | x.z  y.z  z.z  0 |
    | t.x  t.y  t.z  1 |

    这样一个矩阵就同时编码了平移、旋转、缩放

Matrix ToMatrix(Transform transform) {
    // First, extract the rotation basis of the transform
    // 也可以直接使用 四元数公式展开法 直接得到旋转矩阵
    Vector x = Vector(1, 0, 0) * transform.rotation; // Vec3 * Quat (right vector)
    Vector y = Vector(0, 1, 0) * transform.rotation; // Vec3 * Quat (up vector)
    Vector z = Vector(0, 0, 1) * transform.rotation; // Vec3 * Quat (forward vector)
    
    // Next, scale the basis vectors
    // 如果上一步使用了 公式展开 此处应该构造缩放矩阵
    x = x * transform.scale.x; // Vector * float
    y = y * transform.scale.y; // Vector * float
    z = z * transform.scale.z; // Vector * float
 
    // Extract the position of the transform
    Vector t = transform.position;
 
    // Create matrix
    return Matrix(
        x.x, x.y, x.z, 0, // X basis (& Scale)
        y.x, y.y, y.z, 0, // Y basis (& scale)
        z.x, z.y, z.z, 0, // Z basis (& scale)
        t.x, t.y, t.z, 1  // Position
    );
}

层级累积 Hierarchy Accumulation

由于

子节点的世界矩阵 = 父节点的世界矩阵 × 子节点的局部矩阵

所以顺序正确与否、是否把非统一缩放混进旋转,都会带来各种“奇怪的插值、体积剪切”等副作用,我, 后面将会继续讨论这些细节,例如

  • 如何高效维护“局部 / 世界”两套变换并懒更新(dirty flag)
  • 如何避免缩放-旋转耦合导致的非正交基向量
  • 获取全局 / 局部位置、旋转、缩放时的注意事项