https://catlikecoding.com/unity/tutorials/custom-srp/hdr/
1 HDR
1.1 HDR反射探针
我们可以在反射探针中开启HDR选项,默认是开启的。
1.2 HDR摄像机
摄像机也有HDR选项,有Off
和Use Graphics Settings
两个选项。Use Graphics Settings
只对应那些允许HDR的摄像机。我们在CustomRenderPipelineAsset
中添加对应的选项:
[SerializeField]
bool allowHDR = true;
并传入到CreatePipeline
中。
CustomRenderPipeline
也添加对应的属性:
bool allowHDR;
…
public CustomRenderPipeline (
bool allowHDR,
…
)
{
this.allowHDR = allowHDR;
…
}
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
)
{
foreach (Camera camera in cameras) {
renderer.Render(
context, camera, allowHDR,
useDynamicBatching, useGPUInstancing, useLightsPerObject,
shadowSettings, postFXSettings
);
}
}
在CameraRenderer
中添加,必须RP和摄像机都允许:
bool useHDR;
public void Render (
ScriptableRenderContext context, Camera camera, bool allowHDR,
bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject,
ShadowSettings shadowSettings, PostFXSettings postFXSettings
)
{
…
if (!Cull(shadowSettings.maxDistance))
{
return;
}
useHDR = allowHDR && camera.allowHDR;
…
}
1.3 HDR渲染纹理
在CameraRenderer.Setup
中,我们创建HDR格式的中间帧缓冲,替换原有的LDR:
buffer.GetTemporaryRT(
frameBufferId, camera.pixelWidth, camera.pixelHeight,
32, FilterMode.Bilinear, useHDR ?
RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default
);
现在帧调试器中可以看到默认的HDR格式,,即每个通道16位。
1.4 HDR后处理
我们需要在HDR范围内进行后处理。在PostFXStack.Setup
中传入参数:
postFXStack.Setup(context, camera, postFXSettings, useHDR);
Setup
:
bool useHDR;
…
public void Setup (
ScriptableRenderContext context, Camera camera, PostFXSettings settings,
bool useHDR
)
{
this.useHDR = useHDR;
…
}
在DoBloom
中使用恰当的纹理格式:
RenderTextureFormat format = useHDR ?
RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
HDR和LDR发光的区别取决于场景的亮度。
1.5 闪烁问题
HDR的一个缺点是会产生亮度远高于周围区域的小型亮部,区域足够小的话,在移动时可能会产生闪烁问题。我们可以在预滤波阶段加大模糊程度来减少闪烁问题。在BloomSettings
中加上选项:
public bool fadeFireflies;
添加一个新的BloomPrefilterFireflies
pass,在DoBloom
中应用:
Draw(
sourceId, bloomPrefilterId, bloom.fadeFireflies ?
Pass.BloomPrefilterFireflies : Pass.BloomPrefilter
);
最直接的方式是将降采样滤波器改为更大的盒状滤波器。
imagefloat4 BloomPrefilterFirefliesPassFragment (Varyings input) : SV_TARGET {
float3 color = 0.0;
float2 offsets[] = {
float2(0.0, 0.0),
float2(-1.0, -1.0), float2(-1.0, 1.0), float2(1.0, -1.0), float2(1.0, 1.0),
float2(-1.0, 0.0), float2(1.0, 0.0), float2(0.0, -1.0), float2(0.0, 1.0)
};
for (int i = 0; i < 9; i++)
{
float3 c =
GetSource(input.screenUV + offsets[i] * GetSourceTexelSize().xy * 2.0).rgb;
c = ApplyBloomThreshold(c);
color += c;
}
color *= 1.0 / 9.0;
return float4(color, 1.0);
}
这还不足以改善问题,我们需要使用基于颜色亮度的加权平均,而不是简单的算术平均。颜色的亮度是其可察觉亮度,我们使用定义在Color.hlsl
中的Luminance
方法。一个采样的权重为,是亮度。
float4 BloomPrefilterFirefliesPassFragment (Varyings input) : SV_TARGET {
float3 color = 0.0;
float weightSum = 0.0;
…
for (int i = 0; i < 9; i++)
{
…
float w = 1.0 / (Luminance(c) + 1.0);
color += c * w;
weightSum += w;
}
color /= weightSum;
return float4(color, 1.0);
}
因为我们在最开始的预滤波步骤后执行高斯模糊,我们可以预中心点邻近的4个样本,减少采样数:
float2 offsets[] = {
float2(0.0, 0.0),
float2(-1.0, -1.0), float2(-1.0, 1.0), float2(1.0, -1.0), float2(1.0, 1.0)//,
//float2(-1.0, 0.0), float2(1.0, 0.0), float2(0.0, -1.0), float2(0.0, 1.0)
};
for (int i = 0; i < 5; i++) { … }
2 散射发光
散射发光是一种摄像机效果,它并不加亮度,只是将光散射开。
2.1 发光模式
我们在BloomSettings
中添加发光模式枚举来支持不同种的发光:
public enum Mode { Additive, Scattering }
public Mode mode;
[Range(0f, 1f)]
public float scatter;
将BloomCombine
更名为BloomAdd
,引入新的BloomScatter
Pass。在DoBloom
的结合阶段应用正确的pass:
Pass combinePass;
if (bloom.mode == PostFXSettings.BloomSettings.Mode.Additive)
{
combinePass = Pass.BloomAdd;
buffer.SetGlobalFloat(bloomIntensityId, 1f);
}
else
{
combinePass = Pass.BloomScatter;
buffer.SetGlobalFloat(bloomIntensityId, bloom.scatter);
}
if (i > 1)
{
buffer.ReleaseTemporaryRT(fromId - 1);
toId -= 5;
for (i -= 1; i > 0; i--) {
buffer.SetGlobalTexture(fxSource2Id, toId + 1);
Draw(fromId, toId, combinePass);
…
}
}
else
{
buffer.ReleaseTemporaryRT(bloomPyramidId);
}
buffer.SetGlobalFloat(bloomIntensityId, bloom.intensity);
buffer.SetGlobalTexture(fxSource2Id, sourceId);
Draw(fromId, BuiltinRenderTextureType.CameraTarget, combinePass);
BloomScatter
和BloomAdd
类似,只不过会将两张源纹理根据强度插值,而不是简单的相加:
float4 BloomScatterPassFragment (Varyings input) : SV_TARGET
{
float3 lowRes;
if (_BloomBicubicUpsampling)
{
lowRes = GetSourceBicubic(input.screenUV).rgb;
}
else
{
lowRes = GetSource(input.screenUV).rgb;
}
float3 highRes = GetSource2(input.screenUV).rgb;
return float4(lerp(highRes, lowRes, _BloomIntensity), 1.0);
}
2.2 散射限制
我们将散射的范围限制在0.05-0.95:
public struct BloomSettings
{
…
[Range(0.05f, 0.95f)]
public float scatter;
}
[SerializeField]
BloomSettings bloom = new BloomSettings {
scatter = 0.7f
};
我们在DoBloom
中不使用高于1的强度来散射发光:
float finalIntensity;
if (bloom.mode == PostFXSettings.BloomSettings.Mode.Additive)
{
combinePass = Pass.BloomAdd;
buffer.SetGlobalFloat(bloomIntensityId, 1f);
finalIntensity = bloom.intensity;
}
else
{
combinePass = Pass.BloomScatter;
buffer.SetGlobalFloat(bloomIntensityId, bloom.scatter);
finalIntensity = Mathf.Min(bloom.intensity, 0.95f);
}
if (i > 1)
{
…
}
else
{
buffer.ReleaseTemporaryRT(bloomPyramidId);
}
buffer.SetGlobalFloat(bloomIntensityId, finalIntensity);
2.3 阈值
散射发光应该只针对很亮的区域使用,我们使用阈值来限制区域。创建一个额外的BloomScatterFinal
pass来进行散射发光最后的绘制:
Pass combinePass, finalPass;
float finalIntensity;
if (bloom.mode == PostFXSettings.BloomSettings.Mode.Additive)
{
combinePass = finalPass = Pass.BloomAdd;
buffer.SetGlobalFloat(bloomIntensityId, 1f);
finalIntensity = bloom.intensity;
}
else
{
combinePass = Pass.BloomScatter;
finalPass = Pass.BloomScatterFinal;
buffer.SetGlobalFloat(bloomIntensityId, bloom.scatter);
finalIntensity = Mathf.Min(bloom.intensity, 1f);
}
…
Draw(fromId, BuiltinRenderTextureType.CameraTarget, finalPass);
}
唯一的区别在于,为第分辨率pass奖赏了一些值:
float4 BloomScatterFinalPassFragment (Varyings input) : SV_TARGET {
float3 lowRes;
if (_BloomBicubicUpsampling)
{
lowRes = GetSourceBicubic(input.screenUV).rgb;
}
else
{
lowRes = GetSource(input.screenUV).rgb;
}
float3 highRes = GetSource2(input.screenUV).rgb;
lowRes += highRes - ApplyBloomThreshold(highRes);
return float4(lerp(highRes, lowRes, _BloomIntensity), 1.0);
}
3 色调映射
3.1 额外的后处理步骤
我们在发光后进行色调映射,添加DoToneMapping
方法:
void DoToneMapping(int sourceId)
{
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
}
我们需要调整发光的结果,因此添加一个全分辨率的临时渲染纹理,在DoBloom
中作为最后的目标:
int
bloomBucibicUpsamplingId = Shader.PropertyToID("_BloomBicubicUpsampling"),
bloomIntensityId = Shader.PropertyToID("_BloomIntensity"),
bloomPrefilterId = Shader.PropertyToID("_BloomPrefilter"),
bloomResultId = Shader.PropertyToID("_BloomResult"),
…;
…
bool DoBloom (int sourceId)
{
//buffer.BeginSample("Bloom");
PostFXSettings.BloomSettings bloom = settings.Bloom;
int width = camera.pixelWidth / 2, height = camera.pixelHeight / 2;
if (
bloom.maxIterations == 0 || bloom.intensity <= 0f ||
height < bloom.downscaleLimit * 2 || width < bloom.downscaleLimit * 2
)
{
//Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
//buffer.EndSample("Bloom");
return false;
}
buffer.BeginSample("Bloom");
…
buffer.SetGlobalFloat(bloomIntensityId, finalIntensity);
buffer.SetGlobalTexture(fxSource2Id, sourceId);
buffer.GetTemporaryRT(
bloomResultId, camera.pixelWidth, camera.pixelHeight, 0,
FilterMode.Bilinear, format
);
Draw(fromId, bloomResultId, finalPass);
buffer.ReleaseTemporaryRT(fromId);
buffer.EndSample("Bloom");
return true;
}
调整Render
,让其基于发光结果执行色调映射:
public void Render (int sourceId)
{
if (DoBloom(sourceId))
{
DoToneMapping(bloomResultId);
buffer.ReleaseTemporaryRT(bloomResultId);
}
else
{
DoToneMapping(sourceId);
}
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
3.2 色调映射模式
在PostFXSettings
中添加ToneMappingSettings
结构体:
[System.Serializable]
public struct ToneMappingSettings
{
public enum Mode { None }
public Mode mode;
}
[SerializeField]
ToneMappingSettings toneMapping = default;
public ToneMappingSettings ToneMapping => toneMapping;
3.3 莱因哈特
加入枚举:
public enum Mode { None = -1, Reinhard }
在DoToneMapping
中使用:
void DoToneMapping(int sourceId)
{
PostFXSettings.ToneMappingSettings.Mode mode = settings.ToneMapping.mode;
Pass pass = mode < 0 ? Pass.Copy : Pass.ToneMappingReinhard;
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);
}
ToneMappingReinhardPassFragment
:
float4 ToneMappingReinhardPassFragment (Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb /= color.rgb + 1.0;
return color;
}
我们会因为精度限制得到错误值,因此可以将颜色进行限制:
color.rgb = min(color.rgb, 60.0);
color.rgb /= color.rgb + 1.0;
3.4 中性色调映射
使用Color.hlsl
中的NeutralTonemap
方法,,为颜色,为经过调整的白点,,其中为输入颜色,其余为可配置常量:
real3 NeutralCurve(real3 x, real a, real b, real c, real d, real e, real f)
{
return ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
}
real3 NeutralTonemap(real3 x)
{
// Tonemap
const real a = 0.2;
const real b = 0.29;
const real c = 0.24;
const real d = 0.272;
const real e = 0.02;
const real f = 0.3;
const real whiteLevel = 5.3;
const real whiteClip = 1.0;
real3 whiteScale = (1.0).xxx / NeutralCurve(whiteLevel, a, b, c, d, e, f);
x = NeutralCurve(x * whiteScale, a, b, c, d, e, f);
x *= whiteScale;
// Post-curve white point adjustment
x /= whiteClip.xxx;
return x;
}
float4 ToneMappingNeutralPassFragment (Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = min(color.rgb, 60.0);
color.rgb = NeutralTonemap(color.rgb);
return color;
}
3.5 ACES
使用AcesTonemap
方法:
float4 ToneMappingACESPassFragment (Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = min(color.rgb, 60.0);
color.rgb = AcesTonemap(unity_to_ACES(color.rgb));
return color;
}
网友评论