MadcoreTom

Sand Simulation in WASM

I like those sand simulation things, in games like Noita, and I figured I could give it a go

Anakin sand meme

The Demo

Try it out in the demo before we go any further. Hopefully your browser supports WASM.

The Algorithm

We start with a grid of numbers:

  • 0 = empty
  • 1 = sand
  • 2 = solid

Each frame we loop over the grid and update it following these simple rules. This diagram shows how we reference neighbouring positions.

A, B above C and D

The first case is easy, if the tile is empty and there's sand above, drop the sand.

Dropping Sand

We process from the bottom-up, otherwise we'd be chasing the sand all the way to the bottom in one go.

The second case is diagonals. is we go right-to-left on each row, then we're looking for diagonal moves from the left where the space below it is solid (otherwise it would go right down).

Diagonal Sand

On each frame we alternate left-to-right and right-to-left. This has the added benefit that sand dropping straight down moves twice as fast.

Trying it out on Codepen

I like to "sketch" things out on Codepen to see if it will work. Here's an ASCII version

Check it out in Codepen (you can mess around with the algorithm here too)

Trying it out in wasm.

All of this was an exuse to try WASM. All of this looping and updating might be more efficient in c++. The idea is that it should be faster. at the end of each frame we can just write the whole image buffer to the canvas.

Here's the algorithm in c++. getVal and setVal set both an array of empty/sand/solid, and the iamge buffer, plust hey perform some bounds checks.

dir = dir == 1 ? -1 : 1; // flip each frame
for (int y = HEIGHT - 1; y >= 0; y--)
{
    for (int x2 = 0; x2 < WIDTH; x2++)
    {
        int x = dir > 0 ? x2 : WIDTH - 1 - x2;
        unsigned char a = getVal(x - 1 * dir, y - 1);
        unsigned char b = getVal(x, y - 1);
        unsigned char c = getVal(x - 1 * dir, y);
        unsigned char d = getVal(x, y);
        if (d == 0)
        {
            if (b == 1)
            {
                // down
                setVal(x, y - 1, 0);
                setVal(x, y, 1);
            }
            if (a == 1 && b == 0 && c != 0)
            {
                // down-right
                setVal(x - 1 * dir, y - 1, 0);
                setVal(x, y, 1);
            }
        }
    }
}

This funky code here writes our unsigned char imageData[BUFFER_SIZE] to the canvas. It might be less efficient but its good enough for now.

EM_ASM(
    {
        var canvas = document.querySelector('canvas');
        var ctx = canvas.getContext('2d');
        var imageData = ctx.createImageData($1, $2);
        imageData.data.set(HEAPU8.subarray($0, $0 + ($1 * $2 * 4)));
        ctx.putImageData(imageData, 0, 0);
    },
    imageData,
    WIDTH,
    HEIGHT);

You can check out all the code here on GitHub

https://github.com/MadcoreTom/sand-wasm

How I build WASM

I'm using docker so I don't need to install anything globally

    docker run --rm -v c:/code/wasm-sand:/src emscripten/emsdk emcc src/index.cpp -o www/index.js -s EXPORTED_FUNCTIONS="['_main','_draw','_reset']" -sSTACK_SIZE=1MB -sENVIRONMENT=web -lm,
  • -v c:/code/wasm-sand:/src is the mounted source directory
  • emcc src/index.cpp -o www/index.js are the input and output
  • EXPORTED_FUNCTIONS="['_main','_draw','_reset']" are the functions i can call from javascript

Calling it from js

My code dynamically adds the WASM code <script> tag

const wasmElement = document.createElement("script") as HTMLScriptElement;
        wasmElement.type = "text/javascript";
        wasmElement.async = true;
        wasmElement.addEventListener("load", (r) => {
            console.log("WASM loaded");
            setTimeout(() => this.onWasmLoad(), 100);
        });

then on-load it gets references to the "draw" and "reset" methods

    private onWasmLoad() {
        this.wasmDraw = createExportWrapper('draw');
        this.wasmReset = createExportWrapper('reset');
        this.requestAnim();
    }

Every frame I just call the draw method

 private draw(time: number) {
        const MAX_FRAME_TIME_MS = 200;
        const timeDelta = Math.min(time - this.lastTime, MAX_FRAME_TIME_MS);
        this.lastTime = time;
        this.wasmDraw(time, timeDelta,
            this.mousePos[0], this.mousePos[1], this.mouseDown,
            this.bigBrush);

        this.requestAnim();
    }

Edit 1 - sharing ImageData more efficiently

Querying for the canvas and then creating an ImageData object every frame is probably not the best, so I dug around for other options. What we can do is share the array between our javascript and C++ code.

On the c++ side, I have a global uint8_t array

uint8_t *wasmView; // shared pixel array
// ...
extern "C"
{
    // EMSCRIPTEN_KEEPALIVE
    uint8_t *getArray(int n)
    {
        wasmView = new uint8_t[n]; // Create
        for (int i = 0; i < n; i++) // Initialise
        {
            wasmView[i] = i % 4 == 2 ? 50 : i;
        }
        return wasmView;
    }
    //...
}

On the javascript (typescript) side I have a few snippets

I have a pixel array, and a "view"

private pixelArray: Uint8Array;
private arrayBufferView: Uint8Array;

Get the getArray function on load

const getArray = Module._getArray;
// Link up WASM array with this.myArray
const len = WIDTH * HEIGHT * 4;
const arr = getArray(len);
// This is the magical line:
this.pixelArray = new Uint8Array(Module.HEAP8.buffer, arr, len);
// Create and initialise the image data
this.imageData = this.ctx.createImageData(canvas.width, canvas.height);

It's pretty cool, we can now modify the array from web assembly code or the javascript/typescript code.

And each frame I draw the pixel array to the canvas

if (this.pixelArray) {
    this.imageData.data.set(this.pixelArray); // Efficiently copies the contents
    this.ctx.putImageData(this.imageData, 0, 0);
}