本篇将基于基础篇实现一个较为完整的基于直方图的自动曝光功能
算法流程
- 拿到Scene Color
- 计算Avg Luminance
- 基于每个Pixel Color→计算Luminance
- 直方图分成64个部分,每部分视作一个桶,将Luminance换算成”挡位”,丢进64个桶
- 最暗与最亮的桶,不纳入后续的计算
- 剩下的桶,加权平均
- 还原成Luminance,得到Luminance_{Avg}
- $Luminance_{Avg} = Clamp[Luminance_{Avg}, Luminance_{Min}, Luminance_{Max}]$
- 计算曝光系数
Exposure Scale = MiddleGrey * exp2(EC) / Luminance_{Avg}
其中,EC意味手动控制的曝光强度
算法实现
整体可以分成三个Pass:
- Clear Histogram
- Build Histogram
- Compute Exposure Scale
Build Histogram
核心部分:丢进64个桶
groupshared uint GSLocalHistogram[HISTOGRAM_SIZE];
[branch]
if (GroupIndex < HISTOGRAM_SIZE)
{
GSLocalHistogram[GroupIndex] = 0;
}
GroupMemoryBarrierWithGroupSync();
uint2 SourceSize = (uint2)g_SourceSize.xy;
if (SamplePos.x < SourceSize.x && SamplePos.y < SourceSize.y)
{
float3 SourceColor = LoadTexture2D(g_SourceTexIndex, SamplePos);
float Luminance = AutoExposureLuminance(SourceColor);
uint BinIndex = LuminanceToHistogramBin(Luminance, g_HistogramScale, g_HistogramBias);
InterlockedAdd(GSLocalHistogram[BinIndex], 1);
}
GroupMemoryBarrierWithGroupSync();
[branch]
if (GroupIndex < HISTOGRAM_SIZE)
{
InterlockedAdd(HistogramBuffer[GroupIndex], GSLocalHistogram[GroupIndex]);
}
Compute Exposure Scale
核心部分:计算曝光系数
groupshared uint GSGlobalHistogram[HISTOGRAM_SIZE];
if (groupIndex < HISTOGRAM_SIZE)
{
HistogramRT[UINT2(groupIndex, 0)] = GSGlobalHistogram[groupIndex] = HistogramBuffer[groupIndex];
}
GroupMemoryBarrierWithGroupSync();
uint SumBin = 0;
[unroll]
for (UINT i = 0;i < HISTOGRAM_SIZE; i++)
{
SumBin += GSGlobalHistogram[i];
}
float MinBin = (float)SumBin * saturate(g_LowPercent);
float MaxBin = (float)SumBin * saturate(max(g_HighPercent, g_LowPercent));
float TotalEv = 0;
float TotalCount = 0;
[unroll]
for (UINT i = 0; i < HISTOGRAM_SIZE; i++)
{
float CurrBin = GSGlobalHistogram[i];
float CurrSub = min(CurrBin, MinBin);
CurrBin -= CurrSub;
MinBin -= CurrSub;
MaxBin -= CurrSub;
CurrBin = min(CurrBin, MaxBin);
MaxBin -= CurrBin;
float HistogramPos = float(i) / (float(HISTOGRAM_SIZE) - 1);
TotalEv += ComputeLogLuminanceFromHistogramPosition(HistogramPos, g_HistogramScale, g_HistogramBias) * (float)CurrBin;
TotalCount += (float)CurrBin;
}
float AverEV = TotalEv * rcp(max(TotalCount, 1e-5));
float AverLuminance = ComputeLuminanceFromLogLuminance(AverEV);
float ClampAverLuminance = clamp(AverLuminance, g_MinAverageLuminance, g_MaxAverageLuminance);
float MiddleGreyLuminance = g_MiddleGrey * ComputeLuminanceFromLogLuminance(g_ExposureCompensationEV);
float ExposureScale = MiddleGreyLuminance / max(ClampAverLuminance, 1e-5);
如何正确应用曝光系数
在考虑这个问题时,需要搞清楚渲染管线中,曝光系数会对哪些后效,产生很严重的影响(大大影响其本来的效果,将其带偏),没错就是”Tonemap”
也就是说,需要考虑是在Tonemap之前、之中、之后,计算、应用曝光系数
答案是在Tonemap之前计算曝光系数,之前应用曝光系数
为什么是Tonemap之前计算曝光系数
因为tonemap曲线非线性,会压暗部、亮部,且计算自动曝光的时候,亮度会和中灰对比
经过非线性映射后亮度全部都被映射到[0,1],整体不会亮暗不会被区分开
为什么是Tonemap之前应用曝光系数
假设是Tonemap后(亮度被映射到[0,1]),应用曝光系数,亮暗不会拉开,自动曝光的收益不高
而应用Tonemap前(亮度范围很宽),此时应用曝光系数,亮暗对比会被拉的很开,效果很明显
目前的效果
开启自动曝光:
未开启自动曝光:
额外的功能
预曝光
- 方案:创建一个Buffer,存储两个float,一个是历史帧的预曝光,另一个是当前帧的预曝光
- 为什么要两个?
因为渲染器包含TAA,历史帧也需要乘预曝光,所以也就是说,Scene Color Lighting乘上当前帧的预曝光后,后续在TAA Pass,计算历史帧 Color时,还需要乘上历史帧的预曝光 / 当前帧的预曝光
-
为什么不都在TAA阶段应用,而是Scene Color Lighting 与历史帧
因为Scene Color Lighting Pass计算时有32精度,但存储之后只有16精度,后续在TAA读到的可能是有误的数据
-
如何更新
在计算曝光系数时,用当前帧的预曝光更新历史帧的预曝光,再用计算得到的曝光系数更新当前帧的预曝光
-
还需要考虑Skybox Pass
- 为什么要两个?
时间平滑
- 方案:无需额外Buffer,直接使用存储非Log版的exposure scale的值,作为上一帧;再计算当前帧的平均亮度,再计算档位,最后插值平滑
-
实现
float AvgLuminance = ComputeAvgLuminance(SumBin); float OldExposureScaleWithoutEV = GetExposureScaleWithoutEV(ExposureScaleBuffer); float4 ExposureData = ComputeTemporalSmooth(AvgLuminance, OldExposureScaleWithoutEV);计算档位
float MiddleGrey = max(g_MiddleGrey, 1e-5); float TargetAvg = clamp(AvgLuminance, g_MinAverageLuminance, g_MaxAverageLuminance); float TargetExposure = TargetAvg / MiddleGrey; float OldExposure = rcp(max(OldExposureScaleWithoutEV, 1e-5)); float AutoExposureAdapt(float OldExposure, float TargetExposure, float FrameTime, float SpeedUp, float SpeedDown, float StartDistance, float ForceTarget) { OldExposure = max(OldExposure, 1e-5f); TargetExposure = max(TargetExposure, 1e-5f); FrameTime = clamp(FrameTime, 1e-4f, 0.1f); float LogTarget = log2(TargetExposure); float LogOld = log2(OldExposure); float LogDiff = LogTarget - LogOld; float Speed = max((LogDiff > 0.0f) ? SpeedUp : SpeedDown, 0.0f); float M = AutoExposureExponentialM(Speed, StartDistance); float LogExp = AutoExposureExponentialAdapt(LogOld, LogTarget, FrameTime, Speed, M); float LogLin = AutoExposureLinearAdapt(LogOld, LogTarget, FrameTime, Speed); float LogAdapted = abs(LogDiff) > StartDistance ? LogLin : LogExp; float Adapted = exp2(LogAdapted); return lerp(Adapted, TargetExposure, saturate(ForceTarget)); }处理线性到指数的切变:
先设定一个距离,档位差小于等于这个距离,从线性衰减切换到指数衰减(每次走剩余距离的百分比)
需要注意:由于从线性衰减切换到指数衰减,肯定会有切变,UE做了对应处理,在差不多等于距离时,执行了个换算,将指数衰减的值变换到线性衰减(同一衰减距离),防止切变
(这里直接copy的ue的代码)
float AutoExposureExponentialM(float Speed, float StartDistance) { const float kFrameTimeEps = 1.0f / 60.0f; float StartTime = StartDistance / max(Speed, 1e-3f); float Denom = (1.0f - exp2(-kFrameTimeEps * Speed)) * StartTime; return kFrameTimeEps / max(Denom, 1e-5f); } float AutoExposureExponentialAdapt(float Current, float Target, float FrameTime, float Speed, float M) { float Factor = 1.0f - exp2(-FrameTime * Speed); return Current + (Target - Current) * Factor * M; } float AutoExposureLinearAdapt(float Current, float Target, float FrameTime, float Speed) { float Offset = FrameTime * Speed; return (Current < Target) ? min(Target, Current + Offset) : max(Target, Current - Offset); }






Comments | NOTHING