Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions indra/llrender/llshadermgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
3 changes: 3 additions & 0 deletions indra/llrender/llshadermgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions indra/newview/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ set(viewer_SOURCE_FILES
alfloatersceneexplorer.cpp
alfloatersceneexplorerfilters.cpp
alfloatertransactionlog.cpp
aldofbokeh.cpp
alsceneexplorerpredicate.cpp
alfloaterwebprofile.cpp
allegacynotificationwellwindow.cpp
Expand Down Expand Up @@ -941,6 +942,7 @@ set(viewer_HEADER_FILES
alfloatersceneexplorer.h
alfloatersceneexplorerfilters.h
alfloatertransactionlog.h
aldofbokeh.h
alsceneexplorerpredicate.h
alfloaterwebprofile.h
allegacynotificationwellwindow.h
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions indra/newview/aldofbokeh.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
}
61 changes: 61 additions & 0 deletions indra/newview/aldofbokeh.h
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions indra/newview/app_settings/settings_alchemy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,61 @@
<key>Value</key>
<integer>0</integer>
</map>
<key>RenderDoFBokehBlades</key>
<map>
<key>Comment</key>
<string>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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>S32</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>RenderDoFBokehRoundness</key>
<map>
<key>Comment</key>
<string>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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<real>0.0</real>
</map>
<key>RenderDoFBokehRotation</key>
<map>
<key>Comment</key>
<string>Rotation of the bokeh aperture polygon, in degrees. Only meaningful when RenderDoFBokehBlades is 3 or more. Range 0 to 360.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<real>0.0</real>
</map>
<key>RenderDoFAnamorphicRatio</key>
<map>
<key>Comment</key>
<string>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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<real>1.0</real>
</map>
<key>RenderDoFBokehIntensity</key>
<map>
<key>Comment</key>
<string>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.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<real>1.0</real>
</map>
<key>AllowNoCopyRezRestoreToWorld</key>
<map>
<key>Comment</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ 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)
uniform float bokeh_intensity; // exponent on the highlight weight (1 = classic, higher = more defined)

in vec2 vary_fragcoord;

void dofSample(inout vec4 diff, inout float w, float min_sc, vec2 tc)
Expand All @@ -44,10 +49,15 @@ 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 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 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(clamp(lum, 1e-5, 1024.0), bokeh_intensity));

diff += wg*s;

Expand All @@ -59,16 +69,47 @@ 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 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(clamp(lum, 1e-5, 1024.0), bokeh_intensity));

diff += wg*s;

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

// 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)
{
// 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);
}
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);

void main()
Expand All @@ -94,10 +135,9 @@ void main()
for (int i=0; i<its; ++i)
{
float ang = sc+i*2*PI/its; // sc is added for rotary perturbance
float samp_x = sc*sin(ang);
float samp_y = sc*cos(ang);
// you could test sample coords against an interesting non-circular aperture shape here, if desired.
dofSampleNear(diff, w, sc, vary_fragcoord.xy + (vec2(samp_x,samp_y) / screen_res));
// Aperture-shaped tap offset (polygonal / anamorphic bokeh); circular when off.
vec2 samp = bokehOffset(sc, ang);
dofSampleNear(diff, w, sc, vary_fragcoord.xy + (samp / screen_res));
}
sc -= 1.0;
}
Expand All @@ -114,10 +154,9 @@ void main()
for (int i=0; i<its; ++i)
{
float ang = sc+i*2*PI/its; // sc is added for rotary perturbance
float samp_x = sc*sin(ang);
float samp_y = sc*cos(ang);
// you could test sample coords against an interesting non-circular aperture shape here, if desired.
dofSample(diff, w, sc, vary_fragcoord.xy + (vec2(samp_x,samp_y) / screen_res));
// Aperture-shaped tap offset (polygonal / anamorphic bokeh); circular when off.
vec2 samp = bokehOffset(sc, ang);
dofSample(diff, w, sc, vary_fragcoord.xy + (samp / screen_res));
}
sc -= 1.0;
}
Expand Down
15 changes: 15 additions & 0 deletions indra/newview/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@

// newview includes
#include "llagent.h"
#include "aldofbokeh.h"
#include "llagentcamera.h"
#include "llappviewer.h"
#include "lltexturecache.h"
Expand Down Expand Up @@ -8737,6 +8738,20 @@ void LLPipeline::renderDoF(LLRenderTarget* src, LLRenderTarget* dst)
post_program.uniform1f(LLShaderMgr::DOF_MAX_COF, CameraMaxCoF);
post_program.uniform1f(LLShaderMgr::DOF_RES_SCALE, CameraDoFResScale);

// Bokeh aperture shaping (polygonal iris + anamorphic squeeze). Baked
// once per pass so the gather shader avoids per-tap trig; this reshapes
// where taps land, not how many, so the O(CoC^2) cost is unchanged. All
// controls default to neutral, leaving the classic circular bokeh intact.
static LLCachedControl<S32> bokeh_blades(gSavedSettings, "RenderDoFBokehBlades", 0);
static LLCachedControl<F32> bokeh_roundness(gSavedSettings, "RenderDoFBokehRoundness", 0.f);
static LLCachedControl<F32> bokeh_rotation(gSavedSettings, "RenderDoFBokehRotation", 0.f);
static LLCachedControl<F32> bokeh_anamorphic(gSavedSettings, "RenderDoFAnamorphicRatio", 1.f);
static LLCachedControl<F32> 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);

Expand Down
Loading
Loading