Sunday, February 10, 2013

Animating 2D Sprites in OpenGL

In working on my latest game, Praecursor, it came time to develop a system for easily animating sprites on the screen. I found a very nice sprite for a hero on OpenGameArt, and wanted to integrate his various animations into player actions. In the process, I created a CAnimation class that would extend the CRigidBody class, making it eligible to act just like a physical entity and be rendered on the screen with relative ease. The entire process was fairly challenging, requiring a custom file format parser and a new fragment shader. The following is a tutorial-like thought process that went into the development of animate-able sprites.

Layout

The CAnimation class is supposed to act exactly like a regular physical entity, but should support loading of custom .icanim files and have various functions related to animation. These include toggling animation, setting an automatic animation rate, quick-swapping sprite sheets, and manually switching sprites.

Quick-swapping seems like a pointless functionality of an animation class; after all, why not just create a separate instance and load it with the new sprite sheet? Well, I didn't think about this until I started trying to swap-out animations for running, jumping, and standing with the main player instance. When I would just have a list of animations and do m_Player = m_allAnimations[JUMP], the player would lose his physics properties, such as gravity or jump force. I tried a few workarounds, but none of them turned out like expected, so I decided to add a SwapSpriteSheet() method to the CAnimation class. This will attach a new texture to the material and give the shader new parameters based on width and heights.

In the future, I think I will change the class to incorporate animation boundaries, so I don't need to load a separate image for each animate-able action. I would be able to do something like SetAnimationIndex(0, JUMP), and the animation would only loop through sprites [0:JUMP], then if I wanted to just play the standing animation, I could do SetAnimationIndex(JUMP + 1, STAND), assuming the standing animation comes after the jumping one in the sprite sheet. This would likely all but eliminate the need for the swapping method.


Here is a snippet of the CAnimation class we will be developing in this post:


.icanim Files and Loading Animations

IronClad uses a custom header attached to a generic Targa (.tga) image file to convey sprite sheet information. The header looks something like this (without brackets and quotes):
"[integer image width] [integer image height] [horizontal sprite count] [vertical sprite count][Raw TGA data]"

A future revision may eliminate the total w/h parameters, since those are found when the TGA data is loaded anyway.

The image loading method is as follows:

The methods with loading a mesh and inserting it into a vertex buffer on the GPU are pretty specific to my engine, but I'm sure others reading this post as a guide will be able to adapt this to their own methodologies.

Texture Offsets and the Animation Shader

In the loading method, we specified that the texture coordinates are in the [0, 1] range. This would map the entire sprite sheet texture onto the individual sprite meshes, something clearly undesirable. The reason for the [0, 1] range is so that it is properly interpolated within the fragment shader, whereas the real work of figuring out exactly where in the sheet we will be is done by passing uniforms that specify the current sprite offset and the individual sprite dimensions.

The fragment shader looks like this:

Texture offsets are calculated via special uniform values. The tc_offset uniform represents individual sprite dimensions expressed as a tex-coord range. For example, a 32x32 sprite would be <0.03125, 0.03125>, calculated as 1 / w, 1 / h. This would then be interpolated from [0, 0.03125], properly rendering all of the texels in the desired sprite. The tc_start uniform represents the starting offset within the sprite sheet. For example, if you wanted the 2nd sprite of the 3rd row of a sprite sheet with 16x16 sprites, you'd pass <2/16, 3/16> to the fragment shader from your program. You can see this being done for the first time in lines 99 and 100 in the LoadFromFile() method above, and below when I show the NextSprite() method that animates the sprites.

Animating

Each time m_delay is reached, we want to iterate over to the next sprite. This is handled within Update(), and I'm showing here how the new texture offset is calculated each time. 


So for the first sprite, the shader will display the texels in the range [0, 1/w] .. [1/w, 1/h], right? Then for the next one, we give it an offset of <1/w, 1/h> so now we are rendering texels [1/w, 2/w] .. [2/w, 2/h], which corresponds to the second sprite in the atlas. I just realized that this will likely only work with single-row sprite sheets, so I may need to tweak this code somewhat. I currently only exclusively use single-row animations for the hero.

Quick Swapping Sprite Sheets

If I want to change the hero from a standing animation to a jumping one, I can (assuming a list of valid animations) do m_Player.SwapSpriteSheet(m_Animatons[JUMP]->GetHeader()) and from then on, the renderer will use a new texture and have new shader parameters for rendering. Here is the method in action:

In order to minimize data transfer to the shader, the sprite size is passed only a single time, on loading and on swapping. Otherwise, only the offset is passed.

Summary

So there is a technical inside-look at how sprite animations are implemented in IronClad. You can examine the full header and implementation files here and here, respectively. This stuff will be tweaked and perfected in the future, so this post may not completely accurately reflect the inner workings of the engine. 
Please feel free to add constructive criticism, questions, or general thoughts in the comments below!

1 comment:

  1. I was pretty pleased to uncover this great site.

    I need to to thank you for your time just for this
    fantastic read!! I definitely loved every bit of it and i also have
    you saved to fav to look at new information on your web site.



    My weblog; just click the next document

    ReplyDelete