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.