bevy_flora 0.47.0

Parametric, stylized procedural trees and shrubs for Bevy that grow over time: 20 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.

Under `AllBranches` a leaf has one place on its branch, whatever the age
(`add_distributed_leaves`, `docs/research/leaf-places.md`). A grown branch has `N` leaf
slots (`distributed_leaf_count` of `Node::full_length`, the exact grown length: `length /
progress` is it only to a rounding error): slot `j` sits at `0.3 + 0.7 t^2` of the branch
(`0.1 + 0.9 t` on a strand), `t = (j + u_j) / N`, with the draws of its own (`u_j`, size,
direction, turn, tint, hue; `draw_per_leaf`). A branch `n = distributed_leaf_count(length)`
leaves long shows the `n` slots that appear first (`leaf_ranks`: the tip, then bisecting, so
the leaves are spread over the whole branch at every `n`). A branch that gains a leaf moves
none of the others, and the CPU tree and `GpuGrowth` put every leaf in the same place: the GPU
tags a leaf with its rank (`gpu/mesh.rs`), `state.rs` writes `n`, and the shader shows the
leaves whose rank is under it, at `start + length * c`. `gpu::tests` compares every leaf's
vertices with the CPU mesher's at seven ages (`grown_leaves_and_flowers_match_the_cpu_meshers`).
A grown branch shows every slot, in slot order, so a mature tree is what it always was.

## 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`.

## Hull crowns and prism trunks (`crown_hull.rs`)

`TreeParams::crown` (`CrownStyle`) and `TreeParams::trunk_style` (`TrunkStyle`) draw a low-poly conifer: both default to
`Leaves` and `Round`, which every preset keeps, and change nothing else of a tree.

- **The hull** (`CrownStyle::Hull { tiers, sides, lift, jitter, trunk_only }`) replaces the leaf cards; fruit and flowers are drawn as
  ever. `build_foliage` takes a `Crown`: with a hull it skips each point's cards, colour and sway, keeps the rest (fruit,
  flowers, `shade_crown` for those) and appends the hull's mesh last. The pure module `crown_hull.rs` builds the hull from
  the cloud `foliage::hull_cloud` makes of every leaf's position and tip (`position + up * size`): the cloud is cut into
  `tiers` bands from its 5th-percentile height (lifted to `lift` of the top) to its top; a band's ring has the 90th
  percentile of its points' distance from the trunk (never wider than the band below, at least 0.06 of the crown's height,
  0.85 of the ring below with fewer than three points) times 1.08, its points knocked about by `jitter` (radius by up to
  16%, height by up to 12% of a band, each tier's turn) from `SeededRng::from_parts(seed ^ HULL_SALT, tier * 64 + side)`,
  so a hull depends on age only through the cloud. Each ring is a cone: `sides` side faces to an apex 0.55 of a band above
  the band's top (the top tier's apex is the crown's top) and `sides` underside faces to a point 0.35 of a band above the
  ring. No hull under 8 cloud points or 0.5 m.
- **`trunk_only`** draws the wood as the trunk and its leader (`skeleton_mesh_inner` skips every other node); `pipeline::trunk_top` is the top of that chain, so the hull's apex covers the leader's tip.
- **Its vertices** are flat: three to a facet, the face's normal, uv0 (0.5, 0.5), uv1 (1, 1) (snow's shelter needs y = 1),
  the leaf colour formula (`leaf_colour`) from draws of the facet's own (`seed ^ HULL_FACET_SALT`, tier, side, under) and a
  sway of `(0, 0, 0, phase)` with one phase for the side face and the underside face on the same two ring points
  (`seed ^ HULL_PHASE_SALT`). Colour and phase are functions of the seed, the tier and the side alone, so they never change
  with age; the season's moment, fire and `falling.rs` read the phase as they read a leaf's.
- **Levels of detail** (`lod.rs`, `Source::coarse_leaves`): a coarse level builds its hull from every leaf, not only the
  ones it keeps (those carry the fruit and flowers), with `LodSettings::hull_detail` (default `[1.0, 0.7, 0.5]`) scaling the
  stack (`tiers' = max(2, round(tiers * d))`, `sides' = max(3, ...)`), and clamps the corners into level 0's bounds, so the
  culling box of level 0 still holds it. Pools, impostors and `TreeLod` take hulls through those.
- **The prism** (`TrunkStyle::Prism { sides }`): `skeleton_mesh_inner` gives the trunk `sides`, caps branch tubes and collars
  at `sides`, forces no smoothing and ends with `MeshData::flat_shaded` (per-triangle vertices, face normals, the other
  attributes copied); `buck_trees` meshes its trunk the same way. `mesh::cut` joins cut segments by position, so a
  flat-shaded trunk is notched, felled and bucked like a round one; `log_end` takes the corners of the cut only, as a flat
  trunk's cut has a point in the middle of each side too.
- **Shading**: `FloraUniform::new` sets `tree.y = 1` for a hull and zeroes `leaf.x`, `leaf.z`, `leaf.w` (no light through,
  no veins, no paler underside); `wood()` clears it. Under `FLORA_LEAF` and `flora.tree.y > 0.5`, `fragment.wesl` replaces
  `leaf_surface` with `hull_surface` (roughness 0.85, the crown visibility of `uv1.y`) and skips `leaf_transmission`; the
  impostor skips the gloss gate; `TextureLibrary::leaves` treats a hull as unscanned and unveined; the plain `StandardMaterial`
  fallback paints it with `material` (no diffuse transmission). Snow is the leaf's: it lies on the facets that face up.
- **GPU growth**: the growth shader reproduces neither the hull (rebuilt from the cloud at every age) nor flat shading, so
  `gpu::grows_on_gpu(has, support, params)` (the one place the five checks of `plugin.rs` and `gpu/render.rs` ask) is false
  for a tree that is not `TreeParams::gpu_growable` and warns once; `sync_growth_mode` also reacts to
  `Changed<ProceduralTree>`, so a change of parameters moves a tree between the paths.
- **Falling leaves** from a hull are chips, not facets: `falling::hull_chips` makes `CHIPS` (96) isosceles triangles 0.12 m
  long in the plane of facets picked by area, in the facet's colour, each a card of one phase (its facet's, nudged by its
  index), so a chip falls while its facet is on the tree.

## 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`).

**Strands** (`BranchParams::strands`, `skeleton::hang_strands`; the weeping habit, off in every preset but the
willow). After the skeleton is seated, every branch gets `density * length * (1 - start)` slender twigs of
`NodeKind::Strand`, one level below it, hung from its outer part (`point_at(t)` on its bent axis, `t` from
`start` to 1): straight (no gravity bend, so two rings of wood; its branch's twist is not drawn on it), plumb
within `spread`, as long as `length` times the trunk height or down to `clearance` above the floor, a rope's
radius (`min_twig_radius`, never over the parent's). Strands come after every other node (a parent still
precedes its children) and draw from streams of their own (`STRAND_SALT`, keyed by the branch id), so a tree
without them is untouched and one with them keeps every other branch where it was. A strand grows as a
sub-branch does (born when its branch reaches `attach_t`), carries `AllBranches` leaves along its whole length
rather than bunched at its base, and its leaf `i` keeps its place on the grown strand at every age (the first
`distributed_leaf_count` leaves show, as `GpuGrowth` shows them). The pipe model leaves a strand its own
radius, and its floor check keeps wood `pipe_radius_min` above the floor when it is on.

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.
- **One order for every frame.** Every pair of the plugin's systems that touch the same data has an order between them, so a frame runs the same however the executor schedules it, and a system added anywhere (by the game or a release) does not change what a frame does: the pool systems (pooled trees, deadfall, recolouring, shapes) run after the procedural chain (both make textures and write `TreeShape`s), `drop_fallen_fruit` after `buck_trees` and before the pool systems (all move `Transform`s), `index_trees` after `AssetEventSystems` (it reads `AssetEvent<TreePool>`, so a pool added this frame is indexed this frame). `plugin::tests` builds `PreUpdate`, `Update` and `PostUpdate` with ambiguity detection as an error, so an unordered pair fails the tests; run it with `--features bevy/debug` to see the systems' names.
- `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` = 189 `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 four, the foliage pushers' 33 and the gliding looks' 129; the fire's grid follows while one exists), 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 whose lean is `MAIN_BEND * sway * |rel|`, linear in the vertex's own distance from the root (`rel = world - origin`, in world units, so it already carries the tree's `Transform` scale and `FloraUnits`: `wind_bend` takes no scale, and a tree leans the same share of its drawn height at any scale), so every plant leans a fixed share of its own height and the species' `sway` (0.9-1.6 in the presets) is a multiplier on it; the bend keeps every vertex's distance from the root (Crysis' rescale). Per-leaf flutter uses 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. `research/wind.md` (under `docs/`) has the sources and D-073 (fixed, #90: the lean once multiplied the scale in a second time).
- **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 four vectors in the shared buffer (`SEE_THROUGH_SLOT`, 23 to 26): the camera's position and the clear radius; the target and 1, or 2 when the camera's `Projection` is orthographic; the camera's forward direction and the share kept (`keep`); the feather's start as a share of the radius (`inner`), where the cut ends before the target (`in_front`, -1 for the last tenth of the way) and the floor (`-1e30` for none). `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 above the floor: under a perspective camera within a cone a third as wide at the camera, under an orthographic one within a cylinder through the target along the forward direction (its distance before the target is its depth along the view); fully inside `inner` of the radius and through a 4x4 ordered dither out to it, `1 - keep` of it, closing before the target over the last tenth of the way (`smoothstep(0, way / 10, before)`, the old `1 - smoothstep(0.9, 1, t)` exactly) or from `in_front` over a tenth of the radius. 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 wood (`AllBranches`: a bent branch's axis, which arches past both of its ends, so the box follows the axis samples and not just the two ends) or its 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. A tree felled with `FellTree::lying` (restored from a save) skips all of it: `drive_felling` sets its angle to the rest angle the cut found, gives it its still materials and marks it at rest the frame it is cut, with no message and no shake. The fall always ends exactly at that angle (each contact clamps it there and the bounces settle on it), so the restored tree lies as the fallen one did.
- **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.
- **Obstacles** (`index.rs`). A `TreeStump` carries its capsule in its own space, from its foot (y = 0) to the middle of the cut, with the radius of the cut's largest outline (`log_end`, as a log's ends): felled stumps from the felling cut, pooled ones from `Deadfall::outline` and `cut_y`. `FloraIndex` keeps the lying wood in a grid of its own (`obstacle_cells`, the same 8 m cells; an entity's capsules together, under the cells their xz bounds reach), apart from the standing trunks: `index_obstacles` (`PostUpdate`, after `index_trees`) re-reads a stump or a `TreeLog` when it is new or its `GlobalTransform` changed, a `Felled` tree with a `TreeShape` when it, its shape or its place changed (nothing while it falls; once `at_rest`, every capsule of its shape clipped to the wood above `Felled::cut_y`, the hinge's height, with `WoodCapsule::above`), and a pooled deadfall's lying shape child (`LyingWood { cut_y }`) the same way; each kind's `RemovedComponents` takes an entity's wood out. `obstacles_within` finds the cells, takes an entity once (in the first of its cells the query covers) and keeps the capsules whose ground distance (`Obstacle::distance_to`, point to xz segment minus the radius) is within the radius.
- **Index order and undergrowth** (`index.rs`). Each cell's list of entities, trees and lying wood alike, is kept sorted by where they stand (`in_order`: x, then z, then the entity only for two at one spot; `partition_point` on insert), so `within` and `obstacles_within`, which walk the cells row by row, answer in an order set by the forest and not by the order of spawning or indexing, and `nearest` (the first of the nearest in that walk) breaks ties the same way; `iter` still follows the hash map. `IndexedTree::undergrowth` is `TreeParams::undergrowth` of the tree's own parameters or its pool's.
- **Condition** (`condition.rs`, `index.rs`). `TreeCondition { Healthy, Sick, Dying }` is a component on the tree's entity (`ProceduralTree` or `PooledTree`), absent for a healthy tree, so a pooled forest carries none. `IndexedTree::condition` mirrors it: `index_trees` reads the component whenever it inserts a tree (a new, moved, rebuilt, grown or reloaded one, so a condition set before a pool loads applies when the tree is indexed) and, between those, patches the entry in place on `Changed<TreeCondition>` and `RemovedComponents<TreeCondition>` (removals first, so a remove and an insert in one frame end at the insert; `set_condition` ignores an entity the index does not hold, which covers a felled or despawned tree, whose removal record also appears there). Nothing in the crate writes it: the condition is the game's, the index only reflects it, and a pool recycle or felling never clears it. **The look.** The class is drawn by the shaders from per-instance data, the `MeshTag` of every mesh the tree is drawn with: bits 8 and 9, above the glide tag of a pooled look (the low byte, `condition::TAG_SHIFT` and `common.wesl`'s `CONDITION_SHIFT`; `glide_tint` masks it), so instances of one mesh and material still batch whatever their conditions and no material is made for a condition (a test holds the material count). `spawn_pooled_trees` and `sync_pooled_looks` write it with the look's tag (`condition::tag_for`), and `sync_condition_tags` (PostUpdate) writes it on every other mesh, on `Changed<TreeCondition>`, `Changed<Children>` and removal; its queries only touch trees that have the component. The vertex stages and the impostor read a mesh's tag only while the glide header of the shared buffer says so (`tags_read` in `common.wesl`: lane x is 1 while a look glides, lane y while any tree has a `TreeCondition`, kept by `glide::sync_condition_flag`), so a forest of healthy trees that glides nowhere pays no per-instance fetch. The leaf vertex stage of the main pass, the prepass, and the GPU-growth twins (`vertex.wesl`, `prepass.wesl`, `growth.wesl`, `growth_prepass.wesl`) then use it: `leaf_defoliated` moves the vertices of the leaves the class has lost (a per-leaf draw from the leaf's phase, `leaf_decline_moment`, below `TreeCondition::defoliation`) out of the clip volume, in the shadow maps too, and `leaf_withering` / `leaf_withered_tint` move the colour multiplier the season gives the leaf (1 in summer, the autumn ratio once turned) toward `FloraUniform::withered` (the ratio of the dry leaf colour, `condition::WITHERED_LEAF`, to the species' leaf colour, as `autumn` is of the autumn colour) by the class's `withering` and the leaf's own draw: a mix, not a product, because both ratios are large and their product turned an autumn maple white. Flowers and fruit (vertex alpha below 0.5) are not touched, so what `TreeFruit` counts is what is drawn; a hull crown keeps its facets. The impostor does the same per patch of baked texels (`impostor.wesl`: the colour moves on the leaf core, the coverage thins on the leaf edge, and where a pixel spans several patches the share replaces the patch, as for the season). `falling.rs` mirrors the draw on the CPU (`condition::Decline`, with `Card::phase`): a leaf the shaders hide is not chosen to fall, and the burst carries how far each leaf that falls had withered in the fourth lane of its `ATTRIBUTE_FALL_FACE`, which `falling.wesl` turns into the same mix. The class has no effect on a device with plain materials (`FloraMaterialSupport` absent), as a glide has none.

## 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 picks the cards, seeded by where the tree stands (`place_seed`, a hash of its world position) and the time, with a stream per drop of the same tree in the frame: never by its `Entity`, whose number follows the order of spawning, so a streamed chunk, a restored save or one more system anywhere would drop other leaves (`falling::tests` hold the same oak, spawned after other entities, to the same leaves). `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`, taken nearest a camera first (ties by x, then z, so the order is the world's and not the order of spawning, and it is the farther trees that keep their leaves when `max_leaves` runs out), 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; each tree draws from a `SeededRng` of its own (`place_seed` and the time). `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.

## Fruit (`foliage.rs`, `harvest.rs`)

- **The mesh.** A fruit is in the leaf mesh, right after its leaf's card, so every path that carries leaves carries it: `GpuGrowth` tags it with its leaf's tag (the per-point vertex ranges of `firsts[0]`), the coarse levels keep the fruit of the leaves they keep, pools and felled trees take it with the leaves. Its vertex alpha is 0.25 (`FRUIT_ALPHA`; a flower's 0, a leaf's 1), its colour the fruit's over the leaf colour (the material multiplies the vertex colour by the leaf colour) with the underside darker, its `sway.w` its leaf's phase plus a half: its own moment, and a phase apart from its leaf's, which is what `falling.rs` tells cards apart by. `shade_crown` leaves its normals round. Which leaves bear fruit is a hash of the leaf's tint and hue draws, which never change as the tree grows.
- **The shaders.** `is_fruit` (alpha 0.125..0.5); `leaf_hidden` hides a fruit while the blossom is out, once the leaf fall passes two thirds of its moment, and once picked (alpha above 0.3125); `leaf_season_tint` leaves its colour; `leaf_fire_alpha` keeps its alpha; the fragment stage skips the leaf surface, sets its roughness to 0.35 and lets no light through it. The impostor bake drops it with the flowers.
- **Picking.** `track_fruit` (on `TreeGenerated`) reads a CPU-drawn fruiting tree's fruit from its level-0 leaf mesh (runs of fruit vertices with one phase; the phase's bits are the key) into `TreeFruit` and clears what it marked, as the meshes are new; `mark_picks` (on `Changed<TreeFruit>`) sets the alpha of picked fruit to 0.375 in every leaf mesh of the tree (`TreeRender::leaf_meshes`, levels of detail included) and back to 0.25 for fruit no longer picked. `regrow_in_spring` clears the picks while the blossom is out.
- **Shakes.** `shake_fruit_loose` turns each new `TreeShake` into a share of each reached tree's hanging fruit (`per_shake * strength`, the leaves' falloff), each by a draw from its key and the shake's start: picked, and spawned as a `FallenFruit` with a `Transform` and `GlobalTransform` (no frame at the origin), a shared sphere and a `StandardMaterial` per colour. `drop_fallen_fruit` (Update) moves it under gravity, bounces it on `FloraGround` (or the tree's height) keeping 0.3 of its speed down, stops it below 0.8 m/s and despawns it `rest` seconds later.

## 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 fire keeps the rectangle of cells holding any char or heat (`touched`): `ignite`, `scorch` and the cells a step lights grow it, and `settle` refits it and recounts the burning cells, scanning only inside it, after every step, `extinguish` and `restore`. A step walks only that rectangle, the shader header's bounds come from it, `burning` reads only inside it and `touched_bounds` hands it out.
- **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, the see-through fade, the pushers and the gliding looks (`FIRE_SLOT` = `WIND_VECTORS` = 189): 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 189 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`).
- **Burnt trees.** `report_burnt` (after `burn`, when the fire resource changed: it stepped or the game set its cells) reads `char_at` where each tree without `Burnt` stands (`ProceduralTree` or `PooledTree`) and, past `BURNT` (0.95), inserts `Burnt` and sends `TreeBurnt`. The marker is what keeps it to once, and what a save restores so loading reports nothing.
- **Reading and saving.** `burning` walks the touched rectangle and hands out each cell with heat. `hottest(square, n)` sums those cells into a `BTreeMap` keyed by their world square (`floor(centre / square)`, the square at least a cell), the heat and the heat-weighted centre, and sorts the squares by heat with a stable sort, so of two as hot the one first in the map's (x, then z) order comes first; `share` is the heat over `(square / cell)²`. `snapshot` lists `(index, char, heat)` of every touched cell holding either (`FireSnapshot`, serde with the feature); `restore_snapshot` refuses a snapshot of another `side`, clears the cells and their lit times, sets the listed ones (lit now where they hold heat, as `restore` does), grows the touched rectangle by each and settles.

## Foliage pushers (`push.rs`, `shading/common.wesl`)

- **The slots.** `FoliagePusher { radius, height, strength }` is a component; its entity's translation is the middle of the body. `sync_pushers` (in `PostUpdate` after transform propagation, every frame) keeps each pusher's positions over the last half second (`Trails`, by entity, every frame) and writes the `MAX_PUSHERS` (4) nearest the first `Camera3d` into the shared buffer at `PUSH_SLOT` (27, after the see-through fade): a header with the number of samples, then two vectors per sample, (x, y, z, radius) and (half height, strength times weight, 0, 0), for the body now and 0.15, 0.3 and 0.45 s ago at weights 1, 0.6, 0.3 and 0.1. It writes in place only when the samples changed (a still body costs nothing), and `sync_wind` keeps them (`PushSlots`) when it rewrites the weather. The 33 vectors sit before the fire's header, so `FIRE_SLOT` is 60.
- **The shaders.** `arm_push` sets `push` on every flora and growth material while any entity has a `FoliagePusher` (`FloraKey::push` adds `FLORA_PUSH` to the vertex defs; impostors are too far to need it). The four mesh vertex stages (main and prepass, CPU and growth) add `push_offset(world, origin, flex)` to the bent world position: for every sample within 1.5 radii across the ground and within half a radius past its ends, a monotonic, continuous map of the distance from its axis (the inner `PUSH_CORE`, 0.6, of the radius spreads to 1.05 radii, the rest to 1.25, easing back to the plant's own place at 1.5), out across the ground and 0.3 of it down; the samples are blended by the cube of their push, so the field has no seams. A first version that took the strongest sample and pushed from the axis straight to the body's surface tore leaf cards that straddled the axis or a seam between samples into streaks across the body. `flex` is 1 for leaves and `1 - smoothstep(0.012, 0.05, radius)` for wood (`uv_b.x`, the growing radius on `GpuGrowth`, times the tree's scale); the push fades in over the plant's lowest 15 to 65 cm, so it stays rooted.

## World units (`units.rs`)

Trees are built in metres and a game places them with a `Transform` in its own unit, so what the crate measures in the world has to be told that unit: `FloraUnits { units_per_metre }` (default 1.0). The rule, on every field: **a field that sizes or times the crate's own simulation is metres and is multiplied by `units_per_metre` where it meets the world; a position or a size that addresses the world is in world units.**

- **On the CPU**, at the place of use: `felling.rs` (`GRAVITY`, the 0.5 m minimum length of the fall's pull, `LOG_GAP`, the 5 cm saw and notch margins, `cut_height`, `TreeNotch::height`, `BuckTree`, and the crash shake's radius, which is metres again: the crown's world reach divided by the unit), `harvest.rs` (`GRAVITY`, `SETTLE`, the knock velocities, the shake radius), `falling.rs` (`DRIFT`, `SWING`, `SPEED`, `LOWEST`, `autumn_distance`, the shake radius; `Fall::units` carries it into `BurstBuilder::add`), `fire.rs` (`FireSpread::speed` and `reach` in `step` and `refuel`), `push.rs` (a pusher's radius and half height as it is packed), `pool.rs` and `plugin.rs` (`band(start, end, units)`: the distances and the 2 m cross-fade), `see_through.rs` (the radius as it is packed).
- **In the shared buffer**, `wind_vectors(.., units)` packs what is metres as world units (a gust's speed and width, a shake's radius) and puts the unit itself in lane w of slot 2 (`UNITS_SLOT` / `WETNESS_SLOT`; lane z is the wetness). The shaders read it with `world_per_metre()` where they size something themselves: `push_offset` (the 15 cm rooting height and the 65 cm it grows over), `wood_flex` (the 1.2 and 5 cm radii), and `falling_common.wesl` (`DRIFT`, `LIFT`, the swing). `sync_wind` runs when `FloraUnits` changed.
- **Not scaled**: positions and sizes that address the world (`FloraFire::new`'s size, `ignite`/`extinguish`/`scorch` radii, `FloraIndex` query radii, `Scatter`, `FloraGround`) and everything in the tree's own space (meshes, `TreeShape`, `contact_points`' 4 cm limb radius). `FloraWind::speed` multiplies time, not a length. Bands are made when a pool's trees are spawned and a `TreeLod` tree's levels are built, so set the unit before planting.

## 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 materials every variant shares, a wood and a leaf material for each look (`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. `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, each child with its level's `Aabb` and `NoAutoAabb`: Bevy's `calculate_bounds` recomputes the box of every entity whose `Mesh3d` is new from all of its mesh's vertices and overwrites the one given, which made loading 100 000 trees take 6.8 s instead of 2.2 and gave an impostor the quad's own box instead of the wider one it needs (below). 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, and that the children keep the pool's boxes with `calculate_bounds` running.
- **Pools built in the background** (`pool::BackgroundPools`). `build_pool` is two halves: `build_pool_parts`, which needs no world (every variant's `build_lods` on scoped threads, bounds, reach, `TreeShape`, the flowers merged into the leaves, the meshes made `Mesh`es), and `assemble_pool`, which adds the meshes to `Assets<Mesh>` and puts the `TreePool` together. `BackgroundPools::build` reserves a handle from the pool assets' `AssetHandleProvider` (kept in the `PendingPools` resource when `TreePool` is registered, so the system parameter borrows no `Assets<TreePool>`), runs `build_pool_parts` as an `AsyncComputeTaskPool` task and keeps the task with a strong handle of its own; `finish_background_pools`, first in the pool systems' chain, assembles and inserts a finished pool under its handle and drops a build whose handle it holds alone (the game dropped the pool). Both entry points run the same two halves, so the pools are equal mesh for mesh (`pool::tests`).
- **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, elevation)` frames a tree's bounding cylinder as the orthographic camera sees it from the pool's elevation (10° above the horizon by default; below), 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 (`impostor_corner`, below), bends it with the same `wind_bend` as the meshes, and applies 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 camera prepass (`enable_prepass` is `false`) and a shadow pipeline of its own (below), culls nothing, and is alpha-masked. The quad's `Aabb` is set by hand and reaches `size.x / 2` and a crown's reach 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.
- **Impostors for cameras that look down.** `ImpostorFrame::new(height, radius, elevation)` frames the picture from the pool's `LodSettings::impostor_elevation` (clamped to 0..60°; the quad is `1 / cos` as tall as the picture) and the bake's cameras stand at that height (`ImpostorFrame::toward_camera`). Where the quad stands is `impostor_common.wesl`'s `impostor_corner`, shared by the main pass and the shadow pass: it turns the quad about the trunk toward the viewer (the view's backward axis under an orthographic view, a directional light's shadow view included), picks the nearest baked yaw, and in the main pass moves each corner toward the camera along its own view ray by the crown's reach (`ImpostorMaterial::params.z`, the variant's radius, over the ray's horizontal share, at least 0.2): the picture on screen is unchanged, its depth is the crown's front, and the picture's lowest part (the near edge of a crown seen from above) is no longer under the ground. The culling box grows by a reach across and five up. The material enables shadows but not camera prepasses: shadow maps draw impostors with `impostor_prepass.wesl` (the quad in the trunk's plane, turned toward the light, cut out by the colour's coverage less the season's fallen leaves, with Bevy's range dither), and `spawn_pooled_trees` gives an impostor `NotShadowCaster` unless its pool has `impostor_shadows` (then `CastingImpostor`, and `NotShadowCaster` only if its tree has it).
- **Shadow opt-out.** `spawn_pooled_trees` gives the level children of a tree that has `NotShadowCaster` the marker too; `pool::sync_shadow_casting` (`PostUpdate`) copies it to the mesh children of a `PooledTree` or `ProceduralTree` that gains it or whose children changed (a rebuild, `TreeLod`'s levels, cut faces) and takes it back off them when it leaves the tree, sparing the impostors of pools that do not cast (those without `CastingImpostor`).
- **Shadow range** (`shadows.rs`). `FloraShadows { radius }` (metres times `FloraUnits`) is kept by `cut_far_shadows`, in `PostUpdate` after `index_obstacles` and before `sync_shadow_casting` (which then hands the marker to the meshes the same frame). The centre is the main camera's place on the ground: its position under a perspective camera, the point its forward ray meets the ground (`pool::ground_distance`, shared with the zoom) under an orthographic one. A pass runs when that place moved more than a hundredth of the radius, the radius changed or `FloraIndex` changed: the trees within it (`within`, or past as many cells as there are trees every tree by distance) that carry `ShadowCut` lose it and `NotShadowCaster`, and every standing tree that casts (no `NotShadowCaster`, no `Felled`) outside it gets both. After a pass the casting trees are the near ones, so a pass costs about those, not the forest. A tree with `NotShadowCaster` and no `ShadowCut` is the game's and never touched; `Added<Felled>` gives a cut tree its shadow back; with the resource gone, every `ShadowCut` tree casts again.
- **Levels by zoom** (`LodScale`, `pool::follow_orthographic_zoom`, `pool::scale_lod_bands`, in `PostUpdate` after transform propagation and before `VisibilitySystems::CheckVisibility`). Every level child, pooled or `TreeLod`, impostors included, carries its unscaled band (`LodBand { start, end }`) beside its `VisibilityRange`, spawned at a scale of 1. `follow_orthographic_zoom` keeps `ZoomScale` at 1 unless the main camera (the active `Camera3d` with the lowest `order`) is orthographic; then it is the camera's distance to where its forward ray meets the ground (the plane y = 0, or `FloraGround` refined four times), over `area.height() / (2 tan 22.5°)`, the distance from which a 45° perspective camera sees as tall a view, moved only when it changes by more than 2% (`ZOOM_STEP`). `scale_lod_bands` multiplies `LodScale` by it and, when the product changed, rewrites every child's range (distances and fades times the scale, in parallel); otherwise, at a scale other than 1, only the children whose `LodBand` is new or changed. Bevy still measures from the camera's position, so an orthographic camera close to its focus draws the trees low on the screen finer than those at the top.
- **Looks and recolouring** (`pool.rs`, `recolor.rs`, `impostor/`). A look is a `PoolColors` (wood, leaves, `unlit`); `TreePool::looks[0]` is the parameters' own and `add_look` pushes more, so `TreePool::materials` is one `PoolMaterials` pair per look, made lazily for the looks the pool has when it is first drawn (`make_pool_materials`, which takes any `RecolorPool` colours the pool already has from the `PoolRecolors` resource). A look's `Surface`s are made from the pool's parameters painted in its colours (`PoolColors::surfaces`: the scan's mean is divided out of the colour and the autumn and blossom ratios are computed from the leaf colour, exactly as for a pool built with those parameters, which a test compares field for field). `Surface::unlit` sets `StandardMaterial::unlit` on the base and is part of the `SharedFlora` `Look` key; `fragment.wesl` returns the base colour in place of `apply_pbr_lighting`, `leaf_transmission` and the toon rim when `STANDARD_MATERIAL_FLAGS_UNLIT_BIT` is set (after wet, snow and fire have edited it, and with the ember glow still added), and keeps `main_pass_post_lighting_processing`. A pool's flora materials come from `PoolFlora`, a `SharedFlora` of the pools' own (apart from the procedural trees', so the looks that draw with a pool material are found by scanning the pools): pools whose surfaces are equal (the ages of a species, looks of one wood colour) share them (`pool::tests`: two ages of the spruce share look for look; 72 materials of a 4-species, 3-age, 3-look forest are at most 24, 5.49 to 4.55 ms). `spawn_pooled_trees` picks the pair `PooledLook % materials.len()` for a tree; `sync_pooled_looks` re-points the children of a tree whose `PooledLook` changed (by which role their current material plays in any look). `recolor_pools` reads `RecolorPool` messages, keeps the last per look of a pool, records the colours in `PoolRecolors`, writes plain materials and each variant's impostor material for the look (the pool's own) where a value differs (`write_standard`, `write_impostor`), and for flora looks finds, per material and role, the looks that draw with it: when all of them asked for the same surface and no material of that surface is drawn already, the material is written in place (`write_flora`) and re-keyed (`SharedFlora::rekey`), as a palette gliding over a species' pools does every frame; otherwise the looks that asked resolve their new surfaces through `PoolFlora` (copy on write), the pool's `materials` change with `Assets::get_mut_untracked` (a `Modified` pool would be re-indexed tree by tree) and the children of that pool's trees of that look move from the old handles to the new (`Repoint`).
- **Impostor looks.** The bake draws the pictures neutral grey (linear 0.5, `NEUTRAL_GREY`), so one atlas serves every look: `ImpostorAtlas::materials` has one `ImpostorMaterial` per look, all sharing the albedo and surface images, each with its own `ImpostorTint` (binding 4: twice the look's linear wood and leaf colour), its own `FloraUniform` (the look's autumn and blossom ratios) and `params.w` (1 for unlit). `impostor.wesl` multiplies the sampled albedo by the wood tint or the leaf tint (`baked.a > 0.5`, the translucency the surface row holds) before the season recolours the leaves; an unlit look outputs the base colour with the fire's ember added.
- **Trees that are not pooled** (`recolor.rs`). `RecolorTree` is applied by `recolor_trees` (after `buck_trees` in the plugin's chain): the entity's `ProceduralTree` is painted in the colours (`PoolColors::apply` on its parameters; `TreeColors` on the tree keeps them, and `rebuild_trees`, `start_felling`, `carve_notches` and `buck_trees` all paint from `TreeColors` over the parameters), its materials come from `SharedFlora` and replace the paint's (`plugin::repaint_flora`, coarse levels included while the tree stands), a felled tree that has landed has the calm (`calm_params`: sway 0) materials in its children and one that is still falling gets its `Fall::still` list restyled for the landing, and the cut faces swap between the two `CutFace` materials (end grain lit and unlit). Stumps and logs carry `CutWood` (the tree's painted parameters, unlit, and whether the wood is calm: a log's is), which is what lets a `RecolorTree` on a stump or a log dress its wood once the tree is gone. A pooled tree that is felled or notched becomes the `ProceduralTree` of `TreePool::tree_params_for(variant, look)` with `TreeColors` of its look as it stands now (`felling::pooled_to_tree`, `PoolRecolors` over `TreePool::looks`).
- **Felled wood follows its pool.** Where a pooled tree leaves its pool (`start_felling` and `carve_notches`), it gains `FromPool { pool, look }` (`felling::from_pool`) with its `TreeColors`; `start_felling` copies it to the stump and `buck_trees` to each log. `recolor::recolor_from_pools`, just before `recolor_trees` in the chain, reads every `RecolorPool` and writes a `RecolorTree` for each entity whose `FromPool` is that pool and look, leaving out a stump whose tree (`TreeStump::tree`) still carries `FromPool`, as the tree's message reaches its stump (`Felled::stump`). A pooled stump or lying tree (`PooledDeadfall`) needs none of this: it draws in its look's materials.
- **Glides** (`glide.rs`). Every mesh of a pooled tree (level children and impostor, `spawn_pooled_trees`, re-tagged by `sync_pooled_looks`) and of pooled deadfall (`DeadMaterials::tag`, `dress`) carries `MeshTag(k)`, `k` the tag `GlideSlots` gives the pool and look (1 to `MAX_GLIDE_LOOKS`, 64; 0 once they are taken). The shared buffer's `GLIDE_SLOT` (60, after the pushers; the fire's grid now starts at 189) holds a header (x 1 while a look glides on the GPU) and two vectors per tag, what the look's wood and leaves are multiplied by (`GlideTints`, 1 at rest; `sync_wind` keeps them when it repacks). The main-pass pipelines of flora materials and impostors get `FLORA_GLIDE` and `VERTEX_OUTPUT_INSTANCE_INDEX`: while the header says so, `vertex.wesl` multiplies a leaf's vertex colour by `leaf_glide_tint` (`mix(glide, autumn ratio, turn)`, a fruit by the multiplier, a flower by the blossom ratio alone), `fragment.wesl` the wood's base colour, and `impostor.wesl` the picture through the same autumn mix, each reading the tag with `mesh_functions::get_tag(instance_index)`. `glide::glide_looks` (in the pool chain after `finish_background_pools`, before `recolor_pools` and `restyle_deadfall`) reads every `RecolorPool` with its own `MessageCursor` over `ResMut<Messages<RecolorPool>>` (a reader and a writer of one message type cannot share a system): a gliding one starts a `Glide { base, from, to, start, seconds, tag }` per pool and look (`base` the colours the materials hold, `PoolRecolors`, kept when a glide replaces a running one; `from` what the look shows), one at once stops the look's glide and resets its tag. Each frame a glide on the GPU writes `blend / base` per linear channel (at most 1000, 1 where `base` is black) into its tag's vectors and the buffer in place, and its felled wood (`FromPool`) a `RecolorTree` of the blend; on arrival it writes a `RecolorPool` at once of `to`, which every reader applies as any other, and resets the tag in the same frame. A glide with no tag (no flora support, or past 64 looks) writes its blend as a `RecolorPool` at once every frame. Every other reader skips gliding messages. Writing the blend into the materials every frame was measured first and dropped: `AssetChanged<MeshMaterial3d<M>>` has Bevy re-extract and re-specialize every mesh drawn with a changed material (+9 ms of CPU a frame for 5 000 pooled trees, +60 ms for 20 000).
- 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.

## Pooled deadfall (`deadfall.rs`)

`build_pool_with_deadfall` cuts each variant once, as `start_felling` and `buck_trees` would at scale 1 on flat ground (`saw_height`, `cut`, `trunk_alone`, `log_end`): `Deadfall` has the stump, the wood above the cut per level, both faces, at most 600 contact points, the trunk's cut outline and the `PoolLog`s. `spawn_pooled_deadfall` (Update, with the pool's) places a `PooledDeadfall`: the hinge (`hinge_point` of the outline in the instance's frame), `rest_angle` of the contacts against `FloraGround` (clearance as `start_felling`'s), and every mesh child of the lying part carries the pivot as its own transform; logs are placed by `felling::lay_log`, which `buck_trees` uses too, in the stump's space. Materials are chosen in one place (`dead_materials`, calm look, once per pool). `sync_shadow_casting` and fire's `AnyTree` include `PooledDeadfall`; the index does not.

`IntoDeadfall(kind)` on a `PooledTree` is turned by `turn_into_deadfall`, right after `finish_background_pools` and before `spawn_pooled_trees` in the pool chain (whose commands are applied between systems), so a tree spawned with it is deadfall from its first frame and never gets a standing tree's children: once the pool is loaded, the entity loses its children, `PooledTree`, the spawned and shape-filled markers and any `TreeShape`, and gains `PooledDeadfall { pool, variant, kind }`; `PooledLook` and `NotShadowCaster` stay for `spawn_pooled_deadfall` to read, and the index drops the tree as for any `PooledTree` removed. A variant without `deadfall` keeps the tree standing and drops the component, with one warning.

## 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. That index is `ScatterPoint::id`, a tree's name for saves.
- **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.
- **Stands** (`points_with`): each kept tree's shares, `weight(at, species)` clamped to at least 0, divide its distances to the grove centres, so the nearest centre in that weighted sense wins: all weights 1 give exactly the tree of `points_where` (which is `points_with` with weights of 1), a weight of 0 skips a species' centres, and a weight fading out over a rim moves the border with the centres instead of along the rim. The search stops once `reach * cell / largest share` passes the best. With no centre of a weighted species (or no groves) the tree draws its species from the global weights times its shares, with the same draw as before.