Thumbnail for You're in for a World of Dirt

You're in for a World of Dirt

Carter Schmidt ♦ 2026-03-16

Soil composition is a complicated field that people have dedicated their whole lives to studying. It would be an injustice to continue with Soil's development without a more sophisticated representation of the soil matrix than filling half the screen with pixels of "Dirt."

Soils Are Like Ogres

They have layers! Now, it's not uncommon that my various nature excursions tend to bring me into confrontation with sheer walls of earthen substrates. And usually said walls reveal a thin layer of topsoil over solid rock. For example:

Maquoketa Caves State Park rock arch in the forest

But on a recent walk along the beach, I got an unexpected glimpse into the interactions between layers of different dirt compositions. The first sight to draw me in were these exposed roots. The tree had grown long ago in solid earth while the sea steadily eroded the coastline until it washed the dirt out from under the roots, leaving me with a beautiful view of the branching structures that these organisms produce to suck water and nutrients from the soil.

Tree roots dangling where the sea washed away the soil they grew in

But that was just an appetizer, as a short while later I heard the burble of running water. I looked down and noticed the sand was fully saturated with water, and it was flowing out from the woods. Followed it, ducking my head under the root canopy, and discovered an absolutely stunning display of geology and hydrology at work.

I don't know much about these things, but this hard gray clay appeared to me like shale rock that it hadn't been compressed enough to become actual rock. And on top of it was a fluffy layer of rich soil, laden with organic matter, and infiltrated every which way by tree roots. This was a few days after the last rainfall, and the water seems to have seeped through the topsoil and formed a subterranean river atop the less absorbent clay.

Further still along the beach, the shoreline rose up, becoming a feeder bluff, or wall of sediment exposed to the weather which continuously erodes into the sea, feeding it with nutrients. There were so many distinct layers, some reddish with iron oxide, some smooth, some with gravel mixed in toward the bottom, and most striking was this giant seam of gray. I can't help but wonder if it's the same stuff as above, just deposited thinner and higher.

sand stone cliff face with gray clay line across it

So, we get it, soils have layers. Time to start coding them!

Building From the Ground Down

As I touched on in my last post, I introduced the scenes to my game engine, allowing me to create a world gen interface. I set about adding a compute shader pass bound to the "atoms buffer" that holds the material ID for every pixel in the world which would populate it.

Dirt particles are classified in the literature by size, with clay being the smallest, then silt, sand, and gravel. Dirt as a whole is classified by its combination of each of these types, for example "loam" is the ideal farming soil and has around 20% clay, 40% silt, and 40% sand. So these are the materials I'll use to replace my monolithic "Dirt" material.

Of course all my simulation code depends on dirt being represented by the number 1 stored in the first 8 bits of a 32 bit integer. So replacing dirt with 4 materials, each with their own number, means rewriting everything. While I was at it, I replaced Air with some of the more charismatic molecules in our atmosphere, Nitrogen, Oxygen, and CO2. If you zoom in on the demo at the bottom, I colored them slightly so you can see them jiggling around. This'll give me something to work with when I tackle plants 2.0.

Random mixture of brown pixels on the bottom half of the image

Now to layer it! Apparently soil layers are called "horizons" but my understanding of this is that horizons form due to non-geological effects, such as water percolation and plant growth, while strata are geologically formed layers. So I'm calling them strata. The aim is for these strata to be like biomes, with the different dirt compositions affecting water percolation and creating unique ecological niches which will eventually form something like horizons. Water will percolate through sand quickly so roots in it may dry out, but it will pool ontop of clay resulting in an aquifer. Clay will be harder for roots to grow through which creates a barrier between organisms above and below. Maybe worms will be able to tunnel through clay just fine, so they'll eventually dissapate the barrier.

At first I considered simulating the deposition of various soil compositions sequentially by spawning it in the air and letting it fall. But that seemed complicated as it'd depend too much on my still underdeveloped soil integrity system, so I opted for a generate all at once approach. I treat each pixel independently and calculate what strata it should inhabit. I map each strata number to a set of parameters, and the pixel uses those parameters to pick which material to be.

I start at the surface and pick a start and end point of a line across the screen, then check if a pixel is above or below it. I got the equation for this on math stack exchange. I do this for each strata line and each pixel keeps track of the last strata number that is above it.

Then, I added some parameters to inject randomness. Variance offsets the whole line, both start and end point together. Skewiness makes lines diagonal which can overlap each other to create wedge shapes. Roughness uses fractal perlin noise to offset the coordinates used in each pixel's calculation of whether it lies below a line to blend the transitions.

adjusting sliders to demonstrate soil strata changing

A Random Tangent

After spending some time on Shadertoy, I found that compared to getting random numbers in CPU land from libraries in Python, Java, or Rust, shader programmers need to wear their PRNGs on their sleeves more, and the process has lost some of it's mystique.

I have 2 types of pseudo-random number generator functions in my shaders. The first are hashing functions. Here's a WGSL u32 hashing function that been living in my Obsidian vault for years, I don't know where it came from.

fn uhash(seed: u32) -> u32 {
    var x = seed;
    x = ((x >> 16u) ^ x) * 0x45d9f3bdu;
    x = ((x >> 16u) ^ x) * 0x45d9f3bdu;
    x = ((x >> 16u) ^ x);
    return x;
}

The other type of randomness I'm using is cumulative, I prime it, then every time I call "next_prng()" I get a new number using the private address space to store the seed information. Useful when I need multiple independent random numbers per pixel. I ported this to WGSL from David Blackman and Sebastiano Vigna's C code.

var<private> seed: array = array(0xb523952e, 0x0b6f099f, 0xccf5a0ef, 0x1c580662);
fn next_prng() -> u32{
  let t = seed[1] << 9;
  seed[2] ^= seed[0];
  seed[3] ^= seed[1];
  seed[1] ^= seed[2];
  seed[0] ^= seed[3];
  seed[2] ^= t;
  seed[3] = (seed[3] << 11) | (seed[3] >> 21);
  return seed[0] + seed[3];
}

fn prime_prng() {
  let JUMP = array( 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b );
  var s0: u32 = 0;
  var s1: u32 = 0;
  var s2: u32 = 0;
  var s3: u32 = 0;
  for(var i: u32 = 0; i < 4; i++) {
    for(var b: u32 = 0; b < 11; b++) {
      if (JUMP[i] & (1u << b)) != 0 {
        s0 ^= seed[0];
        s1 ^= seed[1];
        s2 ^= seed[2];
        s3 ^= seed[3];
      }
  	  next_prng();
    }
  }

	seed[0] = s0;
	seed[1] = s1;
	seed[2] = s2;
	seed[3] = s3;
}

// Example Usage
seed[0] += global_id.x;
seed[1] += u32(uniforms.time);
prime_prng();
let random = next_prng();

If I need a random f32 value, I use a u32->f32 conversion function copied from Mark Tension's blog, which is worth a look if you're interested in alife.

I added 2 more parameters that use these random numbers: Density to mix gas particles into dirt, and heterogeneity to mix together dirt materials, so a layer with high hetergeneity will be more loamy.

I can also use these prng primitives for continuous noise functions, with reference to The Book of Shaders and the Lygia shader library. I use fractal perlin noise for the dirt surface variation and strata roughness, and I use worley noise for rocks.

The Rocks!

I added a rock frequency and scale parameter. Worley noise is calculated by picking random seed points, then calculating each pixel's distance to those seed points. Frequency affects the density of seed points and scale determines the distance to the point that should be rock material. The distance threshold is perturbed with perlin noise to make wobbly edges.

And like with density and heterogeneity, the hash of the strata number is used to pick a random value for these and the strata's rock parameters between 0 and the value set in the UI. A downside of this aproach is that each strata calculates the worley noise independently, so rocks can't cross strata lines. It would be cool if a big rock in one layer pushed into the layer above, or maybe even through multiple. But I'm calling this good enough for now.

And lastly, I googled the words "clay," "silt," and "sand" and color picked from the images. It looks much better than the floats I blindly picked before.

Tweaking UI parameters demonstrating new colors and gravel generation

Playable Demo

Only supports desktop on browsers with WebGPU support. Sorry mobile users. You may need to enable WebGPU in your browser's settings.