bevy_flora 0.26.0

Parametric, stylized procedural trees and shrubs for Bevy that grow over time: 16 presets, bent and forking branches, CC0 bark and leaf scans, painterly and toon looks, deterministic per seed.
Documentation
# bevy_flora Architecture

Pure-Rust Bevy 0.20 crate that generates parametric, stylized trees and grows them over time.

## Pipeline

```
TreeParams
  → generate_skeleton        (skeleton.rs: Node tree, parent links, deterministic from seed;
                              gravity sets each branch's `bend`, children sit on the bent parent axis)
  → assign_birth_times       (growth.rs: birth + duration per node, from topology)
  → Skeleton::aged(age)      (drops unborn nodes; scales length/radius by progress)
  → apply_pipe_radius        (optional, mesh/branch.rs)
  → skeleton_mesh            (mesh/: trunk, collars, tubes, optional smoothing)
  → collect_leaf_points → foliage_mesh   (foliage.rs)
  → MeshData → bevy Mesh     (mesh/mod.rs)
```

`pipeline::build_tree(params, age)` runs all of it and returns `TreeMeshes`
(`trunk`, `foliage`, aged `skeleton`, `mature_at`).

## Growth model

Progress is linear. A primary is born when the trunk reaches its attachment
height; a sub-branch is born when its parent reaches `attach_t`. Radii thicken
over `duration * thickening_lag`. Leaves appear after `leaf_threshold` progress
and scale to full size. Each node draws leaf randomness from its own RNG stream
(`SeededRng::from_parts(seed, node.id)`), so aging never re-rolls existing leaves.
`Skeleton::aged` keeps each node's full-grown `child_count`, so tip placements
(`TerminalBranches`, `TipClusters`) pick the same branches at every age and a
leaf never disappears as the tree grows.

## Leaf look (baked into the mesh)

The mesh carries what the leaf shader reads (see Leaf shading and wind): leaf-shaped
cards, crown-bent normals, a per-leaf vertex colour (tint x `color_variation`, hue x
`hue_variation`), `uv1` (x = upper side, y = crown visibility from
`FoliageParams::occlusion`) and the sway attribute. The meshes also draw correctly
with a plain `StandardMaterial`, just without the leaf shader.

- **Leaf-shaped cards.** `CrossedPlanes`, `SingleQuad` and `StarBurst` emit an
  eight-point pointed oval (`LEAF_OUTLINE`, about half the area of the old square
  card) as a triangle fan, twice with opposite winding. Cutting the silhouette into
  the geometry keeps hidden-surface removal intact, which an alpha-masked texture
  (`discard`) would not on tile-based GPUs. `CrossedPlanes` and `SingleQuad` seat the
  card's base on the leaf point and reach one `size` out along the basis `up`, so a
  leaf grows out of its twig instead of being threaded on it.
- **Scanned leaves.** With `FoliageParams::scan` the same styles emit the scan's outline instead (see Leaf scans and venation).
- **Crown-bent normals.** `foliage_mesh` lerps every normal toward the direction out
  of the crown centre (mean leaf position) by `FoliageParams::normal_bend`, so the
  crown shades as one soft volume instead of per-card light and dark. Bent normals
  intentionally disagree with face winding; winding is checked at `normal_bend = 0`.
- **Per-leaf tint.** `LeafPoint::tint` is drawn from its own RNG stream
  (`seed ^ LEAF_TINT_SALT`), so it never moves another draw or re-rolls with age,
  and `foliage_mesh` writes it as a vertex colour (`MeshData::colors`,
  `Mesh::ATTRIBUTE_COLOR`) scaled by `FoliageParams::color_variation`.

## Mesh conventions

Front faces are counter-clockwise; the helpers in `mesh/rings.rs` own the winding
convention. Never fix winding in an individual mesher.

A branch's shape is `Node::axis` (bent by `Node::bend`) sampled at
`Node::axis_points()` points: 6 for a lightly bent branch, up to 16 when gravity
turns its direction by more than ~4.6° between points, so a long drooping limb is
smooth. The tube mesher, child placement and leaf placement (`Node::point_at`)
all use the same count, so wood, sub-branches and leaves agree. Sub-branches
spread around the parent's bent tangent where they attach (a drooping limb hands
its twigs the direction it has there, so they hang). Generated radii are floored
by `BranchParams::min_twig_radius` at the base and half of it at the tip, and
never exceed the wood they grow from (`skeleton::floored_radii`), so twigs taper
but never shrink to hairlines. Sub-branches spreading toward the trunk and
tip-cluster leaves landing inside it are mirrored away (`skeleton::trunk_depth`);
after seating and the gravity bend, a sub-branch whose wood still enters the trunk
gets its bend relaxed, or is shortened (`Node::keep_out_of_trunk`).
Each primary's tube and collar get a `root_segment` reaching into the trunk
(`primary_root`); a branch hugging the trunk (less than half of its direction
pointing away from it) gets a closed, rounded `root_dome` instead, since a long
extension would show as a ridge down the trunk. The dome has four rings and a cap
with twice the tube's side count, joined to the tube's own base ring, so it is
round even on a hexagonal branch. Either way
junctions have no open ends. Smoothing (Taubin λ|μ, so it does not shrink the
wood) welds coincident vertices (UV seams) and pins open boundaries, the ground
ring, branch-end caps and the parent surface under each child's seat, so it
never tears, lifts, retracts or detaches the wood. Wood finer than ~100 weld
tolerances (hairline twigs) stays as generated, and normals are recomputed only
around moved vertices, pooled across seam twins, so unmoved pieces keep their
generated shading. Root segments are added after it, so junctions keep their shape.

## Bevy integration (plugin.rs)

- `ProceduralTree(TreeParams)` + `TreeAge(f32)` (default `INFINITY` = mature) + optional `Growing { rate }`.
- `FloraPlugin` rebuilds meshes on `Changed<ProceduralTree>` / `Changed<TreeAge>`, keeping a wood child and a leaf child. An empty trunk (age 0) leaves the wood child hidden with no mesh; the mesh is created once the trunk has geometry.
- The full-grown skeleton is cached per tree (`TreeRender.full`) and regenerated only when `ProceduralTree` changes; a `TreeAge`-only frame (growth) runs `pipeline::build_aged` on the cache. `rebuild_trees` works in three phases: it collects the trees whose parameters or age changed, builds their meshes with `build_in_batches` on the compute task pool (a few per thread at a time, bit-identical to the serial result, inline for fewer than two), then applies the results one by one. Materials are never written in place: a tree resolves its two `FloraMaterial`s from `SharedFlora` by look (colour, leaf flag, the species part of the uniform), so trees with equal looks (same colours, trunk height, wind response and leaf shading; the seed is irrelevant) share them, and a tree whose look changes is given other shared materials.
- `TreeGenerated` message reports current height/radius after each rebuild.
- **Background builds** (`background.rs`). A tree with `BuildInBackground` is not built in the frame: its job (the same `RebuildJob`/`GrowthJob` and the same pure `build_job`/`build_growth` the frame uses) goes to `AsyncComputeTaskPool` as a `PendingBuild<R>` component holding the `Task`, and each rebuild system starts by polling its own pending tasks (`check_ready`), applying the finished ones through the same `apply` closure as the in-frame results, before collecting new jobs. Replacing the component (a newer change) or despawning the tree drops the task, so its result is never seen. A result is discarded if the tree's parameters changed in the frame it lands (a newer build was just started) or the tree changed paths (CPU/GPU); the CPU apply resolves materials when the skeleton was regenerated (`RebuiltTree::regenerated`), not when `ProceduralTree` changed this frame, which a late result cannot know. Because both systems now handle trees that did not change this frame, their queries have no change filters: they test `is_changed` on `Ref<ProceduralTree>` and `Ref<TreeAge>` themselves (the same per-entity tick checks a `Changed` filter does). A background CPU tree is rebuilt by state, not by change: whenever `TreeRender::age` (the age its meshes show) differs from `TreeAge` and no build is under way, or its parameters changed. A change of age during a build is therefore caught up when the build lands, which change detection could not do (a system does not see its own `set_changed` on its next run), and age-only rebuilds take a copy of `TreeRender::full`, not the skeleton itself, so felling a tree mid-build still finds it. A background GPU tree takes the age it finds when its mature meshes land (the state update is in the frame anyway) and skips its state updates while a build is under way, so it never ages the old skeleton with new parameters.
- `TreeRender.paint` says how a tree is drawn: `Paint::Flora` (CPU-rebuilt meshes, shared `FloraMaterial`s), `Paint::Standard` (CPU-rebuilt, plain `StandardMaterial`s, where `FloraMaterialSupport` is absent) or `Paint::Growth` (GPU growth, below). `sync_growth_mode` drops the render state of a tree whose `GpuGrowth` appeared, went, or whose device support changed, and the matching rebuild system recreates it.

## GPU growth (`gpu/`)

A tree with `GpuGrowth` is built once at maturity and grown in the vertex shader. Why that works:

- `Skeleton::aged` changes only `start`, `length`, the two radii and `progress`; `direction` and `bend` never change, so every tube ring has the same frame (`RingBasis::from_axis(direction)`) and the same axis shape at every age (`gpu::tests::ageing_never_turns_or_bends_wood`). A tube vertex is therefore `start + length * c + radius * o` with `c` (axis offset in node lengths) and `o` (unit radial offset) measured once on the mature mesh. Leaves are `start + length * c + leaf_scale * d`.
- `mesh.rs` builds the mature meshes with the CPU meshers (`skeleton_mesh_with` records which node and piece every run of vertices belongs to; `foliage_mesh_with` records each leaf's first vertex), tags every vertex (`frame.rs`: `Class`, `Tag`) and measures its offset on the final, smoothed positions, so smoothing rides along as a deformation. The wood always carries the trunk's top cap and (pointed trunks) its tip cone; the shader hides the one the age does not have.
- `state.rs` writes the only per-frame upload: a header, 5 `vec4` per node (`NodeState`: start, length, radii, leaf scale and count, collar ends and radii, root point, piece flags) and 2 per trunk ring (centre, radius, root bulge, twist). It calls the same helpers the meshers call (`aged_skeleton`, `collar_geometry`, `primary_root`, `root_segment_emitted`, `node_leaf_scale`, `trunk_rings`), so the shader and the CPU path agree on which pieces exist at an age. The buffer is a `ShaderBuffer`, rewritten in place (`queue.write_buffer`); the materials are not touched.
- `growth_common.wesl` mirrors `frame.rs::position` (the Rust reference the tests check against the CPU meshers at many ages); `growth.wesl` and `growth_prepass.wesl` are Bevy's vertex stages with the position replaced, the prepass one also serving shadow maps. `GrowthExtension` (an `ExtendedMaterial<StandardMaterial, _>`) binds the node buffer at slot 100, the `FloraUniform` at 101 and `FloraWindBuffer` at 102, and appends the three growth attributes (locations 8-10), plus the leaf sway attribute (11) on leaf materials, to the layout the base material chose.
- The trunk's ring twist is `trunk.twist * t` with `t` scaled by progress, so unlike the branches it changes with age; trunk offsets are stored with the twist taken out and the shader turns them by the ring's current twist.
- `GpuGrowthSupport` is set from the device's limits (`storage_buffers_are_unsupported`) once the renderer exists; without it, trees with `GpuGrowth` use the CPU path.


## Leaf shading and wind (`shading/`)

Every tree the plugin draws uses `FloraMaterial = ExtendedMaterial<StandardMaterial, FloraExtension>` (also `GrowthMaterial` and `ImpostorMaterial`, which carry the same `FloraUniform` at binding 101 and call the same shader library, `common.wesl`). Without the renderer's plugins, or with `FloraPlugin` added before them, `build` registers only the asset type, `FloraMaterialSupport` is never inserted, trees get plain `StandardMaterial`s and `finish` warns once when a renderer exists (the plugin was added before it) and says nothing when there is none.

- **The wind is not in the materials.** `FloraUniform` (binding 101) holds what belongs to a tree and its species: `tree` (1 / trunk height), `response` (`TreeParams::wind`), `leaf` and `tint` (`LeafShading`), and `autumn` (see Seasons). The weather, the season and the shakes are the same for every tree, so they live once, in `FloraWindBuffer` (a `ShaderBuffer` of `WIND_VECTORS` = 25 `vec4`: `wind`, `motion`, `rate`, `front`, `front_shape`, `season`, the shakes' header and two per shake slot, packed from `FloraWind`, `FloraSeason` and `TreeShakes` by `wind_vectors`, then the see-through fade's two), bound at binding 102 by `FloraExtension`, `GrowthExtension` and `ImpostorMaterial`. `sync_wind` writes it in place (when any of the three resources changed; a same-size `ShaderBuffer` write is a `queue.write_buffer`, which prepares no material), so changing the wind, the season or the shakes modifies no material asset. That matters because Bevy re-specializes every entity of a material whenever the material asset is modified: with the wind in the materials a pooled forest of 20 000 trees took 212 ms a frame while the wind changed every frame, and now 3.3 ms. Devices without storage buffers in the vertex stage (WebGL2) cannot bind it: `detect_flora_support` then leaves `FloraMaterialSupport` out, trees get plain materials and pools no impostors. `wood()` zeroes only `leaf`, `tint` and `autumn`: wood bends with the species' `sway`. The struct in `common.wesl` must list the fields in the Rust order.
- **Wind** (`wind_bend`, `flutter_offset`) follows Crysis' vegetation animation (GPU Gems 3, chapter 16): a main bend that grows with the square of the height over the trunk and keeps every vertex's distance from the trunk, and per-leaf flutter with smooth triangle waves of unrelated frequencies. A `GustFront` is a Gaussian band, `strength * exp(-(along / width)^2)` with `along = dot(origin.xz - front.origin, direction) - speed * (time - start)`, added to the wind's strength for flutter and to its push along the front's own direction for the bend; `start` is `Time::elapsed_secs_wrapped`, which is the `globals.time` the shaders read. It runs in the vertex stage of `vertex.wesl` and of `prepass.wesl` (the shadow maps), in the GPU growth shaders and in `impostor.wesl`, so shadows follow the motion. There are no motion vectors.
- **Shakes** (`TreeShakes`, `shake_at` in `common.wesl`) ride on the same bend. `wind_vectors` packs up to `TreeShakes::MAX` (8) shakes with a strength as (at, radius, strength) and (direction, start, duration), and a header with the render time the last of them ends (-1 for none), so `shake_at` returns at once, for every vertex, while none is under way. Otherwise it sums, per slot, `strength * near * fade² * sin(2π f age)` along the direction (or away from `at`), `near = 1 - smoothstep(0, radius, distance to the trunk)`, `fade = 1 - age / duration`, and `f` the tree's swing rate, `3 √(1 / trunk height)` a second, varied ±10% per tree by `tree_phase`. The lean is added to the wind's push with its own reach, without `FloraWind::bend` (a still day has knocks too), so the bend keeps every vertex's distance from the trunk as the wind does; the summed amounts, times `SHAKE_RUSTLE`, add to the leaves' flutter strength. Every vertex stage (`vertex.wesl`, `prepass.wesl`, both growth shaders, `impostor.wesl`) calls `shake_at` once and hands the result to `flutter_offset` and `wind_bend` (calling it inside both cost leaves twice the loop). `TreeShakes::shake` stamps the render clock and, when full, replaces the stored shake that ends soonest; `expire_shakes` (just before `sync_wind`) drops a shake once it has died away, or once the clock's wrap has left it in the future, so the header goes back to -1 and the resource changes only in the frames a shake starts or ends.
- **Leaf look** (`fragment.wesl`, `leaf_surface`, `transmitted_light`): upper and lower side from `uv1.x`, veins from the card's UVs (never on `NeedleCluster` and `ClusterSphere`, whose UVs are not a leaf), crown visibility from `uv1.y` as diffuse and specular occlusion, and Unreal's two-sided foliage term, the back side's wrapped N·L times a forward-scattering GGX lobe times the leaf colour and tint, for every directional light. Leaf materials add the `FLORA_LEAF` shader def and the vertex attribute `ATTRIBUTE_LEAF_SWAY` (location 11) in `specialize_flora`. The specular reflectance of a leaf fades as the camera faces a directional light (`leaf_gloss_toward_light`: 1 until `-V·L` reaches 0.4, 0 from 0.8, the strongest light wins), so a backlit card does not mirror the sun into the camera; the impostor does the same for its leaf texels (baked translucency). Views with the sun beside or behind the camera are unchanged.
- **See-through fade** (`see_through.rs`, `see_through` in `common.wesl`, `see_through_prepass.wesl`). `SeeThroughTrees` on a camera puts two vectors in the shared buffer's last slots (`SEE_THROUGH_SLOT`, 23 and 24): the camera's position and the clear radius, and the target and 1. `sync_see_through` writes them in place after transform propagation (`PostUpdate`), so the position is the one the frame is drawn from, and `sync_wind` keeps them when it rewrites the buffer. `see_through` cuts a fragment between the camera and the target within a cone a third as wide at the camera, fully inside 70% of the radius and through a 4x4 ordered dither outside it, closing over the last tenth of the way; it cuts only in a view whose `view.world_position` is the fading camera's, which is what keeps shadow maps (and other cameras) whole. The discard is compiled in only while a camera has the component: `arm_see_through` sets `see_through` on every `FloraMaterial`, `GrowthMaterial` and `ImpostorMaterial` (and on materials made later, the frame after), it is part of their keys, and `specialize_flora` then adds `FLORA_SEE_THROUGH` to the main pass and, in a prepass pipeline (`PREPASS_PIPELINE` among the vertex shader's defs; not a directional light's cascades, `UNCLIPPED_DEPTH_ORTHO`), swaps Bevy's `pbr_prepass.wesl` for `see_through_prepass.wesl` (the same outputs, the cut first) or makes that the fragment stage where a depth-only prepass has none. Without that, a `DepthPrepass` keeps the tree's depth where the main pass cut it, and the main pass's depth test then hides what is behind. A point or spot light's shadow pipeline has the same key as a perspective depth-only camera prepass and gets the fragment stage too; the view test keeps it whole. `specialize` has no access to the world, so the prepass shader's handle, loaded when the shaders are embedded, is kept in a process-wide lock (`see_through::prepass_fragment`). While no camera has the component, nothing changes (424 gallery scenarios at 0.000); while one has it, 1 000 individual oaks cost 10% more (a discard keeps the GPU from rejecting hidden fragments early) and a 100 000-tree pool nothing measurable.
- WGSL compile errors in these shaders only show as `ERROR bevy_render pipeline_cache` lines when a real render runs (`forest --screenshot`, `gallery --gpu`), never in a headless test. `class`, `patch` and `target` are reserved WGSL words.

## Seasons (`shading/`, `impostor/impostor.wesl`)

`FloraSeason { autumn, leaf_fall, snow, blossom }` is the sixth `vec4` of the shared buffer (`flora_wind[5]`), clamped to 0..1. What a species turns to is per material: `FloraUniform::autumn` is the autumn colour over the leaf colour, per linear channel (capped at `AUTUMN_RATIO_MAX`, 1000: the maple's dark green needs 47 in red), with `w` 1, or all zero for an evergreen (`FoliageParams::autumn` is `None`) and for wood; it is part of the `SharedFlora` look. The shaders multiply the leaf's vertex colour by `mix(1, autumn.rgb, turn)`, so a leaf that has turned is the autumn colour times its own tint and hue variation and its scan's detail (every foliage mesh carries vertex colours).

- **One moment per leaf.** `leaf_season_moment` hashes the leaf's flutter phase (`ATTRIBUTE_LEAF_SWAY.w`, one per leaf, both faces and both crossed cards alike) to 0..1. The phase is `foliage::leaf_phase`, a hash of the leaf's colour and hue draws, which never change as the tree ages (it used to hash the leaf's position, which moves while a tree grows), so a leaf keeps its phase at every age, on the mature mesh `GpuGrowth` shrinks and on the levels of detail. A leaf turns over `smoothstep(0.8 m, 0.8 m + 0.2, autumn)` and has fallen while `m < leaf_fall`, so leaves fall in the order they turned. A fallen leaf's vertices are moved outside the clip volume (`FALLEN_LEAF_CLIP`) in `vertex.wesl`, `prepass.wesl` and both growth shaders, so it draws nothing in the main pass and in the shadow maps. The test comes first, straight after the vertex is fetched, and returns: a hidden vertex skips the shake, the flutter, the growth and the wind bend (before 0.20 it ran them all and was clipped after, so a bare winter tree cost its summer's vertex work).
- **Impostors** bake summer (the bake materials ignore the buffer) and apply the season per pixel to leaf texels (baked translucency): the colour turns in patches of 3x3 atlas texels, each with a hashed moment in 0..0.8, and where `fwidth` says a pixel spans more than one patch it takes the share of patches turned, `saturate((autumn - 0.1) / 0.8)`, instead (one patch's moment flickered as the camera moved: twice the frame-to-frame change of a summer forest's far field). Leaf fall scales the leaf texels' coverage by `1 - leaf_fall`, so the baked wood stays.
- **Snow** (`FloraSeason::snow`, `flora_wind[5].z`) is fragment-only: `snow_on` (`common.wesl`) mixes the base colour toward `SNOW_ALBEDO` and the roughness toward 0.9 by `max(snow_cover(n), frost * snow) * shelter`, where `snow_cover` is `snow * smoothstep(e, e + 0.2, n.y)` with `e = 0.85 - 0.6 snow` (more snow reaches steeper faces). Wood passes its geometric world normal, `shelter` 1 and no frost. A leaf passes `facing_normal`, the flat face's normal from screen-space derivatives turned toward the camera, because its shading normal is crown-bent (snow by it whitened the top of a conifer and nothing below); `shelter` is `snow_shelter(sky share)` (`uv1.y`), `smoothstep(0.35, 0.85, ·)`, because snow on an inner card, lit only by what gets in, renders charcoal; and `LEAF_FROST` (0.03) is a hint on every exposed needle (the mix is in linear light: 0.2 turned a spruce seen from below grey-olive). The light through a leaf is scaled by `1 - cover`. Impostors pass the baked (crown-bent) normal, `snow_shelter` of the baked sky share and the frost on leaf texels. At `snow` 0 every mix has a weight of exactly 0, so summer renders are unchanged up to the compiler's rounding (a few pixels by one level). Snow in a cast shadow is lit by the ambient light alone and reads grey under a weak one. Snow does not touch the vertex stage, the shadows or the ground.
- **Blossom** (`FloraSeason::blossom`, `flora_wind[5].w`; `FoliageParams::blossom`). Flowers are geometry, not a texture: `foliage::build_foliage` builds, beside each leaf's cards, a double-sided fan of five petals at the leaf's stem facing out along the leaf and up, `FLOWER_SIZE` (0.8) of the leaf's length across: each petal two shoulders, a notched tip and a gap (`PETAL`, 30 outline points) at level 0, its tip and the gap alone on the coarse levels (`PETAL_COARSE`, 10). They go in a mesh of their own (`FoliageMeshes::flowers`, `TreeMeshes::flowers`) with their leaf's colour, flutter (`sway`, the leaf's phase), crown visibility and bent normals, and a vertex colour alpha of 0 (a leaf's is 1; `colors.a` was unused), which is how every shader tells them apart; the centre is `FLOWER_CENTRE` darker. **Out of season they are not drawn.** `ProceduralTree` and `GpuGrowth` trees carry them as a third child (`TreeFlowerMesh`, the leaf material), and `show_flowers` (end of the update chain) sets it `Visibility::Hidden` while `blossom` is 0, touching only new flower children unless `FloraSeason` changed, with `set_if_neq`. They first lived inside each leaf's vertex range, hidden by the vertex stage like fallen leaves, which cost 1 000 individual cherries 42 ms a frame in summer against 23 ms without flowers: a hidden vertex costs about what a drawn one does (an early return before the wind's work saved nothing measurable for them), so the only cheap flower is one not submitted. Pools put each level's flowers after its leaves in the leaf mesh (`MeshData::extend`), where the shaders hide them: a pool draws most trees at its coarse levels or as impostors, and a fourth entity per level would cost a 100 000-tree forest more on the CPU than the flowers cost there. `GpuGrowth` tags each flower vertex with its leaf's node, ordinal and anchor (`tag_leaves` runs the same loop over both meshes' `firsts`), so a flower grows and moves with its leaf; `grown_leaves_and_flowers_match_the_cpu_meshers` holds both to the CPU meshes. In season, `leaf_hidden` (`vertex.wesl`, `prepass.wesl`, both growth shaders) hides a flower whose `flower_moment` (the leaf's phase, hashed differently from `leaf_season_moment`, so flowers do not open in the leaves' order) is not below the blossom, and `leaf_season_tint` multiplies its vertex colour by `FloraUniform::blossom`, the blossom colour over the material's base colour (`Surface::leaves` recomputes it against the base, which for a scanned leaf is the preset over the atlas mean, since a flower wears no scan). The fragment stage skips the leaf scan, the veins and the leaf gloss for a flower (`in.color.a < 0.5`; the opaque material forces alpha back to 1) and lets light through it in its own colour, not the leaves' tint. The impostor bake leaves flowers out (clipped by their alpha), and `impostor.wesl` paints them back over the summer picture in patches with their own hashed moments, mixing toward the blossom colour by the flowers' share of what is out (`bloomed / (bloomed + leaves left)`) and keeping a bare crown's coverage at `BLOSSOM_COVER` (0.8) of a leafy one. With plain `StandardMaterial`s (no flora material) a `ProceduralTree`'s flowers are all out while `blossom` is above 0, and a pooled tree's all year.
- `FloraSeason::of_year` keeps a reset of the colour to green to the bare months, brings the leaves back green (from 0.02 to 0.07), lays snow only on trees with at least 90% of their leaves gone, melting it before the leaves return, and opens the blossom on bare branches from the year's turn (after the melt) to 0.015, dropping it from 0.03 to 0.07 as the leaves come back; a test walks the year to hold that.

## Bark (`shading/textures.rs`, `mesh/rings.rs`, `src/textures/`)

Wood wears a bundled CC0 scan (`BarkScan`, `TreeParams::bark`; the files and their licences are in `src/textures/CREDITS.md`, produced by `tools/prepare_textures.py`, which is not packaged). The `textures` feature (on by default) compiles the JPEGs in; without it every scan resolves to `None` and wood is plain colour, exactly as before.

- **Wood UVs are metric and anchored to the mature tree.** `emit_ring` takes a `RingUv { v, u_tiles }`; `v` is the ring's distance along the fully grown wood in tile lengths (`mesh::branch::BarkLayout::ring`), `u` runs a whole number of tiles around (`u_tiles_around`: `round(2π · mature base radius / tile width)`, at least 1), so the seam is invisible. Mature sizes are the aged ones over their progress (`mature_size`), and a trunk ring's `v` is its index fraction times the trunk height, so every vertex keeps its texture coordinate at every age: bark never slides as a tree grows, and the aged CPU mesh agrees with the mature mesh `GpuGrowth` shrinks (`gpu::tests::assert_same_bark`). Collars take the tube's `u_tiles` and `v` by distance along the branch, so the bark carries on across the junction; root segments and domes sit at `v = 0` (hidden in the trunk, or a knot); caps lay their disc out at the bark's real size. Under the pipe model an aged node's radii are not its mature radii scaled, so the tile count and cap size follow the aged radii over their progress: bark may re-tile by a tile while such a tree grows, and differs slightly from `GpuGrowth`'s.
- **`uv1.x` of wood is the vertex's own radius in metres** (`uv1.y` is 1). The fragment shader fades the bark in with it: smooth below `BarkParams::smooth_below`, the whole scan above `fissured_above`, smoothstep between (`bark_surface` in `common.wesl`), so a branch gains relief as it thickens. `growth.wesl` replaces it with `grown_wood_radius` (`growth_common.wesl`; the per-kind scale `grown_position` uses, factored out as `piece_scale`; mirrored by `frame::wood_radius`), so GPU growth fades the bark in as the wood thickens.
- **The base colour is the preset over the scan's mean** (per linear channel, at most 8x; `BarkTexture::base_color`), and the shader multiplies it by `mix(mean, scan colour, fade)`, so wood averages the preset colour at every fade and far trees, impostors (which bake the preset colour) and untextured trees keep their colour. Normals are perturbed with Schüler's cotangent frame from screen-space derivatives (`cotangent_frame`): the meshes carry no tangents. The map is OpenGL's (green up) and `v` runs down the image, so the bitangent is flipped. The relief fades out toward the silhouette (`smoothstep(0.05, 0.4, N·V)`): there a bumped normal turns away from the camera and the light, and the crevices read as black speckle.
- `FloraTextures` decodes each scan on first use (`Image::from_buffer`, CPU mip chain: `with_mip_chain` for the colour map, a vector-averaging chain for the normal map), keeps the handles, and logs one `warn!` naming the file if a scan fails to decode (that scan's trees are plain colour). Materials bind the maps at 103 (colour, with the sampler at 105) and 104 (normal), `FLORA_DETAIL` is on when both are set and the mesh has both UV sets (`FloraKey::detail`, `specialize_flora`), and the scan is part of the `SharedFlora` look, so trees share one material per scan and colour. The plain-material fallback (`Paint::Standard`, `PoolMaterials::Standard`) ignores scans.

## Leaf scans and venation (`foliage.rs`, `venation.rs`, `shading/textures.rs`, `src/textures/leaves.rs`)

- **Scanned outlines.** `FoliageParams::scan` (`LeafScan`) cuts the cards of the leaf-shaped styles to a real leaf: `tools/prepare_textures.py` finds the four largest leaves of an ambientCG leaf set by connected components of its opacity map, orients each (principal axis vertical, petiole at the base), packs them as 2x2 cells of a 512 px atlas with the colour and normal maps dilated past the alpha edge, and generates `src/textures/leaves.rs`: per leaf its outline (marching squares at alpha 0.5, Douglas-Peucker to a 24-point reference polygon, then the 10 of its vertices, base and tip among them, whose polygon has the least symmetric-difference area against the reference, found by exhaustive search; the mean symmetric difference is 5.7% of the leaf, the worst 13.7%, a Japanese maple's lobed leaf), the atlas coordinate of every outline point, an ear-clipping triangulation and its width over length, plus a coarser outline (a subset of the full one, 6 to 10 points) for the distant levels of detail (`foliage_mesh_coarse`, used by `lod.rs`). The 10-point cap is a measured budget: a card side costs 8 triangles and 10 vertices where the procedural fan's cost 8 and 9, and a forest of 1 000 individual oaks (the GPU's vertex work, in the main pass and every shadow cascade) took 0.7.0's frame time plus 4-10% where Douglas-Peucker outlines of 12 points cost 13-19%, 16 points about 60% and 24 points about 75% more. `add_double_sided_leaf` emits the outline twice with opposite windings, the width scaled by the leaf's aspect, the length one `size`, UV0 the atlas coordinates. Which of the four leaves a card is comes from a hash of the leaf's tint and hue draws (`scanned_card`), which never change with age, so a leaf keeps its shape as the tree grows and the mature mesh `GpuGrowth` shrinks has the same cards. `NeedleCluster` and `ClusterSphere` ignore the scan; `uses_scan` is the one test of whether a tree's cards are scanned (the `textures` feature, a scan, a leaf-shaped style).
- **The look of a scanned card.** The material binds the atlas at 103/104 like bark (`FLORA_DETAIL`); `leaf_detail` in `fragment.wesl` runs before `leaf_surface` and multiplies the base colour by the atlas colour (the base colour is the preset over the atlas's mean over the leaves' own pixels, from the generated table, so a crown keeps its colour) and perturbs the card normal with the same cotangent frame as bark. `Surface::leaves` zeroes `FloraUniform::leaf.z` for a scanned card: the atlas has the real leaf's veins.
- **Venation** (`venation.rs`, pure CPU, deterministic per seed): open space colonisation after Runions et al. 2005 inside `LEAF_OUTLINE`: auxin sources by dart throwing (birth distance 0.06 card units), vein nodes grown from the petiole `(0, -1)` in steps of 0.02, each node closest to a source within 0.4 growing toward the mean direction of its sources, a source used up within 0.04 of a node, until none is left or 400 iterations; widths by Murray's law (`w^3 = sum of the children's`, tip 0.008). `rasterize` draws the veins anti-aliased into a 128x256 `R8` image (row 0 at the petiole). `FloraTextures::venation` makes one image, from one fixed seed, with a box-filtered mip chain, and every leaf card without a scan shares it, so trees keep sharing their leaf material whatever their seeds; it is bound at 106 (sampler 107) and `FLORA_VENATION` makes `leaf_surface` sample it with the card's UVs where the formula for the midrib and side veins used to be.

## Render styles (`foliage.rs`, `shading/`, `impostor/`)

`TreeParams::style` (`RenderStyle`: `Natural`, `Painterly`, `Toon`) is a look, not a shape: positions, colours, UVs and the baked leaf attributes are the same in every style, only the leaf normals and the fragment lighting change. The style is part of `Surface` and therefore of the `SharedFlora` look and of `FloraKey`, and `ImpostorMaterial::new` takes it.

- **Clumps** (`foliage.rs`): `LeafPoint::clump` is the `Node::id` of the nearest ancestor of the leaf's node, itself included, at recursion depth 0 (`clump_of`): a primary branch, the leader or one half of a split, with everything that grows from it. Ids are the full skeleton's, so a leaf keeps its clump at every age. `build_foliage` finishes a painterly or toon mesh by replacing every leaf normal with `normalize(position - clump_centre)` (strength 1: `normal_bend` toward the crown does not apply), the centre being the mean position of the clump's leaves. It is a function of the points alone, so the level-0 mesh, `lod.rs`'s coarse levels (centres of the kept leaves), the mature mesh `GpuGrowth` bakes (centres of the mature leaves, constant as the tree grows, where the CPU path's follow the leaves that exist) and the impostor atlases (baked from level 1 meshes) all inherit it. `foliage_mesh` is the natural style; `foliage_mesh_styled` takes one.
- **Toon** (`toon_lighting`, `toon_rim` in `common.wesl`): in place of `apply_pbr_lighting`, each directional light contributes `colour * band(N·L) * shadow`, `band` being 0.15 up to `N·L = 0`, 0.55 up to 0.5 and 1 above, times the surface colour over pi, plus Bevy's ambient light on the diffuse colour; the leaf's transmitted light is added as in the natural path and the sum is multiplied by `mix(0.35, 1, smoothstep(0.05, 0.3, |N·V|))`, the ink. `N` is the geometric normal (`PbrInput::world_normal`, the painterly one), not the one the detail maps perturbed: with the shading normal the bands broke up into the leaf scans' veins and the bark's crevices. Point and spot lights are ignored. Meshes pick it through the `FLORA_TOON` shader def (`specialize_flora`, wood and leaves alike, so a toon and a natural tree never share a pipeline); the impostor shader reads the style at run time from `params.y` (0 natural, 1 painterly, 2 toon: `RenderStyle`'s discriminants, pinned by a test) because its pipeline is shared by every pool.
- Painterly has no shader def: it differs from natural in the normals alone.

## Trees from assets and files (`tree_asset.rs`)

`TreeParams` derives `Asset`; the plugin calls `init_asset` and `register_asset_reflect` for it (and, with the `ron` feature, registers `TreeParamsLoader` for `.tree.ron`) when the asset server exists, in `build` or else in `finish`, like the pool's asset type. `ProceduralTreeAsset(Handle<TreeParams>)` is the handle form of `ProceduralTree`: `sync_tree_assets` (in the main chain, before `grow_trees`, only while `Assets<TreeParams>` exists) collects this frame's `Added`, `Modified` and `LoadedWithDependencies` ids and, for each entity whose component changed or whose asset is among them, inserts `ProceduralTree` the first time and afterwards assigns it only when the parameters differ, so `rebuild_trees` (after the chain's sync point) rebuilds just those trees and an unchanged reload rebuilds nothing. It returns at once when no id came in and no component changed. Entities without a loaded asset draw nothing; a failed reload leaves the asset, and so the tree, as it was.

- **The file format** is the serde derive on the parameter types through `ron`: `to_ron` writes every field (`PrettyConfig::default()`), and every parameter struct has `#[serde(default, deny_unknown_fields)]`, so a missing field takes that struct's `Default` (files written before a field existed keep loading) and an unknown one fails with ron's message, which gives the position and the fields that exist. `TreeFileError` wraps ron's error, an I/O error or bad UTF-8 without exposing ron's types. A test round-trips every preset through `to_ron`/`from_ron` and parses `examples/assets/trees/maple.tree.ron`.
- **Hot reload** is Bevy's (this crate's `file_watcher` feature turns it on): with `watch_for_changes_override`, a saved file is re-read and the asset replaced (a `Modified` event), 0.3 to 0.5 s after the save on macOS. Bevy's watcher cannot strip the root from a path under a symlinked directory (macOS's `/var` temp dir), so the asset root must not be one.

## Gameplay shapes (`shape.rs`)

`TreeShape::from_skeleton(skeleton, params, limb_radius)` is pure CPU and reads only the aged skeleton: the trunk is `TRUNK_PIECES` (3; 1 under 3 m) capsules up its axis whose radii follow `trunk_radius_at` scaled by how far the trunk has thickened (`base_radius / trunk_radius_at(0)`, so pipe radii and growth are honoured), the bottom one resting on the ground with at least the radius at 1 m; every other node with length gives one capsule (two, split at the middle, when gravity bends it) per piece whose mean radius reaches `limb_radius`; the crown box covers every node that bears leaves under the foliage placement (the same rule as `leaf_points_for_node`: terminal unless `AllBranches`, not over `radius_threshold`), from its start (`AllBranches`) or tip, padded by `1.5 x leaf_size x leaf_scale x (1 + leaf_size_variation)`. A test checks the box holds every leaf vertex of every preset at four ages. As a component it is opt-in: `update_tree_shapes` (chained after both rebuild systems) re-ages `TreeRender::full` for every tree in this frame's `TreeGenerated` messages and every newly added shape and writes it with `set_if_neq`; pooled trees get their variant's (`PoolVariant::shape`, or `PoolVariant::skeleton` with their own limb radius) once, from `fill_pooled_shapes`. Capsules are axes: wobble, flare and leaf geometry are not in them. `TreeMature` is written by `grow_trees` the frame a growing tree's age reaches `mature_at`.

## Felling (`felling.rs`, `mesh/cut.rs`)

No shader takes part: a fall is the tree entity's `Transform` turning about a hinge, and the cut is a CPU mesh operation.

- **Start.** `start_felling` (main chain, after `update_tree_shapes`) takes every tree with `FellTree` and removes it once the tree is cut. A `PooledTree` is first replaced by `ProceduralTree(pool.tree_params(variant))` and `TreeAge(pool.age)` (`TreePool` keeps the age it was built at), and `Growing` and `GpuGrowth` are removed; the tree waits until its CPU `TreeRender` exists. The wood is rebuilt at the tree's age (`build_aged`) and cut twice at `cut_height` with `mesh::cut::cut`: the part below becomes the `TreeStump` entity (the tree's wood material plus an end-grain cut face), the part above replaces the trunk child's mesh and gets its own cut face. Leaves are not cut. `rebuild_trees`, `grow_trees`, `sync_growth_mode` and `rebuild_gpu_trees` skip `Felled` trees, so nothing rebuilds the cut mesh.
- **The cut** (`mesh/cut.rs`). Every triangle is clipped at the plane `y = h` with interpolated attributes; the new edges are chained into loops by their quantized xz ends, only the outermost loops (not inside another) are kept, so a branch stub fully inside the trunk adds no face, and each loop is fan-filled from its centroid with radial UVs into the end-grain image (`end_grain_image`, procedural: sapwood, rings, pith, a few radial checks; built once, cached in `FloraTextures`). Faces point out of each part.
- **Hinge and rest angle.** The tree turns about the front edge of the cut (the point of the cut's outline furthest toward `direction`), as a felled trunk turns on its hinge, so it never cuts into its stump. The angle it comes to rest at is found once, at the start: `contact_points` samples the trunk and leader surface (with `trunk_wobble`) and every limb at least `max(0.04, 0.15 * trunk radius)` thick, and the rest angle is the smallest angle at which one of them reaches the ground (in 0.5° steps, then 14 bisections; capped at `REST_LIMIT`, 150°). Twigs and leaves are left out on purpose: a conifer's whorls would prop the trunk at a slant, and a real crown is crushed under the trunk. "Reaches the ground" is a clearance function of a point in the tree's space: its `y` without a `FloraGround` (flat ground at the tree's own height, exactly as before the resource existed), else its world height minus the ground's height under it, over the tree's scale. Points already below the ground before the fall (a limb low on the uphill side) are dropped from the set, or the tree would come to rest at once. With a `FloraGround` the cut is also raised by the ground's rise under the trunk's uphill side (`FloraGround::top` over `base_radius * scale`), so `cut_height` counts from the uphill ground, as a feller measures it.
- **Dynamics.** `drive_felling` steps a rigid rod about its hinge at 240 Hz: `angular acceleration = pull * sin(angle)`, `pull = 1.5 g / L * speed^2` (`L` the distance from the hinge to the farthest wood), from upright with the small push `START_SPIN`. At the rest angle the spin reverses at `RESTITUTION` (0.25); a bounce that would lift it less than `SETTLE_RISE` (0.004 rad) ends the fall (testing the bounce height, not the spin, stops small trees that bounce fast from bouncing forever).
- **Landing.** The first contact sends `TreeLanded` (with the contact point in world space) and two `TreeShake`s: one on its own crown (its leaves rustle) and one at the contact point over its crown radius (the crowns it crashed through swing). The trunk and leaf children switch to materials with `sway = 0` then, resolved at the cut through `SharedFlora`; switching at the cut would snap the crown out of its wind bend, which shows. `Felled.at_rest` turns true when it settles.
- **Bucking.** `buck_trees` (after `drive_felling`) takes trees with `BuckTree` whose `Felled.at_rest` is true. It meshes the aged trunk node alone (`trunk_mesh_with`, the same rings, wobble and bark layout as the tree's trunk; no collars, branches or leaves) and cuts it twice per log with `mesh::cut::cut`, from the felling cut (`Fall.hinge.y`) up in steps of `log_length / scale`, until a log would pass the trunk's top or its top end would be thinner than `min_radius`. Each log's end is the largest outline of its cut: its centroid, and its mean radius times `cos(π / n)` (`n` outline points), the radius to the middle of a side, so a polygonal log neither floats on its corners nor sinks by more than them. Logs are laid in world space, not where the trunk lay (it may be propped on limbs that are cleared): starting `LOG_GAP` (15 cm) past the hinge, along the horizontal direction the trunk points to at rest, each end's centre is set at the ground's height plus its radius over the ground normal's `y` (the height of a cylinder's axis lying on a slope), the far end a log's length away along the slope, and the log's rotation turns its own axis (`c1 - c0` in the tree's frame, through the tree's standing rotation) onto that line, so its bark seams keep the tree's yaw. The next log starts `LOG_GAP` past where this one actually ends (its taper tilts it a little). The tree entity is despawned with its children; the stump stays. Wood takes the trunk child's material (the still `FloraMaterial`, or a `StandardMaterial`); the end faces take the `CutFace` resource's end-grain material, which felling shares.

## Falling leaves (`falling.rs`, `shading/falling*.wesl`)

- **Drops.** `DropLeaves { tree, count }` is the one way in. `shake_leaves_loose` turns each new `TreeShake` (one not in its `Local` list of the shakes it saw last time) into drops from the standing trees whose trunk stands within its radius, `per_shake * strength * (1 - smoothstep(0, radius, d))` each (the falloff of `shake_at`); `shed_landed_leaves` turns each `TreeLanded` into `per_landing`. They run chained before `drop_leaves` in `PostUpdate`, after transform propagation and `AssetEventSystems`, so the trees are where they are this frame and a leaf mesh changed this frame is known; each runs only when it has something to do (`TreeShakes` changed, a message waits, a burst lives), so nothing runs while nothing falls.
- **Bursts.** `drop_leaves` takes the tree's level-0 leaf mesh and leaf material (`leaf_source`): a CPU tree's foliage child with `Paint::Flora`, or the pool variant's `leaves[0]` with `PoolMaterials::Flora` (GPU-grown and plain-material trees drop none). `leaf_cards` splits the mesh into cards: the foliage mesher writes each leaf's vertices and triangles as runs and gives the leaf one phase (`sway.w`), so a run of one phase is a card, with its moment in the season and whether it is a flower (vertex alpha 0); flowers and, on a deciduous tree, cards whose moment is under the season's leaf fall are left out. Card lists are kept per mesh (`FallCaches`, cleared past 64 meshes), dropped on its `Modified`, `Removed` or `Unused` event and read again when its vertex or index count is not the one read. A partial shuffle seeded by the tree and the time picks the cards, `max_leaves` minus the leaves of the live `LeafBurst`s caps them, and a `BurstBuilder` copies them into one mesh in world orientation and scale relative to its origin (the tree's position; a felled tree's leaves fall from where it lies) with three attributes after the leaf's sway (11): `FallAnchor` (12: the card's centre, the render time it lets go), `FallPath` (13: the ground under where it lands, a seed, its rest, its speed) and `FallFace` (14: the card's face normal). The ground is `FloraGround` where the wind will have carried it (two passes of `landing_time` and the drift), else the height the tree, or a felled tree's stump, stands at; cards less than 5 cm above it stay. The burst gets an explicit `GlobalTransform` (it is spawned after propagation) and an `Aabb` around every card where it hangs and where it lands, plus the swing; `expire_bursts` despawns it at `gone_at`, its latest `release + landing + rest + shrink`.
- **Material.** A falling-leaf material is a `FloraMaterial`: a copy of the tree's leaf material with `FloraExtension::falling` set, one per leaf material, made again when that material changes. `FloraKey::falling` makes `specialize_flora` put `falling.wesl` in as the vertex stage (`falling_prepass.wesl` in a prepass or shadow pipeline: `PREPASS_PIPELINE` among the vertex defs; handles kept by `falling::set_shaders` when the shaders are embedded) and add the three attributes; everything else is the leaf material's own (`fragment.wesl`: scan, tint, veins, season, snow, toon, and the see-through fade). A material type of its own cost a 100 000-tree pooled forest 0.08 ms a frame with nothing falling (Bevy's render systems run per material type over every visible mesh), so there is none.
- **Shaders.** `falling.wesl` (main pass) and `falling_prepass.wesl` (prepasses and shadows) call `falling_leaf` in `falling_common.wesl`. Before its release time a card is where it grew. Then its centre falls `v (t - τ (1 - e^(-t/τ)))` (τ 0.35 s, `v` 0.9-1.5 m/s) until `landing_time` (Newton's method, mirrored in Rust), drifts `wind.xy * strength * 1.5` m/s and swings up to 0.4 m along a heading of its own; the card tumbles about a seeded axis (Rodrigues), and over the last 0.3 s turns its face (`FallFace`) up or down and folds what stands across it (crossed planes, stars, clusters) to 12% of its height, normals with it. It lies 3 cm above the ground for its rest, shrinks to its centre over a second and is clipped. Its crown visibility (`uv1.y`) opens to 1 over its first second. The render clock wraps hourly: an age below minus half an hour is taken as wrapped.
- **Season.** `shed_with_season` (after `drop_leaves`, only while the season changed or leaves it passed are still to go: `season_sheds` on `SeasonShed`) keeps the leaf fall shed up to, the render time it was, and last frame's leaf fall. A fall that goes down (spring) or rises more than `SEASON_JUMP` (0.1) in one frame (a season set) moves the mark without dropping; otherwise every `SHED_EVERY` (0.1 s) the deciduous trees (CPU or pooled) within `autumn_distance` of a `Camera3d` give the cards whose moment lies in `[from, to)`, found by binary search in `MeshCards::by_moment` (the leaf cards sorted by moment, built with the card list), each letting go `((moment - from) / (to - from) - 1) * elapsed` seconds before now, the time the fall passed it assuming it rose evenly. `BurstBuilder` takes cards of several trees around the first one's position, so the season makes one burst per leaf material per tick (`LeafBurst::tree` `None`), all under `max_leaves`. The cards and the falling materials live in the `FallCaches` resource, which `drop_leaves` shares; `forget_changed_meshes` drops a mesh's cards on its asset events.

## Fire (`fire.rs`, `shading/common.wesl`)

- **The grid.** `FloraFire` is a square grid of `FireCell { char, heat }` (0..1 each) over the world, `side` cells a side (default 128, at most 512), with a per-cell fuel and the time each cell was lit on the fire's own clock. It is a resource the game inserts; nothing exists before that. `ignite`, `extinguish` and `scorch` set cells directly, so `char_at` / `heat_at` (bilinear between cell centres, zero off the grid: the shader's `fire_at`) read them back at once.
- **The spread.** `burn` (in `PostUpdate` after transform propagation, only while `fire_burns`) steps the fire in fixed `STEP`s of 0.1 s. Fuel is laid down from the trunks of every `ProceduralTree` and `PooledTree` (full within half of `FireSpread::reach`, fading to the ground's `ground` at `reach`), again whenever the number of trees (counted by archetype, nothing per tree), the reach or the ground changed. A lit cell's heat rises over the kindling (`KINDLE` 2 s or a quarter of `burn_time`) and its char by `heat * STEP / burn_time`; once charred through it cools over `cool_time`, and a cell with no fuel goes out in a second. The front is an arrival time, not dice: a burning cell lights each of its eight neighbours `distance / (speed * fuel * exp(2 * wind * along))` after it was lit itself, times a per-cell `jitter` of 0.75..1.25 from the seed, with the time kept exact between steps, so the front moves at `speed` whatever the cells' size (a stochastic per-step chance did not: finer cells meant more parallel paths and a 3x faster front) and the same fire burns the same way twice.
- **The buffer.** The grid rides in the shared buffer after the weather (`FIRE_SLOT` = `WIND_VECTORS` = 25): a header (the grid's corner x, z, 1 / cell size, cells per side, 0 while no cell holds anything, where the shaders stop), the rectangle around the touched cells grown by a cell (trees outside it read no cells), then the cells two to a `vec4`. `sync_wind` writes only its own 25 slots. `sync_fire` (when the fire changed or went) writes the grid in place, or rebuilds the buffer at its new length (the fire inserted, resized or removed) and sets `FireRebind`, so that `rebind_fire` touches every flora, growth and impostor material the next frame: Bevy prepares materials in no fixed order with the buffers they bind, so a material re-prepared the same frame could keep the old buffer. The whole chain runs before `AssetEventSystems`, so its asset changes reach the render world the same frame. While the fire burns the grid is written ten times a second (8 bytes a cell), and a wind that changes every frame now uploads the grid with it.
- **The shaders.** `arm_fire` sets `fire` on every flora, growth and impostor material while a `FloraFire` exists (like the see-through fade): `FloraKey::fire` adds `FLORA_FIRE` to the vertex and fragment defs, so without a fire no pipeline carries any of it. Leaves (vertex stages, main pass and prepasses, CPU and growth): `fire_at` at the tree root; a leaf whose season moment is below `leaf_burn_front(char)` = `char * 1.45 - 0.45` is put outside the clip volume like a fallen leaf, so its shadow goes too, and the main pass hands the fragment `1 + scorch + glow` in the vertex alpha (one value for all of a leaf's vertices, as they share the moment; flowers keep 0): scorched over `LEAF_SCORCH` (0.35) of moments ahead of the front, glowing (`heat`) within `LEAF_GLOW` (0.08) of it. Wood: the vertex stage puts its height in trunk heights (`local.y * flora.tree.x`) in `uv_b.y` (1 in the mesh, unused by wood otherwise); the fragment reads `fire_at` at the pixel, chars `wood_char(char, h)` of it (the trunk by char 0.4, the crown's wood by 1) to `CHARCOAL` split by a Voronoi seam pattern on the bark's UVs (`char_cracks`), duller, and adds `EMBER` light (900 nits times `heat²`, flickering) in the seams after lighting (after the toon rim). The impostor's fragment does the same per pixel: leafy texels burn away in hashed patches against the front and scorch, wood darkens to charcoal, a tenth of the embers' light. `falling.rs` drops none of a tree's burnt leaves (`burnt_front`, the Rust `leaf_burn_front`).

## Ground (`ground.rs`)

`FloraGround` wraps `Arc<dyn Fn(Vec2) -> f32 + Send + Sync>`: the height of the ground at world (x, z). It derives `Resource` and is otherwise pure CPU. The crate never draws or meshes the ground; it only stands and fells trees on it, so it must be the function the game's terrain is built from (or a lookup into its heightmap).

- **Footing.** `footing(at, radius)` is the minimum of the heights at `at` and at 16 points on a circle of `radius` around it: the lowest ground under the trunk's base, so standing the tree's origin there leaves no gap under the downhill side (the trunk mesh starts with a flat bottom cap at y = 0 that would show). `TreeParams::base_radius()` is the mature trunk's radius at the ground: `radius * flare`, times `1 + root_flare_spread` when there are root ridges (the bottom ring carries the full ridge bulge). A growing tree is narrower, so the footing sinks it a little more than it needs. Sampling a circle, not the plane through the slope, keeps it right on a ridge or in a hollow; a feature narrower than the trunk between two samples can still leave a sliver.
- **Slope.** `normal(at)` is the cross product of central differences 25 cm to either side; `slope(at)` its angle from the vertical. Neither is used by the plugin: they are for the caller's density function.
- **Placement is the caller's.** Nothing in the plugin moves a tree onto the ground: `ScatterPoint::transform_on` and `footing` compute a `Transform` once, and a tree stays where it was put. Felling is the only system that reads the resource.

## Forests: levels of detail, pools, impostors (`lod.rs`, `pool.rs`, `impostor/`)

A forest cannot give every tree its own meshes (a few thousand oaks of 20 000 triangles saturate the GPU). The forest path builds a few variants once, shares their meshes and materials between every tree placed from them, and draws less of a tree the further away it is. It never touches the `GpuGrowth` path; pooled trees are static. The CPU path borrows its levels: `TreeLod` draws one `ProceduralTree` at them (below).

- **Levels of detail are built from the skeleton** (`lod::build_lods`), not by simplifying a mesh, so the wood comes from the same meshers as level 0 and no dependency is needed. Level 0 is `build_aged` unchanged. A coarse level `prune`s the aged skeleton (every non-trunk node thinner than a radius is dropped with its descendants, parent indices remapped, `child_count` kept so terminal-branch decisions do not move, and the nodes whose tips reach furthest along each axis are kept with their ancestors so the silhouette does not shrink), meshes it with `skeleton_mesh_coarse` (a side cap, a ring cap that subsamples the full-resolution axis so every ring is still on the true curve, no collars, a flat disc instead of a root dome, no smoothing, no adaptive sides) on a trunk with half the height segments per level (a trunk with root flares keeps its sides, or the lobes flatten), and takes every `n`th leaf point of the *full* aged tree plus the leaves that reach furthest where the rest fall short, each scaled by up to `sqrt(n)` as far as the room to level 0's bounding box allows. `lod::tests` hold every preset to: triangles strictly fall from level to level; level 2 at most 15% of level 0 (or 500 triangles for the palm, bonsai and dead tree); the bounding box within 10% per axis; valid indices.
- **Instancing is Bevy's.** `TreePool` owns the two materials every variant shares, wood and leaves (`PoolMaterials`: `FloraMaterial`s where the plugin registered the renderer's side, `StandardMaterial`s otherwise; made lazily by `make_pool_materials`, so `build_pool` takes no materials) and, per variant, three wood meshes, up to three leaf meshes and an `Aabb` per level (so `calculate_bounds` does not walk 100 000 copies of a mesh). `spawn_pooled_trees` gives each `PooledTree` a child per mesh and `VisibilityRange` band (`pool::band`: each end a 2 m margin around the distance, the next level's start equal to this level's end, the last end infinite), at most 4096 trees a frame. Entities that share a `Mesh3d` and a `MeshMaterial3d` are batched into one draw by the renderer; `pool::tests` check that 100 trees of two variants draw 12 meshes and 2 materials.
- **Levels on one tree** (`TreeLod`, `plugin::sync_levels`). `RebuildJob` carries the component's `LodSettings`, and `build_job` builds levels 1 and 2 with `lod::coarse_levels` from the level 0 it has just aged (the same `Source::coarse` as `build_lods`, so a tree's levels are exactly a pool's), merging the flowers into the coarse leaves as a pool does. `TreeRender::lod` keeps the settings and a wood and a leaf child per coarse level (`TreeLodMesh(level)`); `sync_levels` gives them `pool::band` ranges (level 2 to infinity: no impostor) and the full tree's trunk, leaf and flower children `band(0, lod1)`, with `insert_if_new` so a level-0 child spawned later gets its band without rewriting the others every frame, and strips both when the component goes. Staleness is a state test (`render.lod`'s settings against the component), so a change made while a background build runs is caught up after it. `GpuGrowth` trees ignore it (`TreeRender::lod` stays `None`), `sync_growth_mode` despawns the coarse children with the others, and `start_felling` despawns them and removes the level-0 bands at the cut, so a felled tree falls in full.
- **Impostors** (`impostor/`): `ImpostorFrame::new(height, radius)` frames a tree's bounding cylinder as the orthographic camera sees it 10° above the horizon, and gives the upright quad that covers the picture. `bake.rs` renders each variant's level 1 meshes, eight copies each turned an eighth of a turn about Y, twice (once per picture), one orthographic camera per copy and picture, each into its own 256x256 viewport of a 2048x512 target. Row 0 holds the colours (`ImpostorBakeMaterial::colour`, render layer 31), row 1 the surface (`ImpostorBakeMaterial::surface`, layer 30): the normal in the tree's own frame, octahedral-encoded, in rg, the crown visibility (`uv1.y`, or 1 for wood) in b and the translucency (1 for leaves, 0 for wood) in a. Nothing is lit; the scene has no light. Cameras, copies and layers are at `BAKE_ORIGIN`, far from the game's world. The clear is transparent both in the main pass and in the output (the upscaling pass clears the target with the camera's `output_mode`, which would otherwise paint the app's sky colour into the atlas), and every camera writes with `blend_state: Some(BlendState::REPLACE)`: `None` means "replace for the first camera of the target, alpha blending for the rest", which drops every surface texel written with alpha 0 (the wood, D-055). No MSAA, no tonemapping and no debanding. At most `MAX_CONCURRENT_BAKES` (4) variants bake at once: every camera costs Metal a few command buffers a frame, and more than 4096 in flight lose the device. A `Screenshot` reads the target back; a capture with fewer opaque colour pixels than 1% of the atlas (the first frames of a new scene render nothing while pipelines compile) is discarded and retaken. The target is read through an sRGB view, so the bytes are sRGB-encoded: `linearize_surface` decodes the surface row (alpha was never encoded), `atlas_image` / `surface_image` build six mip levels in linear light weighted by the colour row's coverage (`surface_mip_chain` averages the encoded normals and the numbers, and keeps a texel with nothing under it at zero), and the pool gets an `ImpostorAtlas` (colour image, surface image, view count, quad size, centre height, material, quad mesh). `impostor.wesl` builds the quad in the vertex stage: the tree's position and scale from `world_from_local`, the quad's right vector along the camera's own right (`up × toward-camera`, ground plane only), the view from the camera's yaw in the tree's local frame, the same `wind_bend` as the meshes, and Bevy's `visibility_range_dither` in the fragment stage. The fragment stage decodes the normal, turns it from the tree's frame into the world with the tree's yaw, and lights it with `apply_pbr_lighting` (the scene's own directional lights, shadows where the cascades reach, ambient light, fog) with the crown visibility as diffuse occlusion and as the directional shadow factor, plus `transmitted_light` scaled by the baked translucency. The material has no prepass or shadow pipeline (`enable_prepass` and `enable_shadows` are `false`), culls nothing, and is alpha-masked. The quad's `Aabb` is set by hand and reaches `size.x / 2` in every horizontal direction, because the quad turns about Y while the culling box turns with the tree.
- Plugin order: `impostor::build` registers `ImpostorMaterial` and the `ImpostorSupport` marker only when the renderer's plugins and `PbrPlugin` are already there, like `gpu::build`; the bake systems run only while the marker exists, and a tree waits for its variant's bake to settle (success or give-up) before it gets children, so a first spawn is a few frames late and later spawns are not.
- Measured numbers, and why windowed numbers were not used, are in `CHANGELOG.md` (0.5.0 and 0.6.0, 'Measured'). Shader compile errors show only in a real render (`cargo run --release --example forest -- --mode pool --trees 400 --screenshot out.png`), never in a headless test.

## Planting forests (`scatter.rs`)

Pure CPU and deterministic per seed, no ECS (`ScatterPoint::transform` builds a plain `Transform`; `transform_on` takes its height from a `FloraGround`, see below).

- **Trunks** are Bridson's fast Poisson-disc sampling (SIGGRAPH 2007 sketch): a grid of `spacing / √2` cells holding at most one point each, an active list, 30 candidates uniform in the annulus from one to two spacings around a random active point, a candidate kept when no point in the 5x5 cells around it is closer than `spacing`, a point retired when none of its candidates fits. Every spot of the area ends within about two spacings of a trunk, and the density is about 0.6 to 0.7 trunks per `spacing²` (a test holds both, and the spacing). The grid panics past `u32::MAX` cells rather than allocate without bound.
- **Per tree** (yaw, scale, variant, the keep draw and the species draw) comes from the tree's own `SeededRng` stream (`TREE_STREAMS + index`) in a fixed order, so `points_where` only removes trees: a tree it keeps is equal to the one `points` returns.
- **Groves**: with `grove_size > 0` and two or more species, grove centres are a second Poisson disc, `grove_size` apart over the area grown by one grove on every side, each given a species by the weights; a tree takes the species of the centre nearest its position after a smooth warp (value noise at half the grove size, displacing by up to 0.35 grove sizes), so borders bend instead of being the straight edges of nearest-centre cells; a per-tree random displacement would have dithered the species across the whole border band instead. `Groves::species_at` widens its search square until nothing outside it can be nearer than the best centre found.