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
6 changes: 3 additions & 3 deletions doc/ARCHITECTURE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions indra/alrender/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ set(

set(
alrender_HEADERS
alblendstencil.h
alcutoutimage.h
aldepthpyramid.h
alfarplane.h
Expand Down
55 changes: 55 additions & 0 deletions indra/alrender/alblendstencil.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @file alblendstencil.h
* @brief The blended pass's bit in the screen's stencil, and the stencil states that write and test it.
*
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
* Alchemy Viewer Source Code
* Copyright (C) 2026, Rye <rye@alchemyviewer.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* $/LicenseInfo$
*/

#pragma once

#include "llgl.h"
#include "llglstates.h"

/// The screen's depth carries stencil in every render-target pack, cleared with the G-buffer each frame, and the
/// water keeps its facts in the first two bits (alwaterstencil.h). The blended pass keeps one more, in the next.
namespace ALBlendStencil
{

/// Where a blended surface shades: set by the forward alpha pass wherever its fragments pass the depth test. The
/// G-buffer stops at the opaque surface behind a blended one, so what it knows of those pixels (the albedo the
/// exposure meter divides by) is not what is on screen there.
constexpr GLuint BLENDED_BIT = 1u << 2;

/// The forward alpha pass's draws: the blended bit set wherever a fragment passes the scene's depth test, and no
/// other bit touched.
class Mark : public ALGLStencil
{
public:
Mark() : ALGLStencil(GL_ALWAYS, BLENDED_BIT, 0u, GL_KEEP, GL_KEEP, GL_REPLACE, BLENDED_BIT) {}
};

/// The exposure meter's mask draw: where the blended bit is set.
class OnBlended : public ALGLStencil
{
public:
OnBlended() : ALGLStencil(GL_EQUAL, BLENDED_BIT, BLENDED_BIT, GL_KEEP, GL_KEEP, GL_KEEP, 0u) {}
};

} // namespace ALBlendStencil
5 changes: 5 additions & 0 deletions indra/llrender/llshadermgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,8 @@ bool LLShaderMgr::saveCachedProgramBinary(LLGLSLShader* shader)
}

//virtual
/// Fills the reserved attribute and uniform name lists. The order of the pushes is the order of the enums in
/// llshadermgr.h, so a name added to one is added at the same place in the other.
void LLShaderMgr::initAttribsAndUniforms()
{
//MUST match order of enum in LLVertexBuffer.h
Expand Down Expand Up @@ -2012,6 +2014,9 @@ void LLShaderMgr::initAttribsAndUniforms()
mReservedUniforms.push_back("meter_params");
mReservedUniforms.push_back("meterMap");
mReservedUniforms.push_back("meterWaterMap");
mReservedUniforms.push_back("meterAlbedoMap");
mReservedUniforms.push_back("meterBlendMap");
mReservedUniforms.push_back("meter_albedo_params");
mReservedUniforms.push_back("exposureHistogram");

// Stars/Aurora/Meteors
Expand Down
3 changes: 3 additions & 0 deletions indra/llrender/llshadermgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ class LLShaderMgr
METER_PARAMS, // "meter_params" the exposure meter's per-pass parameters
METER_MAP, // "meterMap" the exposure meter's grid
METER_WATER_MAP, // "meterWaterMap" the G-buffer's normals under the water, for the meter
METER_ALBEDO_MAP, // "meterAlbedoMap" the G-buffer's albedo, which the meter divides out
METER_BLEND_MAP, // "meterBlendMap" where a blended surface shades, which the albedo cannot speak for
METER_ALBEDO_PARAMS, // "meter_albedo_params" strength (0 meters reflected light, 1 the light on surfaces), albedo floor, trusted from, to
EXPOSURE_HISTOGRAM, // "exposureHistogram" 64x1, per bin (weight, weight * log2 luminance)

// Alchemy-specific uniforms for Sky
Expand Down
26 changes: 26 additions & 0 deletions indra/newview/app_settings/settings_alchemy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2707,6 +2707,32 @@
<key>Value</key>
<real>0.25</real>
</map>
<key>AlchemyExposureIncidentStrength</key>
<map>
<key>Comment</key>
<string>How far automatic exposure meters the light falling on surfaces (incident metering) instead of the light they reflect (reflective metering). 0 is reflective, which exposes a white wall in dim light as middle grey; 1 is incident, so white stays white and a grey card stays grey. The incident light is estimated by dividing the reflected light by the G-buffer's albedo. 0 to 1.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<real>1.0</real>
</map>
<key>AlchemyExposureIncidentTuning</key>
<map>
<key>Comment</key>
<string>Tuning for incident metering (AlchemyExposureIncidentStrength), for debugging. x: the albedo floor, below which a surface is read as reflecting this much so that a black one is not the brightest light in the frame (0.1). y, z: between these a patch's mean albedo earns its share of the exposure, none below y and all of it above z, so that near black surfaces say nothing about the light (0.02, 0.12).</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Vector3</string>
<key>Value</key>
<array>
<real>0.1</real>
<real>0.02</real>
<real>0.12</real>
</array>
</map>
<key>AlchemyScopeLayout</key>
<map>
<key>Comment</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
// pixels (position.z 1). Each point lands in the histogram bin of its log2 luminance, and the
// target's additive blend sums, per bin, the points' weights and their weighted log2
// luminances. The sums keep the mean exact; the bins only decide which points the percentile
// trim keeps.
// trim keeps. A non-sky point also counts only as much as its patch's albedo has earned (the grid's
// alpha, luminanceF.glsl), which is all of it unless the meter is reading the light on surfaces.

in vec3 position; // xy: the meter texel, z: which part of it

Expand Down Expand Up @@ -53,16 +54,19 @@ float meterWeight(vec2 uv, float mode)
return 1.0 - smoothstep(0.15, 0.3, sqrt(d2));
}

/// Places one of the grid's points (each texel makes two, its ground and its sky) in the histogram bin of its log2
/// luminance, carrying its weight and its weight times its EV, the sums that the percentile trim reads (exposureF.glsl).
void main()
{
ivec2 texel = ivec2(position.xy);
vec4 grid = texelFetch(meterMap, texel, 0);
bool is_sky = position.z > 0.5;
float L = is_sky ? grid.g : grid.r;
float share = is_sky ? grid.b : 1.0 - grid.b;
float earned = is_sky ? 1.0 : grid.a;

vec2 uv = (position.xy + 0.5) / vec2(textureSize(meterMap, 0));
float w = share * meterWeight(uv, meter_params.x) * (is_sky ? meter_params.y : 1.0);
float w = share * earned * meterWeight(uv, meter_params.x) * (is_sky ? meter_params.y : 1.0);

// Held to the histogram's range, so the first and last bins cannot drag the mean with
// values far outside it.
Expand Down
49 changes: 47 additions & 2 deletions indra/newview/app_settings/shaders/class1/deferred/luminanceF.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,40 @@
// Every pixel of the patch counts: bilinear taps about two texels apart each average a 2x2
// block. A meter that point-samples the scene sees a sun disc or a glint appear and vanish as
// the camera moves, and the exposure pumps with it.
//
// The light a surface reflects is its albedo times the light that falls on it, so a meter of the
// reflected light cannot tell a white wall in dim light from a grey one in bright light, and
// exposes both as middle grey. With meter_albedo_params.x above 0 (incident metering, at 1) the
// non-sky mean is divided by the patch's mean albedo (from the G-buffer) to that power, which
// estimates the light that falls on the patch: a white wall stays white and a grey card stays
// grey. The share of the patch's exposure that its albedo has earned goes to alpha, because a
// near black surface says nothing about the light on it; at strength 0 the grid is what it
// always was, with alpha 1.
//
// What the estimate leaves alone: an emissive surface is metered as the light it is, and may go
// hot. The water never writes the G-buffer, so over the world the albedo under it is the
// seabed's, a fair stand-in in clear water (over nothing it is sky, below). Blended surfaces are
// not in the G-buffer either, so the pass that draws them marks its pixels in the stencil, and
// meterBlendMap carries that mask here: those pixels are taken to reflect NOMINAL_ALBEDO, the
// key, which meters them exactly as reflective metering does, whatever lies behind them.

out vec4 frag_color;

uniform sampler2D diffuseRect; // the HDR scene, before bloom
uniform sampler2D depthMap; // the scene's depth, the far plane wherever the sky is
uniform sampler2D meterWaterMap; // the G-buffer's normals under the water's own pixels, 0 elsewhere
uniform vec4 meter_params; // xy: a patch's size in scene texels, zw: taps across it per axis
uniform sampler2D meterAlbedoMap; // the G-buffer's albedo, decoded to linear
uniform sampler2D meterBlendMap; // r: 1 where a blended surface shades, else 0
// x: 0 meters the light a surface reflects, 1 the light that falls on it.
// y: a surface that reflects less than this is read as if it reflected this much, since dividing by next to nothing
// would make a black surface the brightest light in the frame.
// zw: between these a patch's mean albedo earns its share of the exposure, none below z and all of it above w.
uniform vec4 meter_albedo_params;

// What a pixel the G-buffer cannot speak for is taken to reflect: the key (EXPOSURE_KEY in pipeline.cpp). The light
// over it is then its luminance over the key, which is what reflective metering reads, at any strength.
const float NOMINAL_ALBEDO = 0.18;

// The sky is what the world leaves at the far plane (deferredUtil.glsl).
bool isFarDepth(float d);
Expand All @@ -59,6 +86,9 @@ float lum(vec3 col)
return dot(vec3(0.2126, 0.7152, 0.0722), col);
}

/// One patch of the meter's grid: taps across the patch of the scene, told apart by depth into sky and ground, written as
/// the ground's mean luminance (divided by its mean albedo under incident metering), the sky's mean luminance, the sky's
/// share, and the share of the exposure the albedo has earned.
void main()
{
vec2 scene_size = vec2(textureSize(diffuseRect, 0));
Expand All @@ -67,7 +97,9 @@ void main()
vec2 tap_step = meter_params.xy / meter_params.zw;
ivec2 taps = ivec2(meter_params.zw);

bool by_albedo = meter_albedo_params.x > 0.0;
float ground = 0.0;
float ground_albedo = 0.0;
float sky = 0.0;
float sky_taps = 0.0;
for (int j = 0; j < taps.y; ++j)
Expand All @@ -87,14 +119,27 @@ void main()
else
{
ground += L;
if (by_albedo)
{
bool blended = texelFetch(meterBlendMap, ivec2(px), 0).r > 0.5;
ground_albedo += blended ? NOMINAL_ALBEDO : lum(texelFetch(meterAlbedoMap, ivec2(px), 0).rgb);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

float count = float(taps.x * taps.y);
float ground_taps = count - sky_taps;
frag_color = vec4(ground_taps > 0.0 ? ground / ground_taps : 0.0,
float ground_mean = ground_taps > 0.0 ? ground / ground_taps : 0.0;
float earned = 1.0;
if (by_albedo && ground_taps > 0.0)
{
float albedo = ground_albedo / ground_taps;
ground_mean /= pow(max(albedo, meter_albedo_params.y), meter_albedo_params.x);
earned = mix(1.0, smoothstep(meter_albedo_params.z, meter_albedo_params.w, albedo), meter_albedo_params.x);
}
frag_color = vec4(ground_mean,
sky_taps > 0.0 ? sky / sky_taps : 0.0,
sky_taps / count,
1.0);
earned);
}
11 changes: 10 additions & 1 deletion indra/newview/lldrawpoolalpha.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "llspatialpartition.h"
#include "llglcommonfunc.h"
#include "llvoavatar.h"
#include "alblendstencil.h"

#include "llenvironment.h"

Expand Down Expand Up @@ -248,6 +249,9 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass)
}
}

/// Draws the blended surfaces over the lit scene: source-alpha blending, the destination's alpha kept as glow (or as
/// coverage in a bake), and ALBlendStencil::BLENDED_BIT marked where its colour pass shades, for the exposure meter.
/// @param rigged true to draw the rigged surfaces, with depth writes; false for the rest.
void LLDrawPoolAlpha::forwardRender(bool rigged)
{
gPipeline.enableLightsDynamic();
Expand Down Expand Up @@ -290,7 +294,12 @@ void LLDrawPoolAlpha::forwardRender(bool rigged)

// If the face is more than 90% transparent, then don't update the Depth buffer for Dof
// We don't want the nearly invisible objects to cause of DoF effects
renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged);
{
// The albedo the exposure meter divides by stops at the opaque surface behind a blended one, so this pass marks
// where it shades (alblendstencil.h). Not the depth-only pass for depth of field, which shades nothing.
ALBlendStencil::Mark blended;
renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged);
}

gGL.setColorMask(true, false);

Expand Down
1 change: 1 addition & 0 deletions indra/newview/llpresetsmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ void LLPresetsManager::getLooksControlNames(std::vector<std::string>& names)
"AlchemyExposureSpeedDown",
"AlchemyExposureMeteringMode",
"AlchemyExposureSkyWeight",
"AlchemyExposureIncidentStrength",
// Color LUT
"RenderColorGrade",
"RenderColorGradeLUT",
Expand Down
Loading
Loading