Dev Log — SOUP: Building a Voxel Terrarium with PyTorch

Sprited DevSprited Dev
3 min read

After re-bootstrapping SOUP codebase from JavaScript into PyTorch in the last phase, it's time to get our hands dirty—literally. We're building a voxelized terrarium: a tiny world of air, soil, and water that lives and breathes in tensors.

Goal

Implement setup_world() in main.py.

We’re simulating a sealed terrarium—a glass ecosystem where light enters, water cycles, and life thrives. These environments are famous for sustaining themselves with minimal input, thanks to the closed loop of evaporation, condensation, and photosynthesis.

We want to replicate this behavior inside a voxel grid.

First Step: Create the World

We're going with a DxHxW grid, where:

  • D = Depth (4)

  • H = Height (32)

  • W = Width (64)

        ──────────────────── 
      ╱                    ╱│
     ╱                    ╱ │ 32
     ────────────────────   │
    │                    │  │
    │                    │   
    │                    │ ╱ 
    │                    │╱  4
     ────────────────────    
             64

Voxel Representation Options

We debated a few approaches:

  • Atomic Numbers — pure science, but overkill

  • Tile Type Enums — classic and simple

  • Probabilistic Channels — elegant, scalable, maybe overengineered for now

We're going with Option 2: Tile Type One-Hot Channels, keeping things simple and PyTorch-friendly.

Basic Tile Types:

0 - Air
1 - Water
2 - Soil

Life in a Tile

Grass, moss, and small plants will decorate the top of soil tiles, without owning the whole voxel.

Big trees? That’s the soul of SOUP (Super Organism Upbringing Project).

Tree-Specific Tiles:

3 - Tree Cell
4 - Leaf Cluster

We could differentiate between shoots, roots, and saplings, but early on we’ll keep it unified. Environmental context will define their behavior:

  • Surrounded by dirt? It’s root.

  • Alone? Seedling.

  • Green on top? That’s leaves, baby.

To prevent tree-merging chaos, we’ll use an ID channel to encode a unique tree identifier (cycled from 0 to 255).

Tensor Structure

We'll represent the entire world as a 8x4x32x64 tensor:

ChannelMeaningRange
0Air amount0–1
1Water amount0–1
2Soil amount0–1
3Tree cell presence0–1
4Leaf cluster presence0–1
5Tree ID / Identifier0–1
6Mineral concentration0–1
7Sugar (post-photosynthesis)0–1

Just 65k floats—small enough to be snappy, rich enough to grow a whole biome.

Initialization Plan

  • Channels [0–2] — randomly filled (to simulate initial air/water/soil distribution)

  • Channels [3–4] — zeroed (no trees yet)

  • Channels [5–6] — random values in [0, 1] (unique identifiers + minerals)

  • Channel [7] — zero (no sugar until the sun hits)

What Happens Next?

Gravity! Not with for-loops—we're going convolutional.

For example, if two stacked tiles each contain 0.5 water, in one pass the lower one should accumulate all 1.0, and the top one turns into air. That’s the behavior we want: emergent, not hardcoded.


Next Steps

I gotta go pick up my kid (real-world priority > voxel trees).
But when I’m back, the mission is clear:

Figure out how to apply settling forces in PyTorch using convolutions—no for-loops allowed.

Stay tuned. The forest is just beginning to wake up.

Sprited Dev 🌿

0
Subscribe to my newsletter

Read articles from Sprited Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sprited Dev
Sprited Dev