Skip to main content
Ba Blog

Fluid Simulation

gray scale fluid simulation

Showing off a Golang implementation of Joe Stam's fluid simulation algorithm.

Joe Stam #

I've had an interest in simulating fluid dynamics ever since my boy and I played "Where's My Water" on the iPad back in the meaty part of the last decade. It's a fun game, totally built around the fluids.

Fast-forward to June of this year. I'm sat in a lovely public library of suburban Helsinki, digging up ideas for a gamey sort of project. The now teenage boy, is still sleeping in our nearby room :)

And up pops, Real-Time Fluid Dynamics for Games. Looks workable! I've seen Stam's name mentioned quite a lot in relation to fluid simulation and the paper has a more or less complete implementation.

Here is the diffuse function from the paper:

void diffuse ( int N, int b, float * x, float * x0, float diff, float dt )
{
  int i, j, k;
  float a=dt*diff*N*N;

  for ( k=0 ; k<20 ; k++ ) {
    for ( i=1 ; i<=N ; i++ ) {
      for ( j=1 ; j<=N ; j++ ) {
        x[IX(i,j)] = (x0[IX(i,j)] + a*(x[IX(i-1,j)]+x[IX(i+1,j)]+
                    x[IX(i,j-1)]+x[IX(i,j+1)]))/(1+4*a);
      }
    }
    set_bnd ( N, b, x );
  }
}

Translating to Golang was kind of fun, got my head into the algorithm, and only resulted in one bug!

I did scratch my head quite a bit around the concept of an incompressible fluid while the simulation clearly models a varying density. Turns out, the density being modeled is that of a dye in an incompressible fluid such as water, aha!

Also a significant exercise left for the reader of the paper is how one is meant to interface with the sim code from the larger application. It's a very "C" kind of a thing, by way of shared data structures that represent different aspects of the simulation depending on which step is being executed.

Still, all very approachable and in short order I had density diffusion apparently working and built from there.

Ebiten #

Or Ebitengine. I love playing around with simple 2D game engines and this one's a gem!

Also, "games" built with it can be compiled to WASM (foreshadowing).

The Golang #

As per usual, the code is out there on GitHub.

Here is the Golang rendition of the diffuse function from above:

// stam.go
func (fl *Fluid) diffuse(bnd int, diff float64, xx, x0 ifc.Gridder) {

  a := fl.dt * diff * float64(fl.size*fl.size)

  for k := 0; k < 20; k++ {
    for i := 1; i <= fl.size; i++ {
      for j := 1; j <= fl.size; j++ {

        num := x0.Get(i, j) + a*(xx.Get(i-1, j)+xx.Get(i+1, j)+xx.Get(i, j-1)+xx.Get(i, j+1))
        result := num / (1 + 4*a)

        xx.Set(i, j, result)
      }
    }
    fl.setBnd(bnd, xx)
  }
  return
}

Totally punted on unit testing this baby. I'd just as totally do this on a rewrite, he says.

A significant feature of the Stam C code is swapping vectors (arrays) as the algorithm moves from step to step.

Here's how it turned out:

// vektor.go
type Vektor struct {
  dim  int
  vals *[]float64
}

func (vk *Vektor) Swap(other ifc.Gridder) {

  ov, ok := other.(*Vektor)
  if !ok {
    panic(fmt.Sprintf("somehow asked to swap a non-vector: %#v", other))
  }
  if vk.dim != ov.dim {
    panic("will not swap vectors of differing dimentions")
  }

  tmp := vk.vals
  vk.vals = ov.vals
  ov.vals = tmp
}

Where Vektor implements Gridder allowing it to be injected into simulation code. Not sure I'd go with this again, but was an interesting exercise :)

I did like the way simulation and game code factored relative to each other, as hinted at in game.New:

// game.go
func New(gridSize, scale int) (game *Game, err error) {

  fnt, err := fonts.Font(fontSize, fontDpi)
  if err != nil {
    return
  }

  game = &Game{
    font:      fnt,
    help:      true,
    gridImage: ebiten.NewImage(gridSize, gridSize),
    gridSize:  gridSize,
    gridMid:   gridSize / 2,
    scale:     scale,
    fluid:     stam.NewFluid(gridSize, visc, diff, dt, factory),
  }
  return
}

WASM #

Coming back to this project the day after Thanksgiving, it's been super straightforward to publish to the web via WebAssembly.

Compile #

~/proj/stam$ env GOOS=js GOARCH=wasm go build -ldflags "-w -s" -o bin/fluidsim.wasm cmd/fluidsim/main.go

Javascript #

First, copy bin/fluidsim.wasm and .../go/misc/wasm/wasm_exec.js into ba blog.

Then sprinkle a little js boilerplate into an html page:

<!DOCTYPE html>
<script src="/public/wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("/public/fluidsim.wasm"), go.importObject).then(result => {
    go.run(result.instance);
});
</script>

Give it a click! #

It's ........................ fluidsim browser-edition

Kind of a cool toy, yeah?

Caveats #

Already lackluster performance took a hit with WASM. I cranked the grid size down from 80 to 40 to keep things moving along.

Almost certainly won't work on a phone, as I've only handled mouse events.

Postscript #

It works! And it's another project in a passably finished state :)

The next time I'm summering in the Baltic maybe I'll take a crack at something along the lines of Sebastian Lague's particle-based fluid simulation which is a lot more like the one I recall from "Where's My Water" and super awesome.

Thanks for reading and I hope your holiday season is off to a great start.