How To Create Cool Effects with Custom Shaders in OpenGL ES 2.0 and Cocos2D 2.X

This is a post by iOS Tutorial Team member Krzysztof Zablocki, a passionate iOS developer with years of experience. Shaders may very well be the biggest step forward in computer graphics since the introduction of 3D into games. They allow programmers to create completely new effects and take full control of what’s seen on the […] By .

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

How to Create & Use Your Own Shader

Most 2D games consist of sprites, and they usually have four vertices per sprite. Since this is not much to work with, most 2D effects are created in fragment shaders. You’re going to use a default Cocos2D vertex shader to add a new custom fragment shader.

The goal here is to create a simple effect in which you use a secondary texture to modify an original texture color. You’ll be manipulating the original texture color using your “ramp” texture. This is an effect that can be used to create game skins or ToonShading.

Here is the texture we’ll be using (that you downloaded earlier in the resources zip):

We will be working with colors three components (red, green and blue), expressed as a percentage between 0 and 1. If you’re used to a range between 0-255, just divide what you’re used to by 255.

Notice how the image goes from white (RGB 0.0, 0.0, 0.0) to black (RGB 1.0, 1.0, 1.0). For each RGB value in the original image, we’re going to “look up” the corresponding entry in this color mask, effectively “reversing” the color of the original image. For example:

  • If the original image has a red/green/blue values of black (0, 0, 0) we will look at 0% inside the color ramp and see (1.0, 1.0, 1.0) (switching black to white).
  • If the original image has a red/green/blue value of white (1.0, 1.0, 1.0) we will look at 100% inside the color ramp and see (0, 0, 0) (switching white black).
  • If the original image has a red/green/blue value of the yellow in the Cocos2D logo (0.99, 0.76, 0.42), we will look at the corresponding percentages for each component inside the color ramp and see (0.01, 0.28, 0.58) (switching yellow to blue).

Also, since the color ramp is only 64 pixels wide, it will result in a smaller range of color values than the original image, resulting in a “banding” effect.

OK, let’s try this out! As you probably noticed when you ran your project earlier, you currently have two menu options – Achievements and Leaderboards – when you run your project. Since you don’t really need achievements or leaderboards to play with textures, you need to change the app menu so that it navigates to a custom shader test screen.

Create a new file with the iOS\cocos2d v2.x\CCNode class template. Make it a subclass of CCLayer and save the class as CSEColorRamp.

Open HelloWorldLayer.m and import CSEColorRamp.h at the top of the file as follows:

#import "CSEColorRamp.h"

Then replace the existing code for HelloWorldLayer’s init method with the following:

-(id) init {
	if( (self=[super init])) {
		// 1 - create and initialize a Label
		CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World" fontName:@"Marker Felt" fontSize:64];
		// 2 - ask director the the window size
		CGSize size = [[CCDirector sharedDirector] winSize];
		// 3 - position the label on the center of the screen
		label.position =  ccp( size.width /2 , size.height/2 );
		// 4 - add the label as a child to this Layer
		[self addChild: label];
		// 5 - Default font size will be 28 points.
		[CCMenuItemFont setFontSize:28];
		// 6 - color ramp Menu Item using blocks
		CCMenuItem *itemColorRamp = [CCMenuItemFont itemWithString:@"Color Ramp" block:^(id sender) {
			CCScene *scene = [CCScene node];
			[scene addChild: [CSEColorRamp node]];
			[[CCDirector sharedDirector] pushScene:scene];
		}];
		// 7 - Create menu
		CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, nil];
		// 8 - Configure menu
		[menu alignItemsHorizontallyWithPadding:20];
		[menu setPosition:ccp( size.width/2, size.height/2 - 50)];
		// 9 - Add the menu to the layer
		[self addChild:menu];
	}
	return self;
}

If you compare the previous code to the new code, you’ll notice that the biggest change is that we’ve removed the existing menu items for Achievements and Leaderboards and have added a new item for “Color Ramp”. Very straightforward :]

Compile and run the app, and you should see this:

Tapping Color Ramp just brings you to a black screen. Let’s make it more interesting!

Open CSEColorRamp.m (the implementation, not the header!) and replace the existing @implementation CSEColorRamp line (only that line) with the following:

@implementation CSEColorRamp {
    CCSprite *sprite;  //1
    int colorRampUniformLocation;  //2
    CCTexture2D *colorRampTexture; //3
}

We just added some private instance variables which do the following:

  1. You need a sprite that whose color you’ll soon be changing.
  2. To send data to a uniform variable in a shader, we need to keep track of the uniform’s location, so we create a variable for that here.
  3. Your ramp texture.

Next, initialize your variables by adding the following code just below the code you previously added:

- (id)init
{
  self = [super init];
  if (self) {
    // 1
    sprite = [CCSprite spriteWithFile:@"Default.png"];
    sprite.anchorPoint = CGPointZero;
    sprite.rotation = 90;
    sprite.position = ccp(0, 320);
    [self addChild:sprite];

    // 2
    const GLchar * fragmentSource = (GLchar*) [[NSString stringWithContentsOfFile:[CCFileUtils fullPathFromRelativePath:@"CSEColorRamp.fsh"] encoding:NSUTF8StringEncoding error:nil] UTF8String];
    sprite.shaderProgram = [[CCGLProgram alloc] initWithVertexShaderByteArray:ccPositionTextureA8Color_vert
                                       fragmentShaderByteArray:fragmentSource];
    [sprite.shaderProgram addAttribute:kCCAttributeNamePosition index:kCCVertexAttrib_Position];
    [sprite.shaderProgram addAttribute:kCCAttributeNameTexCoord index:kCCVertexAttrib_TexCoords];
    [sprite.shaderProgram link];
    [sprite.shaderProgram updateUniforms];

    // 3
    colorRampUniformLocation = glGetUniformLocation(sprite.shaderProgram->program_, "u_colorRampTexture");
    glUniform1i(colorRampUniformLocation, 1);

    // 4
    colorRampTexture = [[CCTextureCache sharedTextureCache] addImage:@"colorRamp.png"];
    [colorRampTexture setAliasTexParameters];

    // 5
    [sprite.shaderProgram use];
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, [colorRampTexture name]);
    glActiveTexture(GL_TEXTURE0);
  }
  return self;
}

What are you doing here?

  1. Load your base sprite, and set it so that it covers the whole screen.
  2. Instruct the sprite to use your own shader program. It will use the default vertex shader ccPositionTextureA8Color_vert and your own fragment shader CSEColorRamp.fsh. Then you specify that your vertices data will have position and texCoords attributes (as CCSprite has them). Then you link the shaders together and update Cocos2D uniforms.
  3. Having linked your shaders, you can ask OpenGL for the hardware location of your secondary texture which is identified by the name “u_colorRampTexture”. Then you initialise this hardware location to with the value 1 (since we will be storing the texture in slot 1, slot 0 is the sprite’s texture).
  4. Load your ramp texture and disable linear interpolation on it, since you want raw values here.
  5. Bind your shader and set the secondary texture to your ramp texture (note that in a real project, you should set this each time your sprite is going to be drawn, as other rendering can set it to some other value). This binds your custom texture to uniform “u_colorRampTexture”.

Now you need a fragment shader to help this layer render properly.

Create a new file with the iOS\Other\Empty template. Name the new file CSEColorRamp.fsh, select the file location, remove CocosShaderEffects from the Targets list and then click Create. The reason you removed the file from the targets list is because this is not a source code file, but rather a resource (Xcode by default would add it to the Compile Sources phase).

Now you need to add the new fragment shader file to the Copy Bundle Resources build phase. In project navigator, select CocosShaderEffects at the top of the navigation tree. In project settings select your target, then select the Build Phases tab. Expand the Copy Bundle Resources section (click on the triangle) and add the CSEColorRamp.fsh file to it. (Hint: use the plus (+) icon).

Now, open CSEColorRamp.fsh and add the following:

#ifdef GL_ES
precision mediump float;
#endif

// 1
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform sampler2D u_colorRampTexture;

void main()
{ // 2
  vec3 normalColor = texture2D(u_texture, v_texCoord).rgb;

  // 3
  float rampedR = texture2D(u_colorRampTexture, vec2(normalColor.r, 0)).r;
  float rampedG = texture2D(u_colorRampTexture, vec2(normalColor.g, 0)).g;
  float rampedB = texture2D(u_colorRampTexture, vec2(normalColor.b, 0)).b;

  // 4
  gl_FragColor = vec4(rampedR, rampedG, rampedB, 1);
}

Note: There is a simple action you can take to make the above code more readable. Select Editor/Syntax Coloring/GLSL from the menu, and you will have syntax coloring enabled for shaders :]

What does the above code do?

  1. The shader expects texCoords passed from the vertex shader, and two uniform textures sent from application code.
  2. First, get the real color from the texture.
  3. Use each real color channel value as an address to get the modified color from your ramp texture.
  4. Create the fragment color (the final color of your pixel) based on the ramped values for color and make it fully opaque (the 1 at the end).

Build and run the application. When you select the Color Ramp menu option, you should see:

Cocos2D just changed its skin color!

You can play around with the color band image for different effects. Here’s one thing to try: reduce the width of the image so there’s a smaller range of colors, and use it as a simple toon shader!