Trees and Forests¶
This page describes part of the Python framework that comes in the
acron wheel. The tree generator, the mesher, the LODs, the texture
painter and the forest’s culling are native and available to C++ as well:
see Procedural Trees.
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 |
|---|---|---|
|
1 – 60 m |
the tree’s height (Weber-Penn |
|
0.3 – 2.5 |
the first branch level’s length |
|
0.2 – 3 |
the deepest level’s branches and the leaves |
|
0.3 – 3 |
the leaf card size |
|
-1 – 2 |
0 as grown, 1 weeping |
|
0 – 4 |
the random bends of every level |
|
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.