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

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!

Contributors

Over 300 content creators. Join our team.