A Much Needed Update to the 2D Skeletal System
Fri 26 November 2021The 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.
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: