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
14 changes: 14 additions & 0 deletions package/Shaders/Common/Color.hlsli
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ namespace Color
return pow(abs(color), 1.0 / 2.2);
}

// Luminance-weighted average of four HDR colors.
// Each input is weighted by rcp(1 + luminance) so isolated bright outliers
// (fireflies) contribute far less than their neighbors.
// Technique: Brian Karis, "Real Shading in Unreal Engine 4", SIGGRAPH 2013, p.10
// https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
float3 KarisWeightedAverage(float3 a, float3 b, float3 c, float3 d)
{
float wa = rcp(1.0 + RGBToLuminance(a));
float wb = rcp(1.0 + RGBToLuminance(b));
float wc = rcp(1.0 + RGBToLuminance(c));
float wd = rcp(1.0 + RGBToLuminance(d));
return (a * wa + b * wb + c * wc + d * wd) / (wa + wb + wc + wd);
}

#if defined(PSHADER) || defined(CSHADER) || defined(COMPUTESHADER)
// Attempt to match vanilla materials that are darker than PBR
const static float PBRLightingScale = ENABLE_LL ? 1.0 : 0.65;
Expand Down
47 changes: 44 additions & 3 deletions package/Shaders/ISBlur.hlsl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Common/Color.hlsli"
#include "Common/DummyVSTexCoord.hlsl"
#include "Common/FrameBuffer.hlsli"
#include "Common/Math.hlsli"

typedef VS_OUTPUT PS_INPUT;

Expand Down Expand Up @@ -29,6 +30,36 @@ float4 GetImageColor(float2 texCoord, float blurScale)
return ImageTex.Sample(ImageSampler, texCoord) * float4(blurScale.xxx, 1);
}

# if defined(BRIGHTPASS)
static const float kBrightPassSoftKneeRatio = 0.5;

// Sample a 2x2 neighborhood and Karis-average to suppress isolated HDR outliers.
float3 SampleKarisFireflySuppress(float2 uv, float2 texelSize)
{
float3 s0 = ImageTex.SampleLevel(ImageSampler, uv + float2(-0.5, -0.5) * texelSize, 0).rgb;
float3 s1 = ImageTex.SampleLevel(ImageSampler, uv + float2(0.5, -0.5) * texelSize, 0).rgb;
float3 s2 = ImageTex.SampleLevel(ImageSampler, uv + float2(-0.5, 0.5) * texelSize, 0).rgb;
float3 s3 = ImageTex.SampleLevel(ImageSampler, uv + float2(0.5, 0.5) * texelSize, 0).rgb;
return Color::KarisWeightedAverage(s0, s1, s2, s3);
}

float3 ApplyBrightPass(float3 hdrColor)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be needed now. probably isn't very effective

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you're saying.

{
float threshold = max(BlurBrightPass.x, 0.0);
float scale = max(BlurBrightPass.y, 0.0);

// Soft-knee threshold avoids binary bloom popping when highlights hover near threshold.
float knee = max(threshold * kBrightPassSoftKneeRatio, EPSILON_DIVISION);
float brightness = max(Color::RGBToLuminance(hdrColor), EPSILON_DIVISION);
float soft = saturate((brightness - threshold + knee) / max(2.0 * knee, EPSILON_DIVISION));
soft = soft * soft * (3.0 - 2.0 * soft);
float contribution = max(brightness - threshold, 0.0) + soft * knee;
float weight = contribution / brightness;

return hdrColor * weight * scale;
}
# endif

PS_OUTPUT main(PS_INPUT input)
{
PS_OUTPUT psout;
Expand All @@ -45,9 +76,22 @@ PS_OUTPUT main(PS_INPUT input)
blurScale = 1;
# endif

# if defined(BRIGHTPASS)
float avgLum = Color::RGBToLuminance(AvgLumTex.Sample(AvgLumSampler, input.TexCoord.xy).xyz);
uint imgWidth, imgHeight;
ImageTex.GetDimensions(imgWidth, imgHeight);
float2 texelSize = rcp(float2(imgWidth, imgHeight));
# endif

for (int blurIndex = 0; blurIndex < blurRadius; ++blurIndex) {
float2 screenPosition = BlurOffsets[blurIndex].xy + input.TexCoord.xy;
float4 imageColor = 0;
# if defined(BRIGHTPASS)
{
float2 sampleUV = (BlurScale.x < 0.5) ? FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(screenPosition) : screenPosition;
imageColor.rgb = ApplyBrightPass(SampleKarisFireflySuppress(sampleUV, texelSize));
}
Comment on lines +90 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep blurScale.y in the BRIGHTPASS sample path.

This branch no longer goes through GetImageColor, so any non-1 blurScale.y stops affecting the HDR sample before bright-pass. That changes the cutoff and bloom energy relative to the previous path in COLORRANGE builds.

Suggested fix
 		{
 			float2 sampleUV = (BlurScale.x < 0.5) ? FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(screenPosition) : screenPosition;
-			imageColor.rgb = ApplyBrightPass(SampleKarisFireflySuppress(sampleUV, texelSize));
+			float3 hdrColor = SampleKarisFireflySuppress(sampleUV, texelSize) * blurScale.y;
+			imageColor.rgb = ApplyBrightPass(hdrColor);
 		}

If blurScale.y is supposed to influence the Karis weights too, thread it into SampleKarisFireflySuppress and apply it per tap instead of after the average.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
float2 sampleUV = (BlurScale.x < 0.5) ? FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(screenPosition) : screenPosition;
imageColor.rgb = ApplyBrightPass(SampleKarisFireflySuppress(sampleUV, texelSize));
}
{
float2 sampleUV = (BlurScale.x < 0.5) ? FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(screenPosition) : screenPosition;
float3 hdrColor = SampleKarisFireflySuppress(sampleUV, texelSize) * blurScale.y;
imageColor.rgb = ApplyBrightPass(hdrColor);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/Shaders/ISBlur.hlsl` around lines 103 - 106, The bright-pass branch
bypasses GetImageColor so BlurScale.y is not applied, changing HDR cutoff and
bloom energy; restore BlurScale.y influence by either (A) using BlurScale.y when
computing sampleUV (e.g., apply dynamic-resolution adjustment that incorporates
BlurScale.y before calling SampleKarisFireflySuppress) or (B, preferred if
vertical blur should affect Karis weights) extend SampleKarisFireflySuppress to
accept BlurScale.y and apply it per tap inside SampleKarisFireflySuppress
(instead of averaging then scaling) so ApplyBrightPass receives the correctly
scaled HDR sample; update the call site that currently passes sampleUV to pass
the BlurScale.y parameter if you choose option B.

# else
[branch] if (BlurScale.x < 0.5)
{
imageColor = GetImageColor(FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(screenPosition), blurScale.y);
Expand All @@ -56,14 +100,11 @@ PS_OUTPUT main(PS_INPUT input)
{
imageColor = GetImageColor(screenPosition, blurScale.y);
}
# if defined(BRIGHTPASS)
imageColor = BlurBrightPass.y * max(0, -BlurBrightPass.x + imageColor);
# endif
color += imageColor * BlurOffsets[blurIndex].z;
}

# if defined(BRIGHTPASS)
float avgLum = Color::RGBToLuminance(AvgLumTex.Sample(AvgLumSampler, input.TexCoord.xy).xyz);
color.w = avgLum;
# endif

Expand Down
49 changes: 42 additions & 7 deletions package/Shaders/Tests/TestColor.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,44 @@
ASSERT(IsTrue, overSat.b >= 0.0f);
}

/// @tags color, gamma, colorspace
[numthreads(1, 1, 1)] void TestGammaConversionRoundtrip()
/// @tags color, bloom, firefly
[numthreads(1, 1, 1)] void TestKarisWeightedAverage()
{
// Test 1: Identical inputs should return that same value (weights cancel)
float3 gray = float3(0.3, 0.3, 0.3);
float3 result = Color::KarisWeightedAverage(gray, gray, gray, gray);
ASSERT(IsTrue, abs(result.r - gray.r) < 0.001f);
ASSERT(IsTrue, abs(result.g - gray.g) < 0.001f);
ASSERT(IsTrue, abs(result.b - gray.b) < 0.001f);

// Test 2: One extreme firefly among three dark neighbors.
// The average should stay close to the dark neighbors, not blow up.
float3 dark = float3(0.1, 0.1, 0.1);
float3 firefly = float3(100.0, 100.0, 100.0);
float3 mixed = Color::KarisWeightedAverage(dark, dark, dark, firefly);
// Result should be much closer to dark than to firefly
ASSERT(IsTrue, Color::RGBToLuminance(mixed) < 1.0f);

// Test 3: Two bright, two dark - result should sit between them
float3 bright = float3(5.0, 5.0, 5.0);
float3 between = Color::KarisWeightedAverage(dark, dark, bright, bright);
float betweenLum = Color::RGBToLuminance(between);
ASSERT(IsTrue, betweenLum > Color::RGBToLuminance(dark));
ASSERT(IsTrue, betweenLum < Color::RGBToLuminance(bright));

// Test 4: Output should never exceed the maximum input luminance
float3 a = float3(0.2, 0.5, 0.1);
float3 b = float3(0.8, 0.3, 0.4);
float3 c = float3(0.1, 0.9, 0.2);
float3 d = float3(0.6, 0.1, 0.7);
float maxInputLum = max(max(Color::RGBToLuminance(a), Color::RGBToLuminance(b)),
max(Color::RGBToLuminance(c), Color::RGBToLuminance(d)));
float3 avg = Color::KarisWeightedAverage(a, b, c, d);
ASSERT(IsTrue, Color::RGBToLuminance(avg) <= maxInputLum + 0.001f);
}

/// @tags color, gamma, colorspace
[numthreads(1, 1, 1)] void TestGammaConversionRoundtrip() {
float3 testColors[3] = {
float3(0.5, 0.5, 0.5),
float3(0.2, 0.7, 0.3),
Expand Down Expand Up @@ -118,8 +153,9 @@
}
}

/// @tags color, luminance
[numthreads(1, 1, 1)] void TestRGBToLuminanceVariants() {
/// @tags color, luminance
[numthreads(1, 1, 1)] void TestRGBToLuminanceVariants()
{
float3 testColor = float3(0.6, 0.4, 0.3);

float lum1 = Color::RGBToLuminance(testColor);
Expand All @@ -134,9 +170,8 @@
ASSERT(IsTrue, abs(lum1 - lum3) < 0.2f);
}

/// @tags color, lighting
[numthreads(1, 1, 1)] void TestDiffuseAndLight()
{
/// @tags color, lighting
[numthreads(1, 1, 1)] void TestDiffuseAndLight() {
float3 color = float3(0.5, 0.3, 0.7);

float3 diffuse = Color::Diffuse(color);
Expand Down
Loading