Thumbnail for Something's Growing in my Soil Sim

Something's Growing in my Soil Sim

Carter Schmidt ♦ 2025-12-06

Building on the day night cycle from my last update, this week I've been prototyping my first organism: a plant that grows using photosynthesis.

I plan to implement 2 broad classes of organisms in my game, pixel-based organisms, intended as plant and fungi analogues, and free organisms which can move, turn, collide, and otherwise interact with the environment without being bound to its pixel grid. I am starting with the pixel based organisms because I want them to die and leave behind their organic matter for free organisms to feast on. By starting with plants, I can prep the environment with a reward system (food) for free organisms to build behaviors around.

To Make a Plant from Scratch One Must First Invent The Sun

The simplest possible pixel-based "organism" I could implement would just be a pixel that turns neighbors into clones of itself. Sprinkle in some randomness or neighbor based rules and the shapes will turn into blobs or Conway's Game of Life. Go look at any 2D cellular automata and behold, blobs!

But I want plant-like blobs, so my pixels need sunlight to grow. I have the direction and intensity of sunlight in my simulation from the day night cycle, but in order to utilize them for photosynthesis I needed to take a detour and implement shadows.

With light stored as a 2D vector, say [-.7, .7], every pixel can say "the light is coming from my upper left" and grow that way. But how does a pixel know if somewhere along the light ray there is some dirt or another plant casting a shadow?

Since my sim is GPU based, I added a compute shader to calculate the light value for every pixel in the simulation with raycasting. Each pixel checks all pixels along the line from itself to the world border in the direction of the sun. If the ray encounters an opaque pixel, like dirt, that means it's in shadow.

However, in a 2D game it looks bad to have just 1 pixel block all the light, and I want water to transmit some light through it. I want the light to fade as the ray passes through more material, and each material should affect the light differently.

I defined a lookup table for materials with their opacity. So dirt, which has material ID=1, looks up OPACITY[1]. I sum up the opacity of all pixels along the line as my "shadow" value, capped at 1 for full shade. This approach is branchless since the opacity of air is 0, which is good for GPU performance. With some trial and error I found opacity values that looked nice.

To render lighting, in my materials rendering fragment shader, I multiply the pixel color by min(0.1, 1.0-shadow). While the light values range between 0 and 1, I don't want anything to be completely black on screen, so I'm visually clamping the light to a min of 0.1.

Lit and shadowed dirt and water pixels

It's Alive!

Now that each pixel in the simulation knows how bright it is, I can make a new type of pixel in the game, Plant, that responds to it. My materials are stored as a big array of unsigned 32 bit integers. The material type is stored in the first 8 bits, and I can use the remaining 24 for whatever type specific data I want. Dirt uses 8 of those bits for its structural integrity to determine whether it should fall or not.

For plants, I'm using 8 bits to store an energy level. I increase the energy based on how much light the pixel is receiving and decrement it slowly over time. If a pixel reaches its max energy, 255, it decreases its energy and clones itself either upwards if that space is empty, or else towards the left or right, whichever is the brightest.

If the energy reaches 0, it dies and becomes a new material that doesn't grow which I'm calling Organic. Since I'm coloring pixels between a dead brown and a vibrant green based on energy, this creates an interesting tree ring like effect, where the light penetrates a few pixels deep during the day, energizing the plant pixels, but the deeper pixels fade.

Using 8bit data blocks is convenient because WGSL has a handy pack4xU8 function. There are also functions for packing 8 and 16bit floats which I suspect I will use when I inevitably encounter a need to store more than 24 bits of data for a material.

Next Steps

At this point, progress is very much subject to the learning process. Learning how to program in Rust better, discovering limitations of the WebGPU and wgpu's implementation of it, learning about performance characteristics of GPU based workloads, and experimenting with my engine's architecture. But learning is the reason for doing any of this!

My vision for this game entails several technical hurdles that I've been chewing on, each will likely take time and iteration to get right. The main goals and problems I'm considering right now are these:

These are all challenges that I'm excited to tackle in my pursuit of beautiful emergent properties in this game.

Playable Demo

Only supports desktop Chrome and Firefox browsers.