Shadow-LOD selection needs a number the existing LOD data cannot supp… - #124
Merged
Conversation
…ly. GeometryLod::error is an RMS quadric value — an average, which a locally bad region hides inside: measured on a UV sphere it read 0.0085 against a true surface movement of 0.0170, half the real deviation. Selecting a shadow level against it would silently exceed any budget it was given. Two existing channels were measured and rejected before adding a third. MeshCollapse::deviationRadius is point-to-PLANE, so it reads ~0 for a coplanar collapse whose removed vertex lands outside every surviving face — exactly the in-plane silhouette movement a shadow shows and a camera view barely does. supportRadius is safe but measured 12x loose on a sphere and 21,000x on a near-flat plate, where it grows toward the object's own size; it would pin every planar caster to LOD0 forever. So the simplifier records a dedicated Euclidean channel, from a distance it already computed and discarded: CollapseDeviation::shadow accumulates into MeshCollapse::shadowDeviationRadius, reduces per cut through the pure shadowDeviationForCut (max over the exact collapseCount prefix) onto ProgressiveLod::shadowDeviation, and is carried — validated — to GeometryLod::shadowDeviation. It is deliberately an ESTIMATE, not a bound, and the docs say so: it measures the tangential displacement of the removed sample and is one-sided. On a sphere at LOD2 the simplified surface sat 0.069 from the original while original->simplified sampling reported 0.044; the accumulated estimate covered both at 2.27x, but that is empirical cushion rather than a guarantee. A certified symmetric bound stays a swappable future metric instead of hidden unfinished work here. Invalid data is rejected uniformly. A deviation is a magnitude, so negative, NaN and non-finite all mean invalid, never a cheap collapse, and each becomes infinity to force LOD0. std::max(0.0f, x) is precisely the wrong idiom for that (it returns 0 for a NaN and for a negative) and is used nowhere in the chain. A prefix longer than the recorded stream asserts in debug and returns infinity in release rather than clamping into a plausible under-estimate. The model itself (graphics/shadow_view.hpp) is Vulkan-free and headless: - ShadowView is encapsulated — private state, static factories — so an invalid view cannot be assembled by bypassing a convention, and the perspective factory normalises its own forward. Orthographic covers cascades, world-only cascades and self-shadow layers; each point face is its own perspective view. - Depth is the minimum signed forward projection over all eight bounds corners. A centre distance hides a caster straddling the light; a radial distance overstates depth off-axis (40 units to the side reads 9 deep, not 41). - projectShadowErrorTexels is radial texel displacement, stated as such. The perspective factor sqrt(1 + 2*tanHalfFov^2) is the Jacobian's largest singular value at a frustum corner, evaluated at depth minus the world error because the displacement is finite and the projection steepens toward the light — a frustum-corner test measured an actual 7.4256 texels against a 7.4021 first-order bound. - selectShadowLod validates the LOD chain before selecting, so an invalid deviation reports InvalidCaster rather than a plausible-looking Selected LOD0. Refinement may jump multiple levels to restore the budget; coarsening reaches the coarsest level clearing the stricter threshold. previousLevel is valid only within one draw and one logical view — out of range is reported, never clamped, so a punctual slot reassigned to another light cannot apply one caster's hysteresis to another's geometry. largestSingularValue moves to math/singular_value.hpp, shared with VDPM rather than duplicated, and now rejects non-finite elements up front: std::max(finite, NaN) keeps the finite operand, which would have handed back a plausible scale. No selection behaviour changes: nothing outside tests calls selectShadowLod yet. What does change at runtime is that the simplifier computes one more channel per collapse, and the overlay renders shadow LOD reasons by iterating the enum (a two-column table, since the new fallback names would clip a single line). SH-03 owns the tuning this deliberately refuses to guess: kShadowLodPixelBudget and the coarsening ratio in render/constants.hpp threaded explicitly into the selector, retiring kShadowLodBias, and calibrating both against SH-01's captures and diagnostics. The hysteresis default is an invalid sentinel so no caller inherits an unmeasured value.
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.
…ly. GeometryLod::error is an RMS quadric value — an average, which a locally bad region hides inside: measured on a UV sphere it read 0.0085 against a true surface movement of 0.0170, half the real deviation. Selecting a shadow level against it would silently exceed any budget it was given.
Two existing channels were measured and rejected before adding a third. MeshCollapse::deviationRadius is point-to-PLANE, so it reads ~0 for a coplanar collapse whose removed vertex lands outside every surviving face — exactly the in-plane silhouette movement a shadow shows and a camera view barely does. supportRadius is safe but measured 12x loose on a sphere and 21,000x on a near-flat plate, where it grows toward the object's own size; it would pin every planar caster to LOD0 forever.
So the simplifier records a dedicated Euclidean channel, from a distance it already computed and discarded: CollapseDeviation::shadow accumulates into MeshCollapse::shadowDeviationRadius, reduces per cut through the pure shadowDeviationForCut (max over the exact collapseCount prefix) onto ProgressiveLod::shadowDeviation, and is carried — validated — to GeometryLod::shadowDeviation.
It is deliberately an ESTIMATE, not a bound, and the docs say so: it measures the tangential displacement of the removed sample and is one-sided. On a sphere at LOD2 the simplified surface sat 0.069 from the original while original->simplified sampling reported 0.044; the accumulated estimate covered both at 2.27x, but that is empirical cushion rather than a guarantee. A certified symmetric bound stays a swappable future metric instead of hidden unfinished work here.
Invalid data is rejected uniformly. A deviation is a magnitude, so negative, NaN and non-finite all mean invalid, never a cheap collapse, and each becomes infinity to force LOD0. std::max(0.0f, x) is precisely the wrong idiom for that (it returns 0 for a NaN and for a negative) and is used nowhere in the chain. A prefix longer than the recorded stream asserts in debug and returns infinity in release rather than clamping into a plausible under-estimate.
The model itself (graphics/shadow_view.hpp) is Vulkan-free and headless:
largestSingularValue moves to math/singular_value.hpp, shared with VDPM rather than duplicated, and now rejects non-finite elements up front: std::max(finite, NaN) keeps the finite operand, which would have handed back a plausible scale.
No selection behaviour changes: nothing outside tests calls selectShadowLod yet. What does change at runtime is that the simplifier computes one more channel per collapse, and the overlay renders shadow LOD reasons by iterating the enum (a two-column table, since the new fallback names would clip a single line).
SH-03 owns the tuning this deliberately refuses to guess: kShadowLodPixelBudget and the coarsening ratio in render/constants.hpp threaded explicitly into the selector, retiring kShadowLodBias, and calibrating both against SH-01's captures and diagnostics. The hysteresis default is an invalid sentinel so no caller inherits an unmeasured value.