How To Mask a Sprite with Cocos2D 1.0
Sometimes in your games you might find it handy to display only a portion of a sprite. One way you can do this is by using a second image called a mask. You set the mask image to be white wherever you want the image to show up – and transparent everywhere else. Then you […] By Ray Wenderlich.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Mask a Sprite with Cocos2D 1.0
10 mins
Sometimes in your games you might find it handy to display only a portion of a sprite.
One way you can do this is by using a second image called a mask. You set the mask image to be white wherever you want the image to show up – and transparent everywhere else.
Then you can use the technique we’ll cover in the tutorial to combine the mask and the original image so that only the part you want comes through!
You’ll find this handy to accomplish a ton of interesting effects – such as creating a puzzle piece from a sprite, cutting out someone’s head to put it on a funny body, or to create a neat image frame effect – like we’ll be covering in this tutorial!
This tutorial will showing you how to mask a sprite in Cocos2D 1.0 using a handy class that comes with Cocos2D called CCRenderTexture.
This tutorial assumes you have familiarity with Cocos2D. If you are new to Cocos2D, you should check out some of the other Cocos2D tutorials on this site first.
Getting Started
Start up Xcode, go to File\New\New Project, choose iOS\cocos2d\cocos2d, and click Next. Name the new project MaskedCal, click Next, choose a folder to save the project in, and click Create.
Next, download the resources for this project and drag the folder to your Xcode project. Make sure that “Copy items into destination group’s folder (if needed)” is checked, and click Finish.
We’ll start things off with some jazz. Open up AppDelegate.m and make the following changes:
// Add to top of file
#import "SimpleAudioEngine.h"
// At end of applicationDidFinishLaunching, replace last line with the following 2 lines:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"TeaRoots.mp3" loop:YES];
[[CCDirector sharedDirector] runWithScene: [HelloWorldLayer sceneWithLastCalendar:0]];
This plays some cool music made by Kevin MacLeod and calls a new initializer we’re about to write.
Next open up HelloWorldLayer.h make the following changes:
// Add new instance variable
int calendarNum;
// Replace the +(CCScene*) scene declaration at the bottom with the following:
+ (CCScene *) sceneWithLastCalendar:(int)lastCalendar;
- (id)initWithLastCalendar:(int)lastCalendar;
In this scene we’re going to display a random calendar image (we have three to choose from). Here we’re storing the calendar number we’re displaying, and we modify the initializers to take the last calendar number shown as a parameter (so we can include some logic to not show the same calendar twice in a row).
Then switch to HelloWorldLayer.m and make the following changes:
// Replace +(CCScene *) scene with the following
+(CCScene *) sceneWithLastCalendar:(int)lastCalendar // new
{
CCScene *scene = [CCScene node];
HelloWorldLayer *layer = [[[HelloWorldLayer alloc]
initWithLastCalendar:lastCalendar] autorelease]; // new
[scene addChild: layer];
return scene;
}
// Replace init with the following
-(id) initWithLastCalendar:(int)lastCalendar
{
if( (self=[super init])) {
CGSize winSize = [CCDirector sharedDirector].winSize;
do {
calendarNum = arc4random() % 3 + 1;
} while (calendarNum == lastCalendar);
NSString * spriteName = [NSString
stringWithFormat:@"Calendar%d.png", calendarNum];
CCSprite * cal = [CCSprite spriteWithFile:spriteName];
// BEGINTEMP
cal.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:cal];
// ENDTEMP
self.isTouchEnabled = YES;
}
return self;
}
// Add new methods
- (void)registerWithTouchDispatcher {
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self
priority:0 swallowsTouches:YES];
}
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CCScene *scene = [HelloWorldLayer sceneWithLastCalendar:calendarNum];
[[CCDirector sharedDirector] replaceScene:
[CCTransitionJumpZoom transitionWithDuration:1.0 scene:scene]];
return TRUE;
}
This is just some basic Cocos2D code to display a random calendar image in the center of the screen. It also contains some basic logic so whenever you tap the screen, it makes a new scene with a neat bouncy transition.
Compile and run to try it out, and you should see a random calendar made by my lovely wife displayed each time you tap:
Now that we have a decent shell for our app, let’s implement the masking effect!
Masking and OpenGL Blend Modes
If you look at Art\CalendarMask.png in an image editor, you’ll see it looks like this:
We’re going to use this to mask our calendar image so it looks like a floaty piece of paper rather than a simple square. Wherever the image is transparent, we don’t want the image to show up, but wherever the image is opaque we do want it to show up.
To accomplish this, we’re going to use OpenGL blending.
If you remember back to the How to Create Dynamic Textures with CCRenderTexture tutorial, we discussed OpenGL blending modes, and I pointed out a great online tool that you can use to visualize what various blend modes mean.
To accomplish the effect we want, we’re going to take the following strategy:
- We’re going to first draw the mask sprite, with the src color (the mask) set to GL_ONE and the destination color (an empty buffer) to GL_ZERO. So basically we’ll plop down the mask as-is.
- We’ll then draw the calendar sprite, with the src color (the calendar) set to GL_DST_ALPHA. This is a fancy way of saying “look at the alpha value of what is currently in the buffer (the mask). Wherever it’s opaque, let the calendar show through, but where it’s transparent, don’t show anything!” The dst color (the mask) is set to GL_ZERO, so the existing mask drawn earlier goes away.
Awesome! You might think that you can just draw these two sprites one after another with Cocos2D and setting the blend modes appropriately and you’d be done – but you’d be wrong.
The problem is the above blending algorithm runs into problems if there’s something underneath the sprite you’re drawing – like a background, or another sprite. This is because it makes the assumption that the only thing in the image buffer after step 1 is the mask – no other image data.
So we need some way to have a “blank slate” when drawing this masked texture. And this is where our friend CCRenderTexture comes to the rescue!
Masking and CCRenderTexture
CCRenderTexture is a class that lets you draw to an offscreen buffer.
It is handy for a lot of reasons – you can use it to take screenshots of your game, efficiently cache user drawing, dynamically create sprite sheets at runtime, or what we’re going to use it for in this case – to help us mask a sprite.
To use CCRenderTexture, you take the following steps:
- Create the CCRenderTextuer, specifying the width and height of the texture in pixels.
- Call begin to initialize drawing to the CCRenderTexture.
- Issue OpenGL commands to draw stuff – but it will go to the CCRenderTexture instead of the screen.
- Call end when you are done, and CCRenderTexture will now have a sprite property with a texture you can use (but note it’s flipped upside down). Alternatively, you can add the CCRenderTexture directly as a child if you want.
Don’t freak out about step 3 – since you’re using Cocos2D, you usually don’t have to issue the OpenGL commands manually yourself! If you want to draw a node, all you have to do is call [myNode visit] and it will run all the appropriate OpenGL commands for you!
The only trick is the node needs to be positioned in the render texture’s coordinates. 0,0 is the bottom left of the render texture, so make sure that you position everything inside that space appropriately.
OK you’re probably sick of me blabbing on and are eager to see some code. I aim to please – let’s dive back in!
Masking a Sprite: The Implementation
Open up HelloWorldLayer.m and add the following method right above init:
- (CCSprite *)maskedSpriteWithSprite:(CCSprite *)textureSprite maskSprite:(CCSprite *)maskSprite {
// 1
CCRenderTexture * rt = [CCRenderTexture renderTextureWithWidth:maskSprite.contentSizeInPixels.width height:maskSprite.contentSizeInPixels.height];
// 2
maskSprite.position = ccp(maskSprite.contentSize.width/2, maskSprite.contentSize.height/2);
textureSprite.position = ccp(textureSprite.contentSize.width/2, textureSprite.contentSize.height/2);
// 3
[maskSprite setBlendFunc:(ccBlendFunc){GL_ONE, GL_ZERO}];
[textureSprite setBlendFunc:(ccBlendFunc){GL_DST_ALPHA, GL_ZERO}];
// 4
[rt begin];
[maskSprite visit];
[textureSprite visit];
[rt end];
// 5
CCSprite *retval = [CCSprite spriteWithTexture:rt.sprite.texture];
retval.flipY = YES;
return retval;
}
Let’s go over this section by section.
- Create a CCRenderTexture with the same width/height as the mask sprite.
- Positions the mask sprite and texture sprite so their bottom left is at 0,0.
- Sets up the blend functions for each sprite as discussed earlier.
- Calls begin to start drawing into the CCRenderTexture, draws the mask and then the texture, and then calls end to finalize drawing.
- Creates a new sprite based on the CCRenderTexture’s sprite’s texture. Flips it along the y-axis because the texture is upside down.
After everything we discussed, makes sense no? Now let’s use it! Replace the area between BEGINTEMP and ENDTEMP with the following:
CCSprite * mask = [CCSprite spriteWithFile:@"CalendarMask.png"];
CCSprite * maskedCal = [self maskedSpriteWithSprite:cal maskSprite:mask];
maskedCal.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:maskedCal];
This uses our new function to mask the calendar sprite, and adds the masked version to the scene.
Compile and run, and now you should see a masked sprite with Cocos2D 1.0!
Drawbacks of the CCRenderTexture Method
For this simple little app this works fine, but there are some drawbacks to this method you might start to notice for more complicated apps:
- Creates an additional texture in memory each time you apply a mask. Texture memory is very limited on the iPhone so you have to be very careful about how many textures you have in memory at once. This is fine if you only need to mask a few textures at a time, but what if you have to mask 100?
- Takes time to draw. Drawing with CCRenderTexture is not cheap (especially as your textures get larger). If you are doing this frequently, you might notice a performance hit.
Like I mentioned earlier, there is no way that I know of to get around these drawbacks with OpenGL ES 1.0. With OpenGL ES 2.0 you can mask much more efficiently with a shader – but that’s a topic for another tutorial!
Where To Go From Here?
Here is a sample project with all of the code from the above tutorial.
Check out Part 2 of the tutorial, we’ll get our first look at the new and exciting Cocos2D 2.0 branch, and write a custom shader to mask sprites!
In the meantime, if you have any questions or comments, please join the forum discussion below!