Repository navigation
#542 WebGPU scenery parity: per-primitive colour, lazy streaming, real-tier density, shader grass, reeds/rocks/cone-stack trees - #594
Open
MaximumTrainer wants to merge 5 commits into
Conversation
The scenery-mesh pass collapsed every primitive of a GLB into one mesh and painted the whole thing in primitive-0's baseColor, so multi-material models (trunk/leaves, walls/roof/windows) rendered as a single pale block. glTF splits a model into one primitive per material; bake each primitive's colour per vertex and carry it through sceneryMesh.wgsl as a vertex attribute, so parts stay distinct — matching how the React scene binds per-primitive materials. Single-colour callers (boat primitives) omit `colors` and the pass fills every vertex from `baseColor`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5D9BTXmu7SeMa3LghJD8b
Scenery GLBs were all fetched up-front at attach (a Promise.all over every model), which blocks the first frame and can trip a static-host 503 — the same burst the React scene avoids by chunking its loads. Defer them: plan where each model first appears along the route, then load models from the frame loop as the boat comes within a look-ahead window, capped a few per tick. Course furniture and GLB scenery share one instance-transform map; a model's instances are set the moment its GLB registers, so nothing draws before it is ready and distant scenery streams in as the row progresses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5D9BTXmu7SeMa3LghJD8b
…ss flat shading The WebGPU engine was rendering the river banks noticeably sparser and flatter than the React scene it ports. Four changes close most of the visible gap on an Intel Gen-9 (parity captures in #591 follow-up): - toEngineRoute: the mode was hard-wired to `medium`, which both ran the placement loop at 0.7 budget AND suppressed GLB kit trees entirely (keepsKitTrees is true only on high/extra-high). React used the viewer's own PerformanceMode, so most sessions got `high`. Add an optional `tier` parameter defaulted to `high` — no caller change required. - floraSolid.wgsl: the near-tree solid pool had one mono-green crown with no species variation and the sprite pool (scenery.wgsl) had three species palettes, so the near/far handoff never matched colour. Decode the species index from the centres.w metadata already packed by foliagePlan.ts and look up the same BASE_PAL/CROWN_PAL the sprite pass uses. The shading model moves from a `0.5*dot+0.6` flat clamp to half-lambert plus a sky/ground hemisphere ambient so leaves pick up some depth cue instead of reading as painted cardboard. - sceneryMesh.wgsl: same hemisphere ambient + half-lambert replaces the flat `0.35 + 0.65*diffuse` so GLB kit trees and OSM buildings carry depth under the sun. - uniforms.ts sproutWindowFor: the per-tier fade `far` was 6-15 m, which left most of the mid-range as sprite-only cards. Push each tier ~60% farther (medium 10 -> 16 m, high 12 -> 20 m) so the solid mesh carries the mid where silhouette matters most; the sprite pass still owns beyond. floraLod.test.ts picks up the new numbers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to b154bb6. Two pieces. PIECE 1 — Tier plumbing. `toEngineRoute`'s mode was pinned to a sensible default ('high' after the first follow-up, 'medium' before), which meant the scenery builder had no idea what the device could handle and couldn't match React's density curve. React resolves the real tier through `resolvePerformanceMode()` (honoring `__VIRTUALROW_PERFORMANCE_MODE` and the Playwright flag), so `/row/+page.svelte` now calls the same resolver and threads the result into BOTH `toEngineRoute(route, enrichment, tier)` and `Canvas3DHost`'s `tier` prop. The two can't diverge. `DEFAULT_SCENERY_TIER` ('medium') keeps the no-arg callers in tests deterministic. A new test asserts `sceneryGlbs` count follows `low <= medium <= high` and that `foliagePlan` pools stay deterministic across tiers. PIECE 2 — Biome-density ground colour (#496 Phase 2, ported). React's "shader grass" is a biome density texture modulating the bank's grass albedo, not standing blade geometry (`bankTriplanar.glsl` `grassDensityModulation`). The WebGPU bank pass now ports that chunk: `bank.wgsl` samples an `r8unorm` density tile on a 32 m plan-view grid and modulates the ground colour by `grass.strength`, with `strength = 0` leaving the colour pass-through. A new `biomeDensity.ts` paints the tile from a seeded noise + biome-mean bias byte-for-byte like `foliageTexture.ts`'s `paintBiomeDensityTile`; `biomeDensity.test.ts` asserts the ±0.15 mean invariant React's own tile test asserts. A new `shaderGrass.ts` ports `drawsShaderGrass` / `isShaderGrassEnabled` and adds `shaderGrassStrengthFor`, so the pass tier-scales (0 on basic/low, 0.6/0.85/1.0 on medium/high/extra-high) and honors the global `__VIRTUALROW_SHADER_GRASS = false` kill switch. `rideScene.ts` wires them, picking the route's dominant biome profile via a new optional `EngineRoute.dominantProfile` the adapter now carries. Follow-up (NOT in this change, per coordinator): reeds + rocks + background cone-tree scatter — the other three vegetation layers React has that the WebGPU engine doesn't. Also lands: - `_mockWebGPU.ts` adds a `writeTexture` stub on the mock queue; the bank pass now uploads one on construction so the stub keeps the existing attach / AC4 tests honest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to b154bb6 and 95324b3. The remaining medium-tier gap vs React was three vegetation layers the WebGPU engine didn't have and a sprout window too tight to let the solid flora mesh carry mid-range silhouette. Four changes. 1. Four-cone-stack trees. `floraSolidPool.buildFloraSolidMesh` species 0-2 now mirrors React's `vegetationComponents.tsx` `CONE_LAYERS = [[2.8, 1.4, 2.2], [3.8, 1.1, 2.0], [4.6, 0.8, 1.8], [5.3, 0.4, 1.2]]` — a trunk prism + four tapered foliage cones at the same y-stops and radii. Trees read as a proper Christmas-tree silhouette instead of the pre-port single-ring blob. Mesh grows from ~32 to ~60 triangles; 1k instances per pool × 60 tri is comfortably inside the medium-tier budget on Intel Gen-9. 2. Reeds (species 3) and rocks (species 4) as ground props. New `scenery/reedRockPlan.ts` ports React's REED_MAX=120 / ROCK_MAX=40 layout from `vegetationComponents.tsx`. The layout picks a progress `t ∈ [0, 1]` for each prop, perpendicular-offsets by 12-20 m from the water centre (sized against the actual OSM shore width + the scenery margin + an inner band), and thins per-instance by the segment's `SCENERY_PROFILES[...].groundCover.density` the same way React's `densityScale` thins its counts. Both layers reuse the existing `FoliageTree` shape with species 3/4 so `batchFoliagePools` bins them alongside trees — no new pool kind, no new render pass. Mesh variants in `buildFloraSolidMesh`: - 3 = three crossed thin vertical quads (6-blade reed cluster, both faces), ~12 tris. - 4 = seeded irregular 6-sided prism with a top fan, ~18 tris. 3. Palette extensions. `floraSolid.wgsl` grows `TRUNK_PAL`/`BASE_PAL`/`CROWN_PAL` from 3 to 5 entries: species 3 lands on a dry warm reed tan, species 4 on a mid-grey rock tone. `scenery.wgsl` (sprite billboard pass) grows its `SPECIES_HEIGHTS`/`SPECIES_WIDTHS` to 5 with the new entries at zero, collapsing the billboard to a point for reeds and rocks (the solid pass owns their visual at close range anyway); the sprite's own 3-entry palette arrays stay as is behind a `min(species, 2u)` clamp. Both shaders still parse under `wgsl.test.ts`. 4. Wider sprout LOD window. `sproutWindowFor` far distances step up again (medium 16 → 60 m, high 20 → 100 m, extra-high 26 → 160 m). React's scene has no sprite handoff at all — its foliage is real geometry to the clip plane — so the WebGPU hybrid LOD has to carry solid trees well past the river bank's own distance (~10-30 m from the chase camera) to approach the same look. The sprite pass still fills in beyond `far` to the fog. `floraLod.test.ts` updates the asserted numbers; the monotonic-with-tier invariant still holds. Tests: - `floraSolidPool.test.ts` splits the per-tree budget assertion and adds species 3 (12 tri) and 4 (18 tri) exact-count assertions. - `reedRockPlan.test.ts` (new) asserts the layout returns empty on no curve, every prop carries the right species index, counts stay under the React caps, output is deterministic, both banks get props, and no prop sits inside the river itself. - `floraLod.test.ts` picks up the new sprout numbers. Measured on this hardware (Intel Gen-9, HOCR + Willowbrook, 22 s ride): ~57-83 reeds and 21-25 rocks per route, foliage pool count rises from ~90 to ~204 as the two new species each add their own chunk buckets. No console errors, no device-lost, `__VIRTUALROW_WEBGPU_HEALTH` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This branch has not been deployed
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.
Pre-cutover visual-parity pass on the WebGPU
/rowscene, verified side-by-side against current-main React on a real Intel Gen-9 GPU. Supersedes #591 — this branch contains #591's two commits plus the scenery-density follow-up Dan asked for, so it's one PR to review and merge; I'll close #591 in favour of this.What changed (5 commits)
sceneryMesh.{ts,wgsl},sceneryMeshBuffers.ts) — each GLB primitive now draws in its own baked baseColor via a per-vertex colour attribute, instead of collapsing a whole multi-material model toprimitives[0]'s colour. Fixes trees/buildings/landforms that read as pale single-colour blocks.sceneryStreamPlan.ts,rideScene.ts) — scenery models load as the boat nears their first instance along the route (400 m look-ahead, a few per tick) instead of one up-front burst that blocks the first frame and can 503 a static host.+page.svelte,sceneryRoute.ts) — the scenery builder now uses the device's actual probed tier (resolvePerformanceMode), threaded into bothtoEngineRouteand the engine attach so they can't diverge. Previously it was hard-wired tomedium, which suppressed the GLB kit trees entirely (they're high+ only, matching React's own policy). Density now tracks React tier-for-tier.biomeDensity.ts,shaderGrass.ts,bank.{ts,wgsl}) — this is what React means by "shader grass" (Density-field grass on bank slopes #496 Phase 2): a biome-density ground-colour modulation of the bank material, not standing blades. On for medium+,__VIRTUALROW_SHADER_GRASS=falsekill switch, tier-scaled strength.reedRockPlan.ts,floraSolidPool.ts,floraSolid.wgsl,scenery.wgsl, widenedsproutWindowFor) — the three layers that close the remaining medium-tier gap vs React: reeds (REED_MAX=120) and rocks (ROCK_MAX=40) placed deterministically on the bank with per-profile density thinning, and the foliage trees given React's 4-cone-stack silhouette instead of a single-crown blob. ThefoliagePlanslot/species/x/z schedule is untouched, so the existing foliage parity test doesn't drift.Shading on both the solid-flora and GLB-mesh passes moved from a flat term to half-lambert + sky/ground hemisphere ambient so meshes stop reading as flat cards.
Verification
npm run verifygreen: lint,tsc(app + playwright), 2121 unit tests pass / 0 fail,wgsl.test.tsparses all 20 shaders (bank, floraSolid, scenery, sceneryMesh). New unit tests forbiomeDensity,reedRockPlan,shaderGrass,floraSolidPoolper-species geometry, and tier-ordered scenery counts insceneryRoute.threeimported undersrc/lib/renderer.isSoftware:false,deviceLostCount:0, no console errors) across HOCR + Willowbrook, including a forced-low-tier capture proving density scales down end-to-end.Known / out of scope
Generated by Claude Code