How To Mask a Sprite with Cocos2D 2.0

Ray Wenderlich

This post is also available in: Korean

Learn how to mask a sprite with a custom fragment shader and Cocos2D 2.0!

Learn how to mask a sprite with a custom fragment shader and Cocos2D 2.0!

In the previous tutorial, we showed you how to mask a sprite with Cocos2D 1.0.

This method worked OK, but it had some drawbacks – it bloated our texture memory and had a performance hit for the drawing.

But with Cocos2D 2.0 and OpenGL ES 2.0, we can do this much more efficiently by writing a custom shader!

This tutorial will be our first foray into the new and exciting Cocos2D 2.0 branch. You’ll learn how to get started with the new Cocos2D 2.0 branch, and how to write a custom shader for a CCNode!

To fully understand this tutorial, it will really help to have some basic understanding of OpenGL ES 2.0 first. If you are new to OpenGL ES 2.0, check out the OpenGL ES 2.0 for iPhone tutorial series first.

Without further ado, let’s get masking with shaders!

Introducing Cocos2D 2.0 Branch

Cocos2D 2.0 is a major new version of Cocos2D that uses OpenGL ES 2.0 instead of OpenGL ES 1.0. This means some older devices that don’t support OpenGL ES 2.0 can’t run apps made with thie branch, but most devices are OpenGL ES 2.0 compatible these days, so it’s becoming more and more of a decent business decision to use it.

Because even though some older devices can’t run it – it gives us lots of cool new things to play with, primarily OpenGL ES 2.0 shaders! And as you’ll see, using shaders makes masking much more efficient.

The Cocos2D 2.0 branch is still under active development, but is now available for early adopters.

And that means us! :]

The only way to get the Cocos2D 2.0 branch is by grabbing it from the git repository, so if you don’t have git installed already, download it here.

Then go to Applications\Utilities, and click on your Terminal app. Navigate to a directory of your choosing, and then issue the following command:

git clone https://github.com/cocos2d/cocos2d-iphone.git

Once it finishes downloading, switch to the cocos2d-iphone directory and check out the Cocos2D 2.0 branch (called gles20) as follows:

cd cocos2d-iphone
git checkout gles20

Next, go ahead and install the new Cocos2D 2.0 templates. This will overwrite your current Cocos2D templates, but that’s OK – personally I just keep the 1.0 and 2.0 directories somewhere handy, and run install-templates.sh again from whichever version of Cocos2D I need the templates from.

./install-templates.sh -f -u

Back in Xcode, go to File\New\New Project, choose iOS\cocos2d\cocos2d, and click Next. Name the new project MaskedCal2, click Next, choose a folder to save the project in, and click Create.

If you try to Compile and Run, you’ll see… a blank screen?

This threw me for a loop the first time I was playing around with the new branch. Luckily the solution is simple – the templates don’t include some required files you need.

To add the required files, open the cocos2d-ios.xcodeproj from where you downloaded the github repository, and drag the Resources\Shaders folder into your Xcode project.

Very important: When you drag the folder over, make sure that “Copy items into destination group’s folder” is selected, and “Create folder references for any added folders” is selected. By selecting the “folder references” option, the shaders files will be copied into a subdirectory of your bundle, which is where Cocos2D is looking.

Compile and run, and you should see a Hello World for Cocos2D 2.0 with OpenGL ES 2.0!

Hello, Cocos2D 2.0!

Hello, Cocos2D 2.0!

Creating a Simple Cocos2D 2.0 Project

Let’s get our project set up to just cycle through the list of calendar images like we did before, before we turn to masking.

Like you did before, drag the resources for this project into your Xcode project. Make sure that “Copy items into destination group’s folder (if needed)” is checked and “Create groups for any added folders” is selected, and click Finish.

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 is the same as last time.

Then open up RootViewController.m and inside shouldAutorotateToInterfaceOrientation, replace the return YES with the following:

return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) );

This sets up the app to only support landscape mode (not sure why it’s not set up that way in the Cocos2D 2.0 templates).

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;

This is also the same as last time.

Finally make the following changes to HelloWorldLayer.m:

// 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];
 
        // BEGINTEMP
        CCSprite * cal = [CCSprite spriteWithFile:spriteName];        
        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;
}

Yet again – same as last time (except the begintemp/endtemp positions slightly changed). So you see, the user API didn’t really change much in Cocos2D 2.0 – it just has some extra awesome features you’re about to see :]

Compile and run, and the app should display the cycling list of calendars like before, but using Cocos2D 2.0 and OpenGL ES 2.0 now!

A non masked image in Cocos2D 2.0

Shaders and Cocos2D 2.0

Cocos2D 2.0 comes with several built-in shaders that are used by default. In fact, those are the files missing from the template that you copied in earlier.

Let’s take a look at the shaders that are used to render a normal CCSprite. They’re pretty simple. The vertex shader is Shaders\PositionTextureColor.vert:

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
 
uniform		mat4 u_MVMatrix;
uniform		mat4 u_PMatrix;
 
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
 
void main()
{
    gl_Position = u_PMatrix * u_MVMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}

This multiplies the vertex position by the projection and model/view matrices, and sets the fragment color and texture coord output varyings to the input attributes.

Next take a look at the fragment shader in Shaders\PositionTextureColor.frag:

#ifdef GL_ES
precision lowp float;
#endif
 
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
 
void main()
{
    gl_FragColor = v_fragmentColor * texture2D(u_texture, v_texCoord);
}

This sets the output color to the texture’s color multiplied by the color value on the vertex (set in Cocos2D with setColor).

Cocos2D makes it very easy to replace the shaders you use to draw a node with your own custom shaders. Let’s start by creating a shader to use to implement masking, then we’ll create a subclass of CCSprite and set it up to use the custom shader.

In Xcode, go to File\New\New file, choose iOS\Other\Empty, and click Next. Name the new file Mask.frag, and click Save.

Then click on your Project properties, select your MaskedCal2 target, select the Build Phase tab, look for Mask.frag under the “Compile Sources” tab and move it down into the “Copy Bundle Resources” section. This will make it copy the shader into your app’s bundle rather than trying to compile it.

(By the way, if anyone knows an easier way to make this happen let me know!)

Make sure the fragment shader is in the Copy Bundle Resources phase

Then replace the contents of Mask.frag with the following:

#ifdef GL_ES
precision lowp float;
#endif
 
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform sampler2D u_mask;
 
void main()
{
    vec4 texColor = texture2D(u_texture, v_texCoord);
    vec4 maskColor = texture2D(u_mask, v_texCoord);
    vec4 finalColor = vec4(texColor.r, texColor.g, texColor.b, maskColor.a * texColor.a);
    gl_FragColor = v_fragmentColor * finalColor;
}

Here we set up a new uniform for the mask texture, and read in the pixel value in the calendar texture and mask texture.

Then we construct the final color as the calendar’s RBG, but multiply the alpha component by the mask’s alpha. So wherever the mask’s alpha is 0 (transparent) the calendar will also be transparent.

w00t now we’ve got a shader – so let’s make use of it!

Using a Custom Shader in Cocos2D 2.0

To use a custom shader, you need to create a subclass of CCNode, set its shaderProgram to your custom shader, and (most likely) override its draw method to pass the appropriate parameters to the shader.

We’re going to create a subclass of CCSprite (which is fine because it derives from CCNode), and call it MaskedSprite. Let’s try it out.

Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter CCSprite for Subclass of, click Next, name the new file MaskedSprite.m, and click Save.

Replace MaskedSprite.h with the following:

#import "cocos2d.h"
 
@interface MaskedSprite : CCSprite {
    CCTexture2D * _maskTexture;
    GLuint _textureLocation;
    GLuint _maskLocation;
}
 
@end

Here we have an instance variable to keep track of the mask texture, and two variables to keep track of the texture uniform’s location, and the mask uniform’s location.

Next switch to MaskedSprite.m and replace the contents with the following:

#import "MaskedSprite.h"
 
@implementation MaskedSprite
 
- (id)initWithFile:(NSString *)file 
{
    self = [super initWithFile:file];
    if (self) {
 
        // 1
        _maskTexture = [[[CCTextureCache sharedTextureCache] addImage:@"CalendarMask.png"] retain];
 
        // 2
        self.shaderProgram = 
        [[[GLProgram alloc] 
          initWithVertexShaderFilename:@"Shaders/PositionTextureColor.vert"
          fragmentShaderFilename:@"Mask.frag"] autorelease];
 
        CHECK_GL_ERROR_DEBUG();
 
        // 3
        [shaderProgram_ addAttribute:kCCAttributeNamePosition index:kCCAttribPosition];
        [shaderProgram_ addAttribute:kCCAttributeNameColor index:kCCAttribColor];
        [shaderProgram_ addAttribute:kCCAttributeNameTexCoord index:kCCAttribTexCoords];
 
        CHECK_GL_ERROR_DEBUG();
 
        // 4
        [shaderProgram_ link];
 
        CHECK_GL_ERROR_DEBUG();
 
        // 5
        [shaderProgram_ updateUniforms];
 
        CHECK_GL_ERROR_DEBUG();                
 
        // 6
        _textureLocation = glGetUniformLocation( shaderProgram_->program_, "u_texture");
        _maskLocation = glGetUniformLocation( shaderProgram_->program_, "u_mask");
 
        CHECK_GL_ERROR_DEBUG();
 
    }
 
    return self;
}
 
@end

There’s a lot to discuss here, so let’s go over it section by section.

  1. Gets a reference to the texture for the calendar mask.
  2. Overrides the built-in shaderProgram property on CCNode so that we can specify our own vertex and fragment shader. We use the built-in PositionTextureColor vertex shader (since nothing needs to change there) but specify our new Mask.frag fragment shader. Note that this GLProgram class is the same one from Jeff LaMarche’s blog post!
  3. Sets the indexes for each attribute before linking. In OpenGL ES 2.0, you can either specify the indexes for attributes yourself in advance (like you see here), or let the linker decide them for you and get them after the fact (like I’ve done in the OpenGL ES 2.0 tutorial series).
  4. Calls shaderProgram link to compile and link the shaders.
  5. Calls shaderProgam updateUniforms, which is an important Cocos2D 2.0-specific method. Remember those projection and model/view uniforms in the vertex shader? This method keeps track of where these are in a dictionary, so Cocos2D can automatically set them based on the position and transform of the current node.
  6. Gets the location of the texture and mask uniforms, we’ll need them later.

Next, we have to override the draw method to pass in the appropriate values for the shaders. Add the following method next:

-(void) draw {    
 
    // 1 
    ccGLBlendFunc( blendFunc_.src, blendFunc_.dst );		
    ccGLUseProgram( shaderProgram_->program_ );
    ccGLUniformProjectionMatrix( shaderProgram_ );
    ccGLUniformModelViewMatrix( shaderProgram_ );
 
    // 2
    glActiveTexture(GL_TEXTURE0);
    glBindTexture( GL_TEXTURE_2D,  [texture_ name] );
    glUniform1i(_textureLocation, 0);
 
    glActiveTexture(GL_TEXTURE1);
    glBindTexture( GL_TEXTURE_2D,  [_maskTexture name] );
    glUniform1i(_maskLocation, 1);
 
    // 3
#define kQuadSize sizeof(quad_.bl)
    long offset = (long)&quad_;
 
    // vertex
    NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
    glVertexAttribPointer(kCCAttribPosition, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));
 
    // texCoods
    diff = offsetof( ccV3F_C4B_T2F, texCoords);
    glVertexAttribPointer(kCCAttribTexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));
 
    // color
    diff = offsetof( ccV3F_C4B_T2F, colors);
    glVertexAttribPointer(kCCAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));
 
    // 4
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);    
    glActiveTexture(GL_TEXTURE0);
}

Let’s go over each section here as well:

  1. This is boilerplate code to get things set up. It sets up the blend function for the node, uses the shader program, and sets up the projection and model/view uniforms.
  2. Here we bind the calendar texture to texture unit 1, and the mask texture to texture unit 2. I discuss how this works in the OpenGL ES 2.0 Textures Tutorial.
  3. CCSprite already contains the code to set up the vertices, colors, and texture coordinates for us – it stores it in a special variable called quad. This section specifies the offset within the quad structure for the vertices, colors, and texture coordinates.
  4. Finally, we draw the elements in the quad as a GL_TRIANGLE_STRIP, and re-activate texture unit 0 (otherwise texture unit 1 would be left bound, and Cocos2D assumes texture unit 0 is left active).

Almost done! Switch to HelloWorldLayer.m and make the following modifications:

// Add to top of file
#import "MaskedSprite.h"
 
// Replace code between BEGINTEMP and ENDTEMP with the following
MaskedSprite * maskedCal = [MaskedSprite spriteWithFile:spriteName];
maskedCal.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:maskedCal];

That’s it! Compile and run, and you now have masked sprites with Cocos2D 2.0 and OpenGL ES 2.0!

A masked sprite with a custom fragment shader and Cocos2D 2.0

The best thing about this method is we don’t have to create any extra textures – we just have the original textures and the mask texture in memory, and can create the masked version dynamically at runtime!

Where to Go From Here?

Here is a sample project with the code from the above tutorial.

That’s it for this tutorial series – hopefully you can start having a lot of fun with masking and Cocos2D – whichever version you choose to use!

If you want to learn more about Cocos2D 2.0, at the time of writing, there isn’t a lot of documentation on Cocos2D 2.0, so the best way to learn is by looking at the source and playing around with the new ShaderTest example.

If you want to learn more about shaders, I recommend Philip Rideout’s iPhone 3D Programming – it’s what I used to get started.

If you have any questions or comments, feel free to 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

38 Comments

[ 1 , 2 , 3 ]
  • HarrisonJackson wrote:Hey all - if anyone has had problems getting this running with the latest cocos2d or opengl es2.0 here is another sample project with a few different examples. You can overlay a sprite, an image, or even text with this mask layer.
    https://github.com/HarrisonJackson/Cocos2D-HJMasked-Sprite


    Thanks for your example.
    I found a problem.
    You use
    ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    on the draw method.

    It is ok for the most time. But if my image's alpha is not 1. There is some problem that image is darker, for example,
    if the color of image is (255, 0, 0, 0.5).
    The masked image will be (179, 0, 0, 1), but not (255, 0, 0, 0.5).
    I know it is about the blend function.
    But if I use ccGLBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    The mask function does not work.

    Does anybody know how to resolve it?
    Thank you in advance.
    sean.wang
  • Does anyone know how I can use it with CCRenderTexture (as a source) instead of CCSprite?
    lesce
  • I am using cocos2dV2.1rca, in the part:
    #import "MaskedSprite.h"

    @implementation MaskedSprite

    - (id)initWithFile:(NSString *)file
    {
    // 3
    [shaderProgram_ addAttribute:kCCAttributeNamePosition index:kCCAttribPosition];
    [shaderProgram_ addAttribute:kCCAttributeNameColor index:kCCAttribColor];
    [shaderProgram_ addAttribute:kCCAttributeNameTexCoord index:kCCAttribTexCoords];

    }

    It seems they changed the KCC defines, I can not get it to build successfully, What should I do.
    Pianisimo
  • please help me!

    I have issuse. Sprite A has animation waves. I want to mask sprite A with sprite B: sprite A animation and sprite A display a part in sprite B.
    I do "How To Mask a Sprite with Cocos2D 2.0" but affer mask, sprite A hasn't animation and Sprite A show full in sprite B.

    typhuit
  • how to unmask the sprite plz reply me
    dilip.mariya
  • This tutorial needs updating for recent cocos2d. Also, how would you animate, as in CCMoveTo... (etc) the mask and the masked image independently?
    it's me, you know
  • Can anyone provide updated code for this tutorial? I'm using 2.1 and am unable to get even the texture to display without the mask. The color makes it through the shader and out, but neither of the textures do. I've even tried copying the draw() function from CCSprite and no luck so far.

    These ccGLUniformProjectionMatrix( shaderProgram_ );, ccGLUniformModelViewMatrix( shaderProgram_ ); are deprecated so I'm not sure if there's any necessary replacement. But the CCSprite draw function doesn't have them so, I'm guessing it's not a problem. Since CCSprite shows the texture ok, there must be some call somewhere I'm missing that the textures aren't getting properly bound?
    tettoffensive
  • Thanks for the great tutorial!
    And it would be very awesome if anybody had a hint on how to make this work on cocos2D v3!
    kolt
[ 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 September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Jake Gundersen

... 50 total!

Update Team

  • Riccardo D'Antoni

... 14 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • David Hidalgo
  • Cosmin Pupaza
  • David Xie

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!