Custom SRP 13 Color Grading
https://catlikecoding.com/unity/tutorials/custom-srp/color-grading/
本篇教程将覆盖
-
实现 color grading 流程
-
实现 URP/HDRP 中的几个 color grading 工具
-
使用 LUT 优化 color grading 和 tone mapping
1 Color Adjustments
到目前为止,我们仅仅向 image 应用了 tone mapping,将 HDR 图像映射到 LDR.除此之外,还可以执行一些其它的颜色调整.对于视频,照片,数字图像的颜色调整有三个步骤:首先是颜色校正-color correction,让渲染结果符合我们在现实中对场景的观察.然后是颜色渐变-color grading,让渲染结果具有某种特定的效果或感觉,不必符合现实情况.这两个步骤通常合并成一个 color grading 步骤.然后是 tone mapping,将 HDR 映射到 LDR 的显示范围.
应用 tone mapping 后,除非图像很亮,否则颜色会显得很平,或者说色彩饱和度降低了.ACES 增加了较暗的颜色的对比度,但是不适合做 color grading.本教程将基于 Neutral tone mapping 来实现 color grading.
1.1 Color Grading Before Tone Mapping
Color Grading 要在 Tone Mapping 之前,因此在 hlsl 中定义 Color Grading 函数,先实现为将颜色值限制在60以内,其它的 Tone Mapping 都调用该函数。同时定义新的 none tone mapping pass,该pass 仅调用 color grading
// color grading
float3 ColorGrading(float3 color)
{color = min(color.rgb, 60);return color;
}// tone mapping - none
float4 ToneMappingNonePassFragment(Varyings input) : SV_TARGET
{float3 color = GetSource(input.screenUV).rgb;color = ColorGrading(color);return float4(color, 1.0);
}// tone mapping - reinhard
float4 ToneMappingReinhardPassFragment(Varyings input) : SV_TARGET
{float3 color = GetSource(input.screenUV).rgb;color = ColorGrading(color);color /= color + 1.0;return float4(color, 1.0);
}// tone mapping - neutral
float4 ToneMappingNeutralPassFragment(Varyings input) : SV_TARGET
{float3 color = GetSource(input.screenUV).rgb;color = ColorGrading(color);color = NeutralTonemap(color.rgb);return float4(color, 1.0);
}// tone mapping - ACES
float4 ToneMappingACESPassFragment(Varyings input) : SV_TARGET
{float3 color = GetSource(input.screenUV).rgb;color = ColorGrading(color);color = AcesTonemap(unity_to_ACES(color.rgb));return float4(color, 1.0);
}
之前因为没有为 tone mapping none 实现 pass ,因此我们将 none 定义为 -1,并且当是 -1 时执行 copy pass。现在有专门的 none pass 了,就可以将 ToneMapping.Mode.None = 0 了,同时修改 DoToneMapping 代码
void DoToneMapping(int sourceId){Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);}
.2 Settings
我们将要复刻 URP 和 HDRP 的 Color Adjustments 后效工具,首先为其定义配置对象。
URP 和 HDRP 的 color grading 功能是一样的,我们的配置也和它们一样包括:
-
Post Exposure,任意浮点数
-
Contrast,-100 到 100 的滑动条
-
Color Filter,是一个没有 alpha 值的 HDR 颜色
-
Hue Shift,-180° - 180° 的滑动条
-
Saturation,-100 到 100 的滑动条
color filter 是白色,其它所有参数的默认值都是 0,这些设置对图像没有影响。
[SerializeField]public struct ColorAdjustmentsSettings{public float postExposure;[Range(-100f, 100f)]public float contrast;[ColorUsage(false, true)]public Color colorFilter;[Range(-180, 180)]public float hueShift;[Range(-100,100)]public float saturation;}[SerializeField]ColorAdjustmentsSettings colorAdjustments = new ColorAdjustmentsSettings{ colorFilter = Color.white };public ColorAdjustmentsSettings ColorAdjustments => colorAdjustments;
同时,因为我们要在 tone mapping 的同时执行 color grading,因此把 DoToneMapping 改名为 DoColorGradingAndToneMapping。由于要频繁访问 PostFXSettings 内定义的类型,因此我们通过 using static PostFXSettings 将内部的类型开放出来。
using static 语法有点像 using namespace,只不过 using static 是用来开放类型内的类型。
我们将会定义 shader vector4 和 shader color 来接收 adjustments 参数,因此在DoColorGradingAndToneMapping 的开头,先将参数上传到 shader 。
void ConfigureColorAdjustments(){ColorAdjustmentsSettings colorAdjustments = settings.ColorAdjustments;buffer.SetGlobalVector(colorAdjustmentsId, new Vector4(// 曝光度以档为单位进行测量,因此将 2 提升到所配置曝光值的幂次方。Mathf.Pow(2f, colorAdjustments.postExposure),colorAdjustments.contrast * 0.01f + 1f, // 转换到 0 到 2colorAdjustments.hueShift * (1f / 360f), // 转换到 -1 到 1colorAdjustments.saturation * 0.01f + 1f) // 转换到 0 到 2);// 因为我们是在线性空间进行的渲染,因此需要上传线性空间的颜色buffer.SetGlobalColor(colorFilterId, colorAdjustments.colorFilter.linear);}void DoColorGradingAndToneMapping(int sourceId){ConfigureColorAdjustments();Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);}
1.3 Post Exposure
后曝光的原理是模仿摄像机的曝光,并在其它后效之后,color grading 之前应用。这种效果不是表现真实效果,而是一个艺术工具,用来调整曝光,同时不影响其它效果。
// color grading - post exposure
float3 ColorGradingPostExposure(float3 color)
{return color * _ColorAdjustments.x;
}// color grading
float3 ColorGrading(float3 color)
{color = min(color.rgb, 60);// post exposurecolor = ColorGradingPostExposure(color);return color;
}
1.4 Contrast
对比度调整是通过减去一个中度灰( 0.4135884),然后对颜色进行缩放后再将中度灰度加回来。该灰度颜色我们使用unity 定义的 ACEScc_MIDGRAY。
为了获取更好的效果,该转换需要在 Log C 而不是线性空间。可以通过 Color Core Library 中定义的 LinearToLogC 函数将颜色变换到 Log C,然后再通过 LogCToLinear 变换回来。
当增大对比度时,可能会导致颜色分量变成负值,因此需要加以限制
// color grading - contrast
float3 ColorGradingContrast(float3 color)
{color = LinearToLogC(color);color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;return LogCToLinear(color);
}// color grading
float3 ColorGrading(float3 color)
{color = min(color.rgb, 60);// post exposurecolor = ColorGradingPostExposure(color);// contrastcolor = ColorGradingContrast(color);// 增大对比度可能会导致负值,因此加以限制color = max(color, 0.0);return color;
}
1.5 Color Filter
颜色过滤,就是简单的乘以过滤色
// color grading - filter
float3 ColorGradingColorFilter(float3 color)
{return color * _ColorFilter.rgb;
}// color grading
float3 ColorGrading(float3 color)
{...// color filtercolor = ColorGradingColorFilter(color);return color;
}
1.6 Hue Shift
色相/色调偏移在 color filter 之后执行。通过函数 RgbToHsv 将颜色从 RGB 转换为 HSV,然后向 H 增加色调偏移值,再通过 HsvToRgb 转换回来。因为 hue 是定义在 0-1 的色盘上的,因此要处理超出范围的情况,可以通过 RotateHue 来实现。该函数不接受负值,因此要先消除负值。
// color grading - hue shift
float3 ColorGradingHueShift(float3 color)
{color = RgbToHsv(color);float hue = color.x + _ColorAdjustments.z;color.x = RotateHue(hue, 0.0, 1.0);return HsvToRgb(color);
}// color grading
float3 ColorGrading(float3 color)
{..// 消除可能的负值color = max(color, 0.0);// hue shiftcolor = ColorGradingHueShift(color);return color;
}
1.7 Saturation
最后一项是饱和度调整。首先通过函数 Luminance 来获取颜色的亮度,然后像对比度一样进行计算。这一步也可能产生负值,因此需要消除掉
// color grading - saturation 饱和度
float3 ColorGradingSaturation(float3 color)
{float luminance = Luminance(color);return (color - luminance) * _ColorAdjustments.w + luminance;
}// color grading
float3 ColorGrading(float3 color)
{...// saturationcolor = ColorGradingSaturation(color);// 消除可能的负值color = max(color, 0.0);return color;
}
2 More Controls
URP/HDRP 不但提供了 color grading 来调整颜色,我们也要支持几个其它的工具,还是参考 unity 的方法。
2.1 White Balance
白平衡让我们可以调节图像的色温,需要两个 -100 到 100 的参数。第一个表示色温,让图像看起来偏冷或偏暖。第二个 Tint,调节色温偏向的颜色。
[System.Serializable]public struct WhiteBalanceSettings{[Range(-100,100)]public float temperature;[Range(-100, 100)]public float tint;}[SerializeField]WhiteBalanceSettings whiteBalance = default;public WhiteBalanceSettings WhiteBalance => whiteBalance;
然后通过接口 ColorUtils.ColorBalanceToLMSCoeffs,将参数转换成一个 vector3 后传到 shader 中。
void ConfigureWhiteBalance()
{WhiteBalanceSettings whiteBalance = settings.WhiteBalance;buffer.SetGlobalVector(whiteBalanceId, ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature, whiteBalance.tint));
}void DoColorGradingAndToneMapping(int sourceId)
{ConfigureColorAdjustments();ConfigureWhiteBalance();Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);
}
在 Shader 中,我们在 LMS 空间中将白平衡参数和颜色相乘。通过函数 LMSToLinear 和 LinearToLMS 来完成转换。在 post exposure 后,contrast 之前应用
// color grading - white balance
float4 _WhiteBalance;
float3 ColorGradeWhiteBalance(float color)
{color = LinearToLMS(color);color *= _WhiteBalance.rgb;return LMSToLinear(color);
}// color grading
float3 ColorGrading(float3 color)
{color = min(color.rgb, 60);// post exposurecolor = ColorGradingPostExposure(color);// white balancecolor = ColorGradeWhiteBalance(color);// contrastcolor = ColorGradingContrast(color);...return color;
}
LMS颜色空间
是一种基于人类视觉系统中视锥细胞响应的颜色空间:
-
基于视锥细胞:LMS颜色空间是根据人眼视网膜上的三种视锥细胞对不同波长光的敏感度来定义的。其中,L代表长波(约560-580nm,对应红色)感受器,M代表中波(约530-540nm,对应绿色)感受器,S代表短波(约420-440nm,对应蓝色)感受器。
-
三刺激值:任何颜色都可以用一个L, M, S三元组来表示,这三个值分别对应三种视锥细胞对该颜色的响应强度。这些响应是通过将光谱功率分布与锥体细胞的光谱敏感度函数相乘并积分得到的。
冷色调让图像偏蓝,暖色调让图像偏黄,通常只会作小幅度的调整。
tint 用来补偿不符合预期的颜色平衡,让图像偏向绿色或品红色。
[System.Serializable]public struct SplitTonesSettings{[ColorUsage(false, false)]public Color shadows, highlights;[Range(-100, 100)]public float balance;}[SerializeField]SplitTonesSettings splitTones = new SplitTonesSettings{shadows = Color.gray,highlights = Color.gray};public SplitTonesSettings SplitTones => splitTones;
两个颜色参数需要在 gamma 空间,然后上传到 shader。将 balance 参数转换到(-1,1)的范围,通过一个颜色的 a 分量进行上传
void ConfigureSplitToning(){SplitTonesSettings splitTones = settings.SplitTones;Color splitColor = splitTones.shadows;splitColor.a = splitTones.balance * 0.01f;buffer.SetGlobalVector(splitToneShadowsId, splitColor);buffer.SetGlobalVector(splitToneHighlightsId, splitTones.highlights);}void DoColorGradingAndToneMapping(int sourceId){ConfigureColorAdjustments();ConfigureWhiteBalance();ConfigureSplitToning();Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);}
在 shader 中,我们在一个近似的 gamma 空间中执行 split-toning。该调整需要在 color filter 之后,消除负值后执行。
// color grading - split-toning
float4 _SplitToningShadows;
float4 _SplitToningHighlights;
float3 ColorGradeSplitToning(float3 color)
{// 将颜色变换到近似的 gamma 空间color = PositivePow(color, 1.0 / 2.2);// 根据 balance 和亮度,计算插值参数float t = saturate(Luminance(saturate(color)) + _SplitToningShadows.w);// 从中性的0.5开始,亮度越低,shadows 越偏向参数 splitToningShadowsfloat3 shadows = lerp(0.5, _SplitToningShadows.rgb, 1.0 - t);// 从中兴的0.5开始,亮度越高,highlights 越偏向 splitToningHighlightsfloat3 highlights = lerp(0.5, _SplitToningHighlights.rgb, t);// 混合颜色和阴影color = SoftLight(color, shadows);// 混合颜色和高光color = SoftLight(color, highlights);// 将颜色变换回线性空间return PositivePow(color, 2.2);
}// color grading
float3 ColorGrading(float3 color)
{...// color filtercolor = ColorGradingColorFilter(color);// 消除可能的负值color = max(color, 0.0);// split-toning for shadows and highlightscolor = ColorGradeSplitToning(color);...return color;
}
2.3 Channel Mixer
通道混合让我们可以组合 RGB 颜色创建新的 RGB 颜色。例如可以交换 R G,G=G-B,R=G+R。
混合器其实是一个 3x3 的矩阵,默认是单位矩阵。可以用三个 vector 来为 r g b 通道分别进行配置,每个通道的取值范围为 -100 到 100,vector 的 XYZ 分别代表 RGB。
[System.Serializable]public struct ChannelMixerSettings{[Range(-100, 100)]public Vector3 red, green, blue;}[SerializeField]ChannelMixerSettings channelMixer = new ChannelMixerSettings{red = Vector3.right,green = Vector3.up,blue = Vector3.forward};public ChannelMixerSettings ChannelMixer => channelMixer;
将三个 vector 上传到shader
void ConfigureChannelMixer(){ChannelMixerSettings channelMixer = settings.ChannelMixer;buffer.SetGlobalVector(channelMixerRedId, channelMixer.red);buffer.SetGlobalVector(channelMixerGreenId, channelMixer.green);buffer.SetGlobalVector(channelMixerBlueId, channelMixer.blue);}void DoColorGradingAndToneMapping(int sourceId){ConfigureColorAdjustments();ConfigureWhiteBalance();ConfigureSplitToning();ConfigureChannelMixer();Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);}
在 shader 中,基于三个 vector 构建 3x3 矩阵,用来变换颜色。同时由于我们矩阵元素可以是负值,因此应用变换后结果可能会出现负值,要消除负值。在 split-toning 之后应用该效果。
// color grading - channel mixer
float4 _ChannelMixerRed, _ChannelMixerGreen, _ChannelMixerBlue;
float3 ColorGradeChannelMixer(float3 color)
{return mul(float3x3(_ChannelMixerRed.rgb, _ChannelMixerGreen.rgb, _ChannelMixerBlue.rgb), color);
}// color grading
float3 ColorGrading(float3 color)
{...// split-toning for shadows and highlightscolor = ColorGradeSplitToning(color);// channel mixercolor = ColorGradeChannelMixer(color);color = max(color, 0.0);...return color;
}
2.4 Shadows Midtones Highlights
该效果与 split-toning 类似,区别是它还增加了 midtone 的调节,可以分离阴影和高光区域。
Unity是通过色盘(color wheels)来实现颜色调节的,但是我们无法使用该控件,因此我们通过3个HDR颜色和4个滑动条来完成配置,4个滑动条定义了 shadows 和 highlights 的起始和结束的范围:shadow 的强度从 start 到 end 逐步降低,highlight 的强度从 start 到 end 逐步增加。这些值的范围定义在(0,2),这样有一点HDR,默认值参考 unity ,分别为(0,0.3)和(0.55,1)。颜色默认都是白色
[System.Serializable]public struct ShadowsMidtonesHighlightsSettings{[ColorUsage(false, true)]public Color shadows, midtones, highlights;[Range(0f,2f)]public float shadowsStart, shadowsEnd, highlightsStart, highlightsEnd;}[SerializeField]ShadowsMidtonesHighlightsSettings shadowsMidtonesHighlightsSettings = new ShadowsMidtonesHighlightsSettings{shadows = Color.white,midtones = Color.white,highlights = Color.white,shadowsEnd = 0.3f,highlightsStart = 0.55f,highlightsEnd = 1f};public ShadowsMidtonesHighlightsSettings ShadowsMidtonesHighlights => shadowsMidtonesHighlightsSettings;
简单的说,就是基于亮度值,以及下面4个值定义的三个区间,混合上面定义的三个颜色。
将参数上传到GPU,我们将4个float参数打包成一个 vector 进行上传。注意需要上传线性空间的颜色
void ConfigureShadowsMidtonesHighlights(){ShadowsMidtonesHighlightsSettings smh = settings.ShadowsMidtonesHighlights;buffer.SetGlobalColor(smhShadowsId, smh.shadows.linear);buffer.SetGlobalColor(smhMidtonesId, smh.midtones.linear);buffer.SetGlobalColor(smhHighlightsId, smh.highlights.linear);buffer.SetGlobalVector(smhRangeId, new Vector4(smh.shadowsStart, smh.shadowsEnd, smh.highlightsStart, smh.highlightsEnd));}void DoColorGradingAndToneMapping(int sourceId){ConfigureColorAdjustments();ConfigureWhiteBalance();ConfigureSplitToning();ConfigureChannelMixer();ConfigureShadowsMidtonesHighlights();Pass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);}
在 shader 中,我们分别用三个颜色参数乘以它们的权重,然后再乘以图像颜色,将结果累加起来。权重是基于亮度的。通过函数 smoothstep ,使 shadow 的权重在输入参数 start - end 之间时,结果从1变到0。highlights 同样,只不过结果是从 0 到 1。midtones 的权重等于 1 减去 shadows 和 highlights 的和。思想是 shadows 和 highlights 的区域不会重合,或者仅重合一点,因此 midtones 的权重不会是负值。我们没有在 inspector 上强制限制。
// color grading - shadows midtone highlights
float4 _SMHShadows;
float4 _SMHMidtones;
float4 _SMHHighliths;
float4 _SMHRange;
float3 ColorGradeShadowsMidtonesHighlights(float3 color)
{float luminance = luminance(color);float shadowsWeight = 1.0 - smoothstep(_SMHRange.x, _SMHRange.y, luminance);float highlightsWeight = smoothstep(_SMHRange.z, _SMHRange.w, luminance);float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight;returncolor * _SMHShadows.rgb * shadowsWeight +color * _SMHMidtones.rgb * midtonesWeight +color * _SMHHighliths.rgb * highlightsWeight;
}// color grading
float3 ColorGrading(float3 color)
{...// channel mixercolor = ColorGradeChannelMixer(color);color = max(color, 0.0);// Shadows Midtones Highlightscolor = ColorGradeShadowsMidtonesHighlights(color);...return color;
}
Unity 的颜色盘跟我们的功能是一样的,只不过它对输入颜色进行限制,并且拖动更精确。用 HVS 颜色拾取器可以模拟该功能,只是没有限制。
2.5 ACES Color Spaces
当启用 ACES tone mapping 时,Unity 会在 ACES 颜色空间而不是线性空间执行大多数的 color grading,来获得更好的效果。
Post Exposure 和 white balance 只能在线性空间应用,从对比度开始就可以在 ACES 颜色空间处理了。其它的 color grading,当计算亮度时,如果是 ACES 则调用 AcesLuminance 来计算亮度,因此我们将亮度计算提取成单独的函数。然后所有需要计算亮度的 color grading 都添加是否是 aces 的参数。为 ColorGrading 函数添加 aces 参数,默认值为 false,最后在 ToneMappingACESPassFragment 中调用 ColorGrading 时传入 true。
// color grading - contrast 对比度
float3 ColorGradingContrast(float3 color, bool useACES)
{color = useACES ? ACES_to_ACEScc(unity_to_ACES(color)) : LinearToLogC(color);color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;return useACES ? ACES_to_ACEScg(ACEScc_to_ACES(color)) : LogCToLinear(color);
}// 计算亮度
float Luminance(float3 color, bool useACES)
{return useACES ? AcesLuminance(color) : Luminance(color);
}...// color grading - saturation 饱和度
float3 ColorGradingSaturation(float3 color, bool useACES)
{float luminance = Luminance(color, useACES);return (color - luminance) * _ColorAdjustments.w + luminance;
}
...
float3 ColorGradeSplitToning(float3 color, bool useACES)
{...// 根据 balance 和亮度,计算插值参数float t = saturate(Luminance(saturate(color), useACES) + _SplitToningShadows.w);...
}
...
float3 ColorGradeShadowsMidtonesHighlights(float3 color, bool useACES)
{float luminance = Luminance(color, useACES);...
}
...
// color grading
float3 ColorGrading(float3 color, bool useACES = false)
{color = min(color.rgb, 60);// post exposurecolor = ColorGradingPostExposure(color);// white balancecolor = ColorGradeWhiteBalance(color);// contrastcolor = ColorGradingContrast(color, useACES);// color filtercolor = ColorGradingColorFilter(color);// 消除可能的负值color = max(color, 0.0);// split-toning for shadows and highlightscolor = ColorGradeSplitToning(color, useACES);// channel mixercolor = ColorGradeChannelMixer(color);color = max(color, 0.0);// Shadows Midtones Highlightscolor = ColorGradeShadowsMidtonesHighlights(color, useACES);// hue shiftcolor = ColorGradingHueShift(color);// saturationcolor = ColorGradingSaturation(color, useACES);// 消除可能的负值color = max(color, 0.0);return color;
}
...
// tone mapping - ACES
float4 ToneMappingACESPassFragment(Varyings input) : SV_TARGET
{float3 color = GetSource(input.screenUV).rgb;color = ColorGrading(color, true);color = AcesTonemap(unity_to_ACES(color.rgb));return float4(color, 1.0);
}
3 LUT
每个像素都执行 color grading 的每一步,工作量很大。我们可以创作那些仅应用需要的步骤的变体,但是就需要很多的 keywords 或 pass。我们可以将 color grading 烘焙到一张 lookup table 中,即LUT,然后采样该图来变换颜色。LUT是3D贴图,通常是 32x32x32。渲染这张图,然后采样来执行 color grading,工作量要小很多。URP,HDRP就使用了这种方法。
3.1 LUT Resolution
32 像素尺寸通常就足够了,但是我们依然为其添加配置参数。该参数是 Quality 设置,因此定义在 CustomRenderPipelineAsset 中,然后一步步传入 PostFXStack 对象中,并缓存,然后在后续使用该参数。
public class CustomRenderPipelineAsset : RenderPipelineAsset
{...public enum ColorLUTResolution { _16 = 16, _32 = 32, _64 = 64 }[SerializeField] ColorLUTResolution colorLUTResolution = ColorLUTResolution._32;[SerializeField] ShadowSettings shadows = default;protected override RenderPipeline CreatePipeline(){var rp = new CustomRenderPipeline(useSRPBatcher, useDynamicBatching, useGPUInstancing, usePerObjectLights, shadows,postFXSettings,(int)colorLUTResolution);rp.EnableHDR(allowHDR);return rp;}
}
3.2 Rendering to a 2D LUT Texture
LUT 是 3D texture,但是 shader 无法渲染到 3D texture。因此我们创建一个很宽的贴图来代替 3D 贴图,即将 3D texture 中 Z 方向上的每个 texture 横向平铺到2D texture 上。因此贴图高度就是 LUT 尺寸,宽度则是 LUT 尺寸的平方。在 color grading configuring 结束后,用 default HDR 格式创建该贴图的 RT。
因为我们将 color grading 和 tone mapping 放到一起处理,因此将相关 pass 改名为 ColorGrading.然后将 color grading 和 tone mapping 绘制到 lut 上,然后将源图绘制到 camera RT 上(使得画面看起来没有变化),最后释放 lut
void DoColorGradingAndToneMapping(int sourceId)
{...ConfigureShadowsMidtonesHighlights();// 创建 color grading LUT RTint height = colorLUTResolution;int width = colorLUTResolution * colorLUTResolution;buffer.GetTemporaryRT(colorLUTId, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);// 在 LUT 上绘制 color grading 和 tone mappingPass pass = Pass.ToneMappingNone + (int)settings.ToneMapping.mode;Draw(sourceId, colorLUTId, pass);// 暂时将原图绘制到屏幕上Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
}
通过 FrameDebugger,可以看到我们将 color grading 绘制到了压扁的图上
3.3 LUT Color Matrix
要创建合适的LUT,我们需要用颜色变换矩阵来进行填充。这是通过调整 color grading pass 函数,使用基于UV坐标的颜色而不是采样源贴图。添加一个 GetColorGradeLUT,获取颜色并立即在颜色上执行 color grading。然后 color grading pass 中,只需要在这个结果上执行 tone mapping。
float3 GetColorGradeLUT(float2 uv, bool useACES = false)
{float3 color = float3(uv, 0.0);return ColorGrading(color);
}// tone mapping - none only color grading
float4 ColorGradingNonePassFragment(Varyings input) : SV_TARGET
{//float3 color = GetSource(input.screenUV).rgb;//color = ColorGrading(color);float3 color = GetColorGradeLUT(input.screenUV);return float4(color, 1.0);
}// tone mapping - reinhard
float4 ColorGradingReinhardPassFragment(Varyings input) : SV_TARGET
{//float3 color = GetSource(input.screenUV).rgb;//color = ColorGrading(color);float3 color = GetColorGradeLUT(input.screenUV);color /= color + 1.0;return float4(color, 1.0);
}// tone mapping - neutral
float4 ColorGradingNeutralPassFragment(Varyings input) : SV_TARGET
{//float3 color = GetSource(input.screenUV).rgb;//color = ColorGrading(color);float3 color = GetColorGradeLUT(input.screenUV);color = NeutralTonemap(color.rgb);return float4(color, 1.0);
}// tone mapping - ACES
float4 ColorGradingACESPassFragment(Varyings input) : SV_TARGET
{//float3 color = GetSource(input.screenUV).rgb;//color = ColorGrading(color, true);float3 color = GetColorGradeLUT(input.screenUV, true);color = AcesTonemap(unity_to_ACES(color.rgb));return float4(color, 1.0);
}
可以通过 GetLutStripValue 函数来获得 LUT 输入颜色,该函数需要 UV 坐标,和 color grading lut 参数。修改我们的 GetColorGradeLUT 函数:
float4 _ColorGradingLUTParameters;float3 GetColorGradeLUT(float2 uv, bool useACES = false)
{float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);return ColorGrading(color, useACES);
}
首先在 CPU 端上传该参数
void DoColorGradingAndToneMapping(int sourceId)
{...// 创建 color grading LUT RTint height = colorLUTResolution;int width = colorLUTResolution * colorLUTResolution;buffer.GetTemporaryRT(colorGradingLUTId, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);// 上传 lut parametersVector4 lutparams = new Vector4(height, 0.5f/width, 0.5f/height, height/(height-1f) );buffer.SetGlobalVector(colorGradingLUTParametersId, lutparams);...
}
3.4 Log C LUT
目前我们的 LUT 矩阵是在线性空间里的,范围是(0-1)。为了支持 HDR,我们需要扩大范围,我们把输入颜色看作是在 Log C 空间中,在 color grading 前将其变换到线性空间,这可以让范围扩大到 59。
float3 GetColorGradeLUT(float2 uv, bool useACES = false)
{float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);return ColorGrading(LogCToLinear(color), useACES);
}
与线性空间相比,Log C 向暗的颜色增加了更多细节。差不多在 0.5 时就超过了线性空间的最大值。之后强度快速增长,因此矩阵的分辨率就降低了很多(0-0.5对应了的0-1,0.5-1对应了1-59),这对支持HDR是必要的,但是如果不是HDR,那么最好还是在线性空间,否则有一半的分辨率就浪费了,因此增加一个布尔标记来进行控制
bool _ColorGradingLUTInLogC;float3 GetColorGradeLUT(float2 uv, bool useACES = false)
{float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);return ColorGrading(_ColorGradingLUTInLogC ? LogCToLinear(color) : color, useACES);
}
在C# 中,如果开启了 HDR 且开启了 color grading,则表示在 LogC 中,并上传。
// 在 LUT 上绘制 color grading 和 tone mapping
Pass pass = Pass.ColorGradingNone + (int)settings.ToneMapping.mode;
// 上传应用 lut 需要的参数
lutparams = new Vector4(1f / width, 1f / height, height - 1f);
buffer.SetGlobalVector(colorGradingLUTParametersId, lutparams);
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Final);
buffer.ReleaseTemporaryRT(colorGradingLUTId);
inLogC = useHDR && pass != Pass.ColorGradingNone;
buffer.SetGlobalFloat(colorGradingLUTInLogCId, inLogC ? 1.0f : 0f);
Draw(sourceId, colorGradingLUTId, pass);
因为我们的 color grading 不再依赖于渲染的图像,因此不需要再限制值在 60 以内,因为 LUT 已经对值进行了限制
float3 ColorGrade (float3 color, bool useACES = false) {//color = min(color, 60.0);…
}
3.5 Final Pass
为了应用 LUT,我们需要定义一个新的 final pass,获取源图的颜色,并应用 color grading LUT ,将该功能实现在专门的 ApplyColorGradingLUT 函数中。
通过函数 ApplyLut2D 函数应用 LUT,该函数会将 2D LUT 当作 3D 贴图使用。
// 用用 color grading lut
TEXTURE2D(_ColorGradingLUT);
float3 ApplyColorGradingLUT(float3 color)
{return ApplyLut2D(TEXTURE2D_ARGS(_ColorGradingLUT, sampler_linear_clamp),saturate(_ColorGradingLUTInLogC ? LinearToLogC(color) : color),_ColorGradingLUTParameters.xyz);
}float4 FinalPassFragment(Varyings input) : SV_TARGET
{float4 color = GetSource(input.screenUV);color.rgb = ApplyColorGradingLUT(color.rgb);return color;
}
CPU 端上传 ApplyLut2D 需要的参数,并用 final pass 进行渲染,最后释放 lut RT
...// 上传应用 lut 需要的参数lutparams = new Vector4(1f / width, 1f / height, height - 1f);buffer.SetGlobalVector(colorGradingLUTParametersId, lutparams);Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Final);buffer.ReleaseTemporaryRT(colorGradingLUTId);
}
可以进一步优化,如果效果可以接受的话,离线生成LUT。
3.6 LUT Banding
我们使用 LUT 同时完成 color grading 和 tone mapping,而且效果和以前也是一样的。然而,因为 LUT 有限的分辨率,以及我们用双线性插值进行采样,因此原本平滑的颜色过度,会出现条带。通常在分辨率为 32 时这不会特别明显,但是在极高的HDR变化的区域条带就会变得明显。一个例子是在之前教程的场景里,强度为 200 的聚光灯的衰减,照亮了一个均匀的白色表面。
把采样模式临时改为 sampler_point_clamp,条带就会特别明显,因为关闭了LUT 的 2D片内采样的插值,但是在相邻的 2D 片之间,由于我们使用了 ApplyLut2D 函数来采样两个片并进行混合,以模拟 3D 贴图,因此相邻片之间依然是插值的。
如果条带太明显,可以将分辨率提高到 64,但是其实颜色上的细微变化,就能隐藏条带。如果你在颜色过渡非常细微的地方寻找条纹图案,那么就更有可能发现由于 8 位帧缓冲器限制而产生的条纹现象。这种现象并非由 LUT 引起,可以通过 dithering 技术来缓解,但这属于另一个话题了。