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

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 :]