Naijapolis

Engineering blog · Part 8 of 9 · Nature

Growing trees, flowers and water

Plants and rivers as recipes plus a seed: how a few numbers become a baobab, a flower bed and a river with a waterfall.

10 min read · 21 October 2026

All nine parts
  1. One file, no framework
  2. A kernel you can test
  3. The city’s heartbeat
  4. Ground, water and light
  5. Highlife from maths
  6. Sharing without trust
  7. Shipping and staying up
  8. Growing trees, flowers and water
  9. Waste and the politics of rubbish

Every town needs a few trees, some flowers and a bit of water. In Naijapolis these are not models that an artist made once and the game copies. They are designs that a player can shape, grown from a seed, and they sit on the land you read about in Part 4. This part is about how a handful of numbers becomes a baobab, a bed of hibiscus and a river with a waterfall.

A plant is data

A tree design is a small record: how tall, how long the bare trunk is, how much it leans, how many forks, how widely they spread, how much they droop, the crown shape, and a few colours. A flower design has petals, rings, openness, a centre and a stem. There are twelve ready-made trees (oak, willow, pine, acacia, blossom, maple, jacaranda, palm, cypress, baobab, bamboo, a fruit tree) and a set of ready-made flowers (such as daisy, rose, bougainvillea, tulip, hibiscus, lavender and a grass tuft), each one just a design with different numbers. A "surprise" function writes a new design at random, picking a colour palette that goes together.

Designa few numbers, coloursSeedone whole numbertreeLayoutpure: no browser,no game statePartstrunk, branches,leaf clumps,strokes, blossomsMerged meshesa few draw callsnew seed, same design= a sibling, a little talleror wider, never a clone
The layout function is pure: the same design and the same seed always give the same parts, so it can be tested without a browser. Only the last step touches the 3D scene.

That small size matters. A design can be saved with a city, sent to a friend as a short code, and cleaned on load exactly like the city codes in Part 6: every number is clamped, every colour is checked, and there are caps on how many parts a design may ask for, so a hostile design cannot ask for a million leaves.

Growing a tree

The layout is a tiny simulation of growth, in a few steps:

  1. The trunk is a curve of seven points that leans away from vertical, wobbles a little, and thins toward the top.
  2. Forks leave the upper trunk in a ring. Each reaches out, and gradually bends downward by the design’s droop. A willow droops a lot; an acacia hardly at all.
  3. Levels: each fork sprouts smaller, thinner kids partway along, and those can sprout their own. More levels, more twigginess.
  4. Leaf clumps gather first at the branch tips, then fill the rest of the crown according to its shape.
trunk: leans, wobbles, thinsforks: reach out, droopleaf clumps at the tipssmaller branches(more levels, more detail)
A tree is a trunk, forks, kids and clumps. All the shapes you see in the game come from changing a few numbers in this one recipe.

The crown shape is a tiny function that says how wide the tree is at each height. That one idea gives most of the variety in a forest:

// how wide the crown is at a height: t = 0 at the bottom, 1 at the top
function crownProfile(shape, t) {
  switch (shape) {
    case 'cone':     return clamp(1.05 - t, 0.08, 1);              // wide at the bottom, a point at the top
    case 'umbrella': return t > 0.55 ? 1 : 0.12 + t * 0.4;        // bare, then a flat wide canopy
    case 'baobab':   return t > 0.5 ? 1 : 0.18;                   // a fat bare trunk, a bulb of leaves
    case 'oval':     return Math.sqrt(Math.max(0, 1 - (2*t - 1) ** 2)) * 0.85 + 0.1;
    default:         return Math.sqrt(Math.max(0, 1 - (2*t - 1) ** 2)) * 0.9 + 0.12;   // round
  }
}
RoundTallConeUmbrellaCypressBaobab
These silhouettes were drawn with the formulas above. A ten-line function per crown shape is enough for ten recognisable trees.

The same tree, never the same twice

The seed is what makes a grove look alive. It feeds a small, fast random-number generator, so the same seed always gives the same tree, but a different seed gives a sibling: a little taller or wider, with a different lean and different leaf colours. A "variety" setting on each design says how far siblings may differ.

// a few lines make a repeatable generator; this is the classic mulberry32
function mulberry32(seed) {
  let a = seed | 0;
  return () => {
    a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;     // 0 .. 1
  };
}
const rand = mulberry32(seedOfThisTree);
const height = design.height * (1 + (rand() - 0.5) * design.variety * 0.3);   // illustrative

Because the seed is stored with the placed tree, the same city looks the same every time it is opened, on any phone, for any visitor. Nothing is random at draw time.

Colour that does not look flat

A palette of three greens would make a flat green ball if each leaf took one of them. Two tricks fix that. First, mixing rules decide where each colour goes: scattered at random, as a gradient from the bottom to the top, in patches (using a cheap hash of the position, so neighbours share a colour), or only on the outer tips. Second, every colour is nudged a little in hue and lightness for each leaf mass, so no two are quite alike.

function leafColor(design, rand, pos, h, edge) {      // h: 0 bottom .. 1 top; edge: 0 middle .. 1 outer
  const pal = design.colors; let c;
  if (design.mix === 'gradient')     c = paletteAt(pal, 1 - h);
  else if (design.mix === 'patches') c = pal[Math.floor(cellNoise(pos) * pal.length)];
  else if (design.mix === 'tips')    c = edge > 0.55 ? pal[1 + Math.floor(rand() * (pal.length - 1))] : pal[0];
  else                               c = pal[Math.floor(rand() * pal.length)];
  return nudge(c, rand, 0.025 * design.jitter, 0.07 * design.jitter);   // a little hue, a little light
}

Painted, not modelled

The game’s look is "painterly", and trees show it most. A leaf mass is a faceted blob whose faces each take a slightly different tint (lit from above, cooler underneath). Around each blob stand little cards, each showing a brush-stroke leaf, a round dab or a ragged splash. They stick out past the blob’s edge, so the silhouette is ragged like paint on canvas, not a smooth ball. Those three strokes are drawn once on a small offscreen canvas when the game starts. Five styles are on offer (painterly, low-poly, soft clay, leafy and inked), and a plant’s whole geometry is merged into a few meshes so a garden of fifty flowers costs only a handful of draw calls.

A plant’s detail setting follows the same rule as everything else in Part 4: it is chosen from the device and can be changed, and when it changes, every plant is simply grown again from its design and seed.

Flowers: a ring of petals

A flower is a stem that bends, a few leaves, and a head made of rings of petals around a centre. Each ring has a petal count, length, width, how open it is and how far it curls. Change those, and a daisy becomes a rose.

daisy: 12 narrow petalsstar: 5 wide petalscup: 3 rings, closing in
A daisy, a star and a cup, all from the same recipe: petal count, size, openness and rings.

Flowers come in beds and hedges too. A bed scatters several designs over an area using a seed, so a garden has a mix of colours that never repeats exactly. Mixing rules for flowers (alternate, random, by ring, tipped with a second colour) work like the tree ones. A whole bed sways gently in the wind as one object, which is cheap and looks lovely.

Rivers, streams and lakes

Water is harder than plants, because it must obey the ground. A player taps points; the game puts them in flow order (a river runs from high ground to low, so if you started at the bottom, the points are reversed), then builds the river in stages.

1. The player taps pointsput in flow order, high to low2. A smooth curve through themCatmull-Rom spline3. Even steps and a gentle wandera seeded meander, ends fixed4. A bed that follows the grounddown only, deeper where it must5. Carve the channelinto the height map, and record it6. Read the surface backfrom the ground as it is now7. Steep run? A waterfallplunge pool below, mist, foam
Seven steps from a few taps to a flowing river. Steps 4 to 6 are the interesting ones: the water follows the ground, and the ground is changed to hold the water.

The curve. A Catmull-Rom spline passes smoothly through every point. It is resampled at even steps, and an optional "wander" setting adds two slow sine waves across the line, fading to nothing at each tap so the river still passes through them. The width tapers: a little narrower near the source, wider toward the mouth.

The bed. This is the rule that makes it look right. Water never flows uphill, so the bed is built downstream one step at a time, always at least a hair lower than the last, and deeper where the ground rises ahead:

function waterBed(curve, ground, depth) {
  const g = smooth(curve.map(c => ground(c.x, c.z)), 3);      // the ground under each sample
  const bed = [];
  for (let i = 0; i < curve.length; i++) {
    bed.push(i === 0
      ? g[0] - depth
      : Math.min(bed[i - 1] - MIN_SLOPE * STEP,                // never climb
                 g[i] - depth));                               // never sit above the ground
  }
  return smooth(bed, 3);
}

The channel. The bed is cut into the height map, and the banks blend into the surrounding ground. The game remembers exactly which grid cells it changed and what they held before. If the player moves the river, those cells are put back first, so a river can be dragged around without scarring the land. A lake is a bowl cut the same way, with a level.

The surface and the waterfalls. The water is not stored. It is read back from the ground as it is now: bed height plus water depth, never allowed to rise downstream. Wherever that surface drops steeply, the code marks a waterfall:

surface never risesa steep run is a waterfallplunge pool: the bed dipsground
The surface only steps downhill. A steep run becomes a waterfall, and the bed below it dips into a plunge pool, which makes the foam and mist look believable.
const slope = (y[i - 1] - y[i]) / STEP;
fall[i] = clamp((slope - FALL_MIN) / (FALL_MAX - FALL_MIN), 0, 1);   // 0 = calm, 1 = a full waterfall
// then a run of samples where fall > 0.5 becomes one waterfall, with its height and width

Because the surface comes from the ground, anything that changes the ground (a player sculpting the hill upstream) changes the river automatically. Bridges are planned along the same curve. And a check stops anything being built in the water.

Making water look like water

Water has its own shader, written for the GPU, and it layers a handful of simple ideas:

LayerHow it is made
Colour with depthPale at the banks, deeper in the middle: a blend from the shallow colour to the deep one by distance from the centre line. Eight colour looks, from teal to tea brown.
RipplesThree layers of smooth random noise at different scales, scrolling downstream at different speeds. Faster over a waterfall.
Light under the surfaceSlow bands of light built from the same noise, dimmed at night.
Sky at a low angleAt glancing angles the surface shows the sky colour. This one trick (the Fresnel effect) is what makes a flat plane read as water.
FoamWhite along the banks, and strong at the foot of a waterfall, with drifting mist.
Weather and seasonRain rings in a rainstorm, wind ruffles, and in the rainy months the river turns muddy and rises a little.
A mirrorThe town is drawn upside down into a small texture that the shader samples, so buildings reflect in a lake. Only when the device can afford it.

The noise itself is classic and tiny: hash a grid point to a number, then blend between the four corners with a smooth curve.

float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float noise(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);                                   // smoothstep
  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),
             mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
}
// ripples = three octaves, scrolling at different speeds
float rip = noise(p * vec2(2.1, 0.55) - vec2(0, t)) * 0.5
          + noise(p * vec2(4.7, 1.3)  - vec2(0, t * 1.6)) * 0.3
          + noise(p * vec2(9.1, 3.2)  - vec2(0, t * 2.4)) * 0.2;

A little life at the water

Every river and lake gets its own small cast, chosen from its seed so it is the same each time: egrets, ducks, dragonflies, fish, people and canoes. The banks grow reeds, wildflowers and stones, and slow water gets lily pads. Everything is built from the same merged-primitive kit as the props, and the player can switch it all off in the Look panel.

Like the graphics tiers in Part 4, water has a rich and a light setting. Rich has rain rings, wind, the mirror, all the wildlife and full bank growth. Light has plain ripples, half the bank growth, no mirror and fewer animals. The game picks light by itself on a phone with little memory or few cores, and the player can override it.

What we learned

  1. Store the recipe, not the result. A tree is a dozen numbers and a seed; a river is a few points. Saves stay tiny, and everything can be regrown when the ground or the settings change.
  2. Let a seed be the only source of chance. It makes places that look the same every visit, and it makes tests possible.
  3. Derive what you can. The water surface is read from the ground, never stored, so it can never disagree with it.
  4. Make the cheap trick do the heavy lifting: a ring of petals, a width formula, three octaves of noise, and the sky colour at a low angle.

One more part

Gardens are the pleasant half of a town. The other half is the one nobody photographs: what happens to the rubbish, and how a few simple rules turn it into floods, outbreaks and votes.

Now go and build something.

It takes about a minute to lay your first road.

Play Game