Dev Log — SOUP: Building a Voxel Terrarium with PyTorch


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:
Channel | Meaning | Range |
0 | Air amount | 0–1 |
1 | Water amount | 0–1 |
2 | Soil amount | 0–1 |
3 | Tree cell presence | 0–1 |
4 | Leaf cluster presence | 0–1 |
5 | Tree ID / Identifier | 0–1 |
6 | Mineral concentration | 0–1 |
7 | Sugar (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 🌿
Subscribe to my newsletter
Read articles from Sprited Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
