From 112f55a031a2856416ac5d0c1aaf4c0ce6b9075f Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:14:13 -0500 Subject: [PATCH 1/5] Add polygonal + anamorphic bokeh shaping to Depth of Field Give the DoF bokeh real lens character by reshaping the gather kernel into an aperture-blade polygon and/or an anamorphic oval. Both are pure sample- placement reshapes -- the tap count is unchanged, so the O(CoC^2) gather cost is unaffected. All controls default to neutral values, so the existing circular bokeh is preserved bit-for-bit until a user opts in. - postDeferredF.glsl: new bokehOffset() reshapes tap placement at both the near (FRONT_BLUR) and far gather sites, with a coherent early-out that returns the identical circular offset when inactive. Two CPU-baked vec4 uniforms (bokeh_shape, bokeh_lens) carry the kernel. - ALDoFBokeh::bakeKernel (new aldofbokeh.{h,cpp}): pure helper that clamps the settings and pre-bakes the N-gon trig and area-preserving anamorphic scale on the CPU (mirroring the chromatic-aberration path). Covered by a TUT unit test (aldofbokeh_test.cpp). - pipeline.cpp renderDoF(): reads the settings, bakes once per pass, pushes the two uniforms onto the active post program. - settings_alchemy.xml: 4 opt-in settings (RenderDoFBokehBlades / Roundness / Rotation, RenderDoFAnamorphicRatio), all neutral by default. - llshadermgr.{h,cpp}: register the two reserved DoF bokeh uniforms. Lens/chromatic aberration is intentionally out of scope (a screen-space CA already exists) and left for a follow-up. Co-Authored-By: Claude Opus 4.8 --- indra/llrender/llshadermgr.cpp | 2 + indra/llrender/llshadermgr.h | 2 + indra/newview/CMakeLists.txt | 3 + indra/newview/aldofbokeh.cpp | 63 +++++++++++ indra/newview/aldofbokeh.h | 55 +++++++++ .../newview/app_settings/settings_alchemy.xml | 44 +++++++ .../class1/deferred/postDeferredF.glsl | 40 +++++-- indra/newview/pipeline.cpp | 13 +++ indra/newview/tests/aldofbokeh_test.cpp | 107 ++++++++++++++++++ 9 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 indra/newview/aldofbokeh.cpp create mode 100644 indra/newview/aldofbokeh.h create mode 100644 indra/newview/tests/aldofbokeh_test.cpp diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index eb38126c10..ef3c094918 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1416,6 +1416,8 @@ void LLShaderMgr::initAttribsAndUniforms() mReservedUniforms.push_back("res_scale"); mReservedUniforms.push_back("dof_width"); mReservedUniforms.push_back("dof_height"); + mReservedUniforms.push_back("bokeh_shape"); + mReservedUniforms.push_back("bokeh_lens"); mReservedUniforms.push_back("depthMap"); mReservedUniforms.push_back("shadowMap0"); diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 8e9a251851..60c965c013 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -220,6 +220,8 @@ class LLShaderMgr DOF_RES_SCALE, // "res_scale" DOF_WIDTH, // "dof_width" DOF_HEIGHT, // "dof_height" + DOF_BOKEH_SHAPE, // "bokeh_shape" (vec4: 2pi/N, pi/N, cos(pi/N), roundness) + DOF_BOKEH_LENS, // "bokeh_lens" (vec4: rotationRad, anisoX, anisoY, active) DEFERRED_DEPTH, // "depthMap" DEFERRED_SHADOW0, // "shadowMap0" diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 42c23a6fc6..6146c68ef9 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -173,6 +173,7 @@ set(viewer_SOURCE_FILES alfloatersceneexplorer.cpp alfloatersceneexplorerfilters.cpp alfloatertransactionlog.cpp + aldofbokeh.cpp alsceneexplorerpredicate.cpp alfloaterwebprofile.cpp allegacynotificationwellwindow.cpp @@ -941,6 +942,7 @@ set(viewer_HEADER_FILES alfloatersceneexplorer.h alfloatersceneexplorerfilters.h alfloatertransactionlog.h + aldofbokeh.h alsceneexplorerpredicate.h alfloaterwebprofile.h allegacynotificationwellwindow.h @@ -2371,6 +2373,7 @@ if (BUILD_TESTING) # This creates a separate test project per file listed. SET(viewer_TEST_SOURCE_FILES + aldofbokeh.cpp alsceneexplorerpredicate.cpp llagentaccess.cpp lldateutil.cpp diff --git a/indra/newview/aldofbokeh.cpp b/indra/newview/aldofbokeh.cpp new file mode 100644 index 0000000000..a1d350317a --- /dev/null +++ b/indra/newview/aldofbokeh.cpp @@ -0,0 +1,63 @@ +/** + * @file aldofbokeh.cpp + * @brief Pure CPU-side baking of depth-of-field bokeh (aperture / anamorphic) kernel parameters. + * + * Copyright (c) 2026, Alchemy Viewer Project + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include "aldofbokeh.h" + +#include "llmath.h" + +namespace ALDoFBokeh +{ + Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio) + { + // Clamp to documented ranges up front: a bad debug-setting value must + // never reach the gather shader. Fewer than 3 blades has no polygon. + const S32 clampedBlades = (blades < 3) ? 0 : llmin(blades, 12); + roundness = llclamp(roundness, 0.f, 1.f); + anamorphicRatio = llclamp(anamorphicRatio, 0.25f, 4.f); + + Kernel k; + + if (clampedBlades >= 3) + { + const F32 n = (F32)clampedBlades; + k.seg = 2.f * F_PI / n; + k.halfSeg = F_PI / n; + k.edge = cosf(k.halfSeg); // inscribed radius of the N-gon + } + else + { + // Neutral constants the shader reads as "circular". + k.seg = 0.f; + k.halfSeg = 0.f; + k.edge = 1.f; + } + + k.roundness = roundness; + k.rotationRad = rotationDeg * DEG_TO_RAD; + + // Area-preserving anamorphic squeeze: stretch one axis and shrink the + // other by the same factor so overall blur energy stays constant. + const F32 s = sqrtf(anamorphicRatio); + k.anisoX = 1.f / s; + k.anisoY = s; + + // Only mark the kernel active when it actually reshapes the bokeh, so the + // default (no polygon, ratio 1.0) leaves the gather on its circular path. + k.active = (clampedBlades >= 3) || (anamorphicRatio != 1.f); + + return k; + } +} diff --git a/indra/newview/aldofbokeh.h b/indra/newview/aldofbokeh.h new file mode 100644 index 0000000000..b6f94a92ac --- /dev/null +++ b/indra/newview/aldofbokeh.h @@ -0,0 +1,55 @@ +/** + * @file aldofbokeh.h + * @brief Pure CPU-side baking of depth-of-field bokeh (aperture / anamorphic) kernel parameters. + * + * Copyright (c) 2026, Alchemy Viewer Project + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#ifndef AL_DOFBOKEH_H +#define AL_DOFBOKEH_H + +#include "stdtypes.h" + +// Depth-of-field bokeh shaping. +// +// The DoF gather (postDeferredF.glsl) places its blur taps on a circle. To give +// the bokeh real lens character we reshape WHERE each tap lands -- into an +// aperture-blade polygon and/or an anamorphic oval -- without changing HOW MANY +// taps are taken, so the O(CoC^2) gather cost is unaffected. +// +// The trig the shader needs is constant across the whole frame, so we bake it +// once here on the CPU (mirroring the chromatic-aberration path in +// LLPipeline::colorCorrect) and hand the shader a ready-to-use form. +namespace ALDoFBokeh +{ + // Shader-ready bokeh kernel. Packs directly into two vec4 uniforms consumed + // by postDeferredF.glsl: + // bokeh_shape = (seg, halfSeg, edge, roundness) + // bokeh_lens = (rotationRad, anisoX, anisoY, active ? 1 : 0) + struct Kernel + { + bool active; // false => shader takes the plain circular path (today's look & cost) + F32 seg; // 2*pi / blades (0 when no polygon) + F32 halfSeg; // pi / blades (0 when no polygon) + F32 edge; // cos(pi / blades) -- inscribed N-gon radius factor (1 when no polygon) + F32 roundness; // 0 = crisp polygon .. 1 = circle + F32 rotationRad; // aperture rotation, radians + F32 anisoX; // anamorphic per-axis scale, area preserving: anisoX * anisoY == 1 + F32 anisoY; + }; + + // Bake the shader kernel from raw setting values. Inputs are clamped to their + // documented, sane ranges here so an out-of-range debug setting can never feed + // garbage into the gather: blades below 3 disable the polygon (circular); + // roundness clamps to [0, 1]; anamorphicRatio clamps to [0.25, 4] (1 = off). + Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio); +} + +#endif // AL_DOFBOKEH_H diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index f0251c620f..6470ff998f 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2761,6 +2761,50 @@ Value 0 + RenderDoFBokehBlades + + Comment + Depth of Field bokeh aperture blade count. Fewer than 3 gives a round bokeh (off/default); 5-9 emulates a partially-closed lens iris (pentagon..enneagon). Range 0 to 12. + Persist + 1 + Type + S32 + Value + 0 + + RenderDoFBokehRoundness + + Comment + How rounded the bokeh polygon edges are when RenderDoFBokehBlades is 3 or more. 0 is a crisp straight-edged polygon; 1 is a full circle. Range 0 to 1. + Persist + 1 + Type + F32 + Value + 0.0 + + RenderDoFBokehRotation + + Comment + Rotation of the bokeh aperture polygon, in degrees. Only meaningful when RenderDoFBokehBlades is 3 or more. Range 0 to 360. + Persist + 1 + Type + F32 + Value + 0.0 + + RenderDoFAnamorphicRatio + + Comment + Anamorphic bokeh squeeze. 1.0 is circular (off/default); greater than 1 stretches the bokeh into a vertical oval for an anamorphic-lens look. Area preserving. Range 0.25 to 4. + Persist + 1 + Type + F32 + Value + 1.0 + AllowNoCopyRezRestoreToWorld Comment diff --git a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl index 7167c8e170..7b9aa4420f 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl @@ -34,6 +34,10 @@ uniform vec2 screen_res; uniform float max_cof; uniform float res_scale; +// Bokeh aperture shaping, CPU-baked in LLPipeline::renderDoF() via ALDoFBokeh::bakeKernel(). +uniform vec4 bokeh_shape; // (2pi/N, pi/N, cos(pi/N), roundness); N = aperture blade count +uniform vec4 bokeh_lens; // (rotation_radians, anamorphic_x, anamorphic_y, active flag) + in vec2 vary_fragcoord; void dofSample(inout vec4 diff, inout float w, float min_sc, vec2 tc) @@ -69,6 +73,28 @@ void dofSampleNear(inout vec4 diff, inout float w, float min_sc, vec2 tc) w += wg; } +// Aperture-shaped bokeh tap offset. Reshapes WHERE a tap lands (aperture polygon +// and/or anamorphic squeeze); the caller's tap COUNT is unchanged, so this does +// not affect the O(CoC^2) gather cost. When the kernel is inactive it returns the +// exact circular offset the gather used before, so the default look and cost are +// preserved bit-for-bit. +vec2 bokehOffset(float r, float a) +{ + if (bokeh_lens.w < 0.5) + return vec2(sin(a), cos(a)) * r; // fast path: unchanged circular bokeh + + a += bokeh_lens.x; // aperture rotation + float shape = 1.0; + if (bokeh_shape.x > 0.0) // polygon active (blades >= 3) + { + // Fold the angle into one blade segment and pinch the ring onto the + // inscribed N-gon edge, then blend back toward a circle by roundness. + float s = mod(a, bokeh_shape.x) - bokeh_shape.y; + shape = mix(bokeh_shape.z / cos(s), 1.0, bokeh_shape.w); + } + return vec2(sin(a), cos(a)) * (r * shape) * bokeh_lens.yz; // anamorphic per-axis squeeze +} + vec3 clampHDRRange(vec3 color); void main() @@ -94,10 +120,9 @@ void main() for (int i=0; i bokeh_blades(gSavedSettings, "RenderDoFBokehBlades", 0); + static LLCachedControl bokeh_roundness(gSavedSettings, "RenderDoFBokehRoundness", 0.f); + static LLCachedControl bokeh_rotation(gSavedSettings, "RenderDoFBokehRotation", 0.f); + static LLCachedControl bokeh_anamorphic(gSavedSettings, "RenderDoFAnamorphicRatio", 1.f); + ALDoFBokeh::Kernel bokeh = ALDoFBokeh::bakeKernel(bokeh_blades, bokeh_roundness, bokeh_rotation, bokeh_anamorphic); + post_program.uniform4f(LLShaderMgr::DOF_BOKEH_SHAPE, bokeh.seg, bokeh.halfSeg, bokeh.edge, bokeh.roundness); + post_program.uniform4f(LLShaderMgr::DOF_BOKEH_LENS, bokeh.rotationRad, bokeh.anisoX, bokeh.anisoY, bokeh.active ? 1.f : 0.f); + mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); diff --git a/indra/newview/tests/aldofbokeh_test.cpp b/indra/newview/tests/aldofbokeh_test.cpp new file mode 100644 index 0000000000..398feb6c99 --- /dev/null +++ b/indra/newview/tests/aldofbokeh_test.cpp @@ -0,0 +1,107 @@ +/** + * @file aldofbokeh_test.cpp + * @brief Unit tests for the pure DoF bokeh-kernel baking (ALDoFBokeh::bakeKernel) + * + * Copyright (c) 2026, Alchemy Viewer Project + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include "../test/lltut.h" + +#include "../aldofbokeh.h" + +#include "llmath.h" + +namespace tut +{ + struct dofbokeh_data + { + }; + + typedef test_group dofbokeh_group; + typedef dofbokeh_group::object dofbokeh_object; + tut::dofbokeh_group dofbokehinst("ALDoFBokeh"); + + using namespace ALDoFBokeh; + + // The default/neutral setting values must produce an INACTIVE kernel whose + // packed constants are the identity the shader's circular fast-path relies on. + template<> template<> + void dofbokeh_object::test<1>() + { + Kernel k = bakeKernel(0, 0.f, 0.f, 1.f); + ensure("neutral input is inactive", !k.active); + ensure_equals("no polygon segment", k.seg, 0.f); + ensure_equals("edge factor is unity", k.edge, 1.f); + ensure_distance("aniso x unity", k.anisoX, 1.f, 1e-6f); + ensure_distance("aniso y unity", k.anisoY, 1.f, 1e-6f); + } + + // Any blade count below 3 is treated as "no polygon" (a 2-gon is degenerate). + template<> template<> + void dofbokeh_object::test<2>() + { + for (S32 n = 0; n < 3; ++n) + { + Kernel k = bakeKernel(n, 0.5f, 0.f, 1.f); + ensure("blades<3 is inactive", !k.active); + ensure_equals("blades<3 has no polygon segment", k.seg, 0.f); + } + } + + // A hexagonal aperture bakes exactly the documented N-gon constants. + template<> template<> + void dofbokeh_object::test<3>() + { + Kernel k = bakeKernel(6, 0.f, 0.f, 1.f); + ensure("hexagon is active", k.active); + ensure_distance("seg = 2pi/6", k.seg, F_PI / 3.f, 1e-5f); + ensure_distance("halfSeg = pi/6", k.halfSeg, F_PI / 6.f, 1e-5f); + ensure_distance("edge = cos(pi/6)", k.edge, cosf(F_PI / 6.f), 1e-5f); + } + + // Rotation is converted from degrees to radians. + template<> template<> + void dofbokeh_object::test<4>() + { + Kernel k = bakeKernel(6, 0.f, 90.f, 1.f); + ensure_distance("90 deg -> pi/2 rad", k.rotationRad, F_PI * 0.5f, 1e-5f); + } + + // Anamorphic squeeze activates on its own and is area preserving. + template<> template<> + void dofbokeh_object::test<5>() + { + Kernel k = bakeKernel(0, 0.f, 0.f, 1.6f); + ensure("anamorphic alone is active", k.active); + ensure_distance("area is preserved", k.anisoX * k.anisoY, 1.f, 1e-5f); + ensure("long axis grows", k.anisoY > 1.f); + ensure("short axis shrinks", k.anisoX < 1.f); + } + + // Out-of-range inputs are clamped, never passed through. + template<> template<> + void dofbokeh_object::test<6>() + { + // blades 999 -> clamped to 12 (still a valid, active polygon); + // roundness 5 -> clamped to 1; ratio 99 -> clamped to 4 (sqrt = 2). + Kernel hi = bakeKernel(999, 5.f, 0.f, 99.f); + ensure("clamped blades still active", hi.active); + ensure("roundness clamped to <= 1", hi.roundness <= 1.f); + ensure("edge stays a valid cosine", hi.edge > 0.f && hi.edge < 1.f); + ensure_distance("anamorphic ratio clamped high", hi.anisoY, 2.f, 1e-4f); + + // roundness -1 -> clamped to 0; ratio 0.01 -> clamped to 0.25 (sqrt = 0.5). + Kernel lo = bakeKernel(6, -1.f, 0.f, 0.01f); + ensure("roundness clamped to >= 0", lo.roundness >= 0.f); + ensure_distance("anamorphic ratio clamped low", lo.anisoX, 2.f, 1e-4f); + } +} From 904bf5285b6807a12449cdfd6cee560331c45660 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:05:45 -0500 Subject: [PATCH 2/5] Rotate the whole bokeh aperture; add bokeh intensity control Two refinements to the DoF bokeh shaping: - Rotation now rigidly rotates the entire aperture. Previously rotation was folded into the sampling angle while the anamorphic scale was applied on the fixed screen axes, so the oval never tilted (only the polygon rotated). Build the aperture in its own frame, then rotate the whole offset -- polygon AND anamorphic oval spin together. cos/sin(rotation) is loop-invariant so it is hoisted out of the gather loop. - Add RenderDoFBokehIntensity (default 1.0): scales the gather's existing highlight weighting (wg = 0.25 + intensity*(r+g+b)) so bright out-of-focus points form brighter, more defined bokeh discs -- which makes the aperture shape and its rotation legible. Independent of the offset reshape, so it also intensifies a plain round bokeh. 1.0 reproduces the classic weighting exactly; 0 gives a flat soft blur. Baked/clamped in ALDoFBokeh::bakeKernel (unit test extended to 7 cases) and carried by a new bokeh_intensity reserved uniform. Co-Authored-By: Claude Opus 4.8 --- indra/llrender/llshadermgr.cpp | 1 + indra/llrender/llshadermgr.h | 1 + indra/newview/aldofbokeh.cpp | 11 ++++-- indra/newview/aldofbokeh.h | 18 ++++++---- .../newview/app_settings/settings_alchemy.xml | 11 ++++++ .../class1/deferred/postDeferredF.glsl | 31 ++++++++++------- indra/newview/pipeline.cpp | 4 ++- indra/newview/tests/aldofbokeh_test.cpp | 34 ++++++++++++++----- 8 files changed, 80 insertions(+), 31 deletions(-) diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index ef3c094918..669fe37361 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1418,6 +1418,7 @@ void LLShaderMgr::initAttribsAndUniforms() mReservedUniforms.push_back("dof_height"); mReservedUniforms.push_back("bokeh_shape"); mReservedUniforms.push_back("bokeh_lens"); + mReservedUniforms.push_back("bokeh_intensity"); mReservedUniforms.push_back("depthMap"); mReservedUniforms.push_back("shadowMap0"); diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 60c965c013..83c5ee875f 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -222,6 +222,7 @@ class LLShaderMgr DOF_HEIGHT, // "dof_height" DOF_BOKEH_SHAPE, // "bokeh_shape" (vec4: 2pi/N, pi/N, cos(pi/N), roundness) DOF_BOKEH_LENS, // "bokeh_lens" (vec4: rotationRad, anisoX, anisoY, active) + DOF_BOKEH_INTENSITY, // "bokeh_intensity" (float: highlight-weight scale) DEFERRED_DEPTH, // "depthMap" DEFERRED_SHADOW0, // "shadowMap0" diff --git a/indra/newview/aldofbokeh.cpp b/indra/newview/aldofbokeh.cpp index a1d350317a..7a2f3cbd12 100644 --- a/indra/newview/aldofbokeh.cpp +++ b/indra/newview/aldofbokeh.cpp @@ -20,13 +20,14 @@ namespace ALDoFBokeh { - Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio) + Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio, F32 intensity) { // Clamp to documented ranges up front: a bad debug-setting value must // never reach the gather shader. Fewer than 3 blades has no polygon. const S32 clampedBlades = (blades < 3) ? 0 : llmin(blades, 12); roundness = llclamp(roundness, 0.f, 1.f); anamorphicRatio = llclamp(anamorphicRatio, 0.25f, 4.f); + intensity = llclamp(intensity, 0.f, 16.f); Kernel k; @@ -54,8 +55,14 @@ namespace ALDoFBokeh k.anisoX = 1.f / s; k.anisoY = s; - // Only mark the kernel active when it actually reshapes the bokeh, so the + // Highlight-weight scale for the gather. Independent of the offset reshape + // (it also intensifies a plain round bokeh); 1.0 reproduces the classic + // weighting exactly. + k.intensity = intensity; + + // Only mark the kernel active when it actually reshapes the OFFSET, so the // default (no polygon, ratio 1.0) leaves the gather on its circular path. + // Intensity is deliberately excluded -- it is applied regardless. k.active = (clampedBlades >= 3) || (anamorphicRatio != 1.f); return k; diff --git a/indra/newview/aldofbokeh.h b/indra/newview/aldofbokeh.h index b6f94a92ac..b0156c7ae7 100644 --- a/indra/newview/aldofbokeh.h +++ b/indra/newview/aldofbokeh.h @@ -29,13 +29,14 @@ // LLPipeline::colorCorrect) and hand the shader a ready-to-use form. namespace ALDoFBokeh { - // Shader-ready bokeh kernel. Packs directly into two vec4 uniforms consumed - // by postDeferredF.glsl: - // bokeh_shape = (seg, halfSeg, edge, roundness) - // bokeh_lens = (rotationRad, anisoX, anisoY, active ? 1 : 0) + // Shader-ready bokeh kernel. The geometry packs into two vec4 uniforms + // consumed by postDeferredF.glsl, plus a scalar intensity uniform: + // bokeh_shape = (seg, halfSeg, edge, roundness) + // bokeh_lens = (rotationRad, anisoX, anisoY, active ? 1 : 0) + // bokeh_intensity = intensity struct Kernel { - bool active; // false => shader takes the plain circular path (today's look & cost) + bool active; // false => shader takes the plain circular path for the OFFSET F32 seg; // 2*pi / blades (0 when no polygon) F32 halfSeg; // pi / blades (0 when no polygon) F32 edge; // cos(pi / blades) -- inscribed N-gon radius factor (1 when no polygon) @@ -43,13 +44,16 @@ namespace ALDoFBokeh F32 rotationRad; // aperture rotation, radians F32 anisoX; // anamorphic per-axis scale, area preserving: anisoX * anisoY == 1 F32 anisoY; + F32 intensity; // highlight-weight scale: 1 = default, higher = more "pop", 0 = flat. + // Independent of `active` -- it also intensifies a round bokeh. }; // Bake the shader kernel from raw setting values. Inputs are clamped to their // documented, sane ranges here so an out-of-range debug setting can never feed // garbage into the gather: blades below 3 disable the polygon (circular); - // roundness clamps to [0, 1]; anamorphicRatio clamps to [0.25, 4] (1 = off). - Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio); + // roundness clamps to [0, 1]; anamorphicRatio clamps to [0.25, 4] (1 = off); + // intensity clamps to [0, 16] (1 = off). + Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio, F32 intensity); } #endif // AL_DOFBOKEH_H diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 6470ff998f..aa86f49393 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2805,6 +2805,17 @@ Value 1.0 + RenderDoFBokehIntensity + + Comment + How strongly bright out-of-focus highlights dominate the bokeh. 1.0 is the default; higher makes highlights "pop" into brighter, more defined bokeh discs (which makes the aperture shape and rotation easier to see); 0 gives a flat, uniform soft blur. Range 0 to 16. + Persist + 1 + Type + F32 + Value + 1.0 + AllowNoCopyRezRestoreToWorld Comment diff --git a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl index 7b9aa4420f..b0a902a895 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl @@ -35,8 +35,9 @@ uniform float max_cof; uniform float res_scale; // Bokeh aperture shaping, CPU-baked in LLPipeline::renderDoF() via ALDoFBokeh::bakeKernel(). -uniform vec4 bokeh_shape; // (2pi/N, pi/N, cos(pi/N), roundness); N = aperture blade count -uniform vec4 bokeh_lens; // (rotation_radians, anamorphic_x, anamorphic_y, active flag) +uniform vec4 bokeh_shape; // (2pi/N, pi/N, cos(pi/N), roundness); N = aperture blade count +uniform vec4 bokeh_lens; // (rotation_radians, anamorphic_x, anamorphic_y, active flag) +uniform float bokeh_intensity; // highlight-weight scale (1 = classic pop, higher = more defined) in vec2 vary_fragcoord; @@ -48,10 +49,9 @@ void dofSample(inout vec4 diff, inout float w, float min_sc, vec2 tc) if (sc > min_sc) //sampled pixel is more "out of focus" than current sample radius { - float wg = 0.25; - - // de-weight dull areas to make highlights 'pop' - wg += s.r+s.g+s.b; + // de-weight dull areas to make highlights 'pop'; bokeh_intensity scales how + // strongly bright samples dominate the disc (1.0 = classic behavior). + float wg = 0.25 + bokeh_intensity*(s.r+s.g+s.b); diff += wg*s; @@ -63,10 +63,9 @@ void dofSampleNear(inout vec4 diff, inout float w, float min_sc, vec2 tc) { vec4 s = texture(diffuseRect, tc); - float wg = 0.25; - - // de-weight dull areas to make highlights 'pop' - wg += s.r+s.g+s.b; + // de-weight dull areas to make highlights 'pop'; bokeh_intensity scales how + // strongly bright samples dominate the disc (1.0 = classic behavior). + float wg = 0.25 + bokeh_intensity*(s.r+s.g+s.b); diff += wg*s; @@ -83,7 +82,8 @@ vec2 bokehOffset(float r, float a) if (bokeh_lens.w < 0.5) return vec2(sin(a), cos(a)) * r; // fast path: unchanged circular bokeh - a += bokeh_lens.x; // aperture rotation + // Build the aperture in its own (unrotated) frame: N-gon radius, then the + // anamorphic per-axis squeeze. float shape = 1.0; if (bokeh_shape.x > 0.0) // polygon active (blades >= 3) { @@ -92,7 +92,14 @@ vec2 bokehOffset(float r, float a) float s = mod(a, bokeh_shape.x) - bokeh_shape.y; shape = mix(bokeh_shape.z / cos(s), 1.0, bokeh_shape.w); } - return vec2(sin(a), cos(a)) * (r * shape) * bokeh_lens.yz; // anamorphic per-axis squeeze + vec2 local = vec2(sin(a), cos(a)) * (r * shape) * bokeh_lens.yz; + + // Rigidly rotate the whole aperture -- polygon AND anamorphic oval -- into + // screen space. bokeh_lens.x is a uniform, so this cos/sin is loop-invariant + // and gets hoisted out of the gather loop by the compiler. + float cs = cos(bokeh_lens.x); + float sn = sin(bokeh_lens.x); + return vec2(local.x * cs - local.y * sn, local.x * sn + local.y * cs); } vec3 clampHDRRange(vec3 color); diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index f59923bef0..74e4b5439d 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -8746,9 +8746,11 @@ void LLPipeline::renderDoF(LLRenderTarget* src, LLRenderTarget* dst) static LLCachedControl bokeh_roundness(gSavedSettings, "RenderDoFBokehRoundness", 0.f); static LLCachedControl bokeh_rotation(gSavedSettings, "RenderDoFBokehRotation", 0.f); static LLCachedControl bokeh_anamorphic(gSavedSettings, "RenderDoFAnamorphicRatio", 1.f); - ALDoFBokeh::Kernel bokeh = ALDoFBokeh::bakeKernel(bokeh_blades, bokeh_roundness, bokeh_rotation, bokeh_anamorphic); + static LLCachedControl bokeh_intensity(gSavedSettings, "RenderDoFBokehIntensity", 1.f); + ALDoFBokeh::Kernel bokeh = ALDoFBokeh::bakeKernel(bokeh_blades, bokeh_roundness, bokeh_rotation, bokeh_anamorphic, bokeh_intensity); post_program.uniform4f(LLShaderMgr::DOF_BOKEH_SHAPE, bokeh.seg, bokeh.halfSeg, bokeh.edge, bokeh.roundness); post_program.uniform4f(LLShaderMgr::DOF_BOKEH_LENS, bokeh.rotationRad, bokeh.anisoX, bokeh.anisoY, bokeh.active ? 1.f : 0.f); + post_program.uniform1f(LLShaderMgr::DOF_BOKEH_INTENSITY, bokeh.intensity); mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); diff --git a/indra/newview/tests/aldofbokeh_test.cpp b/indra/newview/tests/aldofbokeh_test.cpp index 398feb6c99..c3a3d0e83a 100644 --- a/indra/newview/tests/aldofbokeh_test.cpp +++ b/indra/newview/tests/aldofbokeh_test.cpp @@ -33,16 +33,18 @@ namespace tut using namespace ALDoFBokeh; // The default/neutral setting values must produce an INACTIVE kernel whose - // packed constants are the identity the shader's circular fast-path relies on. + // packed constants are the identity the shader's circular fast-path relies on, + // and a neutral (1.0) intensity. template<> template<> void dofbokeh_object::test<1>() { - Kernel k = bakeKernel(0, 0.f, 0.f, 1.f); + Kernel k = bakeKernel(0, 0.f, 0.f, 1.f, 1.f); ensure("neutral input is inactive", !k.active); ensure_equals("no polygon segment", k.seg, 0.f); ensure_equals("edge factor is unity", k.edge, 1.f); ensure_distance("aniso x unity", k.anisoX, 1.f, 1e-6f); ensure_distance("aniso y unity", k.anisoY, 1.f, 1e-6f); + ensure_distance("intensity unity", k.intensity, 1.f, 1e-6f); } // Any blade count below 3 is treated as "no polygon" (a 2-gon is degenerate). @@ -51,7 +53,7 @@ namespace tut { for (S32 n = 0; n < 3; ++n) { - Kernel k = bakeKernel(n, 0.5f, 0.f, 1.f); + Kernel k = bakeKernel(n, 0.5f, 0.f, 1.f, 1.f); ensure("blades<3 is inactive", !k.active); ensure_equals("blades<3 has no polygon segment", k.seg, 0.f); } @@ -61,7 +63,7 @@ namespace tut template<> template<> void dofbokeh_object::test<3>() { - Kernel k = bakeKernel(6, 0.f, 0.f, 1.f); + Kernel k = bakeKernel(6, 0.f, 0.f, 1.f, 1.f); ensure("hexagon is active", k.active); ensure_distance("seg = 2pi/6", k.seg, F_PI / 3.f, 1e-5f); ensure_distance("halfSeg = pi/6", k.halfSeg, F_PI / 6.f, 1e-5f); @@ -72,7 +74,7 @@ namespace tut template<> template<> void dofbokeh_object::test<4>() { - Kernel k = bakeKernel(6, 0.f, 90.f, 1.f); + Kernel k = bakeKernel(6, 0.f, 90.f, 1.f, 1.f); ensure_distance("90 deg -> pi/2 rad", k.rotationRad, F_PI * 0.5f, 1e-5f); } @@ -80,28 +82,42 @@ namespace tut template<> template<> void dofbokeh_object::test<5>() { - Kernel k = bakeKernel(0, 0.f, 0.f, 1.6f); + Kernel k = bakeKernel(0, 0.f, 0.f, 1.6f, 1.f); ensure("anamorphic alone is active", k.active); ensure_distance("area is preserved", k.anisoX * k.anisoY, 1.f, 1e-5f); ensure("long axis grows", k.anisoY > 1.f); ensure("short axis shrinks", k.anisoX < 1.f); } - // Out-of-range inputs are clamped, never passed through. + // Out-of-range geometry inputs are clamped, never passed through. template<> template<> void dofbokeh_object::test<6>() { // blades 999 -> clamped to 12 (still a valid, active polygon); // roundness 5 -> clamped to 1; ratio 99 -> clamped to 4 (sqrt = 2). - Kernel hi = bakeKernel(999, 5.f, 0.f, 99.f); + Kernel hi = bakeKernel(999, 5.f, 0.f, 99.f, 1.f); ensure("clamped blades still active", hi.active); ensure("roundness clamped to <= 1", hi.roundness <= 1.f); ensure("edge stays a valid cosine", hi.edge > 0.f && hi.edge < 1.f); ensure_distance("anamorphic ratio clamped high", hi.anisoY, 2.f, 1e-4f); // roundness -1 -> clamped to 0; ratio 0.01 -> clamped to 0.25 (sqrt = 0.5). - Kernel lo = bakeKernel(6, -1.f, 0.f, 0.01f); + Kernel lo = bakeKernel(6, -1.f, 0.f, 0.01f, 1.f); ensure("roundness clamped to >= 0", lo.roundness >= 0.f); ensure_distance("anamorphic ratio clamped low", lo.anisoX, 2.f, 1e-4f); } + + // Intensity is an independent, clamped passthrough: default 1.0 preserved, + // out-of-range clamped, and it does not by itself activate the offset reshape. + template<> template<> + void dofbokeh_object::test<7>() + { + ensure_distance("default intensity passes through", bakeKernel(0, 0.f, 0.f, 1.f, 1.f).intensity, 1.f, 1e-6f); + ensure_distance("a mid intensity passes through", bakeKernel(0, 0.f, 0.f, 1.f, 4.f).intensity, 4.f, 1e-6f); + ensure_distance("negative intensity clamps to 0", bakeKernel(0, 0.f, 0.f, 1.f, -5.f).intensity, 0.f, 1e-6f); + ensure_distance("huge intensity clamps to 16", bakeKernel(0, 0.f, 0.f, 1.f, 999.f).intensity, 16.f, 1e-6f); + + // Intensity alone must not flip the offset reshape on. + ensure("intensity alone does not activate offset", !bakeKernel(0, 0.f, 0.f, 1.f, 8.f).active); + } } From c0ad6dd15fba082b6ee5c7a687212b87f04e6776 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:28:13 -0500 Subject: [PATCH 3/5] Make bokeh intensity an exponent so its range stays useful The intensity control was a linear weight multiplier, but the gather is a normalized weighted average -- a global weight scale cancels in the ratio, so past ~1.0 the control saturated (0->1 was a strong change, 1->16 barely moved). Reparameterize intensity as an exponent on the highlight weight (wg = 0.25 + pow(lum, intensity)): changing the exponent changes which samples dominate rather than applying a scale that cancels, so every step keeps having an effect. 1.0 stays a bit-exact linear fast path (existing DoF unchanged); the base is clamped for overflow safety and the range tightened to [0, 8] (an exponent of 8 is already extreme). Settings help text and the unit test updated. Co-Authored-By: Claude Opus 4.8 --- indra/newview/aldofbokeh.cpp | 2 +- indra/newview/aldofbokeh.h | 7 ++++--- .../newview/app_settings/settings_alchemy.xml | 2 +- .../class1/deferred/postDeferredF.glsl | 20 ++++++++++++------- indra/newview/tests/aldofbokeh_test.cpp | 2 +- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/indra/newview/aldofbokeh.cpp b/indra/newview/aldofbokeh.cpp index 7a2f3cbd12..703d19df0e 100644 --- a/indra/newview/aldofbokeh.cpp +++ b/indra/newview/aldofbokeh.cpp @@ -27,7 +27,7 @@ namespace ALDoFBokeh const S32 clampedBlades = (blades < 3) ? 0 : llmin(blades, 12); roundness = llclamp(roundness, 0.f, 1.f); anamorphicRatio = llclamp(anamorphicRatio, 0.25f, 4.f); - intensity = llclamp(intensity, 0.f, 16.f); + intensity = llclamp(intensity, 0.f, 8.f); Kernel k; diff --git a/indra/newview/aldofbokeh.h b/indra/newview/aldofbokeh.h index b0156c7ae7..b9767a4562 100644 --- a/indra/newview/aldofbokeh.h +++ b/indra/newview/aldofbokeh.h @@ -44,15 +44,16 @@ namespace ALDoFBokeh F32 rotationRad; // aperture rotation, radians F32 anisoX; // anamorphic per-axis scale, area preserving: anisoX * anisoY == 1 F32 anisoY; - F32 intensity; // highlight-weight scale: 1 = default, higher = more "pop", 0 = flat. - // Independent of `active` -- it also intensifies a round bokeh. + F32 intensity; // highlight-weight EXPONENT: 1 = classic, higher = punchier / more + // defined highlights, 0 = flat. Independent of `active` (it also + // intensifies a round bokeh). }; // Bake the shader kernel from raw setting values. Inputs are clamped to their // documented, sane ranges here so an out-of-range debug setting can never feed // garbage into the gather: blades below 3 disable the polygon (circular); // roundness clamps to [0, 1]; anamorphicRatio clamps to [0.25, 4] (1 = off); - // intensity clamps to [0, 16] (1 = off). + // intensity clamps to [0, 8] (1 = off; the shader applies it as an exponent). Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio, F32 intensity); } diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index aa86f49393..eb12b44c94 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2808,7 +2808,7 @@ RenderDoFBokehIntensity Comment - How strongly bright out-of-focus highlights dominate the bokeh. 1.0 is the default; higher makes highlights "pop" into brighter, more defined bokeh discs (which makes the aperture shape and rotation easier to see); 0 gives a flat, uniform soft blur. Range 0 to 16. + Exponent controlling how sharply bright out-of-focus highlights dominate the bokeh. 1.0 is the default (classic look); higher values (try 2 to 4) make bright points form punchier, more defined bokeh discs, which makes the aperture shape and rotation easier to see; below 1 softens toward a flat, uniform blur (0 = fully flat). Range 0 to 8. Persist 1 Type diff --git a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl index b0a902a895..d46a81e00f 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl @@ -37,7 +37,7 @@ uniform float res_scale; // Bokeh aperture shaping, CPU-baked in LLPipeline::renderDoF() via ALDoFBokeh::bakeKernel(). uniform vec4 bokeh_shape; // (2pi/N, pi/N, cos(pi/N), roundness); N = aperture blade count uniform vec4 bokeh_lens; // (rotation_radians, anamorphic_x, anamorphic_y, active flag) -uniform float bokeh_intensity; // highlight-weight scale (1 = classic pop, higher = more defined) +uniform float bokeh_intensity; // exponent on the highlight weight (1 = classic, higher = more defined) in vec2 vary_fragcoord; @@ -49,9 +49,13 @@ void dofSample(inout vec4 diff, inout float w, float min_sc, vec2 tc) if (sc > min_sc) //sampled pixel is more "out of focus" than current sample radius { - // de-weight dull areas to make highlights 'pop'; bokeh_intensity scales how - // strongly bright samples dominate the disc (1.0 = classic behavior). - float wg = 0.25 + bokeh_intensity*(s.r+s.g+s.b); + // de-weight dull areas to make highlights 'pop'. bokeh_intensity is an + // exponent, not a scale: a scale cancels in the normalized average below, + // so it would saturate; an exponent changes which samples dominate and + // keeps working across its whole range. 1.0 is the classic linear weight + // (bit-exact fast path). Base is clamped for overflow safety. + float lum = s.r+s.g+s.b; + float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(min(max(lum, 0.0), 1024.0), bokeh_intensity)); diff += wg*s; @@ -63,9 +67,11 @@ void dofSampleNear(inout vec4 diff, inout float w, float min_sc, vec2 tc) { vec4 s = texture(diffuseRect, tc); - // de-weight dull areas to make highlights 'pop'; bokeh_intensity scales how - // strongly bright samples dominate the disc (1.0 = classic behavior). - float wg = 0.25 + bokeh_intensity*(s.r+s.g+s.b); + // de-weight dull areas to make highlights 'pop'. bokeh_intensity is an exponent + // (see dofSample): a scale would cancel in the normalized average and saturate, + // while an exponent keeps working across its range. 1.0 = classic linear weight. + float lum = s.r+s.g+s.b; + float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(min(max(lum, 0.0), 1024.0), bokeh_intensity)); diff += wg*s; diff --git a/indra/newview/tests/aldofbokeh_test.cpp b/indra/newview/tests/aldofbokeh_test.cpp index c3a3d0e83a..9fc9f1fa79 100644 --- a/indra/newview/tests/aldofbokeh_test.cpp +++ b/indra/newview/tests/aldofbokeh_test.cpp @@ -115,7 +115,7 @@ namespace tut ensure_distance("default intensity passes through", bakeKernel(0, 0.f, 0.f, 1.f, 1.f).intensity, 1.f, 1e-6f); ensure_distance("a mid intensity passes through", bakeKernel(0, 0.f, 0.f, 1.f, 4.f).intensity, 4.f, 1e-6f); ensure_distance("negative intensity clamps to 0", bakeKernel(0, 0.f, 0.f, 1.f, -5.f).intensity, 0.f, 1e-6f); - ensure_distance("huge intensity clamps to 16", bakeKernel(0, 0.f, 0.f, 1.f, 999.f).intensity, 16.f, 1e-6f); + ensure_distance("huge intensity clamps to 8", bakeKernel(0, 0.f, 0.f, 1.f, 999.f).intensity, 8.f, 1e-6f); // Intensity alone must not flip the offset reshape on. ensure("intensity alone does not activate offset", !bakeKernel(0, 0.f, 0.f, 1.f, 8.f).active); From 807458739c4ac752c201806d9f5d4d9594901781 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:11:35 -0500 Subject: [PATCH 4/5] DoF bokeh: address PR #346 review nitpicks (wording, epsilon, docs, test) - llshadermgr.h / aldofbokeh.cpp: describe bokeh_intensity as a highlight-weight EXPONENT (not "scale"), matching aldofbokeh.h and the shader, which apply it via pow(). - aldofbokeh.cpp: decide `active` with a small epsilon (fabsf(anamorphicRatio - 1) > 1e-3) instead of an exact float inequality, so a ratio that rounds to ~1.0 (UI-slider noise) stays on the circular path. - aldofbokeh.h: document that blades are also capped at 12 (upper bound). - aldofbokeh_test.cpp: add a blades==3 boundary case (the exact activation threshold) asserting it is active and bakes the triangle N-gon constants. Co-Authored-By: Claude Opus 4.8 --- indra/llrender/llshadermgr.h | 2 +- indra/newview/aldofbokeh.cpp | 8 +++++--- indra/newview/aldofbokeh.h | 7 ++++--- indra/newview/tests/aldofbokeh_test.cpp | 12 ++++++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 83c5ee875f..5afb29cc26 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -222,7 +222,7 @@ class LLShaderMgr DOF_HEIGHT, // "dof_height" DOF_BOKEH_SHAPE, // "bokeh_shape" (vec4: 2pi/N, pi/N, cos(pi/N), roundness) DOF_BOKEH_LENS, // "bokeh_lens" (vec4: rotationRad, anisoX, anisoY, active) - DOF_BOKEH_INTENSITY, // "bokeh_intensity" (float: highlight-weight scale) + DOF_BOKEH_INTENSITY, // "bokeh_intensity" (float: highlight-weight exponent) DEFERRED_DEPTH, // "depthMap" DEFERRED_SHADOW0, // "shadowMap0" diff --git a/indra/newview/aldofbokeh.cpp b/indra/newview/aldofbokeh.cpp index 703d19df0e..19b210ed13 100644 --- a/indra/newview/aldofbokeh.cpp +++ b/indra/newview/aldofbokeh.cpp @@ -55,15 +55,17 @@ namespace ALDoFBokeh k.anisoX = 1.f / s; k.anisoY = s; - // Highlight-weight scale for the gather. Independent of the offset reshape + // Highlight-weight exponent for the gather. Independent of the offset reshape // (it also intensifies a plain round bokeh); 1.0 reproduces the classic // weighting exactly. k.intensity = intensity; // Only mark the kernel active when it actually reshapes the OFFSET, so the // default (no polygon, ratio 1.0) leaves the gather on its circular path. - // Intensity is deliberately excluded -- it is applied regardless. - k.active = (clampedBlades >= 3) || (anamorphicRatio != 1.f); + // Intensity is deliberately excluded -- it is applied regardless. A small + // epsilon keeps ratios that round to ~1.0 (e.g. UI-slider noise) on the + // circular path, since they are visually indistinguishable from it. + k.active = (clampedBlades >= 3) || (fabsf(anamorphicRatio - 1.f) > 1e-3f); return k; } diff --git a/indra/newview/aldofbokeh.h b/indra/newview/aldofbokeh.h index b9767a4562..a0fefcff77 100644 --- a/indra/newview/aldofbokeh.h +++ b/indra/newview/aldofbokeh.h @@ -51,9 +51,10 @@ namespace ALDoFBokeh // Bake the shader kernel from raw setting values. Inputs are clamped to their // documented, sane ranges here so an out-of-range debug setting can never feed - // garbage into the gather: blades below 3 disable the polygon (circular); - // roundness clamps to [0, 1]; anamorphicRatio clamps to [0.25, 4] (1 = off); - // intensity clamps to [0, 8] (1 = off; the shader applies it as an exponent). + // garbage into the gather: blades below 3 disable the polygon (circular) and + // blades above 12 are capped at 12; roundness clamps to [0, 1]; anamorphicRatio + // clamps to [0.25, 4] (1 = off); intensity clamps to [0, 8] (1 = off; the shader + // applies it as an exponent). Kernel bakeKernel(S32 blades, F32 roundness, F32 rotationDeg, F32 anamorphicRatio, F32 intensity); } diff --git a/indra/newview/tests/aldofbokeh_test.cpp b/indra/newview/tests/aldofbokeh_test.cpp index 9fc9f1fa79..8686089d82 100644 --- a/indra/newview/tests/aldofbokeh_test.cpp +++ b/indra/newview/tests/aldofbokeh_test.cpp @@ -120,4 +120,16 @@ namespace tut // Intensity alone must not flip the offset reshape on. ensure("intensity alone does not activate offset", !bakeKernel(0, 0.f, 0.f, 1.f, 8.f).active); } + + // blades == 3 is the exact activation boundary (the most off-by-one-prone value): + // it must be active and bake the triangle N-gon constants. + template<> template<> + void dofbokeh_object::test<8>() + { + Kernel k = bakeKernel(3, 0.f, 0.f, 1.f, 1.f); + ensure("blades==3 is active", k.active); + ensure_distance("seg = 2pi/3", k.seg, 2.f * F_PI / 3.f, 1e-5f); + ensure_distance("halfSeg = pi/3", k.halfSeg, F_PI / 3.f, 1e-5f); + ensure_distance("edge = cos(pi/3)", k.edge, cosf(F_PI / 3.f), 1e-5f); + } } From f68a193e4386cefb20f664c2300aa4f3f3f7eeb6 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:58:14 -0500 Subject: [PATCH 5/5] DoF bokeh: floor the pop-weight luma so intensity 0 cannot produce NaN pow(lum, bokeh_intensity) with bokeh_intensity == 0 and a pure-black tap (lum == 0) is pow(0,0) -- undefined in GLSL, and NaN on common drivers. Intensity 0 is a legal, documented value ("0 = fully flat" in the RenderDoFBokehIntensity setting text), and one NaN-weighted tap poisons the normalized average for the whole disc, smearing a NaN blotch through the blur. Floor the clamped luma to 1e-5: pow(x>0, 0) == 1 gives exactly the intended flat weight, and at every other intensity the floor is imperceptible under the 0.25 weight base. Both gather sites fixed. Found by an adversarial shader-math review on the follow-on compute-DoF branch, which shares this expression; fixed there in 8f8611596b. Co-Authored-By: Claude Opus 4.8 --- .../shaders/class1/deferred/postDeferredF.glsl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl index d46a81e00f..b3a2be39c1 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/postDeferredF.glsl @@ -53,9 +53,11 @@ void dofSample(inout vec4 diff, inout float w, float min_sc, vec2 tc) // exponent, not a scale: a scale cancels in the normalized average below, // so it would saturate; an exponent changes which samples dominate and // keeps working across its whole range. 1.0 is the classic linear weight - // (bit-exact fast path). Base is clamped for overflow safety. + // (bit-exact fast path). Base is clamped for overflow safety and floored + // so bokeh_intensity==0 ("fully flat", a documented value) cannot hit + // undefined pow(0,0) on a black tap -- NaN there smears across the disc. float lum = s.r+s.g+s.b; - float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(min(max(lum, 0.0), 1024.0), bokeh_intensity)); + float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(clamp(lum, 1e-5, 1024.0), bokeh_intensity)); diff += wg*s; @@ -71,7 +73,7 @@ void dofSampleNear(inout vec4 diff, inout float w, float min_sc, vec2 tc) // (see dofSample): a scale would cancel in the normalized average and saturate, // while an exponent keeps working across its range. 1.0 = classic linear weight. float lum = s.r+s.g+s.b; - float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(min(max(lum, 0.0), 1024.0), bokeh_intensity)); + float wg = 0.25 + (bokeh_intensity == 1.0 ? lum : pow(clamp(lum, 1e-5, 1024.0), bokeh_intensity)); diff += wg*s;