Iāve implemented that CA approach to fluid simulation. I have mixed feelings about it: it does make a pretty decent fluid in most cases, but in the test where you make two connected buckets (like the below), it takes a long time for the fluid levels to equalize.
You can try the demo here. And hereās the code:
/* Simple water/fluid Cellular Automaton (CA)
based on: http://w-shadow.com/blog/2009/09/01/simple-fluid-simulation/
*/
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
using PixSurf;
[RequireComponent(typeof(PixelSurface))]
public class FluidCA : MonoBehaviour {
#region Public Properties
public bool running;
#endregion
//--------------------------------------------------------------------------------
#region Private Properties
// The mass of water at each cell in the grid.
float[,] mass;
// "new" mass, used only during simulation step (but kept around for efficiency)
float[,] newMass;
PixelSurface surf;
int width, height;
const float kMaxMass = 1.0f; // The normal, unpressurized mass of fluid in a cell
const float kMaxCompress = 0.05f; // how much excess fluid a cell can have with just ONE full cell above
const float kMinMass = 0.0001f; // at what mass of fluid we consider a cell to be dry
const float kMaxFlow = 1f; // max units of water to move out of one block to another per timestep
const float kMinFlow = 0.01f;
#endregion
//--------------------------------------------------------------------------------
#region MonoBehaviour Events
void Start() {
surf = GetComponent<PixelSurface>();
width = surf.totalWidth;
height = surf.totalHeight;
mass = new float[width, height];
newMass = new float[width, height];
Reset();
}
void Update() {
// Update the water simulation.
if (running) DoOneStep();
// For testing: add water upon mouse button 0; with mouse button 1, draw/erase ground.
if (Input.GetMouseButton(0)) {
Vector2 pos;
if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
AddFluid((int)(pos.x), (int)(pos.y), kMaxMass);
}
}
if (Input.GetMouseButton(1)) {
Vector2 pos;
if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
bool erase = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
surf.SetPixel((int)pos.x, (int)pos.y, erase ? Color.black : Color.yellow);
ClearFluid((int)pos.x, (int)pos.y);
}
}
}
#endregion
//--------------------------------------------------------------------------------
#region Public Methods
public void DoOneStep() {
// Loop over the grid, updating newMass (which should already be a copy of mass)
// with new values based on fluid flowing out of each cell.
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
float remainingMass = mass[x, y];
if (remainingMass <= 0) continue; // ToDo: kMinMass?
// Flow down into the block below.
if (Available(x, y-1)) {
float massBelow = mass[x, y-1];
float flow = GetStableStateBottom(remainingMass + massBelow) - massBelow;
if (flow > kMinFlow) flow *= 0.5f; // loads to smoother flow?
flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
remainingMass -= flow;
newMass[x, y] -= flow;
newMass[x, y-1] += flow;
}
// Flow left.
if (Available(x-1, y)) {
float flow = (mass[x, y] - mass[x-1, y]) / 4;
if (flow > kMinFlow) flow *= 0.5f;
flow = Mathf.Clamp(flow, 0, remainingMass);
remainingMass -= flow;
newMass[x, y] -= flow;
newMass[x-1, y] += flow;
}
// Flow right.
if (Available(x+1, y)) {
float flow = (mass[x, y] - mass[x+1, y]) / 4;
if (flow > kMinFlow) flow *= 0.5f;
flow = Mathf.Clamp(flow, 0, remainingMass);
remainingMass -= flow;
newMass[x, y] -= flow;
newMass[x+1, y] += flow;
}
// Flow up (if compressed).
if (Available(x, y+1)) {
float flow = mass[x, y] - GetStableStateBottom(mass[x, y] + mass[x, y+1]);
if (flow > kMinFlow) flow *= 0.5f;
flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
remainingMass -= flow;
newMass[x, y] -= flow;
newMass[x, y+1] += flow;
}
if (remainingMass < 0) {
Debug.LogError("Whoops!");
}
// newMass[x, y] = remainingMass;
}
}
// Copy newMass back into mass, now that all flows are accumulated,
// and update the display at the same time.
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
float m = newMass[x, y];
if (mass[x, y] != m) {
mass[x, y] = m;
if (m < kMinMass) surf.SetPixel(x, y, Color.black);
else {
float a = m/(kMaxMass*2);
surf.SetPixel(x, y, new Color(a, a, 1));
}
}
}
}
}
public void AddFluid(int x, int y, float amount) {
mass[x, y] += amount;
newMass[x, y] += amount;
}
public void ClearFluid(int x, int y) {
mass[x, y] = newMass[x, y] = 0;
}
public void Reset() {
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
mass[x, y] = newMass[x, y] = 0;
}
}
surf.Clear(Color.black);
}
#endregion
//--------------------------------------------------------------------------------
#region Private Methods
/// <summary>
/// Figure out whether the given pixel position is available for fluid to use.
/// </summary>
bool Available(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) return false;
Color c = surf.GetPixel(x, y);
// Transparent or pixels are always OK.
if (c == Color.black || c.a < 0.5f) return true;
// As are pixels that look like water (i.e., have full blue).
if (c.b > 0.99f) return true;
// Otherwise, assume it's ground.
return false;
}
/// <summary>
/// Given a total amount of water, calculate how it should be split between
/// two cells stacked vertically. Return the amount of water that should be
/// in the bottom cell in the stable state.
/// </summary>
/// <param name="totalMass"></param>
/// <returns></returns>
float GetStableStateBottom(float totalMass) {
if (totalMass <= kMaxMass) {
return kMaxMass;
}
if (totalMass < 2*kMaxMass + kMaxCompress) {
return (kMaxMass*kMaxMass + totalMass*kMaxCompress) / (kMaxMass + kMaxCompress);
}
return (totalMass + kMaxCompress)/2;
}
#endregion
}
Iām pondering a completely different approach to fluid simulation, where you group all the water pixels into connected regions (i.e. collections of pixels that are connected). Then you just find the highest pixel, and then find the lowest empty space next to any pixel in the group. And you simply teleport it there. Rinse and repeat.
I believe this will make water levels equalize out much more quickly, though the book-keeping to do this efficiently could be a bit thorny.
Anyway, in the meantime, hereās one approach to fluid thatās fun to play with!