Vegetation placed on the GPU, and starter scenes - #10
Merged
Lincoln504 merged 2 commits intoJul 29, 2026
Conversation
The design question is where the instances come from, and every obvious answer routes tens of thousands of transforms through the CPU — and through the wasm boundary — every time the terrain moves, which is every frame of a slider drag. So they never leave the GPU. A compute pass walks a jittered grid over the map, reads the finished heightmap, decides what grows at each slot, and appends it straight into the instance buffer, raising the `instanceCount` word of that species' `drawIndexedIndirect` arguments with an atomic. The render pass issues one indirect draw per species and never learns how many plants it drew. Changing the density is one dispatch of 256 workgroups. Moving the camera is nothing at all. And because placement reads the *same* heightmap texture the terrain is displaced from, a plant cannot end up hovering over ground that moved after it was placed. The constraints did most of the design: - `firstInstance` is 0 in every draw, because non-zero needs the `indirect-first-instance` feature and compatibility mode has no features. A species' slice is reached by offsetting the instance vertex buffer, which is not a feature and cannot be missing. - The heightmap arrives as a *texture* in both stages. Compatibility mode allows zero storage buffers in a vertex stage and four in a compute stage; spending one on heights a texture fetch could supply would leave nothing for the output. - Instance data is one 8-byte vertex stream — `unorm16x2` position and a packed `uint32` — not four one-byte attributes, because V3D charges vertex pull per component and the unpacking is free ALU. - Small solid meshes, not alpha-tested billboards. V3D has no hidden-surface removal and no Forward Pixel Kill, so a discarded fragment costs exactly what a drawn one costs and a cutout quad is *more* expensive than the geometry it was standing in for. It would also want a texture atlas, which is payload. Where things grow is the material's business, not the document's: the species mix comes from the preset and the altitude/slope windows echo the material's own layer windows, so plants never stand on painted rock, on a slope, or under water. What is left for the document is two numbers — how much, and how big. No LOD chain (three draws is 0.6% of Arm's stated 500-draw budget) and no per-frame depth sort. The second one is a real omission rather than a free choice: front-to-back ordering does matter on V3D, and a GPU radix sort simply costs more than the overdraw it saves at this instance count. It is the first thing to revisit if the scatter grows, and that is written down where the decision is.
The Create rail listed the four generators. "Ridged multifractal" is an implementation detail, and now that the filter chain has bedding, erosion concentration, plants and water-as-a-surface, the interesting settings interact: a canyon is bedded rock with the carving biased to the lowlands and a desert palette and no vegetation and the right sky, and reaching it from a generator menu means finding four panels and knowing what to do in each. So the rail offers eight landscapes instead — Alpine, Foothills, Red Canyon, Monument, Island, Fjord, Ashfields, Moonscape. Each is a table entry rather than a function, so the source reads as a description of eight places instead of eight near-identical builders, and each lowers to plain commands: one undo step, no special path through the document, and every part of it reachable by hand afterwards. A preset is a starting point, not a mode. Two things the tests pin down because they are the mistakes this shape invites. A preset that wants no water has to *remove* the water the last one left, or every scene after Island is quietly drowned. And the seed survives, so clicking through the rail shows the same land under different conditions rather than rerolling the world at every click — which is the difference between comparing presets and gambling. The browser smoke test earned its keep here: the rail is mounted before `init()` runs, so reading the preset list at module scope threw inside wasm-bindgen before any of our code could see it. Native tests were green and the app was blank.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
23 files changed, 2017 insertions(+), 37 deletions(-)· gate green at172ac04(fmt, clippy, workspace tests against a real adapter, wasm build, headless-Chromium smoke) · payload 382847 gz (113476 from the previous PR, against the 6 MB NFR-2 budget)Part of a 12-PR stack over
pr/00-ci. Based onpr/03-water-and-bedding; retargets tomainonce that lands.