1
\$\begingroup\$

I'm working on an object picking and translation/rotation/scale gizmos system in my engine where you select objects and perform transformations on them in a 3d editor-like fashion (e.g. Blender)

In my transform struct, I'm storing the position, rotation angle, rotation axis and a uniform scale value. When transforming my objects, I build the TRS matrix which forms my ModelToWorld matrix. All good.

Now I want to add the notion of local vs world translation. i.e. you select an object, and you can either move it along its local x axis, or the global x axis.

The global movement is easy I just add directly to my position vector. But for the local one, I have to build a local forward/right/up vector. e.g.

v3 GlobalUp = V3(0, 1, 0);
v3 LocalUp = m4_Rotate(Rotation->Angle, Rotation->Axis) * GlobalUp;

And then I translate my position by that amount:

Selection->Transform.Position += LocalUp;

I'm kind of torn whether I should store these local up/forward/right vectors in my transform (which means I have to calculate them once every frame in my update function, but every use of them after that point needs no calculation) or only when needed (no need to calculate every frame, but I have to call a function to do the calculation every time I need one, I could still keep the value in a local variable in my usage area if I'm using it more than once)

I'm not sure what approach to go with but I'm leaning towards calculating them as needed.

Are there any other pros/cons to each approach? or is this one of those things where there's no 'absolute right' way to do it?

Any advice is appreciated!

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I'm incline to suggest there is no 'right way' to do this but I think there are 'better ways' and some considerations to make when implementing your own system.

Should an object's transform be authoritatively a world-space matrix, or a local-space matrix?

If you are going to have deep, complex hierarchies, storing a local-to-parent transform (or separate components) as the authority is preferable. The alternative requires transforming a child's world matrix by the inverse of the parent, which is a costly operation and can introduce additional, unnecessary floating point errors. Instead you can calculate the world matrix on demand, concatenating with the parent's world matrix as needed. Local space transformations are then simple enough, and world-space transformations are performed relative to the parent's matrix.

Should an object's transform be a matrix, or the components of a matrix?

It certainly is cheaper to have a matrix, but I would argue that keeping the components separate make more sense here because, again, once a matrix is built the components become effected by floating point errors, as well as interference with one another. Furthermore, when manipulating just a transform, you have to continually renormalize the matrix because the accumulated floating point error will denormalize your matrix, and it will stop being orthogonal. While you can extract scale from the matrices, it is easier to just keep it distinct. I didn't do that in my editor, and sometimes it frustrates me to no end. I extract the Translation, Rotation and Scale (TRS) components when the object is selected, and then manipulate those to build the matrix. Instead I think I would keep the data separate if I did it again.

How to keep it all in sync?

So I think the format of your matrix should look something like this:

class Transform
{
    // Private members behind accessors
    Matrix worldCache;
    Matrix localToParentCache;
    Vector3 Translation; // Position in local X,Y,Z axis
    Vector3 Scale; // Scale in local X,Y,Z axis
    Quaternion Rotation; // rotation axis & magnitude
    enum DirtyFlags
    {
        WorldDirty = ( 1 << 0 ),
        LocalDirty = ( 1 << 1 ),
    }
    DirtyFlags dirtyFlags = WorldDirty | LocalDirty;
}

Encapsulate the data into a class, and hide the worldCache & localToParentCache behind accessors. Use setters on the scale, position & rotation to set the dirty flags, Whenever the position, rotation or scale change, to invalidate the caches. When a parent moves, invalidate the worldCache (but not the localToParentCache). On access rebuild the matrices as required.

Here are some sample methods:

class Transform
{
    // setters on the values:
    public Vector3 Translation
    {
        get { return Translation; }
        set { dirtyFlags |= ( WorldDirty | LocalDirty ); MarkChildrenAsDirty(); Translation = value };
    }

    void MarkChildrenAsDirty()
    {
        // when a node moves, all of it's children's world matrices are invalidated
        // so they need to be marked.
        for ( all children )
        {
            child.dirtyFlags |= WorldDirty;
            child.MarkChildrenAsDirty(); // recurse into children's children
        }
    }

    // now the matrices
    public Matrix World
    {
        get { if ( dirtyFlags & WorldDirty ) RecalculateWorldMatrix(); return worldCache; }
    }

    public Matrix LocalToParent
    {
        get { if ( dirtyFlags & LocalDirty ) RecalculateLocalMatrix(); return localToParentCacheDirty; }
    }

    // And the recalculation:
    private void RecalculateWorldMatrix()
    {
        worldCache = Parent.World * LocalToParent; // depends on your order
        ClearDirty( WorldMatrixDirty );
    }

    private void RecalculateLocalMatrix()
    {
        // build matrix using whatever code you would use
        localToParentCache.MakeMatrix( Translation, Scale, Rotation );

        ClearDirty( LocalMatrixDirty );

        // make world matrix dirty now
        MarkDirty( WorldMatrixDirty );

        // And the children
        MarkChildrenAsDirty();
    }
}

An important consideration here is that when an object has it's world matrix affected, we need to broadcast that dirty state on to the children, since their cached world matrix is invalid - the parent has moved. When any child has it's world matrix requested, it will rebuild the hierarchy from that point up to the first non-dirty world matrix.

This requires the manipulators/gizmos to understand this kind of transform complexity, instead of just performing transforms agains the matrix, but I think it is more concise in and editor. Obviously in a game, at runtime you wouldn't have all of these things if you were not editing the object.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the reply. About the dirty flags, worldCacheDirty I assume this would be true when somebody tries to transform the object in a way, and then in my entity update function I would have something like if (entity->worldCacheDirty) { update entity->worldCache; entity->worldCacheDirty = false; } correct? and same thing for the parent flag when someone changes parent? And then when somebody asks to get the position of an entity, I would have to see if the entity has a parent, in that case I return localToParentCache * Translation otherwise worldCache * Translation correct? \$\endgroup\$
    – vexe
    Dec 30, 2015 at 22:52
  • \$\begingroup\$ @vexe - I would not do it in the update - I would do it in the accessor as I show in the edit above. This way, unless someone requests the data the transformation isn't performed. I've combined the flags into an enum as it is a bit easier for me to read and easier to set both the WorldDirty and LocalDirty at once. \$\endgroup\$
    – Steven
    Dec 30, 2015 at 23:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .