Trees and Forests

acron.trees grows trees from species presets and draws forests of them: thousands of trees from a dozen shapes per species, swaying in the wind, turning with the seasons, lit and shadowed by acron.sky, fading between levels of detail down to octahedral impostors in the distance. Nothing needs art: the leaves and the bark are painted procedurally too, and a game that has its own textures binds them instead.

The growing, meshing, painting and culling are the engine’s native acron.proctree (Procedural Trees); this package holds the species presets, the caches, the impostor bake and the renderer. It uses no numpy. Without the native module it still imports – presets and shader assembly work – and growing or painting raises an ImportError that names the module. The renderer needs the sky’s core profile (OpenGL 4.5 on Windows and Linux, 4.1 on macOS for development; see Sky, Weather and Forests).

A forest

from acron.sky import Environment, SunShadows
from acron.trees import Forest, Species

env = Environment(base)
shadows = SunShadows(env, distance=220)
# (an acron.clouds CloudRenderer, if any, comes here: before the forest)
forest = Forest(env, shadows=shadows, cache_dir="cache/trees")

oak = Species.load("oak")
forest.plant(oak, oak_positions)                    # [(x, y, z), ...]
forest.plant(Species.load("birch"), birch_positions,
             heights=[rng.uniform(12, 18) for _ in birch_positions])
forest.set_season(autumn=0.6)

To place the trees by ecology – species sorted by altitude and moisture, clusters, clearings and edges grown over a terrain – let acron.ecosystem plant them.

plant() takes the ground points under the trunks, in the forest root’s space (world-aligned: it may be translated, not rotated or scaled), as a list of triples or any flat float buffer. Each tree’s size, yaw, tint and prototype default to hashes of its position, so the same positions give the same forest; heights (metres), scales, yaws (radians), tints (RGB multipliers) and prototypes override them, one value for all or one per tree. clear() removes the trees and keeps the grown prototypes.

stats() reports the trees, cells and nodes, how many instances each level draws this frame, how many are fading, and how long growing, painting and baking took.

Species

A Species is a JSON preset: the Weber-Penn parameters by name, the recipe of growth ops, the colours (leaf, bark, an autumn ramp, a spring tint, translucency), the painter’s leaf and bark styles, mesher settings and the levels of detail. The engine ships oak, maple, birch, aspen, ash, pine, fir, spruce, willow, palm and bush (acron/trees/presets/); a game loads its own with Species.load("path/to/cedar.json").

The friendly controls vary a species without learning the paper. Each is a documented mapping onto several parameters:

Control

Range

What it does

height

1 – 60 m

the tree’s height (Weber-Penn scale)

crown_width

0.3 – 2.5

the first branch level’s length

density

0.2 – 3

the deepest level’s branches and the leaves

leaf_size

0.3 – 3

the leaf card size

droop

-1 – 2

0 as grown, 1 weeping

gnarl

0 – 4

the random bends of every level

lean

0 – 30 deg

a random trunk lean

old_oak = Species.load("oak").with_controls(height=22, gnarl=1.4)
print(old_oak.describe())             # what it is, its shape, its sliders
tall = old_oak.with_params(levels=4)  # any TreeParams value by name

Species.schema() lists every key a preset may hold, with defaults and the native parameter documentation. Presets are checked when they load (validate()).

How trees grow

The growth model is Weber and Penn’s (1995): a trunk and up to three levels of branches, each level with its own length, curvature, splitting, branching angles and counts, a crown shape that sets how long each branch may be along the trunk, and a pruning envelope. The native generator adds extensions from SeedThree (a growth force that bends thin wood more than thick, a low trunk fork, forks at branch tips, whorls, twist) and runs a recipe of ops after MTree’s design: the trunk, the Weber-Penn levels, space colonisation (Runions et al. 2007: attraction points in a crown envelope pull new wood out of the existing stems, radii by the pipe model), roots, leaves and pruning. Each op draws its random numbers from a stream of its own, and each stem from one keyed by its place in the tree, so editing one op or a twig leaves the rest where it was – and the same seed always gives the same tree, byte for byte, on every machine.

All randomness is in the skeleton. The mesh (bark tubes whose ring count follows the radius, stitched without gaps, a flare where a branch leaves its parent; leaf cards, crossed cards, clusters, or folded sprays for conifer needles, with normals bent out of the crown so it shades as a volume), the levels of detail and the impostors are pure functions of it.

Prototypes. A forest does not grow every tree. A PrototypeCache grows a species prototypes times (12 by default) with different seeds and meshes each at three levels of detail; the forest draws thousands of instances of those. The cache is in memory, and on disk under cache_dir keyed by a digest of the preset, the seed and the mesh format, so the second run of a game loads them instead of growing them, even in a build without the native module.

Textures. Without art, paint_species paints the species’ leaf sprites (broadleaf, maple, small, bush, willow strands, needle sprays, pine brushes or palm fronds), its tiling bark (furrowed, smooth, birch, plates, scaly, palm) and their normal maps, natively and deterministically, in about 50 ms a species. The leaf mipmaps keep their alpha-tested coverage (Castaño 2010), so distant crowns do not thin out. species_textures returns the four textures the shaders read (tree_leaves, tree_bark, tree_leaves_normal, tree_bark_normal).

Levels of detail

Each tree takes a level by its distance (in metres) from the camera, from the species’ lods.distances (35, 70, 150 and 1500 m by default). The distances are for a view of 1080 lines over a 40 degree vertical field (Forest(lod_reference=(1080, 40.0))): the levels go by the size a tree takes on the screen, as Unreal’s and Unity’s do, so a smaller window or a wider lens brings them in (a 320 x 180 render uses a sixth of the distances), never past the species’ own.

  • level 0, the full mesh (16000 triangles by default);

  • levels 1 and 2, reductions under the species’ budgets (5000, 1600): rings skipped along the stems, fewer sides, thin wood pruned, fewer and larger leaf cards from the outside of the crown inwards – or, for level 2, a billboard cloud when the species asks for cards: the leaves clustered into a few big crossed cards over the thickest limbs;

  • level 3, an octahedral impostor (Brucks 2018): the prototype photographed

    offscreen from 8 x 8 directions over the upper hemisphere into albedo, normal and depth atlases, the three views nearest the camera’s direction blended in the shader with a parallax correction and a pixel depth offset, so the impostor lights, shadows and intersects the ground like a tree.

A tree changing level fades over fade_time seconds (0.6): the old and the new level draw complementary halves of the same per-pixel noise, so nothing pops and nothing doubles, and the fade runs on the clock, so a camera that stops mid-fade does not leave trees half-dissolved. hysteresis (5 %) keeps a tree on a boundary from flickering between levels.

How a forest is drawn

Engine scenes slow down with many nodes, and merging trees into big meshes costs memory, so a forest does neither. Its trees are rows of an instance table – a float texture per cell, three texels a tree (position and scale; yaw, a random number and the fade; tint and a season offset) – that the vertex shader reads by gl_InstanceID. The world is cut into cells (64 units a side); each cell has one node per level of detail, holding one instanced Geom per prototype it uses. The native ForestCells culls the cells against the view each frame (cells close enough to cast shadows into view are kept), chooses levels, runs the fades and rebuilds only the tables that changed – about 0.1 to 0.2 ms a frame for 600 to 6000 trees.

The forest’s shaders include acron.shaderlib: the sun with its cascaded shadows (and the clouds’ shadow when acron.clouds runs), the sky’s ambient light, aerial perspective and fog. Leaves use wrapped diffuse lighting for a crown lit as a volume and translucency after Barré-Brisebois and Bouchard (2011), so a crown glows against the sun. With a multisampled window (framebuffer-multisample 1, multisamples 4) the leaves are alpha-to-coverage with a sharpened alpha, which keeps their edges smooth without sorting; alpha_to_coverage forces it on or off.

Depth pre-pass. A crown is many layers of alpha-tested leaf cards, and the lighting of a leaf (the sun’s shadow taps, the sky, the air) would run for every layer of a pixel only for the front one to win. So the mesh levels draw twice, as the engines that draw dense foliage do: first depth only, with the shadow casters’ shaders (the same instance, wind, level dither and alpha test) in the acron-trees-depth bin, then lit against that depth (less or equal, writing no depth), and the lighting runs about once a pixel. Both vertex stages keep gl_Position invariant, so the two passes land on the same depth. On a GTX 660 at 1080p this took the nature_stack sample’s scene pass from 126 to about 50 ms. With alpha-to-coverage there is no pre-pass (the partial coverage cannot be held in a depth buffer); the impostors, one quad a tree, draw once.

Impostor cache. With cache_dir the baked impostors are kept on disk beside the prototypes, keyed by the prototypes, the species and seed, the layout and the bake shaders’ source; a later start loads them instead of rendering them.

Shadows. When a SunShadows is given, every level casts into the cascades through the forest’s own depth shaders: the meshes with their wind and alpha test, the impostors as quads facing the light (impostor_shadows=False leaves them out).

Wind and seasons

The wind is the environment’s (Look.wind, metres per second, and its gust), so a weather change sways the trees with no further call. The motion is hierarchical, after Sousa’s vegetation animation in Crysis (GPU Gems 3): the whole tree bends, each branch sways about its stem with a phase that is continuous across forks (a child moves with its parent where they join), and the leaves flutter along their normals. Every branch swings at its own natural frequency, worked out from its length, its thickness and the species’ wood (Wind in the Trees). set_wind_response() scales the strength and each of the three motions.

set_season() takes three numbers from 0 to 1: autumn turns the leaves through the species’ autumn ramp (each cluster in its own order), drop sheds them (1 is bare winter wood) and spring gives fresh, small leaves. Evergreen species ignore all three. Wind and seasons are frame values, so changing them touches no render state.

forest.set_season(autumn=1.0, drop=0.4)       # late October
forest.set_wind_response(strength=1.5)        # an exposed hillside
env.set_look(env.base_look.replace(wind=(12.0, 3.0, 0.0), gust=0.8),
             seconds=10)                      # a gale coming in

The shade of the canopy

A forest floor gets little of the sky: the crowns overhead hide most of it. Screen-space ambient occlusion sees a couple of metres round a pixel and not the canopy ten metres up, so the forest keeps a top-down canopy map of its crowns (Forest.canopy_map: the cover, crowns over crowns adding up as 1 - (1 - a)(1 - b), and the crowns’ base and top heights), painted natively (acron.raster) whenever trees are planted. Under acron.pipeline the GTAO pass reads it: a point under the crowns keeps 1 - canopy_occlusion x cover of the sky light (Forest(canopy_occlusion=0.6), 0 off), fading out through the crowns to nothing above them, so a closed wood’s floor is dark and green under a bright sky and a clearing stays open. canopy_texel (2 m) sets the map’s resolution, at most 1024 texels a side. Without the pipeline the map is made but unused.

Known limitations

  • Up to three branch levels; no fruit, flowers or climbers.

  • The impostors are baked once per prototype: they turn with the autumn colour, but do not shed their leaves, so a bare winter forest still shows leafy crowns in the far distance.

  • The forest root must be world-aligned (translation only).

  • The tree shaders apply the sky’s aerial perspective and fog, but not the clouds’ fog or light shafts yet.

  • Culling is on the CPU, per cell (the engine has no indirect draws).

  • The lit pass of the leaves is the forest’s main cost: on a GTX 660 at 1080p about 17 ms for the nature_stack sample’s forest edge, about 10 of them lighting (the air and fog about 5, the sun’s shadow about 3).

References

  • Jason Weber and Joseph Penn, “Creation and Rendering of Realistic Trees”, SIGGRAPH 1995.

  • Adam Runions, Brendan Lane and Przemyslaw Prusinkiewicz, “Modeling Trees with a Space Colonization Algorithm”, Eurographics Workshop on Natural Phenomena 2007.

  • Ryan Brucks, “Octahedral Impostors”, 2018.

  • Xavier Décoret et al., “Billboard Clouds for Extreme Model Simplification”, SIGGRAPH 2003.

  • Tiago Sousa, “Vegetation Procedural Animation and Shading in Crysis”, GPU Gems 3, chapter 16, 2007.

  • Colin Barré-Brisebois and Marc Bouchard, “Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look”, GDC 2011.

  • Ignacio Castaño, “Computing Alpha Mipmaps”, 2010.

  • The engine’s tree generator ports or adapts ideas from SeedThree, MTree and ez-tree; Procedural Trees lists them.