Shadow cascade caster depth fit - #133
Merged
Merged
Conversation
Groundwork for the caster-aware depth fit, and the measurement that justifies it. No production behaviour changes here: the cascade matrices are untouched, and everything added is diagnostics, a test fixture, and a classification the candidate query will consume. placeCaster (render/cascade_fit) answers where one caster sits relative to one cascade in that cascade's light space: its U/V/W bounds, whether the fitted near/far planes clip it, and how its footprint relates to the cascade rectangle. It is pure and headless, and it is deliberately not a verdict — it separates a caster CLIPPED in depth from one whose footprint sits outside the cascade, which a picture cannot. CascadeFootprintRelation is Invalid / Outside / Inside / Straddles, with edge-touching classified conservatively as Straddles: the candidate query must reject Outside and only Outside, because over-including a caster costs a draw while wrongly excluding one costs a shadow nobody can trace. Straddling is ordinary, not defective — light rays in an orthographic directional map preserve U and V, so the part of a caster outside the rectangle cannot shadow any receiver inside it. The renderer retains each frame's two fit carriers and logs, on the same frames as the cascade fit (periodic plus the --capture-frame frame), where every shadow caster sits in every cascade — keyed by ShadowCasterId and generation, since one object can hold several caster bindings. The log states its own limit: it runs before ShadowDrawFilter, so it proves placement, never rasterisation. ShadowDepthClipDemo's caster is repositioned, and this is the substance of the branch. The first placement — a sphere centred ON cascade 2's legacy near plane — produced a complete ellipse, which is correct rather than a null result: the shadow pass culls FRONT faces, so the surface a caster records is its far side, and a sphere centred on the plane has that entire surface downstream of it. Moving the caster one metre upstream along the sun does not move its shadow (it stays on the same light ray) but makes the recorded surface straddle the plane. The trace then reports clippedNear with W [-45.723, -38.953] against depth W [-41.340, 33.535], and the shadow shrinks from 35253 to 26166 pixels, 306x152 to 263x131 — 14% linearly, 26% by area, against an analytic prediction of 13.4% / 25% for the cap the plane removes from a sphere. That is now SH-06's acceptance gate, recorded pre-fix: the caster-aware depth fit must restore the full-size ellipse. Scope the shape result carefully, and the comments do. A plane perpendicular to the light removes a cap symmetric about the light axis, so THIS fixture's sphere shrinks concentrically rather than acquiring a straight edge; asymmetric geometry can present a straight projected boundary under the same clip. What generalises is the mechanism, not the silhouette. The half-ellipse originally reported against ShadowLodMotionDemo is filed as a separate open question rather than SH-06's motivation. Sweeping the caster's whole animation range finds no pose where it is depth-clipped (closest approach 20.7 m under the fallback sun the scene used to render with, 30.8 m under the authored one), and a 676-row live trace over a ~25 s run reports zero clippedNear events for any caster. Neither null is vacuous: the sweep reproduces the engine's own logged cascade-0 fit to the printed digit, and the trace flags the probe scene. Diagnosing it needs the symptom re-confirmed under the repaired sun, the pass's own drawn verdict beside the placement, and per-pixel cascade / blend factor / projected shadow U/V at the affected receivers — none of which exist yet.
…distance The directional cascade depth range was `centre ± (radius + kShadowDepthBackExtend)`: a fixed 20-world-unit extension in both directions, standing in for "how far behind the slice a caster might be". No constant can answer that for an arbitrary scene. Too small clips a caster; too large spends depth precision on empty space. `fitCasterAwareCascadeDepth` fits the planes to the frame's actual geometry. The near plane reaches back to the furthest-upstream CANDIDATE caster — footprint not `Outside`, since light rays preserve U and V in an orthographic map, so a caster outside the rectangle cannot shadow a receiver inside it. The far plane covers the receiver volume and no further: geometry behind every receiver in the slice cannot shadow one, and reaching it would spend precision on nothing. Both planes get one `worldPerTexel` of slack — the fit's own unit rather than an invented epsilon — which widens the depth span by an XY texel's world size and has nothing to do with depth resolution. The matrix is built with the same `lookAt` / `ortho` calls as the legacy fit, so the only difference between the policies is where the planes sit. The result carries a `CascadeDepthFitMode`: `LegacyFixedExtension` for `fitLegacyCascadeDepth` asked for directly, `CasterAware` for a fitted range, and `LegacyStaleFallback` where the policy declined. Three values, not two, because the legacy function still has its own callers and its direct result is neither caster-aware nor a fallback from anything. `CascadeDepthFit` became an encapsulated class for the same reason `CascadeReceiverFit` is one: a mode has to describe THIS matrix, and a public aggregate would let a caller pair either with the other. Cloth is the unresolved case and is marked as such rather than papered over. A storage-vertex caster's bounds are its bind pose — a compute pass rewrites the vertices — so the box neither bounds the geometry nor establishes which cascade that geometry affects. Ignoring stale entries and fitting the rest is not the safe reading: it can produce a range NARROWER than one covering the cloth, which clips it, arriving at the same defect from the other side. So a single stale caster anywhere in the frame makes every directional cascade fall back to the legacy range, marked `LegacyStaleFallback`. Validation of every Exact bound completes BEFORE that choice, so a frame containing both cloth and a corrupt caster still reports the corruption rather than silently taking the fallback. Cascade blending became a fitting constraint. The shader cross-fades into cascade i+1 over the last tenth of cascade i's view-depth range, so those receivers sample i+1's map — and a cascade fitted tightly to its own slice would not cover them. Each cascade is now fitted from the start of its predecessor's blend band, and the fraction lives in `LightUBO::cascadeParams.x` so the value that decides the band and the value that fits for it are one C++ constant, not a constant plus a shader literal. Corrupt bounds are terminal end to end. `Bounds3::expandChecked` refuses a non-finite point instead of letting std::min/std::max drop it (they return the other operand against NaN, so the old path produced a finite box that did not contain its geometry); the prepass reports such a caster as valid-but-non-finite, which is what it is — this caster has an extent and the engine cannot state it — and the policy fails rather than skipping it, because a range tightened around a caster nobody accounted for is the exact defect being fixed. ACCEPTANCE, on `ShadowDepthClipDemo`, whose caster the legacy fit clipped: 26166 -> 35324 shadow pixels, bounding box 263x131 -> 305x152. Against the geometrically unclipped baseline (35253 px, 306x152) the two differ by 253 pixels grouped into 185 horizontal scanline runs, longest 5 px, median 1 — a one-pixel fringe following the silhouette with no clip-sized interior region. Residual clipping leaves a chunk; the legacy row is exactly that, a concentric 26% loss of area. Evidence re-run on an idle machine: the SH-03 budget table moves slightly with the changed matrices (budget 2 0.243% -> 0.289%, budget 4 0.356% -> 0.374%) and the 0.1% threshold still selects budget 1 by a wide margin; the dead-band sweep again shows zero reversals at every ratio. A sweep that overlapped a Docker CI build reported a 0.12% noise floor where an idle one reports 0.0000%, and eight identical-flag capture pairs differed by zero pixels — that sample was contention, and `constants.hpp` now says to discard any sweep whose noise-floor line is not zero.
To roadmap and shadowplans
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.
No description provided.