Transform Hierarchies

The transform hierarchy is a core concept for most games / engines. It's pretty easy to slap together something that multiplies some matrices together and works. But to have a versitile transform hierarchy there is some subtlety to the math that often goes overlooked.

In this blog we're going to explore two approaches to building a transform hierarchy, and the unexpected artifacts they produce. Both of these methods will represent a transform as three seperate components: position, rotation and scale. All transforms will be orgonized as a tree / hierarchy.

As expected, the final &qout;world&qout; position of a transform will be the effect of all it's parent's accumulated transformations added to it's own. We will represent a transform in code with a structure similar to the following:

struct Transform {
    Transform parent; // Reference

    Vector position;
    Quaternion rotation;
    Vector scale;
}

Graphics API's expect the transformation of an object to be passed as a matrix. Therefore, we need a way to convert a Transform structure to a 4x4 Matrix. To do this conversion, first extract the basis vectors from the rotation of the transform. Next, scale the basis vectors by the scale of the transform. Finally, stuff the basis vectors and the position vector into a Matrix. The code below demonstrates how to do this:

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
    );
}

This makes for the start of a good transform hierarchy. Next, we need to decide how transforms are accumulated, how global or local position / rotation / scale are assigned and how they can be retrieved. The rest of this blog will focus on these topics and the subtle artifacts they introduce.