# 参考 Tone mapping进化论 https://zhuanlan.zhihu.com/p/21983679 ## Filmic tone mapping 2010年Uncharted 2的ToneMapping方法。这个方法的本质是把原图和让艺术家用专业照相软件模拟胶片的感觉,人肉tone mapping后的结果去做曲线拟合,得到一个高次曲线的表达式。这样的表达式应用到渲染结果后,就能在很大程度上自动接近人工调整的结果。 ```c# float3 F(float3 x) { const float A = 0.22f; const float B = 0.30f; const float C = 0.10f; const float D = 0.20f; const float E = 0.01f; const float F = 0.30f; return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; } float3 Uncharted2ToneMapping(float3 color, float adapted_lum) { const float WHITE = 11.2f; return F(1.6f * adapted_lum * color) / F(WHITE); } ``` ## Academy Color Encoding System(ACES) >是一套颜色编码系统,或者说是一个新的颜色空间。它是一个通用的数据交换格式,一方面可以不同的输入设备转成ACES,另一方面可以把ACES在不同的显示设备上正确显示。不管你是LDR,还是HDR,都可以在ACES里表达出来。这就直接解决了VDR的问题,不同设备间都可以互通数据。 >更好的地方是,按照前面说的,ACES为的是解决所有设备之间的颜色空间转换问题。所以这个tone mapper不但可以用于HDR到LDR的转换,还可以用于从一个HDR转到另一个HDR。也就是从根本上解决了VDR的问题。这个函数的输出是线性空间的,所以要接到LDR的设备,只要做一次sRGB校正。要接到HDR10的设备,只要做一次Rec 2020颜色矩阵乘法。Tone mapping部分是通用的,这也是比之前几个算法都好的地方。 ```c# float3 ACESToneMapping(float3 color, float adapted_lum) { const float A = 2.51f; const float B = 0.03f; const float C = 2.43f; const float D = 0.59f; const float E = 0.14f; color *= adapted_lum; return (color * (A * color + B)) / (color * (C * color + D) + E); } ``` ## ToneMapPass 位于PostProcessing.cpp中: ```c# FTonemapInputs PassInputs; PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput); PassInputs.SceneColor = SceneColor; PassInputs.Bloom = Bloom; PassInputs.EyeAdaptationTexture = EyeAdaptationTexture; PassInputs.ColorGradingTexture = ColorGradingTexture; PassInputs.bWriteAlphaChannel = AntiAliasingMethod == AAM_FXAA || IsPostProcessingWithAlphaChannelSupported(); PassInputs.bOutputInHDR = bTonemapOutputInHDR; SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs); ``` 如代码所示需要给Shader提供渲染结果、Bloom结果、曝光结果、合并的LUT。 1. 获取输出RT对象,如果输出RT无效则根据当前设备来设置RT格式,默认为PF_B8G8R8A8。(LinearEXR=>PF_A32B32G32R32F;LinearNoToneCurve、LinearWithToneCurve=>PF_FloatRGBA) 2. 从后处理设置中获取BloomDirtMaskTexture。 3. 从控制台变量获取SharpenDiv6。 4. 计算色差参数(ChromaticAberrationParams)。 5. 创建共有的Shader变量 FTonemapParameters,并将所有参数都进行赋值。 6. 为桌面端的ToneMapping生成排列向量。 7. 根据RT类型使用PixelShader或者ComputeShader进行渲染。 8. 返回右值。 BuildCommonPermutationDomain()构建的FCommonDomain应该是为了给引擎传递宏。其中Settings为FPostProcessSettings。 using FCommonDomain = TShaderPermutationDomain< - FTonemapperBloomDim(USE_BLOOM):Settings.BloomIntensity > 0.0 - FTonemapperGammaOnlyDim(USE_GAMMA_ONLY):true - FTonemapperGrainIntensityDim(USE_GRAIN_INTENSITY):Settings.GrainIntensity > 0.0f - FTonemapperVignetteDim(USE_VIGNETTE):Settings.VignetteIntensity > 0.0f - FTonemapperSharpenDim(USE_SHARPEN):CVarTonemapperSharpen.GetValueOnRenderThread() > 0.0f - FTonemapperGrainJitterDim(USE_GRAIN_JITTER):Settings.GrainJitter > 0.0f - FTonemapperSwitchAxis(NEEDTOSWITCHVERTICLEAXIS):函数形参bSwitchVerticalAxis - FTonemapperMsaaDim(METAL_MSAA_HDR_DECODE):函数形参bMetalMSAAHDRDecode - FTonemapperUseFXAA(USE_FXAA):View.AntiAliasingMethod == AAM_FXAA >; using FDesktopDomain = TShaderPermutationDomain< - FCommonDomain, - FTonemapperColorFringeDim(USE_COLOR_FRINGE): - FTonemapperGrainQuantizationDim(USE_GRAIN_QUANTIZATION) FTonemapperOutputDeviceDim为LinearNoToneCurve与LinearWithToneCurve时为false,否则为true。 - FTonemapperOutputDeviceDim(DIM_OUTPUT_DEVICE):ETonemapperOutputDevice(CommonParameters.OutputDevice.OutputDevice) >; ``` enum class ETonemapperOutputDevice { sRGB, Rec709, ExplicitGammaMapping, ACES1000nitST2084, ACES2000nitST2084, ACES1000nitScRGB, ACES2000nitScRGB, LinearEXR, LinearNoToneCurve, LinearWithToneCurve, MAX }; ``` ### Shader >在当前实现下,渲染场景的完整处理通过 ACES Viewing Transform 进行处理。此流程的工作原理是使用"参考场景的"和"参考显示的"图像。 - 参考场景的 图像保有源材质的原始 线性光照 数值,不限制曝光范围。 - 参考显示的 图像是最终的图像,将变为所用显示的色彩空间。 使用此流程后,初始源文件用于不同显示时便无需每次进行较色编辑。相反,输出的显示将映射到 正确的色彩空间。 >ACES Viewing Transform 在查看流程中将按以下顺序进行 - Look Modification Transform (LMT) - 这部分抓取应用了创意"外观"(颜色分级和矫正)的 ACES 颜色编码图像, 输出由 ACES 和 Reference Rendering Transform(RRT)及 Output Device Transform(ODT)渲染的图像。 - Reference Rendering Transform (RRT) - 之后,这部分抓取参考场景的颜色值,将它们转换为参考显示。 在此流程中,它使渲染图像不再依赖于特定显示器,反而能保证它输出到特定显示器时拥有正确而宽泛的色域和动态范围(尚未创建的图像同样如此)。 - Output Device Transform (ODT) - 最后,这部分抓取 RRT 的 HDR 数据输出,将其与它们能够显示的不同设备和色彩空间进行比对。 因此,每个目标需要将其自身的 ODT 与 Rec709、Rec2020、DCI-P3 等进行比对。 默认参数: r.HDR.EnableHDROutput:设为 1 时,它将重建交换链并启用 HDR 输出。 r.HDR.Display.OutputDevice - 0:sRGB (LDR) (默认) - 1:Rec709 (LDR) - 2:显式伽马映射 (LDR) - 3:ACES 1000-nit ST-2084 (Dolby PQ) (HDR) - 4:ACES 2000-nit ST-2084 (Dolby PQ) (HDR) - 5:ACES 1000-nit ScRGB (HDR) - 6:ACES 2000-nit ScRGB (HDR) r.HDR.Display.ColorGamut - 0:Rec709 / sRGB, D65 (默认) - 1:DCI-P3, D65 - 2:Rec2020 / BT2020, D65 - 3:ACES, D60 - 4:ACEScg, D60 我的测试设备是: - 宏碁(Acer) 暗影骑士24.5英寸FastIPS 280Hz小金刚HDR400 - ROG 枪神5 笔记本 HDIM连接 - UE4.27.2 源码版 经过实际还是无法打开HDR输出,着实有些可惜。所以一般显示器的Shader代码为(使用RenderDoc抓帧): ```c# float4 TonemapCommonPS( float2 UV, float3 ExposureScaleVignette, float4 GrainUV, float2 ScreenPos, float2 FullViewUV, float4 SvPosition ) { float4 OutColor = 0; const float OneOverPreExposure = View_OneOverPreExposure; float Grain = GrainFromUV(GrainUV.zw); float2 SceneUV = UV.xy; float4 SceneColor = SampleSceneColor(SceneUV); SceneColor.rgb *= OneOverPreExposure; float ExposureScale = ExposureScaleVignette.x; float SharpenMultiplierDiv6 = TonemapperParams.y; float3 LinearColor = SceneColor.rgb * ColorScale0.rgb; float2 BloomUV = ColorToBloom_Scale * UV + ColorToBloom_Bias; BloomUV = clamp(BloomUV, Bloom_UVViewportBilinearMin, Bloom_UVViewportBilinearMax); float4 CombinedBloom = Texture2DSample(BloomTexture, BloomSampler, BloomUV); CombinedBloom.rgb *= OneOverPreExposure; float2 DirtLensUV = ConvertScreenViewportSpaceToLensViewportSpace(ScreenPos) * float2(1.0f, -1.0f); float3 BloomDirtMaskColor = Texture2DSample(BloomDirtMaskTexture, BloomDirtMaskSampler, DirtLensUV * .5f + .5f).rgb * BloomDirtMaskTint.rgb; LinearColor += CombinedBloom.rgb * (ColorScale1.rgb + BloomDirtMaskColor); LinearColor *= ExposureScale; LinearColor.rgb *= ComputeVignetteMask( ExposureScaleVignette.yz, TonemapperParams.x ); float3 OutDeviceColor = ColorLookupTable(LinearColor); float LuminanceForPostProcessAA = dot(OutDeviceColor, float3 (0.299f, 0.587f, 0.114f)); float GrainQuantization = 1.0/256.0; float GrainAdd = (Grain * GrainQuantization) + (-0.5 * GrainQuantization); OutDeviceColor.rgb += GrainAdd; OutColor = float4(OutDeviceColor, saturate(LuminanceForPostProcessAA)); [branch] if(bOutputInHDR) { OutColor.rgb = ST2084ToLinear(OutColor.rgb); OutColor.rgb = OutColor.rgb / EditorNITLevel; OutColor.rgb = LinearToPostTonemapSpace(OutColor.rgb); } return OutColor; } ``` 关键函数是这个对非HDR设备进行Log编码。 ```c# half3 ColorLookupTable( half3 LinearColor ) { float3 LUTEncodedColor; // Encode as ST-2084 (Dolby PQ) values #if (DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_ACES1000nitST2084 || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_ACES2000nitST2084 || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_ACES1000nitScRGB || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_ACES2000nitScRGB || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_LinearEXR || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_NoToneCurve || DIM_OUTPUT_DEVICE == TONEMAPPER_OUTPUT_WithToneCurve) // ST2084 expects to receive linear values 0-10000 in nits. // So the linear value must be multiplied by a scale factor to convert to nits. LUTEncodedColor = LinearToST2084(LinearColor * LinearToNitsScale); #else LUTEncodedColor = LinToLog( LinearColor + LogToLin( 0 ) ); #endif float3 UVW = LUTEncodedColor * ((LUTSize - 1) / LUTSize) + (0.5f / LUTSize); #if USE_VOLUME_LUT == 1 half3 OutDeviceColor = Texture3DSample( ColorGradingLUT, ColorGradingLUTSampler, UVW ).rgb; #else half3 OutDeviceColor = UnwrappedTexture3DSample( ColorGradingLUT, ColorGradingLUTSampler, UVW, LUTSize ).rgb; #endif return OutDeviceColor * 1.05; } float3 LogToLin( float3 LogColor ) { const float LinearRange = 14; const float LinearGrey = 0.18; const float ExposureGrey = 444; // Using stripped down, 'pure log', formula. Parameterized by grey points and dynamic range covered. float3 LinearColor = exp2( ( LogColor - ExposureGrey / 1023.0 ) * LinearRange ) * LinearGrey; //float3 LinearColor = 2 * ( pow(10.0, ((LogColor - 0.616596 - 0.03) / 0.432699)) - 0.037584 ); // SLog //float3 LinearColor = ( pow( 10, ( 1023 * LogColor - 685 ) / 300) - .0108 ) / (1 - .0108); // Cineon //LinearColor = max( 0, LinearColor ); return LinearColor; } float3 LinToLog( float3 LinearColor ) { const float LinearRange = 14; const float LinearGrey = 0.18; const float ExposureGrey = 444; // Using stripped down, 'pure log', formula. Parameterized by grey points and dynamic range covered. float3 LogColor = log2(LinearColor) / LinearRange - log2(LinearGrey) / LinearRange + ExposureGrey / 1023.0; // scalar: 3log2 3mad //float3 LogColor = (log2(LinearColor) - log2(LinearGrey)) / LinearRange + ExposureGrey / 1023.0; //float3 LogColor = log2( LinearColor / LinearGrey ) / LinearRange + ExposureGrey / 1023.0; //float3 LogColor = (0.432699 * log10(0.5 * LinearColor + 0.037584) + 0.616596) + 0.03; // SLog //float3 LogColor = ( 300 * log10( LinearColor * (1 - .0108) + .0108 ) + 685 ) / 1023; // Cineon LogColor = saturate( LogColor ); return LogColor; } ``` ## CombineLUTS Pass 实际会在GetCombineLUTParameters()中调用,也就是CombineLUTS (PS) Pass。实际的作用是绘制一个3D LUT图,毕竟ToneMapping实际也就是一个曲线,所以可以合并到一起。核心函数位于PostProcessCombineLUTs.usf的**float4 CombineLUTsCommon(float2 InUV, uint InLayerIndex)** - 计算原始LUT Neutral 。 - 对HDR设备使用ST2084解码;对LDR设备使用Log解码。**LinearColor = LogToLin(LUTEncodedColor) - LogToLin(0);** - 白平衡。 - 在sRGB色域之外扩展明亮的饱和色彩,以伪造广色域渲染(Expand bright saturated colors outside the sRGB gamut to fake wide gamut rendering.) - 颜色矫正:对颜色ColorSaturation、ColorContrast、ColorGamma、ColorGain、ColorOffset矫正操作。 - 蓝色矫正。 - ToneMapping与之前计算结果插值。 - 反蓝色矫正。 - 从AP1到sRGB的转换,并Clip掉gamut的值。 - 颜色矫正。 - Gamma矫正。 - 线性颜色=》设备颜色:OutDeviceColor = LinearToSrgb( OutputGamutColor );部分HDR设备则会调用对应的矩阵调整并用LinearToST2084(). - 简单处理:OutColor.rgb = OutDeviceColor / 1.05; 所以核心的Tonemapping函数为位于TonemaperCommon.ush的**half3 FilmToneMap( half3 LinearColor)**与**half3 FilmToneMapInverse( half3 ToneColor)** ``` // Blue correction ColorAP1 = lerp( ColorAP1, mul( BlueCorrectAP1, ColorAP1 ), BlueCorrection ); // Tonemapped color in the AP1 gamut float3 ToneMappedColorAP1 = FilmToneMap( ColorAP1 ); ColorAP1 = lerp(ColorAP1, ToneMappedColorAP1, ToneCurveAmount); // Uncorrect blue to maintain white point ColorAP1 = lerp( ColorAP1, mul( BlueCorrectInvAP1, ColorAP1 ), BlueCorrection ); ``` ## ue4 后处理中的ToneMapping曲线参数为: Slope: 0.98 Toe: 0.3 Shoulder: 0.22 Black Clip: 0 White Clip: 0.025 ![](https://docs.unrealengine.com/5.0/Images/designing-visuals-rendering-and-graphics/post-process-effects/color-grading/DefaultSettings_FilmicToneMapper.webp)