Files
UnrealEngineUWP/Engine/Shaders/Private/PostProcessVisualizeHDR.usf
eric renaudhoude ef67f95325 Fix inaccurate luminance/illuminance meter values in Visualize HDR mode.
We now use accurate luminance coefficients provided by the working color space. Furthermore, the maximum nit value is read from the calculated luminance, not the RGB channels directly.

#jira UE-204922
#rb chris.kulla

[CL 30925020 by eric renaudhoude in 5.4 branch]
2024-01-26 10:46:18 -05:00

545 lines
17 KiB
Plaintext

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Common.ush"
#include "ScreenPass.ush"
#include "PostProcessCommon.ush"
#include "PostProcessHistogramCommon.ush"
#include "DeferredShadingCommon.ush"
#include "TonemapCommon.ush"
#include "MiniFontCommon.ush"
#include "ColorSpace.ush"
// the prior frame's eye adaptation settings
StructuredBuffer<float4> EyeAdaptationBuffer;
Texture2D SceneColorTexture;
SamplerState SceneColorSampler;
Texture2D HDRSceneColorTexture;
SamplerState HDRSceneColorSampler;
Texture2D HistogramTexture;
SCREEN_PASS_TEXTURE_VIEWPORT(Input)
SCREEN_PASS_TEXTURE_VIEWPORT(Output)
float IlluminanceMeterEnabled;
// only needed for nice visualization
float ComputeHistogramMax(Texture2D HistogramTexture)
{
float Max = 0;
for(uint i = 0; i < HISTOGRAM_SIZE; ++i)
{
Max = max(Max, GetHistogramBucket(HistogramTexture, i));
}
return Max;
}
float ComputePlaceInHistogram(float3 HDRColor)
{
float LogLuminance = log2(dot(HDRColor,float3(1.0f,1.0f,1.0f))/3.0f);
float SumTotal = 0.0;
float SumBelow = 0.0f;
for(uint i = 0; i < HISTOGRAM_SIZE; ++i)
{
float Weight = GetHistogramBucket(HistogramTexture, i);
float LogLuminanceLow = ComputeLogLuminanceFromHistogramPosition(float(i) / (float)(HISTOGRAM_SIZE-1));
float LogLuminanceHigh = ComputeLogLuminanceFromHistogramPosition(float(i+1) / (float)(HISTOGRAM_SIZE-1));
// if log luminace is greater than high, it's 1.0.
// if log luminace is lower than low, it's 0.0
// else it's a lerp between
float T = saturate((LogLuminance - LogLuminanceLow)/(LogLuminanceHigh - LogLuminanceLow));
SumBelow += Weight * T;
SumTotal += Weight;
}
float Percentile = SumBelow/SumTotal;
return Percentile;
}
bool InUnitBox(float2 UV)
{
return UV.x >= 0 && UV.y >= 0 && UV.y < 1 && UV.y < 1;
}
// @param x 0=cold..1=hot
float3 Colorize(float x)
{
x = saturate(x);
float3 Heat = float3(1.0f, 0.0f, 0.0f);
float3 Middle = float3(0.0f, 1.0f, 0.0f);
float3 Cold = float3(0.0f, 0.0f, 1.0f);
float3 ColdHeat = lerp(Cold, Heat, x);
return lerp(Middle, ColdHeat, abs(0.5f - x) * 2);
}
// for printf debugging in the shader, does not have to be positive
// outputs a float number in the form: xx.yy
// @param LeftTop - in pixels
void PrintSmallFloatWithSign(int2 PixelPos, inout float3 OutColor, float3 FontColor, int2 LeftTop, float Number)
{
int2 Cursor = LeftTop;
bool bHasSign = Number < 0;
// Minus Sign
Number = abs(Number) + 0.005; // Round up first digit
bool bIsGreater10 = Number >= 10;
FLATTEN
if (bHasSign && bIsGreater10)
{
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _MINUS_);
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 10));
Number = abs(Number);
}
else if (bIsGreater10)
{
Cursor.x += 8;
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 10));
}
else if (bHasSign)
{
Cursor.x += 8;
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _MINUS_);
}
else
{
Cursor.x += 2*8;
}
// we always print this character, so no ifs needed
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 1));
// period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _DOT_);
// after period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 0.1));
// after period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 0.01));
}
// for printf debugging in the shader, does not have to be positive
// outputs a float number in the form: xx.yyy
// @param LeftTop - in pixels
void PrintMediumFloatWithSign(int2 PixelPos, inout float3 OutColor, float3 FontColor, int2 LeftTop, float Number)
{
int2 Cursor = LeftTop;
bool bHasSign = Number < 0;
// Minus Sign
Number = abs(Number) + 0.005; // Round up first digit
bool bIsGreater10 = Number >= 10;
FLATTEN
if (bHasSign && bIsGreater10)
{
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _MINUS_);
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 10));
Number = abs(Number);
}
else if (bIsGreater10)
{
Cursor.x += 8;
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 10));
}
else if (bHasSign)
{
Cursor.x += 8;
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _MINUS_);
}
else
{
Cursor.x += 2*8;
}
// we always print this character, so no ifs needed
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 1));
// period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, _DOT_);
// after period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 0.1));
// after period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 0.01));
// after period
PrintCharacter(PixelPos, OutColor, FontColor, Cursor, ExtractDigitFromFloat(Number, 0.001));
}
//
void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, float4 SvPosition : SV_POSITION, out float4 OutColor : SV_Target0)
{
float2 UV = UVAndScreenPos.xy;
int2 PixelPos = (int2)SvPosition.xy;
float2 ViewLocalUV = float2(UVAndScreenPos.z * 0.5f + 0.5f, 0.5f - 0.5f * UVAndScreenPos.w);
// retrieve the exposure scale and target exposure scale from the eye-adaptation buffer.
float EyeAdaptationResult = EyeAdaptationBuffer[0].r;
float EyeAdaptationTarget = EyeAdaptationBuffer[0].g;
float EyeAdaptationAverageSceneLuminance = EyeAdaptationBuffer[0].z;
float EyeAdaptationExposureBiasOnly = EyeAdaptationBuffer[0].w;
float EyeAdaptationResultWithoutExposureBias = EyeAdaptationResult / EyeAdaptationExposureBiasOnly;
int2 OutputViewportCenter = (int2)(Output_ViewportMin + Output_ViewportSize / 2);
float2 UVViewportCenter = (Input_UVViewportMin + Input_UVViewportSize / 2);
const float3 NumberColor = float3(0.8, 0.85, 0.85);
// Luminance meter at the center of the screen
{
const float PixelDx = abs(PixelPos.x - OutputViewportCenter.x);
const float PixelDy = abs(PixelPos.y - OutputViewportCenter.y);
if (PixelDx < 180 && PixelDy < 42)
{
float3 HDRSceneColorAvg = 0.0f;
float3 SceneColorAvg = 0.0f;
float SampleCount = 0.0f;
float MeterSize = 10.0f;
for (float OffsetX = -MeterSize; OffsetX <= MeterSize; OffsetX++)
{
for (float OffsetY = -MeterSize; OffsetY <= MeterSize; OffsetY++)
{
float2 UV = UVViewportCenter + float2(OffsetX, OffsetY) * Input_ExtentInverse;
HDRSceneColorAvg += Texture2DSample(HDRSceneColorTexture, HDRSceneColorSampler, UV).rgb;
SceneColorAvg += Texture2DSample(SceneColorTexture, SceneColorSampler, UV).rgb;
SampleCount++;
}
}
HDRSceneColorAvg = HDRSceneColorAvg / SampleCount;
SceneColorAvg = SceneColorAvg / SampleCount;
HDRSceneColorAvg *= View.OneOverPreExposure;
if (PixelDx < MeterSize && PixelDy < MeterSize)
{
if (PixelDx == MeterSize-1 || PixelDy == MeterSize-1)
{
OutColor = float4(1.0, 1.0, 1.0, 1.0); // White border
}
else
{
OutColor = float4(SceneColorAvg, 1.0); // Inner visor average color
}
}
else
{
OutColor = Texture2DSample(SceneColorTexture, SceneColorSampler, UV); // Influenced area default scene color
}
float3 LuminanceAvg = SceneLuminance(HDRSceneColorAvg);
float MaxLuminance = max(LuminanceAvg.r, max(LuminanceAvg.g, LuminanceAvg.b));
// Luminance
int2 TopLeft = OutputViewportCenter + int2(11, 11);
if (MaxLuminance < 1000.0f)
{
PrintFloat(PixelPos, OutColor.xyz, NumberColor, TopLeft, MaxLuminance);
TopLeft.x += 60;
}
else if (MaxLuminance < 999999.0f)
{
PrintFloatNoFractionLarge(PixelPos, OutColor.xyz, NumberColor, TopLeft, MaxLuminance);
TopLeft.x += 60;
}
else
{
PrintFloatNoFraction(PixelPos, OutColor.xyz, NumberColor, TopLeft, MaxLuminance, 10);
TopLeft.x += 100;
}
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _N_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _I_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _T_);
// EV100
TopLeft = OutputViewportCenter + int2(11, 22);
const float EyeAdaptationEV100 = log2((LuminanceAvg/.18)/EyeAdaptation_LuminanceMax);
PrintMediumFloatWithSign(PixelPos, OutColor.xyz, NumberColor, TopLeft, EyeAdaptationEV100);
TopLeft.x += 60;
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _E_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _V_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _1_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _0_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _0_);
return;
}
}
// Illuminance meter for the hemisphere in front of the camera
if(IlluminanceMeterEnabled > 0.0f)
{
OutColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
// IlluminanceMeter variables must match the one in DebugProbes.usf
const float IlluminanceMeterSize = 20;
const float IlluminanceMeterHalfSize = IlluminanceMeterSize / 2;
const float2 IlluminanceMeterCenter = OutputViewportCenter - uint2(0, 100);
const float2 IlluminanceMeterCenterUV = IlluminanceMeterCenter * Input_ExtentInverse;
const float PixelDx = PixelPos.x - IlluminanceMeterCenter.x;
const float PixelDy = PixelPos.y - IlluminanceMeterCenter.y;
const float AbsPixelDx = abs(PixelDx);
const float AbsPixelDy = abs(PixelDy);
if (PixelDx > -20 && PixelDx < 300 && PixelDy < 40 && PixelDy > -20)
{
float3 HDRSceneColorAvg = 0.0f;
float SampleCount = 0.0f;
LOOP
for (float OffsetX = (-IlluminanceMeterHalfSize + 2); OffsetX < (IlluminanceMeterHalfSize-2); OffsetX++)
{
for (float OffsetY = (-IlluminanceMeterHalfSize + 2); OffsetY < (IlluminanceMeterHalfSize-2); OffsetY++)
{
float2 UV = IlluminanceMeterCenterUV + float2(OffsetX, OffsetY) * Input_ExtentInverse;
HDRSceneColorAvg += Texture2DSampleLevel(HDRSceneColorTexture, HDRSceneColorSampler, UV, 0).rgb;
SampleCount++;
}
}
HDRSceneColorAvg = HDRSceneColorAvg / SampleCount;
HDRSceneColorAvg *= View.OneOverPreExposure;
OutColor = Texture2DSample(SceneColorTexture, SceneColorSampler, UV); // Influenced area default scene color
const int IlluminanceMeterHalfSizeInt = int(IlluminanceMeterHalfSize);
const int AbsPixelDxInt = int(AbsPixelDx);
const int AbsPixelDyInt = int(AbsPixelDy);
if (AbsPixelDxInt < IlluminanceMeterHalfSizeInt && AbsPixelDyInt < IlluminanceMeterHalfSizeInt)
{
if (AbsPixelDxInt == (IlluminanceMeterHalfSizeInt - 1) || AbsPixelDyInt == (IlluminanceMeterHalfSizeInt-1))
{
OutColor = float4(1.0, 1.0, 1.0, 1.0); // White border
}
}
float3 Illuminance = HDRSceneColorAvg * PI;
float MeanIlluminance = SceneLuminance(Illuminance);
int2 TopLeft = IlluminanceMeterCenter + int2(11, 11);
// Mean illuminance
if (MeanIlluminance < 1000.0f)
{
PrintFloat(PixelPos, OutColor.xyz, NumberColor, TopLeft, MeanIlluminance);
TopLeft.x += 60;
}
else if (MeanIlluminance < 999999.0f)
{
PrintFloatNoFractionLarge(PixelPos, OutColor.xyz, NumberColor, TopLeft, MeanIlluminance);
TopLeft.x += 60;
}
else
{
PrintFloatNoFraction(PixelPos, OutColor.xyz, NumberColor, TopLeft, MeanIlluminance, 10);
TopLeft.x += 100;
}
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _L_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _U_);
PrintCharacter(PixelPos, OutColor.xyz, NumberColor, TopLeft, _X_);
// Illuminance as RGB
TopLeft = IlluminanceMeterCenter + int2(11, 22);
if (Illuminance.r < 1000.0f)
{
PrintFloat(PixelPos, OutColor.xyz, float3(1, 0.5, 0.5), TopLeft, Illuminance.r);
}
else if (Illuminance.r < 999999.0f)
{
PrintFloatNoFractionLarge(PixelPos, OutColor.xyz, float3(1, 0.5, 0.5), TopLeft, Illuminance.r);
}
else
{
PrintFloatNoFraction(PixelPos, OutColor.xyz, NumberColor, TopLeft, Illuminance.r, 10);
}
TopLeft.x += 100;
if (Illuminance.g < 1000.0f)
{
PrintFloat(PixelPos, OutColor.xyz, float3(0.5, 1, 0.5), TopLeft, Illuminance.g);
}
else if (Illuminance.g < 999999.0f)
{
PrintFloatNoFractionLarge(PixelPos, OutColor.xyz, float3(0.5, 1, 0.5), TopLeft, Illuminance.g);
}
else
{
PrintFloatNoFraction(PixelPos, OutColor.xyz, NumberColor, TopLeft, Illuminance.g, 10);
}
TopLeft.x += 100;
if (Illuminance.b < 1000.0f)
{
PrintFloat(PixelPos, OutColor.xyz, float3(0.5, 0.5, 1), TopLeft, Illuminance.b);
}
else if (Illuminance.b < 999999.0f)
{
PrintFloatNoFractionLarge(PixelPos, OutColor.xyz, float3(0.5, 0.5, 1), TopLeft, Illuminance.b);
}
else
{
PrintFloatNoFraction(PixelPos, OutColor.xyz, NumberColor, TopLeft, Illuminance.b, 10);
}
return;
}
}
if (EyeAdaptation_VisualizeDebugType == 0)
{
OutColor = Texture2DSample(SceneColorTexture, SceneColorSampler, UV);
}
else if (EyeAdaptation_VisualizeDebugType == 1)
{
float4 SceneColor = Texture2DSample(HDRSceneColorTexture, HDRSceneColorSampler, UV);
SceneColor.xyz *= View.OneOverPreExposure;
float Percentile = ComputePlaceInHistogram(SceneColor.xyz);
OutColor.rgb = Percentile;
if (Percentile < EyeAdaptation_ExposureLowPercent)
{
OutColor.rgb = float3(.5,0,0);
}
else if (Percentile >= EyeAdaptation_ExposureHighPercent)
{
OutColor.rgb = float3(0,0,.5);
}
}
else // should never happen?
{
OutColor.rgb = float3(0,0,0);
}
// Compute and apply weight value at this location
float2 NdPos = Output_ViewportSizeInverse * (PixelPos.xy - Output_ViewportMin);
float weight = AdaptationWeightTexture(NdPos);
float2 IDAreaLocalUV = ViewLocalUV * 2 + float2(-1, 0);
// left top of the border
const int2 HistogramLeftTop = int2(Output_ViewportMin.x + 64, Output_ViewportMax.y - 128 - 32);
const int2 HistogramSize = int2(Output_ViewportMax.x - Output_ViewportMin.x - 64 * 2, 128);
const int HistogramOuterBorder = 4;
// (0, 0) .. (1, 1)
float2 InsetPx = PixelPos - HistogramLeftTop;
float2 InsetUV = InsetPx / HistogramSize;
const float3 BorderColor = Colorize(InsetUV.x);
float BorderDistance = ComputeDistanceToRect(PixelPos, HistogramLeftTop, HistogramSize);
// thin black border around the histogram
OutColor.xyz = lerp(float3(0, 0, 0), OutColor.xyz, saturate(BorderDistance - (HistogramOuterBorder + 2)));
// big solid border around the histogram
OutColor.xyz = lerp(BorderColor, OutColor.xyz, saturate(BorderDistance - (HistogramOuterBorder + 1)));
// thin black border around the histogram
OutColor.xyz = lerp(float3(0, 0, 0), OutColor.xyz, saturate(BorderDistance - 1));
if(BorderDistance > 0)
{
// outside of the histogram
return;
}
// inside the histogram
uint Bucket = (uint)(InsetUV.x * HISTOGRAM_SIZE);
float HistogramSum = ComputeHistogramSum(HistogramTexture);
float MinExposure = EyeAdaptation_MinAverageLuminance / 0.18;
float MaxExposure = EyeAdaptation_MaxAverageLuminance / 0.18;
if(InsetUV.x < ComputeHistogramPositionFromLuminance(MinExposure))
{
// < min: grey
OutColor.xyz = lerp(OutColor.xyz, float3(0.5f, 0.5f, 0.5f), 0.5f);
}
else if(InsetUV.x < ComputeHistogramPositionFromLuminance(MaxExposure))
{
// >= min && < max: green
OutColor.xyz = lerp(OutColor.xyz, float3(0.5f, 0.8f, 0.5f), 0.5f);
}
else
{
// >= max: grey
OutColor.xyz = lerp(OutColor.xyz, float3(0.5f, 0.5f, 0.5f), 0.5f);
}
float LocalHistogramValue = GetHistogramBucket(HistogramTexture, Bucket) / ComputeHistogramMax(HistogramTexture);
if(LocalHistogramValue >= 1 - InsetUV.y)
{
// histogram bars
OutColor.xyz = lerp(OutColor.xyz, Colorize(InsetUV.x), 0.5f);
}
{
// HDR luminance >0
float LuminanceVal = ComputeLuminanceFromHistogramPosition(InsetUV.x);
// HDR > 0
float3 AdpatedLuminance = EyeAdaptationResult * float3(LuminanceVal, LuminanceVal, LuminanceVal);
// 0..1
float3 TonemappedLuminance = FilmToneMap(AdpatedLuminance);
float3 DistMask = saturate(1.0 - 100.0 * abs(TonemappedLuminance - (1.0 - InsetUV.y)));
OutColor = lerp(OutColor, float4(1, 1, 1, 0), float4(DistMask, 0.0));
}
{
float ValuePx = ComputeHistogramPositionFromLuminance(EyeAdaptationAverageSceneLuminance/.18f) * HistogramSize.x ;
if(abs(InsetPx.x - ValuePx) < 3)
{
// blue line to show the clamped percentil
OutColor = lerp(OutColor, float4(0, 0, 1, 0), 0.5f);
}
}
// eye adaptation without bias
{
const float OneOverEyeAdaptationResult = 1.0f/EyeAdaptationResultWithoutExposureBias;
const float EyeAdaptationValue = ComputeHistogramPositionFromLuminance(OneOverEyeAdaptationResult);
const float ValuePx = EyeAdaptationValue * HistogramSize.x;
const float EyeAdaptationEV100 = log2(OneOverEyeAdaptationResult);
PrintSmallFloatWithSign(PixelPos, OutColor.xyz, float3(1.0, 0.0, 1.0), HistogramLeftTop + int2(ValuePx + - 6 * 8 - 3, 1), EyeAdaptationEV100);
if(abs(InsetPx.x - ValuePx) < 2 && PixelPos.y > HistogramLeftTop.y + 9)
{
// white line to show the smoothed exposure
OutColor = lerp(OutColor, float4(0.5, 0, .5, 0), 1.0f);
}
}
// eye adaptation
{
const float OneOverEyeAdaptationResult = 1.0f / EyeAdaptationResult;
const float EyeAdaptationValue = ComputeHistogramPositionFromLuminance(OneOverEyeAdaptationResult);
const float ValuePx = EyeAdaptationValue * HistogramSize.x;
const float EyeAdaptationEV100 = log2(OneOverEyeAdaptationResult/EyeAdaptation_LuminanceMax);
PrintSmallFloatWithSign(PixelPos, OutColor.xyz, float3(1, 1, 1), HistogramLeftTop + int2(ValuePx + - 6 * 8 - 3, 1), EyeAdaptationEV100);
if(abs(InsetPx.x - ValuePx) < 2 && PixelPos.y > HistogramLeftTop.y + 18)
{
// white line to show the smoothed exposure
OutColor = lerp(OutColor, float4(1, 1, 1, 0), 1.0f);
}
}
return;
}