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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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!

Contributors

Over 300 content creators. Join our team.