Categories
Programming

Custom Shaders in Cocos2d-x

I wanted to add a feature to the game whereby when an enemy or player ship gets hit and takes damage, there’s a visual cue for the player. I had a look around how some other 2D shooters do it and decided that making the sprite flash white briefly, and showing some pieces getting “blasted off”, would look good.

To cut a long story short, I ended up using a custom OpenGL shader with Cocos2d-x to make it work. The finished effect looks like this:

Particle effect to show pieces being “blasted off” of the enemy ship
Shader effect that flashes the enemy white briefly each time it takes damage

Which Shader Do I Need?

Custom shaders are implemented in their own code files in Cocos2d-x (not sure if that’s true for OpenGL shaders in general). There are two breeds of shader:

  • Vertex Shaders: Transform shapes into their on-screen coordinates
  • Fragment Shaders: These dictate what the actual pixels look like, transformed from the input texture into what appears on screen

For my purposes, I simply need to make every pixel of a given sprite appear white, I’m not altering their positions. As such, I only needed to tinker with the fragment shader.

In Cocos2d-x you can assign a shader to a particular node, so the process should be as simple as:

  1. Write the shader
  2. Assign it to the node
  3. Restore the default shader a fraction of a second later

Writing The Fragment Shader

Now I knew I needed a fragment shader, I searched the web until I found a suitably simple example that I could alter. Here’s the finished version:

#ifdef GL_ES precision mediump float; #endif // v_texCoord is the texture coord position, u_texture is our original texture varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { // get the "usual" colour - although we don't actually use it, could be useful for future dev vec3 normalColor = texture2D(u_texture, v_texCoord).rgb; // set the output pixel colour to white, but keep alpha value from original gl_FragColor = vec4(1, 1, 1, texture2D(u_texture, v_texCoord).a); }
Code language: PHP (php)

The code is straightforward; we simply set the output colour (named gl_FragColor as standard) to maximum RGB float values of 1.0, and keep the original texture’s alpha value (texture2D(u_texture, v_texCoord).a).

Make sure the shader file is saved into your resources directory. I named mine “flash.fsh”.

Using The Shader in Cocos2d-x

To use the shader, we need to create the shader object and assign to a node (the enemy being hit in this case).

Here’s the code I used to create the shader object, which I do just once in the main layer:

// initialise the damage effect shader std::string contents = FileUtils::getInstance()->getStringFromFile("flash.fsh"); flashShader = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, contents.c_str()); flashShader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION); flashShader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD); flashShader->link(); CHECK_GL_ERROR_DEBUG(); flashShader->updateUniforms(); CHECK_GL_ERROR_DEBUG(); flashShader->retain();
Code language: PHP (php)

The shader is then passed as a parameter to the Ship class which will use it for the flash effect. The shader is applied, then a callback function is scheduled to revert to the original shader a fraction of a second later. Notice that we set the current node’s texture as the “u_texture” object referenced in the shader code itself:

void Ship::doDamageFlash(GLProgram* p) { // Here we set up which textures are associated with which shader variables auto glProgramState = GLProgramState::getOrCreateWithGLProgram(p); this->setGLProgramState(glProgramState); glProgramState->setUniformTexture("u_texture", this->getTexture()); this->getGLProgram()->use(); // schedule function to reset shaders this->scheduleOnce(schedule_selector(Ship::resetShaders), 0.05); } void Ship::resetShaders(float dt) { this->setGLProgram(ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP)); this->getGLProgram()->use(); }
Code language: PHP (php)

Using the shader we created named “flashShader” above, it is passed to the Ship class like so:

ship->doDamageFlash(flashShader);
Code language: PHP (php)

Simple!