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

Krzysztof Zabłocki

This is a post by iOS Tutorial Team member Krzysztof Zablocki, a passionate iOS developer with years of experience.

Learn how to make cool effects like this with Cocos2D 2.0 shaders!

Learn how to make cool effects like this with Cocos2D 2.0 shaders!

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 screen. If you aren’t using shaders yet, you will be after reading this tutorial!

Cocos2D is one of the best iOS game frameworks available at the moment, and fortunately for us, Cocos2D 2.X now supports OpenGL-ES 2.0 and shaders. In this tutorial, you’ll learn how to create and use shaders with the help of Cocos2D. You’ll learn:

  • The basics of GLSL (OpenGL Shading Language)
  • How to create and use your own shaders in Cocos2D
  • Three shader examples:
    • How to manipulate colors in your game by using ramp textures
    • How to create emboss-like effects
    • How to create waving grass with just a few lines of code

This is an advanced tutorial so has a number of prerequisites. To get the most out of this tutorial, you need at least some basic knowledge of:

  • Objective-C and iOS. If you are a complete beginner, you may wish to check out some of the other tutorials on this site first.
  • Cocos2D. You’ll want to get familiar with Cocos2D if you aren’t already, perhaps by reading some of the Cocos2D tutorials on this site.
  • OpenGL ES 2.0. You don’t need to be an OpenGL ES 2.0 expert to go through this tutorial, but it will help to at least know the basics. You might want to read some our or OpenGL ES tutorials first.

Lastly, I’ll be using the latest version of Xcode in this tutorial, so make sure you’re fully updated to the latest version available through the Mac App Store.

Without further ado, let’s get shading!

Getting Started

Before exploring shaders and how to use them, you will need to have Cocos2D 2.X and the Cocos2D Xcode templates installed. Then, you should create a new Cocos2D 2.X project. Follow these steps to do so:

  1. Download the latest version of Cocos2D 2.X.
  2. Unpack it into your preferred folder.
  3. Navigate to the Cocos2D folder in the terminal. Then, use the following command to install the Xcode templates: ./install-templates.sh -u -f
  4. Start up Xcode and go to File\New\Project…
  5. Select iOS\cocos2d v2.x\cocos2d iOS and click Next.
  6. Name your project CocosShaderEffects, select iPhone as the Device Family, and click Next.
  7. Choose a folder to save your project in, and click Create.

Next you need to enable ARC in your project. Although Cocos2D doesn’t use ARC, you can enable it for the rest of your project. This will allow you to write less code and have less chance of memory leaks.

Select Edit\Refactor\Convert to Objective-C ARC… from the menu. In the dialog which opens, select main.m, AppDelegate.m and HelloWorldLayer.m (if you don’t see them, click the little triangle next to CocosShaderEffects.app to expand the list and scroll to the very bottom of the files list) and click Check, Next, and Save through the next few dialogs. You will be prompted to enable automatic snapshots before the ARC conversion at this point. You can Disable it for this project but in general, having snapshots on might save you at junctures like this because you have a snapshot of your code before mass changes that you can revert back to, if something went wrong.

Pretty easy, eh? Now you can use the power of Cocos2D while using ARC for your own files :]

Compile and run. You should see this:

Now, download the resources for this project and extract the contents of the ZIP file into a temporary folder.

Drag and drop the extracted files into the project root. Make sure that the “Copy items into destination group’s folder (if needed)” checkbox is ticked if you are dragging and dropping from a temporary folder.

Since you will not be using HD images in this tutorial, if you are running your app on a device with a retina display, you might want to disable HD image loading. Open AppDelegate.m and comment out the following line:

if( ! [director_ enableRetinaDisplay:YES] )

Now you’re all set to begin your adventure with shaders and Cocos2D!

What are Shaders?

A shader is a simple C-like program that is used to execute rendering effects. As an example, as the name implies, a very basic function of a shader might be to add a different shade of color to an object (or portion of an object). A shader is executed on the GPU (graphics processing unit). For mobile devices, there are two kinds of shaders:

  1. Vertex shader: a vertex shader is executed on every vertex being rendered. So, when rendering a simple sprite, which would usually be just four vertices, a vertex shader will be executed four times to calculate the color and other attributes for each vertex in the sprite.
  2. Fragment shader: this type of shader is executed on each pixel that is visible onscreen. This means that when rendering a fullscreen quad on the iPhone, a fragment shader will be called 320×480 times.

Vertex and fragment shaders can’t be used alone: they have to be paired together. A pair of vertex and fragment shaders is called a program. It usually works like this:

  1. A vertex shader will first define the attributes for each vertex to be displayed on screen.
  2. Then, each vertex is broken down in turn in to a series of pixels which are run through a fragment shader (also known as a pixel shader)
  3. The final pixels are then rendered on screen.

Before the introduction of shaders, when you needed rendering effects, you only had access to what was known as the fixed-function pipeline. The fixed-function pipeline allowed you to apply effects that were more or less hard-coded. You could change a few parameters here and there (change a color, modify the position etc.), but that’s all you could do.

The limitations of the fixed-function pipelines prompted the creation of shaders which allow you to programmatically create new render effects. Basically, with shaders you can make any effects you can dream up: shaders allow you to write your own programs that are executed each time something is drawn on screen.

OpenGL-ES 2 shaders are written in the OpenGL Shading Language, GLSL. It’s very similar to Objective-C, so you shouldn’t have a problem using it if you’ve some experience coding, and especially if you’ve read some of the previous tutorials on this site. Don’t worry about reading the specification yet: I’ll walk you through the basics in sample code.

How Do Built-In Shaders Work in Cocos2D?

Cocos2D 2.x uses OpenGL-ES 2.0, and thus you need shaders for even the simplest rendering. So each CCNode has a shaderProgram instance variable which contains a pointer to a shader program which is called when the node is drawn.

Cocos2D also has its own CCShaderCache that allows you to use default shader programs, or cache your own programs so that you don’t need to load them multiple times. Constant definition keys to access predefined shader programs can be found in libs\cocos2D\CCGLProgram.h (such as kCCShader_PositionTextureColor).

You can find the default shaders Cocos2D 2.X uses in libs\cocos2d\ccShader_xxx.h.

In fact, do that now. Using the project navigator, open the Shaders group, then select the ccShader_PositionTexture_vert.h vertex shader. Note that the shader is stored as a string here (for speed of loading in Cocos2D), but we’ll list the code here without the string formatting for readability.

Here’s the code for this simple shader:

//1
attribute vec4 a_position;
attribute vec2 a_texCoord;
 
//2
uniform mat4 u_MVPMatrix;
 
//3
#ifdef GL_ES
varying mediump vec2 v_texCoord;
#else
varying vec2 v_texCoord;
#endif
 
//4
void main()
{
//5
  gl_Position = u_MVPMatrix * a_position;
//6
  v_texCoord = a_texCoord;
}

Before we step through the code in detail, let’s give a high level overview of this shader and its goals. Every shader program takes input, and generates output. For this shader:

  • As input it takes the position of each vertex, which for a sprite is the four corners of the sprite. It also takes the coordinate of the texture to display at each vertex (which will map to the four corners of the texture), and a transform to apply to the entire sprite to position/scale/rotate it. Cocos2D will pass in these input variables before running the shader.
  • As output it determines the final screen coordinate of the vertex (the input position with the transform applied), and the final texture coordinate for the vertex (same as input). The fragment shader will use these output variables, which we’ll cover after this.

Now that you have a high level overview, let’s look at greater detail on a section-by-section basis:

  1. Defines the input vertex data structures. The attribute keyword tells the compiler that this is an input variable that comes with each vertex data structure. Types vec2 and vec4 declare that the data is a vector of floats; vec can have 2-4 components. This declares two of these (one for position and one for texture coordinate).
  2. When you need some external variables passed to the shader from your source code, you need to declare them as uniform. Type mat4 declares that the data is a 4×4 matrix of floats. If you’re rusty on your linear algebra, remember that a matrix is a mathematical way you can use to position, rotate, and scale vectors (among other things).
  3. The vertex shader needs to send some data to the fragment shader. To notate variables that you pass from a vertex shader to a fragment shader, you use the varying keyword.

    The cool thing about varying variables is that they are interpolated. This is a fancy way of saying if you set a value of a varying coordinate of vertex A to 0, and the value of a varying coordinate at vertex
    B to 1.0, when you get to the fragment shader for a pixel right between A and B, OpenGL will automatically set the value of the variable to 0.5. Each fragment has an interpolated value calculated from the vertices that created this fragment. Here you can see precision specifier mediump.

    There are also two other specifiers available: highp and lowp. They define the importance of the quality of calculations/data storage. The higher precision means more precise data types will be used, and the calculations will be slower.

  4. Each shader has to have a main function just as in Objective-C.
  5. Vertex shaders need to fill the built-in variable gl_Position with the transformed vertex position. This shader multiplies the input position by the ModelViewProjection matrix that Cocos2D automatically passes in (to determine the position/scale/rotation of the sprite).
  6. This passes the input coordinates to the fragment shader unchanged by assigning them to the varying variable.

And here’s the code for the fragment shader counterpart of the above vertex shader – it’s in ccShader_PositionTexture_frag.h:

//1
#ifdef GL_ES
precision mediump float;
#endif
 
//2
varying vec2 v_texCoord;
//3
uniform sampler2D u_texture;
 
void main()
{
//4
  gl_FragColor =  texture2D(u_texture, v_texCoord);
}

Remember that by the time you get to the fragment shader, OpenGL is calling this program for every single pixel that makes up the sprite. The goal of this program is to figure out how to color each pixel. The answer is simple for this default shader: just pick the right spot in the texture that matches to that pixel.

Here’s a section-by-section breakdown:

  1. Setting the basic precision for floats at the top of a fragment shader is mandatory in OpenGL ES, so this sets it to medium precision.
  2. Anything that the vertex shader passes as output needs to be defined here as input. The vertex shader is passing the texture coordinate, so it is defined again here.
  3. Fragment shaders can also have uniform variables, which are constant values sent through from code. Cocos2D will pass the texture to use in a uniform variable, so this defines a sampler2D variable for it, which is just a normal texture.
  4. gl_FragColor is the built-in variable that needs to be filled with the final color of the pixel. This gets the color from the uniform texture and uses the texCoords from the vertex shader to determine which pixel should be used.

There’s one more step you should see to understand how everything fits together. These shaders happen to be used by CCGrid.m, so open it up so you can see how it’s used.

First, in init the shader is loaded from the cache:

self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];

If you’re curious you can look into the CCShaderCache code to see how the shaders are compiled and stored. Next, in blit it passes the variables to the shader and runs the program:

-(void)blit
{
    NSInteger n = gridSize_.x * gridSize_.y;
 
    // Enable the vertex shader's "input variables" (attributes)
    ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords );
 
    // Tell Cocos2D to use the shader we loaded earlier
    [shaderProgram_ use];
 
    // Tell Cocos2D to pass the CCNode's position/scale/rotation matrix to the shader
    [shaderProgram_ setUniformForModelViewProjectionMatrix];
 
    // Pass vertex positions
    glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, 0, vertices);
 
    // Pass texture coordinates
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, texCoordinates);
 
    // Draw the geometry to the screen (this actually runs the vertex and fragment shaders at this point)
    glDrawElements(GL_TRIANGLES, (GLsizei) n*6, GL_UNSIGNED_SHORT, indices);
 
    // Just stat keeping here
    CC_INCREMENT_GL_DRAWS(1);
}

You may be wondering where the texture was passed in. This happens to take a little shortcut – if you don’t set a variable it defaults to 0, and the first texture unit is also 0, so it just binds the texture into the first texture unit in afterDraw and never passes it in.

ccGLBindTexture2D( texture_.name );

Now that you have seen one example of how shaders are used in Cocos2D, you might want to dig around and see if you can prove to yourself how CCSprite is doing its rendering.

When you’re done with that, enough analyzing – time to create your own shaders!

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!

Creating An Emboss Shader

Time to try a different (and more complex) shader. Start by copying your existing layer implementation:

  1. Create a new file with the iOS\cocos2d v2.x\CCNode class template. Make it a subclass of CCLayer and save it with the name CSEEmboss.
  2. Copy the implementation section of CSEColorRamp.m into the implementation section of CSEEmboss.m. (If you copy the @implementation line as well, don’t forget to change the class name on the @implementation line.)

Import CSEEmboss.h at the top of HelloWorldLayer.m:

#import "CSEEmboss.h"

Now replace section #7 of init with the following code:

		// 7 - Emboss menu item
		CCMenuItem *emboss = [CCMenuItemFont itemWithString:@"Emboss" block:^(id sender) {
			CCScene *scene = [CCScene node];
			[scene addChild: [CSEEmboss node]];
			[[CCDirector sharedDirector] pushScene:scene];
		}];
		// 7.1 - Create menu
		CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, emboss, nil];

Build and run. Now you should see a new menu item, “Emboss”, on the opening screen. But if you select the Emboss menu item, you’ll note that it shows the same effect as before. To change the effect, you need to change the shader and the layer implementation.

Create a new file with the iOS\Other\Empty template. Name the new file CSEEmboss.fsh, select the file location, remove CocosShaderEffects from the Targets list and then click Create. Then go to the Build Phases tab as before and add the file to the Copy Bundle Resources phase.

Open CSEEmboss.fsh and add the following code:

#ifdef GL_ES
precision mediump float;
#endif
 
// 1
varying vec2 v_texCoord;
uniform sampler2D u_texture;
 
void main()
{
  // 2
  vec2 onePixel = vec2(1.0 / 480.0, 1.0 / 320.0);
 
  // 3
  vec2 texCoord = v_texCoord;
 
  // 4
  vec4 color;
  color.rgb = vec3(0.5);
  color -= texture2D(u_texture, texCoord - onePixel) * 5.0;
  color += texture2D(u_texture, texCoord + onePixel) * 5.0;
  // 5
  color.rgb = vec3((color.r + color.g + color.b) / 3.0);
  gl_FragColor = vec4(color.rgb, 1);
}

Here’s the breakdown:

  1. The shader expects texCoords passed from the vertex shader and a uniform texture passed from application code.
  2. Define the size for one pixel of your texture in texCoords space (normalized 0, – 1).
  3. Copy texCoords (the reason for this will become obvious in our next section).
  4. Start by defining the base color, which will be half the power of white. Then subtract the color of one pixel diagonal movement, and add the opposite diagonal displacement. This will make pixels that are on different color borders stand out, and the pixels of similar color will be pretty close to the base color.
  5. You average the color columns so that you end up with a grayscale image that focuses on color differences in the original texture.

In CSEEmboss.m, go to init and change the fragment shader name in section #2 to “CSEEmboss.fsh”. Remove sections #3, #4, and #5 to remove the code related to initializing the ramp texture.

Build and run. You should see the following:

Adding Movement

That’s pretty cool, but how about adding some simple movement to this emboss effect?

Open CSEEmboss.m and (if you haven’t already) remove the two ramp texture related instance variables (marked #2 and #3) from the implementation. Then, add two new instance variables just below the CCSprite instance variable:

  int timeUniformLocation;
  float totalTime;

You need to get a uniform location, so add the following to the end of init (replacing the current 3-5 section):

		// 3
		timeUniformLocation = glGetUniformLocation(sprite.shaderProgram->program_, "u_time");
 
		// 4
		[self scheduleUpdate];
 
		// 5
		[sprite.shaderProgram use];

The above code gets the uniform location for the time value and then schedules a Cocos2D update which will call the update: method for each frame.

Now add the update: method implementation below init:

- (void)update:(float)dt
{
  totalTime += dt;
  [sprite.shaderProgram use];
  glUniform1f(timeUniformLocation, totalTime);
}

The code simply increases the totalTime value and then passed it on to your custom shader program. You use glUniform1f since you’re sending just a single float value.

Now you need to change your CSEEmboss.fsh shader to use the passed in u_timeuniform. In order to use the value for movement, the simplest function you can use is sine/cosine based on u_time. Add the following line add the end of section #1:

  uniform float u_time;

The above declares uniform float u_time so that you can work with it in your shader code. Now, replace section #3 with the folowwing:

  // 3
  vec2 texCoord = v_texCoord;
  texCoord.x += sin(u_time) * (onePixel.x * 6.0);
  texCoord.y += cos(u_time) * (onePixel.y * 6.0);

The new code changes offset the coordinate specified by texCoords by simple wave-like movement from -6 to +6 pixels.

Build and run. Now, when you select the Emboss menu option you should see a moving embossed Cocos2D logo :]

Simple Moving Grass

For our final act, you’ll use shaders to create some simple moving grass that you can use in your own Cocos2D 2.x games.

Create a new file with the iOS\cocos2d v2.x\CCNode class template. Make it a subclass of CCLayer and save it with the name CSEGrass. Copy the implementation section of CSEEmboss.m into the implementation section of CSEGrass.m. (If you copy the @implementation line as well, don’t forget to change the class name on the @implementation line.) Change the loaded fragment shader name in init to “CSEGrass.fsh.”

Now, replace section #1 in init with the following code:

		// 1
		sprite = [CCSprite spriteWithFile:@"grass.png"];
		sprite.anchorPoint = CGPointZero;
		sprite.position = CGPointZero;
		[self addChild:sprite];

We’ve simply changed the code to use the grass texture and since the grass texture is already in landscape orientations, we’ve removed the rotation.

Next, open HelloWorldLayer.m and import CSEGrass.h at the top:

#import "CSEGrass.h"

Then, add a new menu item so that you can test your new effect by replacing section #7.1 of init with the following:

		// 7.1 - Grass menu item
		CCMenuItem *grass = [CCMenuItemFont itemWithString:@"Grass" block:^(id sender) {
			CCScene *scene = [CCScene node];
			[scene addChild: [CSEGrass node]];
			[[CCDirector sharedDirector] pushScene:scene];
		}];
		// 7.2 - Create menu
		CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, emboss, grass, nil];

Create a new file with the iOS\Other\Empty template. Name the new file CSEGrass.fsh, select the file location, remove CocosShaderEffects from the Targets list and then click Create. Then go to the Build Phases tab as before and add the file to the Copy Bundle Resources phase.

Open CSEGrass.fsh and add the following code:

#ifdef GL_ES
precision mediump float;
#endif
 
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_time;
 
// 1
const float speed = 2.0;
const float bendFactor = 0.2;
void main()
{
  // 2
  float height = 1.0 - v_texCoord.y;
  // 3
  float offset = pow(height, 2.5);
 
  // 4 multiply by sin since it gives us nice bending
  offset *= (sin(u_time * speed) * bendFactor);
 
  // 5
  vec3 normalColor = texture2D(u_texture, fract(vec2(v_texCoord.x + offset, v_texCoord.y))).rgb;
  gl_FragColor = vec4(normalColor, 1);
}

The new shader behaves in such a way that the grass bends a bit to one side and then to the other. The bend influences the top of the grass rather than the whole texture.

Here’s the step-by-step breakdown:

  1. Define some constant values to allow easy modifications to the effect.
  2. Calculate the texture offset based on the height. The texture starts from 0 at the top, so you need to invert it.
  3. You don’t want the height value to influence the bending linearly, as you want the bottom of the texture to remain still. The bend should increase with the height of the grass, so a perfect fit here is an exponential function based on height.
  4. The easiest function to provide the bending is sine. Multiply frequency by your speed constant. The range of the bend values is changed by multiplying the sine value by the bendFactor.
  5. Add your offset to the texCoord.x value. The fract function makes sure that when you get values that are < 0 or > 1, the texture repeats. Then use your offset color as the final fragment color.

As a final step, edit CSEGrass.m to use the new fragment shader.

Build and run the application and when you select the Grass menu option, you should see the following – but of course, instead of being a static image, the grass will actually wave back and forth as if due to a a gentle wind. Or, a rabbit scurrying through the grass :]

Where To Go From Here?

Here is an example project with all of the code from this tutorial.

I hope this tutorial showed you how simple shaders can be. Of course, these are just some of the simplest effects you can create with shaders. If you feel confident about what you’ve learnt here, try creating more advanced effects:

  • Convert the ramp texture into one that will create Toon Shading (you’ll need to decrease the palette of colors).
  • Create a raindrop effect by using the sin(x)/x function (go check it out in Mac Grapher, an app included on your Mac).
  • Play with post-process effects by rendering your scene into a render texture and then using a custom fragment shader to add blur.

Now go and dig some more into shaders and GLSL! I look forward to reading and responding to your comments in the forums.

Krzysztof Zablocki

This is a post by iOS Tutorial Team member Krzysztof Zablocki, a passionate iOS developer with years of experience.

User Comments

36 Comments

[ 1 , 2 , 3 ]
  • merowing wrote:
    lorenzoac7 wrote:How do I make the effect of the first part of this tutorial ignore the transparent pixels. If I put the shader to set the colors to default, the background of the sprite will be black, if I set the inverse, it turns white (as expected). How do I add shaders and keep the transparent background? Thinking it has something to do with either the blendfunc or the alpha, or both.

    Not sure how, or where I'd place these calls.

    Thanks


    Hey, if you mean the color ramp example just change the code to use original alpha value instead of 1 :)
    Code: Select all

    // 2
    vec4 normalColor = texture2D(u_texture, v_texCoord);

    // 4
    gl_FragColor = vec4(rampedR, rampedG, rampedB, normalColor.a);


    Hope this helps



    I've tried this change, and it doesn't seem to affect the result. Alpha is still white. Thoughts?
    OldJames
  • I'll just leave this here for anyone else who may come across the issue

    Code: Select all

    // 2
    vec4 normalColor = texture2D(u_texture, v_texCoord);

    // 4
    gl_FragColor = vec4(rampedR*normalColor.a, rampedG*normalColor.a, rampedB*normalColor.a, 1);


    Multiplying each new RGB value by the original alpha gets alpha to be considered per pixel. The 4th parameter (in my tests) appears to do nothing...
    OldJames
  • Hi all! i try to run sample from this tutorial in cocos2d-x 2.0.1 but it seems has no any effect.

    this is two i have added to my project:

    Code: Select all
    //
    //  Light.h
    //  NewLightTest
    //
    //  Created by roko on 20.08.12.
    //  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
    //

    #ifndef NewLightTest_Light_h
    #define NewLightTest_Light_h

    #include "cocos2d.h"

    using namespace cocos2d;

    class Light : public CCLayer
    {
        CCSprite *sprite;  //1
        int colorRampUniformLocation;  //2
        CCTexture2D *colorRampTexture; //3

        void changeImage();
        void goMenu();

    public:
        virtual ~Light();
        Light();
        bool init();
        LAYER_CREATE_FUNC(Light);
    };

    #endif



    Code: Select all
    //
    //  Light.cpp
    //  NewLightTest
    //
    //  Created by roko on 20.08.12.
    //  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
    //

    #include "Light.h"

    Light::~Light()
    {

    }

    Light::Light()
    {

    }

    bool Light::init()
    {
        if (CCLayer::init()) {

            sprite = CCSprite::create("Default.png");
            sprite->setAnchorPoint(CCPointZero);
            sprite->setRotation(90);
            sprite->setPosition(ccp(0, 320));
            this->addChild(sprite);

            CCGLProgram *shaderProgram_ = new CCGLProgram();
            this->setShaderProgram(shaderProgram_);
            shaderProgram_->initWithVertexShaderFilename("PositionColor.vsh", "PositionColor.fsh");
            shaderProgram_->addAttribute(kCCAttributeNamePosition, kCCVertexAttrib_Position);
            shaderProgram_->addAttribute(kCCAttributeNameTexCoord, kCCVertexAttrib_TexCoords);
            shaderProgram_->link();
            shaderProgram_->updateUniforms();

            // 3
            colorRampUniformLocation = glGetUniformLocation(sprite->getShaderProgram()->getProgram(), "u_colorRampTexture");
            glUniform1i(colorRampUniformLocation, 1);

            // 4
            colorRampTexture = CCTextureCache::sharedTextureCache()->addImage("colorRamp.png");
            colorRampTexture->setAliasTexParameters();

            // 5
            sprite->getShaderProgram()->use();
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, colorRampTexture->getName());
            glActiveTexture(GL_TEXTURE0);

            return true;
        }
        return false;
    }




    and in HelloWorldScene.cpp file i have added this line

    Code: Select all
    this->addChild(Light::create(), 1);



    then i added shaders into resources and in build phases.

    vertex shader "PositionColor.vsh":

    Code: Select all
    //1
    attribute vec4 a_position;
    attribute vec2 a_texCoord;

    //2
    uniform mat4 u_MVPMatrix;

    //3
    #ifdef GL_ES
    varying mediump vec2 v_texCoord;
    #else
    varying vec2 v_texCoord;
    #endif

    //4
    void main()
    {
        //5
        gl_Position = u_MVPMatrix * a_position;
        //6
        v_texCoord = a_texCoord;
    }

    and fragment shader "PositionColor.fsh":

    [code]#ifdef GL_ES
    precision mediump float;
    #endif[/code]

    // 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);
    }
    roko
  • Hi, I have a question about this tutorial.

    When I follow this tutorial, running on simulator for retina or on my iPhone4, these three effects only hold 1/4 of the screen.
    But when I run the tutorial sample code(you provided), it display correctly with fullscreen.
    My Cocos2D is 2.02. I'm sure my code is exactly as same code as the code in tutorial sample.
    I wonder if it is because I use newer version Cocos2D lib than the lib in the tutorial sample code?
    How to fix it.

    Thank you for your job, it helps a lot.


    I found out ...
    In AppDelegate.m
    comment
    if( ! [director_ enableRetinaDisplay:YES] )
    marsz
  • Krzysztof, thanks for a great tutorial!

    Hi All!
    Please help me find information about using multisampling in Cocos2d+OpenGl ES 2.0.
    Can I apply multisampling to a single CCNode, not to the whole project (example below)?

    CCGLView *glView = [CCGLView viewWithFrame:[window_ bounds]pixelFormat:kEAGLColorFormatRGB565
    depthFormat:0
    preserveBackbuffer:NO
    sharegroup:nil
    multiSampling:YES
    numberOfSamples:4];
    Irix
  • Hi,
    Thank you for a very helpful tutorial !
    I was wondering if you can link me to some other custom-shaders tutorials?

    Thanks;)
    Kevin Hury
[ 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!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Gaming!

    Loading ... Loading ...

Last week's winner: Apple TestFlight Tutorial.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in January: WatchKit.

Sign Up - January

Our Books

Our Team

Tutorial Team

  • Dani Arnaout

... 60 total!

Update Team

  • Ray Fix
  • Riccardo D'Antoni

... 12 total!

Editorial Team

... 17 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!