Skip to content

PRD-539 card LOD levels keep the canopy - #463

Closed
jonit-dev wants to merge 6 commits into
developfrom
feat/prd-539-card-coverage
Closed

jonit-dev wants to merge 6 commits into
developfrom
feat/prd-539-card-coverage

Conversation

@jonit-dev

@jonit-dev jonit-dev commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

PRD: docs/PRDs/assets/PRD-539-card-lod-levels-keep-the-canopy.md. PRD-478 (#437) AC-2 and AC-3 depend on it.

Why: on a quiet RTX 2080, Machinefall map-walk's early-route GPU p95 (9.2–9.8 ms) is the main pass drawing 8.2 M triangles. 9.4 M of 9.8 M are trees whose GLB holds only the Fab pack's *_far_LOD2 mesh. The cook skipped those as authored-lod, so the adaptive LOD bias had nothing to coarsen. With a chain, route GPU p95 falls to 7.73 ms max, but the needle cards' unscaled levels thinned the mid-distance canopy to bare trunks.

What:

  1. An asset is authored LOD only when two or more nodes carry a level name; a lone _LOD2 gets a generated chain.
  2. Card levels append copies of their kept cards, scaled about each centroid by sqrt(LOD0 area / kept area) (at most 2), and index those copies. The chain stays index-only, and LOD0's indices never reach a copy.
  3. A streamed world's chained levels cast shadows by default (shadows.castLevels defaulted to 1, and the pines lost their shadows past the first switch).
  4. The model pass's self-check counts the vertices LOD0 draws.

Phase 1: The cook

  • A lone level-named mesh gets a generated chain. lod-generation.spec.ts red-green, 44 passed
  • Card levels index scaled copies. foliage-lod.spec.ts red 0.516 ≥ 0.95, then 8 passed
  • Self-check counts what LOD0 draws. packages/assets: 485 passed, 2 skipped

Phase 2: Both runtimes draw it

  • Core builds a card level that draws the scaled copies. model-lod-loader.spec.ts, 5 passed
  • Chained levels cast by default. world-cells.spec.ts red-green; core + assets 3086 passed
  • Native desktop host draws the same chain. examples/auto-lod card crown: lod-far-desktop/lod-near-desktop pass, cardCopies 1 → 0, cardTriangles 1252 → 2400

Phase 3: Machinefall

  • Re-cook and time it (AC-2)
  • Blind raters and pop series (AC-3). Raters done: 24/24 votes at or above PRD-478's tip (20 prefer, 4 ties); pop series pending

The cook skipped any mesh whose node or mesh name carries a level
token (`hull_LOD1`) as hand-authored LOD. A Fab pack's `_LOD2` exported
as a model of its own has no sibling level, so nothing selects it and
the skip left it at full detail. On Machinefall map-walk these trees
(`*_far_0_001`, mesh `*_far_LOD2`) are about 9.4 M of the 9.8 M
main-pass triangles in the early route, and the adaptive LOD bias
cannot coarsen a slot with one level.

An asset is an authored chain only when it holds two or more
level-named nodes. LOD_GENERATOR_VERSION goes to 4 so a stale cook
cache cannot serve the old skip.

Filed with PRD-539, which also makes card levels keep their coverage:
a chain on these trees thins the needles visibly until it does.
A card level kept 50 % then 25 % of a crown's needle cards unscaled, so
its card area fell to the keep ratio. On Machinefall, once the Fab
`_LOD2` pines got a chain, mid-distance trees read as bare trunks.

Each level now appends copies of its kept cards, scaled about their
centroids by sqrt(LOD0 area / kept area) (at most 2), to the
primitive's own vertex arrays, and its indices point at them. The chain
stays index-only and LOD0's indices never reach a copy, so a stock
loader and both runtime readers need no change.

The model pass's self-check now counts the vertices LOD0's indices
draw, so the copies are not drift; a LOD0 drift still throws.

foliage-lod.spec.ts: red `expected 0.5162100266415837 to be greater
than or equal to 0.95`, then 8 passed; packages/assets 485 passed.
@jonit-dev jonit-dev added the prd:25% PRD phase 1 verified label Oct 8, 2026
…d selects

The card cook now appends scaled copies after LOD0 and indexes them
from the level (PRD-539). The loader and per-frame selection draw them
with no core change.
`shadows.castLevels` defaulted to 1 on the reasoning that a far level's
shadow is sub-texel. That holds only past the shadow window, and a
shadow level draws only the caster clusters its own window covers.
Inside it the shadow is visible: once Machinefall's pines got a chain,
every pine past its first switch, tens of metres out, lost its shadow,
and 3 blind raters preferred the base on 6 of 8 poses ("weak shadows,
flatter"). With every level casting they prefer this build on 20 of 24
votes and tie on 4.

The default is now every level; a number still caps it.
world-cells.spec.ts: red `a far level minted no caster cluster`, then
green; packages/core + packages/assets 3086 passed.
…vels

examples/auto-lod gains a generated 1,200-card crown (cards.glb, from
scripts/make-cards.mjs). The scene reports the crown's drawn triangles
and whether the drawn level reaches a vertex LOD0 never indexes, which
is a scaled copy (PRD-539). Far scenarios require it; near scenarios
drive it back to LOD0. Native desktop: far and near pass
(cardCopies 1 -> 0, cardTriangles 1252 -> 2400).
Native desktop and web far scenarios pass on the auto-lod card crown.
Web lod-near is red on develop too, so it carries no card check.
@jonit-dev jonit-dev added prd:75% PRD phases in, acceptance open and removed prd:25% PRD phase 1 verified labels Oct 8, 2026
@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual progress (laptop Intel GPU captures, same JS bundle)

Rejected 1: chain with unscaled card levels. Mid-distance pines read as bare trunks (middle row).
first-stream: base / unscaled / scaled

Rejected 2: scaled cards, castLevels default 1. 3 blind raters preferred base on 6 of 8 poses ("weak shadows, flatter"): every pine past its first switch lost its shadow (middle row below).

Candidate: scaled cards + every level casts (bottom rows). 3 fresh blind raters, 8 poses: at or above PRD-478's tip on 24/24 votes (20 prefer, 4 ties).
highway: base / castLevels 1 / every level
first-stream: base / castLevels 1 / every level

Known artifact: a few needle specks above the treetops at distance (bottom crop): the bark part's single derived level drops the top 19 % of the trunk while the needles keep theirs. Raters still preferred the candidate on that pose.
treetop specks: base (top) / candidate (bottom)

Timing (quiet RTX 2080, 1 of 3 runs so far): route GPU p95 max 8.04 ms; pooled bound 2.3 % of walking frames over 8.3 ms (pass ≤ 5 %).

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Absorbed into PRD-478's PR #437 at the owner's request (one PR): commit 3cabfde carries this branch's net diff. PRD-539's file stays in the tree, marked ABSORBED, and its open boxes finish with PRD-478.

@jonit-dev jonit-dev closed this Oct 8, 2026
jonit-dev added a commit that referenced this pull request Oct 8, 2026
Reverts d65fa1e on this branch. On its own it gives the Fab LOD2
pines a chain whose unscaled card levels thin the canopy; PRD-539
(#463) ships it together with card scaling. PRD-478 notes now point
there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prd:75% PRD phases in, acceptance open

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant