Fast grid effects for Cocos2D on the iPhone
Cocos2D is a widely used framework for building games for iOS, as it provides a consistent, game oriented, higher-level interface to the OpenGL-ES iOS underpinnings. Cocos2D sports many basic features that are a real life-saver when it comes to squeezing the last FPS from your device or optimizing memory usage, while also offering advanced features that would cost you some effort to implement on your own.
One of those advanced features are known as “cocos2d effects“, which allow you to animate your sprites by applying them distortion effects such as: ripple, twirl, shake, flip, and so on. But they exact a toll in terms of FPS, which can gets as down as to 20 FPS. I don’t know what is your thinking about it, but going down to 20FPS from 60FPS, then back to 60 FPS after the effect ends, is not an option for me. So I set up looking for a solution to this problem, trying to find a way to patch cocos2D’s performance bottleneck.
Those effects are based on the concept of a “grid” which gets distorted while animating the sprite; the texture associated to the sprite is made stick to the grid as a skin adhering to the underneath body and the trick is done. My first suspect was that recalculating the grid at each step of the animation, which included trigonometrical calculations, could slow things down, given also the fact that results were not cached in any way, so I decided to profile a test app to get a clue at what was happening. But, lo, Instruments only showed a fair distribution of computation time among a bunch of functions. What was more interesting to note was that, when leaving the effect on place without anything else going on for a significant time, the OpenGL calls tended to slowly become predominant.
That was bad news for me, because it hinted at an issue I felt utterly unprepared to dealt with, namely some kind of OpenGL optimization. Indeed, by googling further on the topic, I found more evidence that the problem was related to those effects directly accessing the OpenGL FBO at each frame. I also found the key to what would become the solution for me: this “Deformable Textures” bug report included a class, called FastGrid, which tried and implement the “light effects” as mentioned in the previous cocos2d article I linked above. The fact is, when I tried to used a FastGrid instead of my “normal” sprite to apply the effect, it did not work as expected. My textures where indeed distorted according to the effect applied, but they were also in a more general way, like scaled down in one direction and up in the other, not to mention the displacement.
Oh, my! It seemed that those effects should remain a dream for me, with my limited OpenGL skills. But I decided to give it a try, so I embarked in a journey through cocos2D inner workings, textures, vertices, grids and so on, and finally found out that the scaling was related to the way that textures are loaded into memory, i.e., by enlarging their dimensions separately by the maximum power of two that makes the textures still fit within the maximum texture size allowed for a given device. That means, e.g., that a 50×160 texture is enlarged to 400×320 (50 by 8, 160 by 2) on the iPhone, which has a maximum texture size of 512×512. The displacement was related to the differing origin that the drawing space has in cocos2D and OpenGL, i.e., the origin is bottom-left for cocos2D, top-left in OpenGL. So, what was needed was simply accounting for the origin shift before drawing the texture.
All in all, I came up with a pretty satisfying solution for me, which is available on my GitHub. The key point is in the draw method:
[sourcecode language=”c”]
– (void)draw {
if(fastGrid_ && fastGrid_.active) {
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glScalef(hscale_, vscale_, 1.0);
ccglTranslate(0, -postOffset_, 0);
[fastGrid_ blit];
glPopMatrix();
} else {
[sprite_ draw];
}
}
[/sourcecode]
where the MODEL_VIEW matrix is applied a scaling factor and an offset calculated at initialization time:
[sourcecode language=”c”]
– (id)initWithTexture:(CCTexture2D*)texture {
if ((self = [super init])) {
self.texture = texture;
sprite_ = [[CCSprite alloc] initWithTexture:texture];
CGSize size = [[CCDirector sharedDirector] winSizeInPixels];
unsigned int POTWide = ccNextPOT(size.width);
unsigned int POTHigh = ccNextPOT(size.height);
hscale_ = sprite_.contentSize.width/[SDSFastGrid maxDimension:sprite_.contentSize.width lessThanPOT:POTWide];
vscale_ = sprite_.contentSize.height/[SDSFastGrid maxDimension:sprite_.contentSize.height lessThanPOT:POTHigh];
postOffset_ = size.height – [SDSFastGrid maxDimension:sprite_.contentSize.height lessThanPOT:POTHigh];
[self setContentSize:sprite_.textureRect.size];
}
return self;
}
[/sourcecode]
A Interaction Design/Usability List
Some resources on the topic:
Smoothing CCGridActions in Cocos2D for iPhone
CCGridActions are a great feature of cocos2d. They provide nice 2D and “3D” effects that you can use to make your app or game appearance more appealing. I have already written about them, mostly about the high cost the exact in terms of CPU time (and FPS), what makes them barely usable in a real, sufficiently complex scenario. In a recent post of mine, I described a solution to this issue, which though not a full replacement for “real” grid actions, makes them usable, fast, and not at all demanding in terms of processing power.
ASIHTTP is "dead", long live to it!
I used ASIHTTP in a couple of projects of mine, and always found it was very useful. Unfortunately, its creator and maintainer decided to end its involvement with the project, due to a bunch of reasons that you can read about in his own post. It’s sad anyway reading about “vitriol” being poured upon anyone who simply gave away his code for free to whom could need it.
Lock-free programming
Lock-free programming is often considered an easy alternative to blocking for handling parallelism. This article offers a very nice introduction to the topic, and it also shows that it is not easy at all! In any case, it makes for an interesting read.