前言
所谓HDR( High dynamic range)是高动态范围的简称,这项技术的本意是使用具有更大动态范围的信息载体去描述颜色亮度(相对低动态范围LDR而言),以期获得更加真实,更加明亮的色彩观感。HDR技术原理非常简单,就是将原先每条颜色通道只有8-bit位宽的帧缓存纹理,升级为拥有更多bit位宽,且能够存储浮点数值的纹理缓存,使得纹素可以存放亮度超过1的数值,从而增加了颜色亮度表达范围。最后输出到显示器前,再通过tone mapping将颜色调节回满足显示设备需求的动态区间。
对Unity而言,HDR的含义分为两个方面,其一是HDR Rendering,主要专注于“颜色”计算过程中的高动态,让输出到帧缓存的画面看起来更加真实自然;其二是HDR Output,是Unity将原生HDR信号输出到具有HDR硬件支持显示设备上的技术。就一般手游项目而言,谈到Unity HDR,我们指的的HDR Rendering,因为HDR Output即便在最新版本的Unity工程上,Built-in和URP管线都不支持。具体参考2022版官方文档中的相关功能表单(链接):
支持的纹理格式:
- FP16模式是ARGBHalf,每个通道16bit,带Alpha通道。
- R11G11B10模式一个像素32bit,R11bit,G11bit,B10bit,不带Alpha通道。
开启HDR Rendering的优点:
- 增加高亮度下颜色的对比度 (高亮度区间有更多可区分的颜色)
- 减少低亮度光照区域的条带感 (低亮度区间有更多可区分的颜色)
- 更好地支持Bloom和Emission效果
开启HDR Rendering的缺点:
- VRAM 使用量增加
- 色调映射(tone mapping)产生额外的计算开销
- 硬件抗锯齿不兼容 HDR 渲染
一些可能有用的Tips
1)在前向渲染模式下,仅当工程启用了后期处理效果时才可能正确支持 HDR。这是出于性能考虑的原因。如果没有添加后期处理效果,则不存在色调映射,部分颜色将会发生强度截断。参考如下HDR文档说明:
When using HDR rendering with SDR output, you must use tonemapping to convert the HDR image buffer to an SDR image for display. Unity provides tonemapping post-processing effects that let you do this (链接)
实际测试后发现,你完全可以只勾选HDR,不开启后处理,Unity工程不会报错,截帧后发现帧缓存数据类型已经升级到HDR格式,只是图像画面会有灰白过曝的感觉。
2)延迟渲染的HDR 模式下,光照缓冲区会被分配为浮点缓冲区。即使没有后期处理效果,延迟渲染也支持HDR。
3)在Build-in管线下,Unity可以很方便得在ProjectSettings -> Graphics -> Tier Settings 页签下选择启用HDR模式,顺带选择承载HDR纹理的帧缓存格式:参考有FP16和R11G11B10两种。其中后者(既R11G11B10)只占用32-bit位宽,但代价是没有A通道。前者(既R16G16B16A16)占用高达64-bit带宽,带有A通道,动态范围也更大,同时也更费VRAM。
4)在URP管线下,需要去URPiplineAsset配置文件中定义是否开启HDR,同时通过脚本修改“Graphics.preserveFramebufferAlpha”或者 “PlayerSettings.preserveFramebufferAlpha”来确定实际使用的HDR纹理:设置为True代表需要保留Alpha通道,那么Unity为你安排FP16格式的大杯;反之Unity为你配置R11G11B10格式的小杯。
5)关于R11G11B10格式RT如何混合图层,经过实践后发现只要不涉及到DstAlpha的混合模式,都没有问题,一旦你要访问DstAlpha,那么返回值一律为1.0(既不透明)。
关于(5)点,实验设计是这样的:创建2张平面前后排列,如下图所示,让摄像机观察到两块平面以及它们的重叠部分。
平面1的材质shader做如下设置:
Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "TransparentCutout" "Queue" = "Transparent" }
Blend One Zero, One Zero
逻辑是渲染100%自己的颜色和Alpha值到背景板(帧缓存),其中材质Alpha值被设置为0(完全透明)。
平面2的材质shader有如下设置:
Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "TransparentCutout" "Queue" = "Transparent" }
Blend DstAlpha Zero
逻辑是让自己的颜色 * 背景板上Alpha的返回值,直接覆盖到背景板上。
(A)当Urp工程处于以下:
- 配置1)开启HDR,设置缓存格式为FP16 关闭HDR
- 配置2)关闭HDR
得到如下图效果:可以看到在都有A通道的前提下,红色平面材质在计算重合部分像素的时候,可以正确从背景板中取到绿色平面预存入的Alpha值(为0.3),而后直接覆盖自身颜色时,重合处的颜色会乘以0.3,导致渲染结果更加暗。
(B)当Urp工程处于以下:
- 配置1)开启HDR,设置缓存格式为R11G11B10
从下图结果看到,重合处颜色为红色平面本色,这是由于取到的背景板Alpha值恒定为1所致。
多提一嘴 第一组对比图中左下方的摄像机视窗截帧图像看起来比较暗沉,其原因在于Unity是以线性读写模式存放HDR缓存的,而我们的输出显示屏自带remove gamma correction效果,会把真实亮度的图像(线性空间)下拉变暗,作为对比右下角的视窗图像(非HDR)是基于ARGB32_SRGB格式创建的,图像被存放在sRGB空间,从而保证了显示器输出的准确性。
Unity shader中的HDR方法
参考如下DecodeHDR方法参考,主要用于解码Unity自身创建的各类贴图纹理(如高动态IBL, SKY-BOX,Lightmap等),以上贴图一般存放在sRGB中,且单通道只有8-bit,因此在HDR品质设置不高的情况下需要使用一些技巧扩展其上颜色的上限,常见的编码类型有RGBM和dLDR。
// Decodes HDR textures
// handles dLDR, RGBM formats
inline half3 DecodeHDR (half4 data, half4 decodeInstructions)
{
// Take into account texture alpha if decodeInstructions.w is true(the alpha value affects the RGB channels)
half alpha = decodeInstructions.w * (data.a - 1.0) + 1.0;
// If Linear mode is not supported we can skip exponent part
#if defined(UNITY_COLORSPACE_GAMMA)
return (decodeInstructions.x * alpha) * data.rgb;
#else
# if defined(UNITY_USE_NATIVE_HDR)
return decodeInstructions.x * data.rgb; // Multiplier for future HDRI relative to absolute conversion.
# else
return (decodeInstructions.x * pow(alpha, decodeInstructions.y)) * data.rgb;
# endif
#endif
}
HDR的开销
主要由HDR缓存本身带来的内存开销,以及最终校色所带来的消耗共两部分组成。一般而言前者带来的性能影响不大,特别是当选择R11G11B10格式时,就连采样数据所占用的IO带宽都与从前一致。所以可以认为HDR的主要消耗来自全屏校色相关的开销,它包括生成LUT和全屏Blit。
在工程开启了HDR,也追加了tonemapping等后处理逻辑后,Unity会执行2个特殊pass,其一叫“ColorGradingLUT”,参考下图:
该Pass内容比较琐碎,详细说明的话恐怕得新开一坑,简言之,这个Pass基于当前帧的参数,分别计算了后曝光、白平衡、对比度、颜色过滤、色调分离、通道混合、阴影中间调高光、色相偏移、饱和度,伽马校正等等各种映射关系和预计算表格,并存储到如下所示的多张LUT中:
在后处理阶段会执行如下图所示的“UberPostProcess”大杂烩Pass,通过读取预生成的LUT,逐像素调节并输出最终的颜色。
测试工程中我给tonemapping设置了ACES映射(一种HDR映射曲线),所以能在pass中看到对应Keywords。实际性能分析时可以详细关注这两个pass在整个渲染流程中的占比,从网上收集的一些信息来看,这两个pass在主流机配置下应当不高于1ms。