Fluid Rendering with Box2D

This is a technical article about how I implemented the fluid in my game “Invasion of the Liquid Snatchers!” which was my entry for the Week of Awesome 5 gamedev competition.

One of the biggest compliments I’ve received about the game is when people think the fluid simulation is some kind of soft-body physics or a true fluid simulation. But it isn’t!

The simulation is achieved using Box2D doing regular hard-body collisions using lots of little (non-rotating) circle-shaped bodies. The illusion of soft-body particles is achieved in the rendering.

The Rendering Process

Each particle is drawn using a texture of a white circle that is opaque in the center but fades to fully transparent at the circumference:

These are drawn to an RGBA8888 off-screen texture (using a ‘framebuffer’ in OpenGL parlance) and I ‘tint’ to the intended colour of the particle (tinting is something that LibGDX can do out-of-the-box with its default shader).

It is crucial to draw each ball larger than it is represented in Box2D. Physically speaking these balls will not overlap (because it’s a hard-body simulation after all!) yet in the rendering we do need these balls to overlap and blend together.

The blending is non-trivial as there are a few requirements we have to take into account:

  • The RGB colour channels should blend together when particles of different colours overlap.
    • … but we don’t want colours to saturate towards white.
    • … and we don’t want them to darken when we blend with the initially black background colour.
  • The alpha channel should accumulate additively to indicate the ‘strength’ of the liquid at each pixel.

All of that can be achieved in GLES2.0 using this blend technique:

glClearColor(0, 0, 0, 0);
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE);

Putting all that together gets us a texture of lots of blurry coloured balls:

 

Next up, is to contribute this to the main backbuffer as a full-screen quad using a custom shader.

The shader treats the alpha channel of the texture as a ‘potential field’, the higher the value the stronger the field is at that fragment.

The shader compares the strength of the field to a threshold:

  • Where this field strength is strong enough then we will snap the alpha to 1.0 to manifest some liquid.
  • Where the field strength is too weak then we will snap the alpha to 0.0 (or we could just discard the fragment) to avoid drawing anything.

For the final game I went a little further and also included a small window around that threshold to smoothly blend between 0 and 1 in the alpha channel, this softens and effectively anti-aliases the fluid boundary.

Here’s the shader:

varying vec2 v_texCoords;
uniform sampler2D u_texture;

// field values above this are 'inside' the fluid, otherwise we are 'outside'.
const float threshold = 0.6;

// +/- this window around the threshold for a smooth transition around the boundary.
const float window = 0.1;

void main() {
    vec4 col = texture2D(u_texture, v_texCoords);
    float fieldStrength = col.a;
    col.a = smoothstep(threshold - window, threshold + window, fieldStrength);
    gl_FragColor = col;
}

 

This gives us a solid edged boundary where pixels are either lit or or not lit by the fluid.
Here is the result after we apply the shader:

Thing are looking a lot more more liquid-like now!

The way this works is that when particles come within close proximity of each other their potential fields start to add up; once the field strength is high enough the shader will start lighting up pixels between the two particles. This gives us the ‘globbing together’ effect which really makes it look like a fluid.

Since the fluid is comprised of thousands of rounded shapes it tends to leave gaps against the straight-edged tilemap. So the full-screen quad is in fact scaled-up to be just a little bit larger than the screen and is draw behind the main scene elements. This helps to ensure that the liquid really fills up any corners and crevices.

Here is the final result:

 

 

And that’s all there is for the basic technique behind it!

 

Extra Niceties

I do a few other subtle tricks which helps to make the fluids feel more believable…

  • Each particle has an age and a current speed. I weight these together into a ‘froth-factor’ value between 0 and 1 that is used to lighten the colour of a particle. This means that younger or faster-moving particles are whiter than older or stationary parts of the fluid. The idea is to allow us to see particles mixing into a larger body of fluid.
  • The stationary ‘wells’ where fluid collects are always a slightly darker shade compared to the fluid particles. This guarantees that we can see the particles ‘mixing’ when they drop into the wells.
  • Magma particles are all different shades of dark red selected randomly at spawn time. This started out as bug where magma and oil particles were being accidentally mixed together but it looked so cool that I decided to make it happen deliberately!
  • When I remove a particle from the simulation it doesn’t just pop out of existence, instead I fade it away. This gets further disguised by the ‘potential field’ shader which makes it look like the fluid drains or shrinks away more naturally. So on the whole the fading is not directly observable.

Performance Optimisations

As mentioned in my post-mortem of the game I had to dedicate some time to make the simulation CPU and Memory performant:

  •  The ‘wells’ that receive the fluid are really just coloured rectangles that “fill up”. They are not simulated. It means I can remove particles from the simulation once they are captured by the wells and just increment the fill-level of the well.
  • If particles slow down below a threshold then they are turned into non-moving static bodies. Statics are not exactly very fluid-like but they perform much better in Box2D than thousands of dynamic bodies because they don’t respond to forces. I also trigger their decay at that point too, so they don’t hang around in this state for long enough for the player to notice.
  • All particles will eventually decay. I set a max lifetime of 20-seconds. This is also to prevent the player from just flooding the level and cheating their way through the game.
  • To keep Java’s Garbage Collector from stalling the gameplay I had to avoid doing memory allocations per-particle where possible. Mainly this is for things like allocating temporary Vector2 objects or Color objects. So I factored these out into singular long-lived instances and just (re)set their state per-particle.

Week of Awesome 5 – Post-Mortem

Well the 5th Week of Awesome Gamedev competition is over! It’s incredible to think that this thing has been running for half a decade.

So the way this competition worked this year is that there are four themes announced at the beginning of the week, which were: Chain Reaction | Alien Invasion | Assassination | Castles.  And each contestant needs to pick two of those and make a game around them, with a week to do so.

My game was “Invasion of The Liquid Snatchers!”.

The Idea

Straight away Chain Reaction and Alien Invasion spoke to me as strong themes that would go well together. I had vague ideas for the other themes too so I spent my 1st day conceptualising ideas for different mixes of themes and trying come up with a viable concept.

In the end I settled on an idea where the game is staged from the perspective of an alien species who are invading Earth to harvest its resources. You play by helping a handful of blue-collar (manual labour) alien minions to undertake the “behind the scenes” activities of an on-going invasion. Activities such as processing Earth’s resources or refuelling a wave of saucerships.

All of this would be driven by the use of the Earth-bound liquid resources that they had been collecting. Water, Magma, Oil and Nuclear Waste were the 4 resources that I settled on. From there I really, really wanted to try and manifest these using a real-time fluid simulation, because that just seemed like a novel and somewhat challenging thing to do – which is exactly the whole point of the gamejam for me: To push myself and do something different.

Each kind of fluid would serve a different function in the game: Water activates hydraulics (raises platforms or opens doors), Oil activates machinery, Nuclear Waste powers saucerships and Magma is supposed to be used by the aliens to forge saucerships.

The chain reaction theme would feature by way of the environment changing in response to these liquids (hydraulic platforms activating or mechanised gears rotating, etc). Most of those changes would be good and allow progression through the level but others would trigger “hazards in the workplace” such as having a row of missiles lined up to fall like dominos that end up crushing your minions!

What Went Well?

Let’s see…

  • Time – I had the week off work. Never done that for previous years where I’ve always worked during the week. So this year I had a lot more free-time in comparison. I didn’t spend it all programming mind you, but it was just nice to not have to hold down the day-job and then spend my evenings coding as well!
  • Existing code-base – From the previous two times I have competed I have built up a thin framework over the top of LibGDX which really helps me hit the ground running. Mostly it’s just resource caches for textures/sounds/etc and some ‘fix your timestep’ gameloop stuff. Nothing too fancy.
  • Shaders – My fluid simulation is rendered using a custom shader. I’m no shader expert having written a merely a handful of simple shaders in my time.Shaders cause(d) other people a lot of agro where their games don’t work on other GPUs, etc. For that alone I tend to avoid them generally. But this year I couldn’t, not for the fluid anyway. Fortunately I had very few issues with the shader authoring. I’ll write a bit more about how the fluid sim works in a follow up technical post.
  • Audio – I was able to find online several soundbite recordings of water sloshing around (each 0-2 seconds long). I picked several that sounded good and classified them into two sets: One for water being emitted and one for water being collected. Then to create the seamless sound of running liquid I select a sound snippet at random every 1 or 2 seconds and play it with sufficient falloff and overlap with the previous snippet while also making sure to never play the same sound twice in a row because humans are too good at noticing repeating patterns. It sounded pretty bad at first but after a but of tuning I’m quite pleased with the end result.
  • Art(?!) – As I say every year: I don’t attribute myself with much of any digital graphics design skills and I’m colour-blind which is a huge pain in the ass for this sort of thing. So I try to mask my artistic failings with code. But I surprised myself with the art work this year, it kind of works I think?! For the first time this is ALL my own art work, I didn’t take anything from the interwebs, I just used Paint.Net and a lot of it is drawn free-hand with the mouse.

What Went ‘Not So’ Well?

  • Liquid CPU performance – To begin with I simply assumed that performance wasn’t going be an issue in my little old game. I was wrong. It took some ingenuity to prevent the sim from becoming a bottleneck. Fortunately I have a copy of JProfiler which is my favourite profiler for finding performance hotspots. I’ll write more about that in the follow up post about the fluid rendering.
  • Level creation – In previous years I would implement an ASCII-based level format that gives a reasonable-ish approximation of how the level will look on-screen. The idea works ok for purely grid-based worlds but it doesn’t scale very well with multiple layers or when things are not grid-aligned. So this year I spent some time mid-way through the week to write a crappy in-game interactive level editor. Even with that I found it very hard to create engaging levels. And since my game is heavily physics-oriented I had to account for all sorts of “physics bullshit” in order to get each level to a point where it is deterministically solvable by the player and not just a big mess of physics stuff that may or may-not play out.
  • Box2D Determinism – It is my belief that Box2D is supposed to be deterministic (as long as the input is the same and the timestep is fixed – which it is). Yet I am definitely seeing slight variation from run-to-run. So I can’t explain that yet. I need to investigate what the ‘f’ is going on there. For this time around I just decoded to design around the problem.

Reception So Far…

So far I have had very positive reviews. First and foremost I’m very pleased to hear that people enjoyed the game.

Many/most of the criticisms I’ve received are about things which I completely agree with too, so no surprises there. I would definitely have fixed them where this not a week-long development. There’s just not time to do everything unfortunately.

It’s not known yet how all the judges feel as that still remains to be seen.

Riuthamus (one of the judges) live-streamed his play-through of my game on Twitch. It was hilarious! He actually found some pretty decent ‘alternative’ ways of solving one or two of the levels that looks to me every bit as good as my intended solution. So that’s fine by me! It was great fun to watch and valuable feedback too.

So How Does That Fluid Simulation Work Anyway?

One of the biggest compliments I’ve had is when people think the fluid simulation is a soft-body physics or a true fluid simulation. It’s really not 😛

I’ve decided to write up a technical article about the fluid simulation and rendering process which you can find over here.

That’s A Wrap!

I had good fun over the week and just want to extend a big thanks to the competition organiser (slicer4ever), to the other judges and to the prize-pool contributors.  They have all done a fantastic job at making this competition happen! Every year I see the competition evolve in very positive ways!

And good luck to all the contestants!