Ever since I began my programming journey as a young adult I've been interested in 2D pixel simulation using cellular automata. It isn't a particularly realistic way to simulate physics, but it looks fun and is quite suitable for games due to being quite performant and has recently been popularized by the Finnish game Noita. Here's some gameplay of my sandbox project exemplifying what can be done with this type of simulation.
The core concept is that each material cell interacts with neighboring cells according to simple rules. Sand, for example, falls down if there is space below, and if it can't fall below it will check diagonally whether there is space there. Liquid would also check horizontally, and gases would move upwards. Liquids would also try to move sideways and gases will try do the same but mirrored upwards. Once you add rules based on other than movement, such as interaction between specific materials, the possibilities become endless with some of that sweet, emergent behavior.
Most games and works of others I've found utilize CPU for the pixel simulation because it's much easier to approach and performs alright if your grid stays below 512x512. It can also be a bottleneck to pass data between CPU and GPU.
However, upon tinkering with CUDA and discovering Vulkan compute shaders through Vulkano I decided to tackle sand simulation with compute shaders. You will see in most tutorials or codebases how much optimization is needed to be able to simulate a grid of size 512 at 60 frames per second. Noita developers had an amazing talk about that at GDC.
However, this tutorial will show how we can simulate larger grids with the highly parallel compute shaders. Here's a peek at grid of size 1536x1536.
We are going to use Bevy for the app, because I like its architecture. However we'll replace the graphics / compute backend with our own implementation and use Vulkano for that. This means that we will have to create our own graphics pipelines for even the most basic of things, such as drawing a quad. Now, you could use bevy's wgpu backend too, but I think having full control of how we do graphics teaches a bit more. And it's not that you could not have full control inside Bevy, but I like Vulkano's abstractions over Vulkan. Perhaps it can be a nice challenge for you to then implement this using only Bevy.
So let's get started. To make things easier, we'll use my Bevy Vulkano library with Egui for user interface. This way we don't have to worry about window handling, and other code boiler plate and we can focus on our graphics and compute pipelines.
For our simulation, we will need the following:
- Ability to draw images on quads
- Ability to interact with the content of the quad (our canvas)
- The actual simulation with compute shaders
The tutorial is split into following parts, and each will build upon the previous parts. If you have built your own compute pipelines and are using other languages or libraries, the simulation part might be the only one you're interested in.
After this you'll have rather good pieces of code that would allow you to expand into larger projects. If you want 3D, just expand on the first part of the tutorial (Camera and draw pipelines). If you want other kind of image manipulation or raytracers, just replace the simulation parts from third section of the tutorial. The possibilities are endless.