How To Mask a Sprite with Cocos2D 1.0

Ray Wenderlich
Learn how to mask a sprite with Cocos2D 1.0!

Learn 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 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:

An image that is not masked

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:

A white and transparent mask image

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:

  1. 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.
  2. 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:

  1. Create the CCRenderTextuer, specifying the width and height of the texture in pixels.
  2. Call begin to initialize drawing to the CCRenderTexture.
  3. Issue OpenGL commands to draw stuff – but it will go to the CCRenderTexture instead of the screen.
  4. 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.

  1. Create a CCRenderTexture with the same width/height as the mask sprite.
  2. Positions the mask sprite and texture sprite so their bottom left is at 0,0.
  3. Sets up the blend functions for each sprite as discussed earlier.
  4. Calls begin to start drawing into the CCRenderTexture, draws the mask and then the texture, and then calls end to finalize drawing.
  5. 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!

A sprite masked with Cocos2D 1.0 and CCRenderTexture

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!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

36 Comments

[ 1 , 2 , 3 ]
  • Have you tried it on the device? I had some issues with it using the simulator which showed in screenshots. My image had a black line always at the same place which shouldn't have been there. On the iPad it worked without a problem.
    falc410
  • For a moment I thought (and hoped) you are right, since I actually didn't test on a real device, and more often than not this really makes the difference for OpenGl pecularities. I am sad to say that in this case the effect is the also on the device itself...
    habitoti
  • Mhm sorry I don't know what to do then either. Maybe you need to tweak the OpenGL code a little. You can try looking at these samples, maybe this will help you (from the cocos2d forums):

    http://www.cocos2d-iphone.org/forum/topic/12557
    http://abitofcode.com/2012/04/ccrendert ... -complete/
    falc410
  • Hi and Thanks.
    When I run your sample and change background color to eg white, the image gets dark in the semi transparent area - the border of the mask. I have implemented it in my own program and tried using other blending modes but I cant make It look right.
    Do you know how to fix this? '

    Best
    Esben
    esben
  • How to animate the mask image. In my case i wanna animate the mask image at specific stage so it's look like the whole world is rotating.
    I am using CCRotate and calling update and Visit the all render texture again inside the CCRenderTexture to update the rotation but it's not working.
    Can you please help me in this.
    hamdullahshah
  • I tried to animate a mask with an Action with CCMoveTo and with a scheduler adding on a y axis, but it doesnt works,
    do you know how to do this ??

    thanks!!
    effectoxx
[ 1 , 2 , 3 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Martin Walsh
  • Riccardo D'Antoni

... 55 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Cosmin Pupaza

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!