From 8d7519a339deb22ec5c1e49cfa6282c300bb1c3c Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 06:15:44 -0400 Subject: [PATCH 1/2] A sun shadow split no wider than a metre across the light is padded as its depth is #548 gave each split's fit at least a metre of depth along the light. The same fit divides by the receivers' extent across the light too: the least squares line's error by their width (size.x), the ortho fit by their x and y ranges, and the perspective fit by its y range (ynear - yfar). Receivers that collapse to a line or a point in the light's view, as a thin sliver of geometry seen edge on does, left infinities in the split's matrix. Since #548 a NaN error takes the ortho fit, which made the zero-width case reach al_ortho with equal left and right planes. The x and y extents take the same metre as the depth, about their middle. A split wider than that fits as before. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 453c82752d..b43490ab78 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -14023,12 +14023,18 @@ void LLPipeline::generateSunShadow(LLCamera& camera) // be infinite, either leaving infinities in the matrices and NaN in every lookup of the split. Each fit takes // at least MIN_SHADOW_DEPTH of depth, the ortho one about the receivers' middle and the perspective one at the // split's far end (fz). Only the depth's scale changes, only in a split with less, and by its depth smoothly. + // Receivers that collapse to a line or a point in the light's view, no wider across it than that, would be + // divided by in the line fit's error and the ortho fit, and the perspective fit's y range would be empty: + // their extent across the light is padded the same way. constexpr F32 MIN_SHADOW_DEPTH = 1.f; - if (max.mV[2] - min.mV[2] < MIN_SHADOW_DEPTH) + for (U32 axis = 0; axis < 3; ++axis) { - const F32 mid = (min.mV[2] + max.mV[2]) * 0.5f; - min.mV[2] = mid - MIN_SHADOW_DEPTH * 0.5f; - max.mV[2] = mid + MIN_SHADOW_DEPTH * 0.5f; + if (max.mV[axis] - min.mV[axis] < MIN_SHADOW_DEPTH) + { + const F32 mid = (min.mV[axis] + max.mV[axis]) * 0.5f; + min.mV[axis] = mid - MIN_SHADOW_DEPTH * 0.5f; + max.mV[axis] = mid + MIN_SHADOW_DEPTH * 0.5f; + } } // Construct a perspective transform with perspective along y-axis that contains From 46fec9d803f98073b29d6e823fef873216f025f0 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 08:26:06 -0400 Subject: [PATCH 2/2] The water no longer sets the sun's split range, only its splits' receivers generateSunShadow takes the split range from the receivers in view: twice the farthest, 16 to 512 m, at most the draw distance. The water's field is among them (getVisibleExtents), and it reaches past the draw distance, so any view of the sea put the farthest receiver at the draw distance and stretched every split as far as the range could go, whatever else was in view. The near cascades lost their resolution to water, whose shadow only dims its glint. The view-wide point cloud that sets the range now leaves the water out (getVisibleExtents and getVisiblePointCloud take whether to count it, on by default). Each split's own cloud still counts it, so a split over open sea is fitted to the water as before. With nothing but water in view the range takes its floor, 16 m, where the empty cloud used to end the pass without sun shadows. waterReceivesShadows is the test both use. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 61 +++++++++++++++++++++++++------------- indra/newview/pipeline.h | 8 +++-- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index b43490ab78..432a245923 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3359,7 +3359,13 @@ bool LLPipeline::visibleObjectsInFrustum(LLCamera& camera) return false; } -bool LLPipeline::getVisibleExtents(LLCamera& camera, LLVector3& min, LLVector3& max) +bool LLPipeline::waterReceivesShadows(LLCamera& camera) +{ + return hasRenderType(LLPipeline::RENDER_TYPE_WATER) && mWaterSurface.inView(camera) && + mWaterSurface.drawnFrom(*LLViewerCamera::getInstance()); +} + +bool LLPipeline::getVisibleExtents(LLCamera& camera, LLVector3& min, LLVector3& max, bool water) { const F32 X = 65536.f; @@ -3395,8 +3401,7 @@ bool LLPipeline::getVisibleExtents(LLCamera& camera, LLVector3& min, LLVector3& // The water has no partition: its field, whole, with its lowest and highest water, and a receiver wherever the // camera meets it. Left out, a view of open sea, or a shadow split holding nothing but water, finds no receivers // and its split is left empty. Not from where the water is not drawn at all. - if (hasRenderType(LLPipeline::RENDER_TYPE_WATER) && mWaterSurface.inView(camera) && - mWaterSurface.drawnFrom(*LLViewerCamera::getInstance())) + if (water && waterReceivesShadows(camera)) { mWaterSurface.addExtents(min, max); res = false; @@ -13242,12 +13247,13 @@ void LLPipeline::addShadowCasters(ALSpotShadowCache::Casters& casters) textured(LLRenderPass::PASS_GRASS); } -bool LLPipeline::getVisiblePointCloud(LLCamera& camera, LLVector3& min, LLVector3& max, std::vector& fp, LLVector3 light_dir) +bool LLPipeline::getVisiblePointCloud(LLCamera& camera, LLVector3& min, LLVector3& max, std::vector& fp, LLVector3 light_dir, + bool water) { LL_PROFILE_ZONE_SCOPED_CATEGORY_PIPELINE; //get point cloud of intersection of frust and min, max - if (getVisibleExtents(camera, min, max)) + if (getVisibleExtents(camera, min, max, water)) { return false; } @@ -13683,9 +13689,13 @@ void LLPipeline::generateSunShadow(LLCamera& camera) main_camera.calcAgentFrustumPlanes(main_camera.mAgentFrustum); LLVector3 min,max; - getVisiblePointCloud(main_camera,min,max,fp); + // The split range is set by the receivers other than the water: its field reaches past the draw distance, and + // among them it would stretch every split to the draw distance whenever the sea is in view. Each split still + // takes the water among its receivers for its own fit, and with nothing but water in view the range takes its + // floor rather than leaving the sun without shadows. + getVisiblePointCloud(main_camera, min, max, fp, LLVector3(0.f, 0.f, 0.f), false); - if (fp.empty()) + if (fp.empty() && !waterReceivesShadows(main_camera)) { if (!hasRenderDebugMask(RENDER_DEBUG_SHADOW_FRUSTA) && !gCubeSnapshot) { @@ -13708,25 +13718,34 @@ void LLPipeline::generateSunShadow(LLCamera& camera) return; } - //get good split distances for frustum - for (U32 i = 0; i < fp.size(); ++i) + // The nearest and farthest receivers' depths, 0 with only water in view. + F32 nearest = 0.f; + F32 farthest = 0.f; + if (!fp.empty()) { - LLVector4a v; - saved_view.affineTransform(LLVector4a(fp[i].mV[0], fp[i].mV[1], fp[i].mV[2], 1.f), v); - fp[i].set(v.getF32ptr()); - } + //get good split distances for frustum + for (U32 i = 0; i < fp.size(); ++i) + { + LLVector4a v; + saved_view.affineTransform(LLVector4a(fp[i].mV[0], fp[i].mV[1], fp[i].mV[2], 1.f), v); + fp[i].set(v.getF32ptr()); + } - min = fp[0]; - max = fp[0]; + min = fp[0]; + max = fp[0]; - //get camera space bounding box - for (U32 i = 1; i < fp.size(); ++i) - { - update_min_max(min, max, fp[i]); + //get camera space bounding box + for (U32 i = 1; i < fp.size(); ++i) + { + update_min_max(min, max, fp[i]); + } + + nearest = -max.mV[2]; + farthest = -min.mV[2]; } - near_clip = llclamp(-max.mV[2], 0.01f, 4.0f); - F32 far_clip = llclamp(-min.mV[2]*2.f, 16.0f, 512.0f); + near_clip = llclamp(nearest, 0.01f, 4.0f); + F32 far_clip = llclamp(farthest*2.f, 16.0f, 512.0f); //far_clip = llmin(far_clip, 128.f); far_clip = llmin(far_clip, camera.getFar()); diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h index 8dfc92f0e2..d9bd461074 100644 --- a/indra/newview/pipeline.h +++ b/indra/newview/pipeline.h @@ -512,8 +512,12 @@ class LLPipeline void updateMovedList(LLDrawable::drawable_vector_t& move_list); void updateMove(); bool visibleObjectsInFrustum(LLCamera& camera); - bool getVisibleExtents(LLCamera& camera, LLVector3 &min, LLVector3& max); - bool getVisiblePointCloud(LLCamera& camera, LLVector3 &min, LLVector3& max, std::vector& fp, LLVector3 light_dir = LLVector3(0,0,0)); + // With `water`, the water's field is among the receivers wherever it receives shadows (waterReceivesShadows). + bool getVisibleExtents(LLCamera& camera, LLVector3 &min, LLVector3& max, bool water = true); + bool getVisiblePointCloud(LLCamera& camera, LLVector3 &min, LLVector3& max, std::vector& fp, LLVector3 light_dir = LLVector3(0,0,0), + bool water = true); + // Whether the water's field receives shadows in `camera`'s frustum: drawn from where the water is, and in view. + bool waterReceivesShadows(LLCamera& camera); // Populate given LLCullResult with results of a frustum cull of the entire scene against the given LLCamera void updateCull(LLCamera& camera, LLCullResult& result);