Files
UnrealEngine/Engine/Shaders/Private/PostProcessHistogramCommon.ush
2025-05-18 13:04:45 +08:00

327 lines
11 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
PostProcessHistogramCommon.ush: PostProcessing histogram shared functions and structures.
=============================================================================*/
#pragma once
float EyeAdaptation_ExposureLowPercent;
float EyeAdaptation_ExposureHighPercent;
float EyeAdaptation_MinAverageLuminance;
float EyeAdaptation_MaxAverageLuminance;
float EyeAdaptation_ExposureCompensationSettings;
float EyeAdaptation_ExposureCompensationCurve;
float EyeAdaptation_DeltaWorldTime;
float EyeAdaptation_ExposureSpeedUp;
float EyeAdaptation_ExposureSpeedDown;
float EyeAdaptation_HistogramScale;
float EyeAdaptation_HistogramBias;
float EyeAdaptation_LuminanceMin;
float EyeAdaptation_BlackHistogramBucketInfluence;
float EyeAdaptation_IgnoreMaterialsEvaluationPositionBias;
float EyeAdaptation_IgnoreMaterialsLuminanceScale;
float EyeAdaptation_IgnoreMaterialsMinBaseColorLuminance;
uint EyeAdaptation_IgnoreMaterialsReconstructFromSceneColor;
float EyeAdaptation_GreyMult;
float3 EyeAdaptation_LuminanceWeights;
float EyeAdaptation_ExponentialUpM;
float EyeAdaptation_ExponentialDownM;
float EyeAdaptation_StartDistance;
float EyeAdaptation_LuminanceMax;
float EyeAdaptation_ForceTarget;
int EyeAdaptation_VisualizeDebugType;
Texture2D EyeAdaptation_MeterMaskTexture;
SamplerState EyeAdaptation_MeterMaskSampler;
float LocalExposure_HighlightContrastScale;
float LocalExposure_ShadowContrastScale;
float LocalExposure_DetailStrength;
float LocalExposure_BlurredLuminanceBlend;
float LocalExposure_MiddleGreyExposureCompensation;
float2 LocalExposure_BilateralGridUVScale;
float LocalExposure_HighlightThreshold;
float LocalExposure_ShadowThreshold;
float LocalExposure_HighlightThresholdStrength;
float LocalExposure_ShadowThresholdStrength;
float CalculateEyeAdaptationLuminance(float3 Color)
{
return max(dot(Color, EyeAdaptation_LuminanceWeights), EyeAdaptation_LuminanceMin);
}
// inverse of ComputeLogLuminanceFromHistogramPosition
// @param LogLuminance
// @return HistogramPosition 0..1
float ComputeHistogramPositionFromLogLuminance(float LogLuminance)
{
return LogLuminance * EyeAdaptation_HistogramScale + EyeAdaptation_HistogramBias;
}
// inverse of ComputeLuminanceFromHistogramPosition
// @param Luminance
// @return HistogramPosition 0..1
float ComputeHistogramPositionFromLuminance(float Luminance)
{
return ComputeHistogramPositionFromLogLuminance(log2(Luminance));
}
// inverse of ComputeHistogramPositionFromLogLuminance()
// @param HistogramPosition 0..1
// @return LogLuminance
float ComputeLogLuminanceFromHistogramPosition(float HistogramPosition)
{
return ((HistogramPosition - EyeAdaptation_HistogramBias) / EyeAdaptation_HistogramScale);
}
// inverse of ComputeHistogramPositionFromLuminance()
// @param HistogramPosition 0..1
// @return Luminance
float ComputeLuminanceFromHistogramPosition(float HistogramPosition)
{
return exp2(ComputeLogLuminanceFromHistogramPosition(HistogramPosition));
}
#ifndef HISTOGRAM_SIZE
#define HISTOGRAM_SIZE 64
#endif
#define HISTOGRAM_TEXEL_SIZE (HISTOGRAM_SIZE / 4)
float4 ComputeARGBStripeMaskInt(uint x)
{
return float4(
(x % 4) == 0,
(x % 4) == 1,
(x % 4) == 2,
(x % 4) == 3);
}
float GetHistogramBucket(Texture2D HistogramTexture, uint BucketIndex)
{
uint Texel = BucketIndex / 4;
float4 HistogramColor = HistogramTexture.Load(int3(Texel, 0, 0));
uint channel = BucketIndex % 4;
float UnweightedValue = HistogramColor.r;
UnweightedValue = (channel == 1) ? HistogramColor.g : UnweightedValue;
UnweightedValue = (channel == 2) ? HistogramColor.b : UnweightedValue;
UnweightedValue = (channel == 3) ? HistogramColor.a : UnweightedValue;
return UnweightedValue;
}
float ComputeHistogramSum(Texture2D HistogramTexture)
{
float Sum = 0;
for(uint i = 0; i < HISTOGRAM_SIZE; ++i)
{
Sum += GetHistogramBucket(HistogramTexture, i);
}
return Sum;
}
// @param MinFractionSum e.g. ComputeHistogramSum() * 0.5f for 50% percentil
// @param MaxFractionSum e.g. ComputeHistogramSum() * 0.9f for 90% percentil
float ComputeAverageLuminanceWithoutOutlier(Texture2D HistogramTexture, float MinFractionSum, float MaxFractionSum)
{
float2 SumWithoutOutliers = 0;
UNROLL for(uint i = 0; i < HISTOGRAM_SIZE; ++i)
{
float LocalValue = GetHistogramBucket(HistogramTexture, i);
// remove outlier at lower end
float Sub = min(LocalValue, MinFractionSum);
LocalValue = LocalValue - Sub;
MinFractionSum -= Sub;
MaxFractionSum -= Sub;
// remove outlier at upper end
LocalValue = min(LocalValue, MaxFractionSum);
MaxFractionSum -= LocalValue;
float LogLuminanceAtBucket = ComputeLogLuminanceFromHistogramPosition(float(i) / (float)(HISTOGRAM_SIZE-1));
SumWithoutOutliers += float2(LogLuminanceAtBucket, 1) * LocalValue;
}
float AvgLogLuminance = SumWithoutOutliers.x / max(0.0001f, SumWithoutOutliers.y);
return exp2(AvgLogLuminance);
}
float ComputeEyeAdaptationExposure(Texture2D HistogramTexture)
{
const float HistogramSum = ComputeHistogramSum(HistogramTexture);
const float AverageSceneLuminance = ComputeAverageLuminanceWithoutOutlier(HistogramTexture, HistogramSum * EyeAdaptation_ExposureLowPercent, HistogramSum * EyeAdaptation_ExposureHighPercent);
const float LumAve = AverageSceneLuminance;
const float ClampedLumAve = LumAve;
// No longer clamping here. We are letting the target exposure be outside the min/max range, and then letting the exposure
// gradually transion.
return ClampedLumAve;
}
float AdaptationWeightTexture(float2 UV)
{
return Texture2DSampleLevel(EyeAdaptation_MeterMaskTexture, EyeAdaptation_MeterMaskSampler, UV, 0).x;
}
float ExponentialAdaption(float Current, float Target, float FrameTime, float AdaptionSpeed, float M)
{
const float Factor = 1.0f - exp2(-FrameTime * AdaptionSpeed);
const float Value = Current + (Target - Current) * Factor * M;
return Value;
}
float LinearAdaption(float Current, float Target, float FrameTime, float AdaptionSpeed)
{
const float Offset = FrameTime * AdaptionSpeed;
const float Value = (Current < Target) ? min(Target, Current + Offset) : max(Target, Current - Offset);
return Value;
}
float ComputeEyeAdaptation(float OldExposure, float TargetExposure, float FrameTime)
{
const float LogTargetExposure = log2(TargetExposure);
const float LogOldExposure = log2(OldExposure);
const float LogDiff = LogTargetExposure - LogOldExposure;
const float AdaptionSpeed = (LogDiff > 0) ? EyeAdaptation_ExposureSpeedUp : EyeAdaptation_ExposureSpeedDown;
const float M = (LogDiff > 0) ? EyeAdaptation_ExponentialUpM : EyeAdaptation_ExponentialDownM;
const float AbsLogDiff = abs(LogDiff);
// blended exposure
const float LogAdaptedExposure_Exponential = ExponentialAdaption(LogOldExposure, LogTargetExposure, FrameTime, AdaptionSpeed, M);
const float LogAdaptedExposure_Linear = LinearAdaption(LogOldExposure, LogTargetExposure, FrameTime, AdaptionSpeed);
const float LogAdaptedExposure = AbsLogDiff > EyeAdaptation_StartDistance ? LogAdaptedExposure_Linear : LogAdaptedExposure_Exponential;
// Note: no clamping here. The target exposure should always be clamped so if we are below the min or above the max,
// instead of clamping, we will gradually transition to the target exposure. If were to clamp, the then we would have a harsh transition
// when going from postFX volumes with different min/max luminance values.
const float AdaptedExposure = exp2(LogAdaptedExposure);
// for manual mode or camera cuts, just lerp to the target
const float AdjustedExposure = lerp(AdaptedExposure,TargetExposure,EyeAdaptation_ForceTarget);
return AdjustedExposure;
}
#define BILATERAL_GRID_DEPTH 32
float CalculateBaseLogLuminance(float BilateralLum, float BlurredLum, float BlurredLumBlend, float ExposureScale)
{
return lerp(BilateralLum, BlurredLum, BlurredLumBlend) + log2(ExposureScale);
}
float CalculateBaseLogLuminance(float LogLum, float BlurredLumBlend, float ExposureScale, float2 UV, Texture3D LumBilateralGrid, Texture2D BlurredLogLum, SamplerState LumBilateralGridSampler, SamplerState BlurredLogLumSampler)
{
float3 BilateralGridUVW;
BilateralGridUVW.xy = UV * LocalExposure_BilateralGridUVScale;
BilateralGridUVW.z = (ComputeHistogramPositionFromLogLuminance(LogLum) * (BILATERAL_GRID_DEPTH - 1) + 0.5f) / BILATERAL_GRID_DEPTH;
float2 BilateralGridLum = Texture3DSample(LumBilateralGrid, LumBilateralGridSampler, BilateralGridUVW).xy;
float BilateralLum = BilateralGridLum.x / BilateralGridLum.y;
float BlurredLum = Texture2DSample(BlurredLogLum, BlurredLogLumSampler, UV).r;
if (BilateralGridLum.y < 0.001)
{
// fallback to blurred luminance if bilateral grid doesn't have data
// this can happen since grid is populated using half resolution image
BilateralLum = BlurredLum;
}
return CalculateBaseLogLuminance(BilateralLum, BlurredLum, BlurredLumBlend, ExposureScale);
}
float CalculateLogLocalExposure(float LogLum, float BaseLogLum, float LogMiddleGrey, float HighlightContrastScale, float ShadowContrastScale, float DetailStrength)
{
float DetailLogLum = LogLum - BaseLogLum;
float BaseCentered = (BaseLogLum - LogMiddleGrey);
float ContrastScale = BaseCentered > 0 ? HighlightContrastScale : ShadowContrastScale;
// Apply threshold
float ThresholdOffset = 0;
{
float M;
float T;
if (BaseCentered > 0)
{
M = max(0.0f, BaseCentered - LocalExposure_HighlightThreshold);
T = smoothstep(LocalExposure_HighlightThreshold, LocalExposure_HighlightThreshold + 1.0f / (1.0f - LocalExposure_HighlightThresholdStrength), abs(BaseCentered));
}
else
{
M = min(0.0f, BaseCentered + LocalExposure_ShadowThreshold);
T = smoothstep(LocalExposure_ShadowThreshold, LocalExposure_ShadowThreshold + 1.0f / (1.0f - LocalExposure_ShadowThresholdStrength), abs(BaseCentered));
}
ThresholdOffset = (BaseCentered - M) * (1 - T);
BaseCentered -= ThresholdOffset;
}
float LogLocalLum = LogMiddleGrey + ThresholdOffset + BaseCentered * ContrastScale + DetailLogLum * DetailStrength;
return LogLocalLum - LogLum;
}
float CalculateLocalExposure(float LogLum, float BaseLogLum, float LogMiddleGrey, float HighlightContrastScale, float ShadowContrastScale, float DetailStrength)
{
return exp2(CalculateLogLocalExposure(LogLum, BaseLogLum, LogMiddleGrey, HighlightContrastScale, ShadowContrastScale, DetailStrength));
}
#define EXPOSURE_FUSION_USE_FILM_TONEMAP 1
#if EXPOSURE_FUSION_USE_FILM_TONEMAP
#include "TonemapCommon.ush"
#endif
float ExposureFusionTonemap(float Lum)
{
#if EXPOSURE_FUSION_USE_FILM_TONEMAP
float TonemappedLuminance = FilmToneMap(Lum).r;
#else
float TonemappedLuminance = Lum / (1 + Lum);
#endif
TonemappedLuminance = sqrt(TonemappedLuminance);
return TonemappedLuminance;
}
float ExposureFusionInverseTonemap(float TonemappedLuminance)
{
TonemappedLuminance = TonemappedLuminance * TonemappedLuminance;
#if EXPOSURE_FUSION_USE_FILM_TONEMAP
float Lum = FilmToneMapInverse(TonemappedLuminance).r;
#else
float Lum = TonemappedLuminance / (1 - TonemappedLuminance);
#endif
return Lum;
}
float CalculateFusionLocalExposure(float Lum, float FusionResult)
{
#if 1
return ExposureFusionInverseTonemap(min(FusionResult, 1.0f)) / Lum;
#else
const float LumTonemapped = ExposureFusionTonemap(Lum);
return FusionResult / LumTonemapped;
#endif
}