Gotchas: Post shader effects in Cocos2d-x - Fri, Jul 4, 2014
This tutorial is compatible with Cocos2d-x v3.1.
There might be times that you want to apply an effect to the whole scene and not just an object. For example, you might want to make your scene black and white or add a noise, vignette or glow to your game. In such cases, setting the same GLProgram
to each single node in the game probably wouldn’t give you the desired outcome, because for each different node, the effect will run independently and is limited to the edges of the node, and thus you can detect a non-homogeneity in the resulting image.
A good solution to achieve the desired outcome is to first render everything as normal, then apply a single effect to the whole scene, a technique known as multi-pass rendering. I will explain how to do this in Cocos2d-x, and in the end I will give you a pre-written code that you can use to quickly do full screen effects without going into any trouble.
As I mentioned, you need to first render everything as usual somewhere. We will create a RenderTexture
(which is basically an off-screen texture) to hold the output of our first render pass. Then we switch back to the normal buffer, and we render our RenderTexture
with the GLProgram
that we want on the screen. Basically here’s a step by step guide to do all this:
Step 1: Create the RenderTexture
We need to create a RenderTexture
which is the same size of our game. To do this, we first get the game size (in Cocos2d Point unit) from the Director
, then we create a RenderTexture
object called renderTexture
with the acquired size.
Size visibleSize = Director::getInstance()->getVisibleSize();
renderTexture = RenderTexture::create(visibleSize.width, visibleSize.height);
addChild(renderTexture);
Step 2: Activate the RenderTexture and draw the nodes
We want to draw our nodes on the RenderTexture
that we just created. To do this, we will simply call the beginWithClear(…)
method of renderTexture
. This way, we make sure that renderTexture
is completely blank (and totally transparent) when we start drawing.
renderTexture->beginWithClear(0, 0, 0, 0);
Now we draw our nodes by calling their visit method:
node->visit(renderer, parentTransform, parentTransformUpdated);
Finally, we finish drawing on renderTexture
by calling its end()
method:
renderTexture->end();
Step 3: Draw renderTexture with an effect
The last step is to create a sprite that draws the contents of renderTexture
. We call this sprite rendTexSprite
and we position it to the bottom left corner of the screen. Since renderTexture
is the same size of our game, this is going to cover all the screen.
rendTexSprite = Sprite::create();
rendTexSprite->setTexture(renderTexture->getSprite()->getTexture());
rendTexSprite->setTextureRect(Rect(0, 0, rendTexSprite->getTexture()->getContentSize().width, rendTexSprite->getTexture()->getContentSize().height));
rendTexSprite->setPosition(Point::ZERO);
rendTexSprite->setAnchorPoint(Point::ZERO);
rendTexSprite->setFlippedY(true);
rendTexSprite->setGLProgram(p);
addChild(rendTexSprite);
Note that we flipped this texture vertically. This is due to the fact that the rendered image is stored backwards in memory and we need to flip it in order to show it correctly.
ShaderLayer class
Following all these steps can be painful, so I made a subclass of Layer that handles all this stuff for you. To use it, you make an instance of it and add it to your scene. Then you simply add your nodes to it instead of your top-level layer. ShaderLayer will handle all the complicated stuff and in the end it will show you a neat output with your effect.
Here’s how to use it:
First, include it in your code file by adding #include "ShaderLayer.h"
at the top of your code. Then make a ShaderLayer
object and pass to it your GLSL files:
auto shaderLayer = ShaderLayer::create(“game.glsl”);
addChild(shaderLayer);
Now by adding your sprites and other nodes to shaderLayer
, you apply a full screen effect on them. You can get the ShaderLayer code from this github link:
https://github.com/samafshari/ShaderLayer.git