Tutorial: Using Box2D Physics In Cocos Creator To Build Liquid Physics

Using Box2D physics in Cocos Creator to build liquid physics

Thanks to the great Cocos Star Developer “What a Coincidence C” for letting us share his tutorial.

[Screenshot of the game “Where’s the water?”]

Implementing interactive liquids in games is a more attractive solution. For example, the popular game “Where’s the water?” has achieved good results.

In Cocos Creator, if you want to realize 2D liquid and consider the operating efficiency, you can use the physical particle effect of Box2D to simulate.

Building a dynamic 2D liquid solution in Cocos Creator will be analyzed in this tutorial.

The structure of this tutorial is as follows:

  1. How to use

  2. Pre-knowledge

  3. Principle analysis

The project file is available at the bottom of this article, it can be used when the situation arises.

1. How to build it

1.1 Scene Construction

Create a new empty scene in Cocos Creator and create a UICanvas:

image

Create a camera for liquid rendering Camera-001:

For this camera, make it ClearFlag = SOLID_COLOR

image

The function of this camera is to draw the liquid to an RT and then project the dynamic texture to a specific place in the sprite UI.

After that, the collider is arranged in the scene. Since it is Box2D, remember to choose 2D for the collider. Otherwise, the collider will use Bullet and have nothing to do with Box2d:

image

Two physics engines, Bullet and Box2D, are packaged inside Cocos Creator, and both are in separate worlds

In Box2d, if you want the collision between the colliders to be effective, at least one party needs to hold the Rigidbody2D component: Therefore, you need to add a RigidBody2D to the collider, whose type is selected as static. In this way, the physics engine will not simulate his speed and force.

1.2 Adding liquid

Create an empty UINode that is a Node with only UITransform components:

image

Add WaterRender to this component:

image

Then specify some of his values:

  1. Custom Material:

image

  1. FixError points to a 2x2 solid color small texture:

image

  1. Water pipes: Water pipes consist of multiple colliders that constrain the liquid to flow where we want it

image

1.3 Run the game to play the demo

bandicam 2022-01-05 10-10-48-84800_00_00--00_00_20

The physics debugging is turned on here, and the running state of the particles can be observed more clearly.

2. Pre-existing knowledge

2.1 Physics engine

image

Box2D is a lightweight 2D game physics engine.

Most of the 2D physics of common game engines are done using Box2D.

In the physics engine simulation, through the force of the center of mass, calculate its speed and acceleration, etc. and finally get the position of the object.

The rendering engine will then read the calculation results of the physics engine and apply them to the rendering

2.2 LiquidFun

image

LiquidFun is an extension library based on Box2D.

The role of this library is to add a particle system that simulates liquid to Box2D.

Google senior programmer Kentaro Suto developed the library, and the source code is written in C++ and translated to JavaScript

2.3 Assembler

In a game engine, when drawing a sprite or a model, it needs to generate specific vertices and call the driver method (OpenGL, DirectX … etc.) to draw it to the screen.

Inside Cocos Creator, if we want to draw a series of vertices to the screen, we need to use the Assembler

The assembler, as the name suggests, assembles vertices for use by rendering components

Through this Assembler, the position, color, texture coordinates, index of vertices can be customized.


//There are various Assemblers in Cocos Creator:
/**
 * simple assembler
 * The assembler is available through `UI.simple`.
 */
export const simple: IAssembler
/**
 Tiled assembler
*/
export const tiled: IAssembler =
...

In this demo, the relevant information of all vertices in the vertex buffer is calculated by reading the position of the particles in the physics engine.

3. Principle Analysis

In render.ts, There are two classes: WaterRender and WaterAssembler

First, parse the WaterRender class

3.1 Analysis of WaterRender

WaterRender is the core class of the entire DEMO, responsible for creating and rendering particles.

3.1.1 Renderable2D

WaterRender is inherited from Renderable2D.

In Cocos Creator, any node object that needs to be rendered will hold a RenderableComponent, which Renderable2D is the base class for rendering 2D components in Cocos Creator.

Customize your own rendering scheme by overriding the _render method.

Here, using a custom _assembler to assemble the geometry that needs to be drawn.

/**
*commitComp will submit the current rendering data to the rendering pipeline
*/

protected _render(render: any) {
    render.commitComp(this, this.fixError, this._assembler!, null);
}

3.1.2 Create a particle system

It can be understood that liquids are composed of many tiny water droplets.

This gives the physics engine the option to use particle systems to simulate the behavior of a large number of water droplets in an efficient manner.

Create the particle system:

var psd_def = {
        strictContactCheck: false,
        density: 1.0,
        gravityScale: 1.0,
        radius: 0.35,  // Here the radius of the particle is specified

        ...
}

this._particles = this._world.physicsWorld.impl.CreateParticleSystem(psd);

Create a particle group:

var particleGroupDef = {
   ...
    shape: null,
    position: {
        x: this.particleBox.node.getWorldPosition().x / PHYSICS_2D_PTM_RATIO,
        y: this.particleBox.node.getWorldPosition().y / PHYSICS_2D_PTM_RATIO
    },
    // @ts-ignore
    shape: this.particleBox._shape._createShapes(1.0, 1.0)[0]
};

this._particleGroup = this._particles.CreateParticleGroup(particleGroupDef);
this.SetParticles(this._particles);

A particle group defines a set of particles for a particle emitter with a custom shape:

// Create the geometry of BoxCollider2D
shape: this.particleBox._shape._createShapes(1.0, 1.0)[0]

By observing liquids, it can be found that liquids have some common properties:

  1. Flow, droplets move along the surface of the collider, and gravityScale: 1.0 defines the coefficient by which the particles are affected by gravity.

  2. Adhesion, which can be observed when two water droplets are close together, will be attracted to each other by the force of the liquid by defining viscousStrength to define the adhesion of the particles.

  3. Compression, the liquid particles will be compressed. The following values define the compression allowed by the particles:

    pressureStrength
    staticPressureStrength
    staticPressureRelaxation
    staticPressureIterations

  4. Surface tension, we all know the experiment of putting a coin on the water. The coin will not sink to the bottom. This is the surface tension of the liquid. The following two properties can adjust the surface tension of a liquid:

    surfaceTensionPressureStrength: 0.2,
    surfaceTensionNormalStrength: 0.2,

3.2 Analysis of WaterAssembler

WaterAssembler and RenderableComponent provides customization of vertex buffers.

Inside this class, 4 separate vertices are generated by accessing the position of each particle of the particle system

let posBuff = particles.GetPositionBuffer();
let r = particles.GetRadius() * PHYSICS_2D_PTM_RATIO * 3;

for (let i = 0; i < particleCount; ++i) {
    let x = posBuff[i].x * PHYSICS_2D_PTM_RATIO;
    let y = posBuff[i].y * PHYSICS_2D_PTM_RATIO;

    // left-bottom
    vbuf[vertexOffset++] = x - r; //x
    vbuf[vertexOffset++] = y - r; //y
    vbuf[vertexOffset++] = 0; // z
    vbuf[vertexOffset++] = x; // u
    vbuf[vertexOffset++] = y; // v
   ...
}

The vertex cache describes the data of the vertices

Finally calculate the index cache:

// fill indices
const ibuf = buffer.iData!;

for (let i = 0; i < particleCount; ++i) {
    ibuf[indicesOffset++] = vertexId;
    ibuf[indicesOffset++] = vertexId + 1;
    ibuf[indicesOffset++] = vertexId + 2;
    ibuf[indicesOffset++] = vertexId + 1;
    ibuf[indicesOffset++] = vertexId + 3;
    ibuf[indicesOffset++] = vertexId + 2;

    vertexId += 4;
}

The index cache specifies the order in which the vertices are drawn

This generates a rectangle based on the center point of the particle, but what you see in the end is a circle, right?

The magic here is solved with materials and the Effect system.

3.3 Material and Shader Analysis

bandicam 2022-01-05 10-10-48-84800_00_00--00_00_20

When simulating, you need to use effect.effect special effects to simulate

Note that the transparent technique is selected here:

image

Within effect.effect, the vert function has two variables of frag: v_corner and v_center. These two variables represent the position of the center point and the corner of the particle position.

  out vec2 v_corner;
  out vec2 v_center;

  vec4 vert () {
    vec4 pos = vec4(a_position.xy, 0, 1);    
    // no a_corner in web version

    // use a_position instead of a_corner
    v_corner = a_position.xy * reverseRes;

    // Since the particle is a solid color, the texCoord records the position of the center point of the particle
    v_center = a_texCoord.xy * reverseRes;
    v_corner.y *= yratio;
    v_center.y *= yratio;

    return cc_matViewProj * pos;
  }

These two frag variables in smoothstep are calculated by interpolation.

smoothstep(edge0, edge1, x)

//This function will calculate the Hermite interpolation based on x.
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);

image

Interpolate within the frag function by calculating the distance between the pixel position and the particle center using smoothstep.

The radius of the particle will be controlled between 1 and 3 times the radius

At the same time, because the calculation is based on the center and radius, the particles will also change from a rectangle to a circle:

  in vec2 v_corner;
  in vec2 v_center;

  vec4 frag () {
    float mask = smoothstep(radius * 3., radius, distance(v_corner, v_center));
    return vec4(1.0, 1.0, 1.0, mask);
  }

The color of the particles drawn at this time is white:

image

Finally, render it blue by display.effect matching the render texture:

display.effect uses the color passed in the property viewer color:

  in vec4 color;

  #if USE_TEXTURE
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
  #endif

  vec4 frag () {
    vec4 o = vec4(1, 1, 1, 1);

    #if USE_TEXTURE
      o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
      #if IS_GRAY
        float gray  = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
        o.r = o.g = o.b = gray;
      #endif
    #endif

    o.a = smoothstep(0.95, 1.0, o.a);

    o *= color;

    ALPHA_TEST(o);

    return o;
  }

At this time, there will be some burrs due to the alpha problem:

image

So by smoothstep(0.95, 1.0, o.a), the alpha values of the pixels are all controlled between 0.95 and 1.

This rendering shows that it is unnecessary to simulate real effects to make games. We can make good results as long as we fool the eyes!

Epilogue

In addition to being suitable for simulating liquids, physical particle systems can also be used to simulate any deformable object.

This is the end of this tutorial. If you are interested in the physics engine, please leave a message in the comment area.

Download link

DEMO download address

Reference Articles & Extended Reading

LiquidFun official website

LiquidFun reference documentation

SmoothStep

3 Likes

Thanks for good instruction.
I’d like to use LiquidFun API with type-safety (I feel very uneasy to treat PhysicsSystem2D.instance.physicsWorld.impl as any ).
I just found a very tricky way (see README of GitHub - taqqanori/cocos-creator-liquid), any better ideas?