How To Mask a Sprite with 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 […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share

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!

Contributors

Over 300 content creators. Join our team.