Ecosystems and Ground Cover

The acron.vegetation module is the native half of plant ecosystems and grass: a terrain description, a simulator that grows woods over it (after Deussen et al., “Realistic Modeling and Rendering of Plant Ecosystems”, SIGGRAPH 1998, and Makowski et al., “Synthetic Silviculture: Multi-scale Modeling of Plant Ecosystems”, SIGGRAPH 2019), and the CPU side of instanced grass (where the blades stand, which cells draw at which level of detail, a trample map). It is CPU code with no graphics dependency beyond writing textures, so a C++ game, a tool or a server can place plants with it directly. The Python tier, acron.ecosystem, adds the presets, the planting into acron.trees and the grass renderer and shaders (see Ecosystems and Grass).

Everything is deterministic: every random draw is a hash of world integers (cell indices, the step, the seed), never a running generator, and the module is compiled without fused multiply-adds or fast maths.

The classes

EcoTerrain

A heightfield over a rectangle of the world (origin its south-west corner, size in metres) from a PNMImage (grey level times a scale plus an offset) or a PTA_float, and optional masks over the same rectangle: moisture (0..1; without it, derived from the relief: hollows wetter, ridges and steep slopes drier, banks wettest), exclusion (1: no plants; paint_path adds a road along a polyline) and water. Images are read with their first row at the north edge. get_height, get_normal, get_slope (degrees), get_moisture, get_exclusion, get_water sample bilinearly; set_altitude(offset, scale) maps heights onto the altitudes species’ limits are written in.

EcoSpecies

A species’ ecology as plain values: max_height, crown_ratio, growth_rate, max_age, maturity_age, shade_tolerance, moisture_pref / moisture_width, slope_max, altitude_min / altitude_max, seed_radius, seed_rate, density (seed rain per hectare a year), crown_opacity, patchiness / patch_scale and understory. light_response(light) is the vigour a species draws from a share of the open sky’s light.

Ecosystem

The simulator: a lattice of cell_size cells (4 m), each with at most one tree and one understory plant, stepped every step years for years years – light from the neighbours’ crowns, vigour from light and site, growth, mortality (background, stress, senescence, windthrow patches: disturbance), seeding within the seed radius plus patchy seed rain. set_clearings keeps a share of the land open, set_initial_cover and set_stand_size set the starting mosaic. simulate_tile(tx, ty), simulate_tiles(coords, threads) (on every core) and simulate_region(lo, hi) are BLOCKING. A tile runs over a halo of (steps + 1) x get_reach() cells that shrinks by the reach each step, so its own cells are exactly those of a simulation of the whole world: tiles are byte-identical in any order, on any thread, and seamless (on one platform: the C library’s exp and pow may round differently on another, and the plants with them). When a thread can’t be started, or with support-threads #f in Config.prc, the tiles left run on the calling thread, with the same bytes. Kept tiles answer get_ground_light(x, y), the light under the crowns.

EcoTile

A simulated tile’s plants, FLOATS (10) floats each in get_table(): x, y, z, species, age, height, crown radius, health, light, layer; per species get_positions (x, y, z triples), get_heights, get_ages, get_healths; and the ground light per cell (get_ground_grid, get_ground_light).

GrassField

Blade layout and cells for instanced grass: a world-anchored jittered grid of density blades per square metre, thinned by get_density_at (exclusion, water, slope, dry ground, an optional density image, an ecosystem’s ground light), gathered in clumps, sorted by a random rank. A cell’s table is one RGBA32F texel a blade (ROW = 512 a row): the root’s x, y, z and a 24-bit integer (height, facing, the clump’s tone). update(camera) picks the cells within the last distance, gives each a level (0, 1, 2) by distance with hysteresis and a draw count (the far ones draw a prefix of their ranked table), builds new cells, frees far ones and reports the changed cells; write_cell_texture uploads one.

GrassTrample

A square map of trodden grass (push direction, flatness) that follows a centre in whole texels; stamp, recover, write_texture (RGBA16F).

Using it

from acron.core import LPoint2f, LVecBase2f, LPoint3f, PNMImage
from acron.vegetation import (EcoTerrain, EcoSpecies, Ecosystem,
                                GrassField)

heights = PNMImage("heights.png")                # 16-bit grey
terrain = EcoTerrain(LPoint2f(0, 0), LVecBase2f(1024, 1024))
terrain.set_heights(heights, 80.0)

birch = EcoSpecies("birch")
birch.shade_tolerance = 0.1
birch.max_height = 20

eco = Ecosystem(terrain, 7)
eco.add_species(birch)
eco.simulate_tile(0, 0)
tile = eco.get_tile(0, 0)
xyz = tile.get_positions(0)                      # 3 floats a plant

grass = GrassField(terrain, 16.0, 1)
grass.set_ecosystem(eco)
grass.update(LPoint3f(100, 100, 2))

Timings (Apple M4 Pro): a square kilometre, ten species, 100 years, in 16 tiles on every core in 0.5 s; grass layout 0.12 s per million blades.