Understanding child-parent transformation

Greetings!

I’m working on a simple editor for my engine that lets you pick objects and move them around. All that I got working fine until I tried to do parent-child relations, where the child would move and rotate along with the parent.

I managed to have the child move and rotate along with the parent, but I’m having trouble when it comes to moving the child independently. Children translates correctly, but rotation is totally busted :smiley:

I really appreciate it if could see how I’m doing things and tell me if there’s a mistake in my interpretation/understanding of things.

Here’s how I’m storing the transform values:



struct transform
{
    v3 Position;
    m4 Rotation;
    r32 Scale; // uniform
    transform *Parent;
};


From what I understood, in order to get the proper ModelToWorld matrix for the child, I have to multiply the child’s transform with its parents’. Something like this:



internal inline m4
TGetTRS(transform *T)
{
    m4 Result;

    transform *Parent = T->Parent;
    if (Parent)
    {
        m4 ParentTR = m4_Translate(&Parent->Rotation, Parent->Position);
        Result = m4_Scale(&ParentTR, Parent->Scale);
    }
    else 
        Result = m4_Identity;

    m4 TR = m4_Translate(&T->Rotation, T->Position);
    m4 TRS = m4_Scale(&TR, T->Scale);
    Result = Result * TRS;

    return (Result);
}

This seems to work in that when I move/rotate the parent around, the child moves/rotates correctly.

However; I’m not really sure how all this parent-child stuff affects the local movement of the child.
From my understanding (correct me if I’m wrong), when an object becomes a child of another, all its movements become relative to that object, i.e. the parent now is the origin of its new coordinate system/space. So if parent was at (1,1,0) and child(local) = (2,1,0) that means child(global) = (3,2,0).

So I don’t have to do much to achieve local movement, so when I translate by an amount vector I just add directly to the child’s position:


internal inline void
TMove(transform *T, v3 Movement, coordinate_space RelativeTo = Space_World)
{
    if (RelativeTo == Space_Local)
        Movement = TLocalDirection(T, Movement);

    T->Position += Movement;
}

internal inline v3
TLocalDirection(transform *T, v3 WorldDirection)
{
    m4 Rotation = TGetGlobalRotation(T); // I'll show this in a bit
    v3 LocalDirection = m4_Mulv(&Rotation, WorldDirection, 0); // 0 for W component
    return (LocalDirection);
}


Is that correct?

I just have to make sure that I adjust the position vector correctly when I first parent the object. Something like:


internal void
TSetParent(transform *Child, transform *Parent)
{
     Child->Position = Child->Position - Parent->Position; // convert to local position
     // adjust rotation as well ...
     Child->Parent = Parent;
}

I guess that code is incomplete when it comes to switching parents. I guess it would be adjusted to


 Child->Position = TGetGlobalPos(Child) - TGetGlobalPos(Parent);

Where TGetGlobalPos will take into consideration the positions of parents:


internal inline v3
TGetGlobalPos(transform *T)
{
     v3 Result = T->Position;
     if (T->Parent)
          Result += TGetGlobalPos(T->Parent);
     return (Result);
}

Is that correct so far?

Now for rotation, do we need to handle it any differently than how we handled translation? i.e. to get the global rotation we look up to our parents and multiply our rotation with theirs’:


internal inline m4
TGetGlobalRot(transform *T)
{
    m4 Result = T->Rotation;

    if (T->Parent)
        Result = TGetGlobalRot(T->Parent) * Result;

    return (Result);
}

And so if I wanted to rotate an object ‘independently’ of its parent I just directly change its rotation matrix because that its’ local rotation if I understand correctly:


internal inline void
TRotate(transform *T, r32 Angle, v3 Axis)
{
    T->Rotation = m4_Rotate(Angle, Axis) * T->Rotation;
}

We also have to remember to adjust the rotation when we change parent, so (in place of my comment in TSetParent):


Child->Rotation = TGetGlobalRo(Child->Parent) * TGetGlobalRot(Child);

But is that even correct?

Hopefully you can see I sort of know what I need to do but just not sure exactly how to do it.

Is the current way of storing a transform good or should I use a single 4x4 matrix instead? (Currently with the way I’m doing it, I guess I don’t need an 4x4 matrix for the rotation, just a 3x3) - Am I missing something? I read in some sources that I need to apply the inverse of the parents’ matrices, but I’m not sure how that fits into the picture. Also what about storing children pointers instead of a parent pointer, does that make any difference/make it easier to transform things or no?

I really appreciate any help/advice/explanations and corrections if necessary on my understandings/interpretations.

Thanks!
-Keithster

Hmmm, I guess I can see one mistake, the TGetTRS function is dealing directly with ->Position and ->Rotation. I guess it doesn’t even need to consider the parent anymore if it uses the TGetGlobalXXX functions cause they’re already considered in those functions correct? Like so:


internal inline m4
TGetTRS(transform *T)
{
    m4 Result;

    m4 TR = m4_Translate(TGetGlobalPos(T)) * TGetGlobalRot(T);
    Result = m4_Scale(&TR, TGetGlobalScale(T));

    return (Result);
}

That looks more correct to me…

[Edit] I know the TSetParent is incomplete, I have to add checks to see if the new parent is null or of it’s the same parent etc.

So with the current code I posted, here are the results:

The first box has no parent, it transforms correctly.

The second box has Dr Freak as a parent. It translates correctly, but rotation is totally busted.

Also, when Dr Freak moves, the box moves along correctly. But when he rotates, the box rotates in place and not around the Dr which is incorrect.

What am I doing wrong?

A few points:

  1. For a transformation hierarchy, the global (absolute) transformation for a child is found by multiplying its parent’s global transformation by the child’s local (parent-relative) transformation. Gc=Gp*Lc. It’s not entirely clear, but it looks like TGetTRS() is using the parent’s local transformation, which only works if the parent is the root node.

  2. If you move an object’s position in the hierarchy (i.e. change its parent), but want to keep its global transformation, you need to re-compute its local transformation from its global transformation and the new parent’s global transformation via the inverse of 1: Lc=Gp-1*Gc. If you’re not storing the global transformation, that would be calculated from the old parent via 1.

  3. If you’re storing decomposed transformations (translation, rotation, scale), you need to be clear on the order in which those operations are applied. Translate-then-rotate/scale is typical; changing an object’s rotation or scale doesn’t change the direction or length of the translation, while changing the translation translates the rotation axis and scale origin with it (so those remain fixed relative to the object). The order of scale relative to rotation doesn’t matter.

4.You need functions to multiply two decomposed transformations and to invert a decomposed matrix, each giving a decomposed result. You need a function to convert a decomposed transformation to a 4x4 matrix (e.g. for rendering). Note that scale must be uniform (you can’t have different X/Y/Z scales).