Fast Cartoon Fluid Simulation

TL;DR: See it here.

Quite recently, I've came across this article to easily simulate water in 2D and I thought it was cool. So I've decided to implement my own version using Matter.js and pixi.js because I've always wanted to try them out and finally found a good excuse to use them.

Physics Simulation

In order to simulate the water particles, we will be using Matter.js for rigid body physics.

Matter.js uses the Matter object for namespacing. All of the examples provided by the documentation does this to shorten the variable names. So, we shall do what the romans do:

var Common = Matter.Common;  
var World = Matter.World;  
var Composite = Matter.Composite;  
var Composites = Matter.Composites;  
var Bounds = Matter.Bounds;  
var Body = Matter.Body;  
var Bodies = Matter.Bodies;  
var Engine = Matter.Engine;  
var Events = Matter.Events;  

Firstly, we need to get our physics engine running:

var w = 200;  
var h = 200;

var container = document.getElementById('canvas-container');  
var engineOpts = {  
    render: {
        options: {
            hasBounds: true,
            height: h,
            width: w
        }
    }
};
var engine = Engine.create(container, engineOpts);  
Engine.run(engine);  

We don't want our water particles to fall into the abyss, so we set up boundaries on the borders of our viewport:

var staticOpts = {  
    isStatic: true,
    render: { visible: false }
};

World.add(engine.world, [  
    Bodies.rectangle(w/2, -25, w + 2*25, 50, staticOpts),
    Bodies.rectangle(w/2, h + 25, w + 2*25, 50, staticOpts),
    Bodies.rectangle(w + 25, h/2, 50, h + 2*25, staticOpts),
    Bodies.rectangle(-25, h/2, 50, h + 2*25, staticOpts)
]);

We don't want too many water particles, so we could instead optimise by rendering a solid water block underneath some free-flowing particles.

var waterBlockOpts = {  
   isStatic: true,
    render: {
         fillStyle: '#fff',
         lineWidth: 0
    }
};

var waterBlock = Bodies.rectangle(w * 0.5,  
                              h * 1.5,
                              w * 2,
                              h,
                              waterBlockOpts);

Then, we create the particles using the built-in stack function:

var particleWidth = 2;  
var numParticles = Math.floor((engine.render.options.width) / (particleWidth + 2));

var particleOpts = {  
    restitution: 0.7,
    friction: 0.2,
    frictionAir: 0,
    density: 0.01,
    render: {
         fillStyle: '#fff',
         lineWidth: 0,
         strokeStyle: '#fff'
    }
};

var waterParticles = Composites.stack(0, h - 50, numParticles, 3, 0, 0, function(x, y, column, row) {  
    return Bodies.circle(x, y, particleWidth, particleOpts, 100);
});

We just place these bodies we created onto the physics world:

World.add(engine.world, [  
        waterParticles,
        waterBlock
    ]);

Now, we need to set the background to a contrasting colour and disable the default wireframe rendering:

engine.render.options.background = '#000';  
engine.render.options.wireframes = false;  

Brownian Motion

The physics simulation should be rendering correct now but there is a problem: the particles are behaving more like sand than water.

To make the particles behave more like water, we need to add one special ingredient which is random movement. This naturally occurring phenomena is called Brownian Motion.

Events.on(engine, 'beforeUpdate', function(event) {  
    var bodies = particles.bodies;
    for (var i = 0; i < bodies.length; i++) {
        var body = bodies[i];

        if (!body.isStatic) {
            Body.translate(body, {
                x: Common.random(-1, 1) * 0.25,
                y: Common.random(-1, 1) * 0.25
            });
        }
    }
});

Rendering Filters

We will be using pixi.js as our renderer because of its advanced features such as fast batch rendering and filters.

Fast Gaussian Blur

Thankfully, pixi.js already has a fast gaussian blur implementation out of he box so we simply use it:

var blurFilter = new PIXI.filters.BlurFilter();  
blurFilter.blur = 80;  
blurFilter.passes = 10;  

Threshold Filter

However, pixi.js doesn't have a threshold filter, but the good news is, its pretty easy to implement:

function ThresholdFilter()  
{
    PIXI.filters.AbstractFilter.call(this,
        // vertex shader
        null,
        // fragment shader
        [
'precision mediump float;',

'varying vec2 vTextureCoord;',

'uniform sampler2D uSampler;',  
'uniform float threshold;',

'void main(void)',  
'{',  
'    vec4 color = texture2D(uSampler, vTextureCoord);',  
'    vec3 blue = vec3(51.0/255.0, 153.0/255.0, 255.0/255.0);',  
'    if (color.a < threshold) {',  
'       gl_FragColor = vec4(vec3(0.0), 1.0);',  
'    } else {',  
'       gl_FragColor = vec4(blue, 1.0);',  
'    }',  
'}',  
        ].join('\n'),
        // custom uniforms
        {
            threshold: { type: '1f', value: 0.5 }
        }
    );
}

ThresholdFilter.prototype = Object.create(PIXI.filters.AbstractFilter.prototype);  
ThresholdFilter.prototype.constructor = ThresholdFilter;

Object.defineProperties(ThresholdFilter.prototype, {  
    threshold: {
        get: function ()
        {
            return this.uniforms.threshold.value;
        },
        set: function (value)
        {
            this.uniforms.threshold.value = value;
        }
    }
});

var thresholdFilter = new ThresholdFilter();  
thresholdFilter.threshold = 0.05;  

The Result

This is the result after the filters. You can also click on the canvas to change the level.

Fill Controls

Unfortunately, the simulation lags in low powered devices and in some browsers. I find that Chrome and Firefox works best due to their good WebGL implementations.

Relevant Modules in NUS

Interested to do physics simulations? I suggest you first pick up kinematics from the following modules:

  • PC1431 Physics IE

The module is only very slightly beyond A'levels difficulty and has very few practical labs. The midterms is MCQ and the finals are open-ended questions. Usually, the bell-curve is very favourable due to inherent difficulty of physics.

Interested to learn more about computer graphics?

You can learn about the concepts in the following modules:

  • CS3241 Computer Graphics
  • CS3242 3D Modelling and Animation
  • CS4247 Graphics Rendering Techniques
  • CS4345 General-Purpose Computation on GPU

CS3241 is very introductory level so do not expect too much depth other than broad concepts. It also a prerequisite for many graphical modules, so it is a must-take.

CS3242 does briefly mentions about physics simulation, but is very cursory at most. CS4247 focuses more on graphical fidelity and photo realism but does not really cover shaders very substantially.

Surprisingly, CS4345 does cover shaders very well even though it technically isn't really a graphics module. The first few assignments test solely on graphical computations using shaders.


I hope you've enjoyed this demonstration.

If you have any feedback/questions or if you noticed any mistakes in my article, please contact me at fazli[at]sapuan[dot]org.

Comment section for placebo effect only. Please use email to contact the author