I have been trying to write a blog post about texture atlases forever now, but I keep getting distracted and then I forget I have a draft up here waiting for me.
SO! I am going to be doing a series of posts about some basic openGL|ES stuff on the iPhone and how to make your cocoa-based core animation application (or game) perform better. Ultimately I will tell you that you should switch to openGL, but for now I am going to keep it simple and talk about core animation layers and texture atlasing. (and for now, we will only be talking about 2d stuff at 1:1 rez, so no mip mapping of the atlases for those of you who are reading ahead, as that gets more complicated than we need right now)
Why do I want to share this information with you? Well, the first game I did on the iPhone: Snowdude was a big ole 2D sprit animation game. I originally did the entire thing in Core Animation and it got about 4 fps, which was not enough :-) So I switched to openGL and could easily get 30 or 40 (though I only needed to do 12 because that was what all the sprite animations were built at), so this is the story of that transition.
The first hurdle we will cross will not require me to get into openGL just yet, but talk about how to optimize your sprite animations, even within CA, so if you only need a teensy bit of performance, you might be able to just get away with this and not actually make a full leap into openGL.
So, lets say I have a sweet animation of a dude face planting that looks like so: (Oh and BTW big mad props to the Lycette Bros makers of the aforementioned Snowdude for letting me use their raw graphics for examples)

10 frames, so 10 images.
if I want to turn this into an animation in the easiest, cocoa-tacular fashion I would do something like so:
UIImageView * spritely = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"frame01.png"]];
spritely.animationImages = [NSArray arrayWithObjects:[UIImage imageNamed:@"frame01.png"],
[UIImage imageNamed:@"frame02.png"],
[UIImage imageNamed:@"frame03.png"],
[UIImage imageNamed:@"frame04.png"],
[UIImage imageNamed:@"frame05.png"],
[UIImage imageNamed:@"frame06.png"],
[UIImage imageNamed:@"frame07.png"],
[UIImage imageNamed:@"frame08.png"],
[UIImage imageNamed:@"frame09.png"],
[UIImage imageNamed:@"frame10.png"],
nil];
spritely.animationDuration = 10.0/fps;
[myBigView addSubview:spritely];
[spritely startAnimating];
this is super duper easy. But it is slow. (well, it isnt particularly slow for a few animations, but get a couple dozen of any decent size and your performance will kinda suck)
Well, I dont know exactly what apple is doing on the backend but very likely the CA stuff is built on top of the openGL stuff. So basically what happens every time the frame needs to be redrawn is that you have to bind a new texture then render that texture out to the screen buffer. Binding textures can be slow unless they are already resident in vram, but even then if you can skip binding a texture and just do a draw call every frame then you will have significant gains) (technical note: the CA stuff is actually kinda half built on openGL and half raw core graphics (and half pixie dust) basically the way the tech dudes explained it is that CA goes out to whichever subsystem is best for the task at hand. That said; no matter what the backend is doing, the texture still needs to be bound to the screenbuffer somehow, and that takes time)
So, how do we skip that texture binding step? Well, we have to abandon the UIImage view. (well, you could subclass it and do all this in a uiimage sublclass, but for our purposes we are going to keep it nice and simple and just make a generic uiview subclass)
But first! we need to make an Atlas!
OK, what is a texture atlas? (and why do we care?)
If you want the very detailed explanation then go read the atlasing whitepaper by NVidia. (which is far more detail than I am going to go into)
A simple explanation of an atlas is just this: a big image with lots of little images inside it. Like so:

hey! that looks an awful lot like the stuff above! and it is in fact, the same thing, but now I am calling it an atlas instead of an example of 10 images. So that is it, a texture atlas is just a bunch of images in a bigger image. How does this help us? Well it means that the larger image only needs to be loaded as a texture once and then you just draw out different bits of it to the video buffer.
How do we use this new fangled atlas thinger?
First, we need to make a new UIView subclass and add a nice CALayer that we can use to store our images:
// our standard frame init method
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// our main layer, holds the main image
imageLayer = [[CALayer layer] retain];
imageLayer.frame = self.bounds;
imageLayer.opaque = YES; // this speeds stuff up if you don't need transparency
[self.layer addSublayer:imageLayer];
}
return self;
}
// set our big atlas image
-(void)setAtlasImage:(UIImage*)myAtlasImage
{
// this is basically binding the big image to our layer
imageLayer.contents = (id)myAtlasImage.CGImage;
imageLayer.masksToBounds = YES;
// this is the magic code here, this tells the layer to only display a
// subset of the larger image.
imageLayer.contentsRect = [self rectForFrameIndex:0];
}
OK, so we build a simple view with a single sublayer. When we set the atlas image for our new animated view, we set the contents to the entire image.
Now if we just did that, you would see the entire atlas (smashed into whatever frame you had defined when you alloced the view). This is not what you want, so we add the contentsRect definition, which allows us to tell the layer to just display one sub rectangle of the entire contents.
The contentsRect is the basis of the whole idea of the texture atlas. So how do we figure that out?
The contentsRect uses normalized coordinates. If you havent done much with contentsRect or the few other CA properties that inexplicable require normalized coords, then it can be kinda confusing at first, but really it is very easy (and a nice way to handle things).
So, lets have a look at our image and see how to generate some normalized rectangle coordinates for our frames:
The sample image above is 480 x 236 pixels (which is a strange size, i know, more on that later, but the upshot is that this is a much smaller version of the raw graphics (to discourage people from stealing them :-) so the example frame sizes are a bit odd... you will get over it I am sure)
A single frame is 121 x 80 (ish) pixels.
OK, quick tangent: this image is a smaller version of the images that the animator built for snow dude. Keen readers will notice some nice hot pink crop marks, those are not part of the animation, but are in there so that I can feed that image into an atlas generator program that I built and it strips out the proper frames for each animation based on the crop marks. For today, we are going to pretend those are part of the animations, and I will talk about optimizing your atlases in another post)
OK, where were we? Oh yes, contentsRect. So if frame 1 is in the upper left, then it is bounded by:
// remember that CALayers have 0,0 at the lower left
CGRect frame1 = CGRectMake( 0.0, 156.0, 121.0, 80.0);
Great! almost there, now we just need to normalize that rectangle. That means that all the values need to be between 0 and 1. so we divide all the widths by 480 (the total width of the image) and all the heights by 236 (to total height of the image). So something like this:
// remember that CALayers have 0,0 at the lower left
-(CGrect)rectForFrameIndex:(NSInteger)frameIndex
{
NSInteger column = frameIndex % 4;
NSInteger row = frameIndex / 4;
row += 1;
CGFloat x = frameIndex * column;
CGFloat y = atlasHeight - (row * frameHeight);
CGFloat normalizedX = x/atlasWidth;
CGFloat normalizedY = y/atlasHeight;
CGFloat normalizedWidth = frameWidth/atlasWidth;
CGFloat normalizedHeight = frameHeight/atlasHeight;
CGRect frameRect = CGRectMake( normalizedX, normalizedY, normalizedWidth, normalizedHeight);
}
so, our frame 1 will look something like this rect:
// remember that CALayers have 0,0 at the lower left
CGRect frame1 = CGRectMake( 0.0, 0.6610, 0.2520, 0.3389);
OK, that was pretty easy, but why do we need normalized coordinates? Well, the short answer is: you need them cuz the methods require them. The long(er) answer is that if you start to use openGL or even doing mipmapping then they become super handy. (in otherwords that normalized frame would work whether i loaded the example atlas that is 480x236 or the original atlas which is 3 times bigger, and you wouldnt have to recalculate the contentsRect. and later, when we start talking about texture binding and UV coordinate space, it will all make so much more sense)
OK! Now all we need is a timer that switches frames for us and we are all set!
Something like this will do the trick:
-(void)timerFire:(id)sender
{
// we want this to be immediate
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
imageLayer.contentsRect = thisFrame;
[CATransaction commit];
frameIndex ++;
}
What is all the CATransaction stuff? well, for better or worse, whenever you do something in core animation that CAN be animated it will be, unless you tell it not to. since we want the next frame to show up immediately, we do not want to animate the contentsRect transition (otherwise your new frame will slide in and the old one will slide out). So we use the CATransaction system to shut off the animations.
AndFinally something to call this animation timer routine:
[NSTimer scheduledTimerWithTimeInterval:1.0/fps target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
And that is it. Super basic texture atlasing.
The pros of this approach: better than loading individual images into your CALayer and animating them that way (or via a UIImageView)
This also makes memory management a bit simple (since you only have to worry about a single image instead of 10)
And the best thing is, this way is halfway to openGL, which is where we want to be.
This particular implementation has lots of drawbacks, like all your frames need to be the same size, and they need to be packed into the atlas in the right way and the right order. Also we are calculating the frame rect over and over again for each frame (this is very quick, but if you have lots of sprites then it seems like a waste when you could just cache them) And because each frame has to be the same size, then we are actually wasting quite a bit of memory with all that whitespace. (turns out transparent pixels take up just as much memory as colored pixels! who knew?!?)
That is OK, we will talk about all this in the next post when i get into openGL.
Cheers!
-B