Integrating LiquidFun with Cocos2d-x: Part II

In Part I I described to how integrated LiquidFun with Cocos2d-x.
In this part (part II) I’ll describe how to render the particles using a basic water effect.

LiquidFun + Cocos2d-x
LiquidFun + Cocos2d-X using Render-To-Texture technique to simulate water.

Part I uses just one glDrawArrays(GL_POINTS, 0, total); to draw the particles. And although that works to draw “particles”, it is not enough to draw “water”.

Drawing “water” requires a more complex rendering algorithm, like the one used in this example. And implementing an algorithm similar that one is what this article describes.

The algorithm works more or less like this:

  • Choose a white circle and blur it.
    • You can blur the circle at runtime
    • Or you can blur it off-line.
  • Create an a new frame-buffer (think of a clean off-screen buffer where you can render whatever you want)
  • Render the particles into the newly created frame-buffer using the blurred circle
  • Now render the frame-buffer into the main color-buffer using a threshold. The threshold could be something like this:
    • If pixel.r < 0.1, discard the pixel (the pixel won’t be drawn)
    • If pixel.r < 0.2, draw a blue pixel (for the border, although this is optional)
    • else draw a white pixel (the inner part of the water)

How to do it using Cocos2d-x and LiquidFun

Let’s take the LFParticleSystemNode from Part I, and “evolve” it:

The first thing to do is to add the “off-screen” frame-buffer into the LFParticleSystemNode class. In Cocos2d-x, the “off-screen” buffers are created with the RenderTexture class. Example:

bool LFParticleSystemNode::init(b2ParticleSystem* particleSystem, float ratio)
{
...
    // create an off-screen frame-buffer with the size of the screen
    auto s = Director::getInstance()->getWinSize();
    _renderTexture = cocos2d::RenderTexture::create(s.width, s.height, Texture2D::PixelFormat::RGBA8888);
    this->addChild(_renderTexture);
    _renderTexture->setAnchorPoint(Point::ANCHOR_MIDDLE);
    _renderTexture->setPosition(Point(s.width/2, s.height/2));

    // Change the default shader. Use a the threshold shader
    auto program = GLProgram::createWithByteArrays(_renderTextureShaderVert, _renderTextureShaderFrag);
    auto programState = GLProgramState::getOrCreateWithGLProgram(program);
    programState->setUniformFloat("u_threshold_discard", 0.15);
    programState->setUniformFloat("u_threshold_border", 0.3);

...
}

And, as mentioned earlier, the RenderTexture (the off-screen frame-buffer) needs a shader with a threshold. The threshold shader should look like the following:

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_threshold_discard;
uniform float u_threshold_border;

void main()
{
    vec4 color = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    if( color.r < u_threshold_discard)
        // black or discard
        color = vec4(0,0,0,0);
    else if( color.r < u_threshold_border)
        // blue for the border
        color = vec4(0.2,0.2,0.9,1);
    else
        // white for the center
        color = vec4(1,1,1,1);
    gl_FragColor = color;
}

The values u_threshold_discard, and u_threshold_border are defined at runtime. In the example, they are set at 0.15 and 0.3 respectively.

The next thing to do is, to render the particles in the RenderTexture.

void LFParticleSystemNode::draw(Renderer *renderer, const Mat4 &transform, uint32_t transformFlags)
{
    // tell RenderTexture to "capture" the particles
    _renderTexture->beginWithClear(0,0,0,0);

    _customCommand.init(_globalZOrder);
    _customCommand.func = CC_CALLBACK_0(LFParticleSystemNode::onDraw, this, transform, transformFlags);
    renderer->addCommand(&_customCommand);

    // tell RenderTexture to stop "capturing" the particles
    _renderTexture->end();
}

 The result is the following

Comparing simple glDraw() with glDraw() + RenderTexture
Left: Without RenderTexutre.  Right: RenderTexture + custom shader with threshold

Further improvements

What I described is just a simple algorithm to render water. But more advanced (and better looking) algorithms could be implemented using the same principle:

  • Render blurred (or any other effect) particles into an off-screen buffer
  • Render the off-screen buffer with an special shader

As an example, the official “LiquidFun – EyeCandy” sample  from Google uses a much more complex shader that involves lighting, refraction, and distortion in the background.

Customizing shaders in Cocos2d-x v3.1+

Cocos2d-x v3.1 introduced a new way to customize shaders. The same GLProgram works like in v3.0, but if you want to add custom attributes or uniforms, instead of subclassing GLProgram, you can can do it by using the GLProgramState class. Subclassing GLProgram still works, but the recommend way is to use GLProgramState. It works like this:

// you create a GLProgram like in v3.0
GLProgram* glprogram = GLProgram::createWithByteArrays(_particleShaderVert, _particleShaderFrag);

// and then you create a GLProgramState with it
GLProgramState* glprogramstate = GLProgramState::getOrCreateWithGLProgram(glprogram);

// attach the ProgramState to a Node
sprite->setGLProgramState( glprogramstate );

The next thing to do is to add custom uniforms and/or attributes using the GLProgramState API.

Adding custom uniforms:

// setting a float
glprogramstate->setUniformFloat("u_name_1", 0.15f);

// setting a vec2
glprogramstate->setUniformVec2("u_name_2", Vec2(0.2, 0.3));

// setting a callback
glprogramstate->setUniformCallback("uniform_name_2", [](GLProgram* program, Uniform* uniform){
  glUniform4fv( uniform->location, 10, _buffer);
});

And the same is true for attributes:

// setting an attribute pointer
glprogramstate->setVertexAttribPointer("a_position", 2, GL_FLOAT, GL_FALSE, 0, _particleSystem->GetPositionBuffer());

// setting a callback
glprogramstate->setVertexAttribCallback("a_color", [](VertexAttrib* vertexAttrib) {
  glVertexAttrib4f(vertexAttrib->index, 0.2, 0.8, 0.7, 1.0);
});

Download and running the sample

Clone the cocos2d-x-samples repo and follow its instructions. Then launch the “LiquidFun – EyeCandy” demo.

$ git clone https://github.com/cocos2d/cocos2d-x-samples.git

… and follow instructions from the site. And then launch it:

$ cd samples/LiquidFun-EyeCandy/proj.ios_mac
$ open LiquidFun-EyeCandy.xcodeproj

Published by ricardoquesada

cocos2d, unicyclist, commodore 8-bit

17 thoughts on “Integrating LiquidFun with Cocos2d-x: Part II

  1. HI Ricardo. Thanks for the followup tutorial and instructions to build the project. But as Cocos2d-x is not updated to 3.2 and Mat4 is kmMat4 now (I think) and a lot has changed. Could you kindly show how to make it work on 3.2? Pls. Thanks.

    1. Hmm.. i just checked the version sry. It is 3.2. But when I created my own project in 3.2 and added the liquidfun box2d folder and added your classes and resources I got a whole slew of errors.

  2. Found the problem. The cocos_console_root is still pointing to my v3.0 project so even if I goto 3.2 and create a new project it doesnt copy the 3.2 version of cocos2d but 3.0. Regarding Mat4, it is the other way around in 3.0 kmMat4 is used and not just Mat4.

    I am in the process of changing the console_root in the environment variable now. Thanks 🙂

  3. ricardoquesada: i clone sample from github, and run the sample ,it’s ok , but when i rebuild a new project using cocos2d-x 3.2, then when i add box2d from liquid, can not pass compile, always telling me that
    can not find box2d stuff, although i have add box2d file from liquid.

  4. Hello Ricardo,
    I very excited with this tutorial. I’m developing a project using water effect like this and it helps me a lot. Thanks you so much!

    But, I want my water is like the game Where’s my water. I mean, the water particle is look like water shape, not just a circle. I think I have to use shader but I don’t enough experience.

    Can you help me? Thanks so much.

    p/s: sorry for my bad English. 🙂

    1. @kosdevil:
      probably you will need to play a bit with the shader. In order to make it look more like a water, you can still use circles. Probably you need to use smaller “solid” circles, and a bigger “gradient” area.
      Using a different shader will also help.
      Please, take a look at the source code of the LiquidFun Demo (it is on the android store). It uses a much more advanced shader and it looks like real liquid.

  5. Hello Ricardo,

    Thank you for the good tutorial. Only one color or one texture is possible. What should I do if I want to use multiple colors or textures?

    b2ParticleGroupDef

    pd pd.color pd.flags = b2_colorMixingParticle

    I want to use the above function, but is it not possible with “LFParticleSystemNode”?

    I tried it alone, but it didn’t work, so I’m leaving a message. Please reply.

    ps: sorry for my bad english.

Leave a comment