A Much Needed Update to the 2D Skeletal System

The Call to Refactor

    When I published my last article on the 2D Skeletal System, I had a friend bring up a good point about the method I used to rotate the sprites.

The Critique.

    Yeah, let's fix that. After brushing up on my precalculus notes from last year on matrices, I was passing a single mat4 into the shader so the game code can do all the transformations instead. This conveniently set me up to be able to tackle the next thing I wanted to implement in my game: nested sprite parenting. Since humans have many joints, it would make sense that you can move your arm, which moves your fingers, which can still be independently moved. Thank god for this, I wouldn't want my hand falling off if I moved my arm. Anyway, This meant that some math was going to be needed.

Attempt One

    Alright, seems simple enough, I can just iterate through parent sprites and keep applying rotation. Nope. The child sprites just rotated in place, which made sense. I then realized I need to translate the sprite around the local parent's anchor point of rotation. Still nope, I tried doing this recursively, but it broke it more. I then thought I can just rotate the child's anchor point of rotation around the parent's, and sure enough, this actually positioned the anchor points correctly, but the offset from the anchor point was still messed up, and it didn't look right.

Attempt Two

    All that fiddling around actually lasted for three days or so. I eventually got frustrated and gave up for a little while, but when I was heading to bed, I got an idea for the rotations, and quickly sketched it down on my phone to try the next day. Sure enough, this worked. The child anchor point can stay, but the sprite has to recursively be translated to the parent's anchor point, rotated about the parent's calculated anchor point (recursively rotated about the parent's anchor points) by the parent's rotation. Finally, it can then be scaled. That explanation was probably horrible, so just look at the code.

The Rotation Code

void GameSprite::Transform( Transform2D sMasterTransform )
{
        apSprite->aMatrix = glm::translate( glm::vec3( apSprite->aTransform.aPos + sMasterTransform.aPos, 0.f ) );
    if ( aChild )
    {
        for ( auto pParent = aParent; pParent != NULL; pParent = pParent->aParent )
        {
            apSprite->aMatrix = glm::translate( apSprite->aMatrix, glm::vec3( CalculateAnchor( pParent ) *
                                              ( pParent->apSprite->aTransform.aScale *
                                                sMasterTransform.aScale ), 0.f ) );
            apSprite->aMatrix = glm::rotate( apSprite->aMatrix, pParent->apSprite->aTransform.aAng, glm::vec3( 0.f, 0.f, 1.f ) );
            apSprite->aMatrix = glm::translate( apSprite->aMatrix, -glm::vec3( CalculateAnchor( pParent ) *
                                               (  pParent->apSprite->aTransform.aScale *
                                                  sMasterTransform.aScale ), 0.f ) );
        }
    }
    /* Translate rotation point to origin.  */
    apSprite->aMatrix = glm::translate( apSprite->aMatrix, glm::vec3( aAnchor * ( apSprite->aTransform.aScale * sMasterTransform.aScale ), 0.f ) );
    /* Rotate the sprite.  */
    apSprite->aMatrix = glm::rotate( apSprite->aMatrix, apSprite->aTransform.aAng, glm::vec3( 0.f, 0.f, 1.f ) );
    /* Translate back to the rotation point.  */
    apSprite->aMatrix = glm::translate( apSprite->aMatrix, -glm::vec3( aAnchor * ( apSprite->aTransform.aScale * sMasterTransform.aScale ), 0.f ) );

    apSprite->aMatrix = glm::scale( apSprite->aMatrix, glm::vec3( apSprite->aTransform.aScale * sMasterTransform.aScale, 1.f ) );
}
/* Iterates through parent sprites and rotates anchor points until child has proper orientation.  */
glm::vec2 GameSprite::CalculateAnchor( GameSprite *spSprite )
{
    if ( spSprite->aChild )
    {
        glm::vec2 v = spSprite->aAnchor;
        auto pParent = spSprite->aParent;
        while ( pParent != NULL )
        {
            /* Magic.  */
            v += ( pParent->apSprite->aTransform.aPos * pParent->apSprite->aTransform.aScale )
                - ( pParent->aAnchor );
            float s = sin( pParent->apSprite->aTransform.aAng );
            float c = cos( pParent->apSprite->aTransform.aAng );
            glm::mat2 m = glm::mat2( c, s, -s, c );
            v = m * v;
            v -= ( pParent->apSprite->aTransform.aPos * pParent->apSprite->aTransform.aScale )
                - ( pParent->aAnchor );

            pParent = pParent->aParent;
        }
        return v;
    }
    return spSprite->aAnchor;
}

    It's still a little messy, but I'm glad that it works. I immediately got to animating that vector sprite I showed off on the creations page. It didn't take long for me to have a moving, breathing character in Crosslight!

    In the code above, you'll see mentions of a "master transformation" which is really just transformation the whole sprite including all it's animation transformations. This allows me to scale the whole thing down, and position it elsewhere on the screen, which is visible in the new demo:

New Demo