diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index eb38126c10..669fe37361 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1416,6 +1416,9 @@ 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("bokeh_intensity");
mReservedUniforms.push_back("depthMap");
mReservedUniforms.push_back("shadowMap0");
diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h
index 8e9a251851..5afb29cc26 100644
--- a/indra/llrender/llshadermgr.h
+++ b/indra/llrender/llshadermgr.h
@@ -220,6 +220,9 @@ 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)
+ DOF_BOKEH_INTENSITY, // "bokeh_intensity" (float: highlight-weight exponent)
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..19b210ed13
--- /dev/null
+++ b/indra/newview/aldofbokeh.cpp
@@ -0,0 +1,72 @@
+/**
+ * @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, 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, 8.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;
+
+ // 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. 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
new file mode 100644
index 0000000000..a0fefcff77
--- /dev/null
+++ b/indra/newview/aldofbokeh.h
@@ -0,0 +1,61 @@
+/**
+ * @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. 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 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)
+ 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;
+ 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) 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);
+}
+
+#endif // AL_DOFBOKEH_H
diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml
index f0251c620f..eb12b44c94 100644
--- a/indra/newview/app_settings/settings_alchemy.xml
+++ b/indra/newview/app_settings/settings_alchemy.xml
@@ -2761,6 +2761,61 @@
Value
0
+ RenderDoFBokehBlades
+
+ RenderDoFBokehRoundness
+
+ RenderDoFBokehRotation
+
+ RenderDoFAnamorphicRatio
+
+ RenderDoFBokehIntensity
+
AllowNoCopyRezRestoreToWorld