【Unity URP源码阅读1】ColorGradingLUT

本系列结合Unity URP源码了解URP渲染背后的真相,今天讨论的是ColorGradingLUT 。

前言

ColorGradingLUT 是 Unity URP 中用于全局颜色风格统一的核心后处理技术,本质是颜色查找表(Lookup Table),通过预定义纹理将原始像素颜色替换为目标色,实现快速调色、风格化统一与性能优化。

  • 查表换色:将每个像素的 RGB 值作为坐标,在 LUT 纹理中采样得到新颜色,批量改变画面色调与亮度。
  • 风格统一:快速套入统一风格(如赛博朋克、复古胶片),避免逐物体调整。
  • 影视级调色:可导出 Photoshop/DaVinci 等软件的 .cube 格式 LUT,还原专业调色效果。
  • 性能高效:采样开销固定,比实时计算更省性能,适合移动端与批量场景。

1. ColorGradingLUT是在哪里设置的

  1. ColorGradingLUT Pass 是在 PostProcessPasses 里创建的,事件被固定成 BeforeRenderingPrePasses,参见PostProcessPasses.cs
cs 复制代码
public void Recreate(PostProcessData data, ref PostProcessParams ppParams)
{
    if (m_RendererPostProcessData)
        data = m_RendererPostProcessData;

    if (data == m_CurrentPostProcessData)
        return;

    if (m_CurrentPostProcessData != null)
    {
        m_ColorGradingLutPass?.Cleanup();
        m_PostProcessPass?.Cleanup();
        m_FinalPostProcessPass?.Cleanup();

        // We need to null post process passes to avoid using them
        m_ColorGradingLutPass = null;
        m_PostProcessPass = null;
        m_FinalPostProcessPass = null;
        m_CurrentPostProcessData = null;
    }

    if (data != null)
    {
        m_ColorGradingLutPass = new ColorGradingLutPass(RenderPassEvent.BeforeRenderingPrePasses, data); // m_ColorGradingLutPass的初始化
        m_PostProcessPass = new PostProcessPass(RenderPassEvent.BeforeRenderingPostProcessing, data, ref ppParams);
        m_FinalPostProcessPass = new PostProcessPass(RenderPassEvent.AfterRenderingPostProcessing, data, ref ppParams);
        m_CurrentPostProcessData = data;
    }
}
  1. UniversalRenderer 每帧判断是否需要生成 LUT(相机后处理开启且 PostProcessPasses 已创建),参见UniversalRenderer.cs
cs 复制代码
bool generateColorGradingLUT = cameraData.postProcessEnabled && m_PostProcessPasses.isCreated;
  1. 真正分配并入队参见UniversalRenderer.csSetup函数:
cs 复制代码
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
	...
	if (generateColorGradingLUT)
	{
	    colorGradingLutPass.ConfigureDescriptor(in renderingData.postProcessingData, out var desc, out var filterMode);
	    RenderingUtils.ReAllocateIfNeeded(ref m_PostProcessPasses.m_ColorGradingLut, desc, filterMode, TextureWrapMode.Clamp, anisoLevel: 0, name: "_InternalGradingLut");
	    colorGradingLutPass.Setup(colorGradingLut);
	    EnqueuePass(colorGradingLutPass);
	}
	...
}
  1. Pass 内部会把RenderTarget 设为内部 LUT RTHandle
    ColorGradingLutPass.cs
cs 复制代码
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    m_PassData.lutBuilderLdr = m_LutBuilderLdr;
    m_PassData.lutBuilderHdr = m_LutBuilderHdr;
    m_PassData.allowColorGradingACESHDR = m_AllowColorGradingACESHDR;

#if ENABLE_VR && ENABLE_XR_MODULE
    if (renderingData.cameraData.xr.supportsFoveatedRendering)
        renderingData.commandBuffer.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled);
#endif

    CoreUtils.SetRenderTarget(renderingData.commandBuffer, m_InternalLut, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, ClearFlag.None, Color.clear);
    ExecutePass(context, m_PassData, ref renderingData, m_InternalLut);
}
  1. 具体的LUT的生成是在ColorGradingLUTPas.cs
cs 复制代码
private static void ExecutePass(ScriptableRenderContext context, PassData passData, ref RenderingData renderingData, RTHandle internalLutTarget)
{
     var cmd = renderingData.commandBuffer;
     var lutBuilderLdr = passData.lutBuilderLdr;
     var lutBuilderHdr = passData.lutBuilderHdr;
     var allowColorGradingACESHDR = passData.allowColorGradingACESHDR;

     using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
     {
         // Fetch all color grading settings
         var stack = VolumeManager.instance.stack;
         var channelMixer = stack.GetComponent<ChannelMixer>();
         var colorAdjustments = stack.GetComponent<ColorAdjustments>();
         var curves = stack.GetComponent<ColorCurves>();
         var liftGammaGain = stack.GetComponent<LiftGammaGain>();
         var shadowsMidtonesHighlights = stack.GetComponent<ShadowsMidtonesHighlights>();
         var splitToning = stack.GetComponent<SplitToning>();
         var tonemapping = stack.GetComponent<Tonemapping>();
         var whiteBalance = stack.GetComponent<WhiteBalance>();

         ref var postProcessingData = ref renderingData.postProcessingData;
         bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
         ref CameraData cameraData = ref renderingData.cameraData;

         // Prepare texture & material
         var material = hdr ? lutBuilderHdr : lutBuilderLdr;

         // Prepare data
         var lmsColorBalance = ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature.value, whiteBalance.tint.value);
         var hueSatCon = new Vector4(colorAdjustments.hueShift.value / 360f, colorAdjustments.saturation.value / 100f + 1f, colorAdjustments.contrast.value / 100f + 1f, 0f);
         var channelMixerR = new Vector4(channelMixer.redOutRedIn.value / 100f, channelMixer.redOutGreenIn.value / 100f, channelMixer.redOutBlueIn.value / 100f, 0f);
         var channelMixerG = new Vector4(channelMixer.greenOutRedIn.value / 100f, channelMixer.greenOutGreenIn.value / 100f, channelMixer.greenOutBlueIn.value / 100f, 0f);
         var channelMixerB = new Vector4(channelMixer.blueOutRedIn.value / 100f, channelMixer.blueOutGreenIn.value / 100f, channelMixer.blueOutBlueIn.value / 100f, 0f);

         var shadowsHighlightsLimits = new Vector4(
             shadowsMidtonesHighlights.shadowsStart.value,
             shadowsMidtonesHighlights.shadowsEnd.value,
             shadowsMidtonesHighlights.highlightsStart.value,
             shadowsMidtonesHighlights.highlightsEnd.value
         );

         var (shadows, midtones, highlights) = ColorUtils.PrepareShadowsMidtonesHighlights(
             shadowsMidtonesHighlights.shadows.value,
             shadowsMidtonesHighlights.midtones.value,
             shadowsMidtonesHighlights.highlights.value
         );

         var (lift, gamma, gain) = ColorUtils.PrepareLiftGammaGain(
             liftGammaGain.lift.value,
             liftGammaGain.gamma.value,
             liftGammaGain.gain.value
         );

         var (splitShadows, splitHighlights) = ColorUtils.PrepareSplitToning(
             splitToning.shadows.value,
             splitToning.highlights.value,
             splitToning.balance.value
         );

         int lutHeight = postProcessingData.lutSize;
         int lutWidth = lutHeight * lutHeight;
         var lutParameters = new Vector4(lutHeight, 0.5f / lutWidth, 0.5f / lutHeight,
             lutHeight / (lutHeight - 1f));

         // Fill in constants
         material.SetVector(ShaderConstants._Lut_Params, lutParameters);
         material.SetVector(ShaderConstants._ColorBalance, lmsColorBalance);
         material.SetVector(ShaderConstants._ColorFilter, colorAdjustments.colorFilter.value.linear);
         material.SetVector(ShaderConstants._ChannelMixerRed, channelMixerR);
         material.SetVector(ShaderConstants._ChannelMixerGreen, channelMixerG);
         material.SetVector(ShaderConstants._ChannelMixerBlue, channelMixerB);
         material.SetVector(ShaderConstants._HueSatCon, hueSatCon);
         material.SetVector(ShaderConstants._Lift, lift);
         material.SetVector(ShaderConstants._Gamma, gamma);
         material.SetVector(ShaderConstants._Gain, gain);
         material.SetVector(ShaderConstants._Shadows, shadows);
         material.SetVector(ShaderConstants._Midtones, midtones);
         material.SetVector(ShaderConstants._Highlights, highlights);
         material.SetVector(ShaderConstants._ShaHiLimits, shadowsHighlightsLimits);
         material.SetVector(ShaderConstants._SplitShadows, splitShadows);
         material.SetVector(ShaderConstants._SplitHighlights, splitHighlights);

         // YRGB curves
         material.SetTexture(ShaderConstants._CurveMaster, curves.master.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveRed, curves.red.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveGreen, curves.green.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveBlue, curves.blue.value.GetTexture());

         // Secondary curves
         material.SetTexture(ShaderConstants._CurveHueVsHue, curves.hueVsHue.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveHueVsSat, curves.hueVsSat.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveLumVsSat, curves.lumVsSat.value.GetTexture());
         material.SetTexture(ShaderConstants._CurveSatVsSat, curves.satVsSat.value.GetTexture());

         // Tonemapping (baked into the lut for HDR)
         if (hdr)
         {
             material.shaderKeywords = null;

             switch (tonemapping.mode.value)
             {
                 case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
                 case TonemappingMode.ACES: material.EnableKeyword(allowColorGradingACESHDR ? ShaderKeywordStrings.TonemapACES : ShaderKeywordStrings.TonemapNeutral); break;
                 default: break; // None
             }

             // HDR output is active
             if (cameraData.isHDROutputActive)
             {
                 Vector4 hdrOutputLuminanceParams;
                 Vector4 hdrOutputGradingParams;

                 UniversalRenderPipeline.GetHDROutputLuminanceParameters(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, tonemapping, out hdrOutputLuminanceParams);
                 UniversalRenderPipeline.GetHDROutputGradingParameters(tonemapping, out hdrOutputGradingParams);

                 material.SetVector(ShaderPropertyId.hdrOutputLuminanceParams, hdrOutputLuminanceParams);
                 material.SetVector(ShaderPropertyId.hdrOutputGradingParams, hdrOutputGradingParams);

                 HDROutputUtils.ConfigureHDROutput(material, cameraData.hdrDisplayColorGamut, HDROutputUtils.Operation.ColorConversion);
             }
         }

         cameraData.xr.StopSinglePass(cmd);


         if (cameraData.xr.supportsFoveatedRendering)
             cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled);

         // Render the lut
         Blitter.BlitCameraTexture(cmd, internalLutTarget, internalLutTarget, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0);

         cameraData.xr.StartSinglePass(cmd);
     }
 }

简单来说,这张颜色查找表是根据 Volume里各种颜色参数计算生成的,它的相关的shader我们从Frame Debugger里也可以看到是LutBuilderHdr

2. RT的名字_InternalGradingLut_1024x32_R16G16B16A16_SFloat_Tex2D 是怎么来的

  1. 基础名 _InternalGradingLut 来自 UniversalRenderer 分配时传入的 name。
    见上面的 UniversalRenderer.cs
cs 复制代码
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
	...
	if (generateColorGradingLUT)
	{
	    colorGradingLutPass.ConfigureDescriptor(in renderingData.postProcessingData, out var desc, out var filterMode);
	    RenderingUtils.ReAllocateIfNeeded(ref m_PostProcessPasses.m_ColorGradingLut, desc, filterMode, TextureWrapMode.Clamp, anisoLevel: 0, name: "_InternalGradingLut");
	    colorGradingLutPass.Setup(colorGradingLut);
	    EnqueuePass(colorGradingLutPass);
	}
	...
}
  1. 尺寸来自 ColorGradingLutPass 的描述符计算:
cs 复制代码
public void ConfigureDescriptor(in PostProcessingData postProcessingData, out RenderTextureDescriptor descriptor, out FilterMode filterMode)
{
    bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
    int lutHeight = postProcessingData.lutSize;
    int lutWidth = lutHeight * lutHeight;
    var format = hdr ? m_HdrLutFormat : m_LdrLutFormat;
    descriptor = new RenderTextureDescriptor(lutWidth, lutHeight, format, 0);
    descriptor.vrUsage = VRTextureUsage.None; // We only need one for both eyes in VR

    filterMode = FilterMode.Bilinear;
}

计算公式是:
H=NH = NH=N
W=N2W = N^2W=N2

其中 NNN 是 lutSize。

这里看到的是1024×32,说明 lutSize=32。

  1. 格式来自 gradingMode 与平台支持:
    HDR 模式优先 R16G16B16A16_SFloat,不支持则降到 B10G11R11_UFloatPack32,再不行降到 R8G8B8A8_UNorm。 参见ColorGradingLutPass.cs
cs 复制代码
public ColorGradingLutPass(RenderPassEvent evt, PostProcessData data)
{
	...
  if (SystemInfo.IsFormatSupported(GraphicsFormat.R16G16B16A16_SFloat, kFlags))
      m_HdrLutFormat = GraphicsFormat.R16G16B16A16_SFloat;
  else if (SystemInfo.IsFormatSupported(GraphicsFormat.B10G11R11_UFloatPack32, kFlags))
      // Precision can be too low, if FP16 primary renderTarget is requested by the user.
      // But it's better than falling back to R8G8B8A8_UNorm in the worst case.
      m_HdrLutFormat = GraphicsFormat.B10G11R11_UFloatPack32;
  else
      // Obviously using this for log lut encoding is a very bad idea for precision but we
      // need it for compatibility reasons and avoid black screens on platforms that don't
      // support floating point formats. Expect banding and posterization artifact if this
      // ends up being used.
      m_HdrLutFormat = GraphicsFormat.R8G8B8A8_UNorm;

  m_LdrLutFormat = GraphicsFormat.R8G8B8A8_UNorm;
  ...
}

这里设置的是hdr

  1. 名字后缀中的 1024x32_R16..._Tex2D 是 RTHandle 自动拼接出来的:
    RTHandleSystem.cs
cs 复制代码
rt = new RenderTexture(width, height, (int)depthBufferBits, colorFormat)
     {
         hideFlags = HideFlags.HideAndDontSave,
         volumeDepth = slices,
         filterMode = filterMode,
         wrapModeU = wrapModeU,
         wrapModeV = wrapModeV,
         wrapModeW = wrapModeW,
         dimension = dimension,
         enableRandomWrite = enableRandomWrite,
         useMipMap = useMipMap,
         autoGenerateMips = autoGenerateMips,
         anisoLevel = anisoLevel,
         mipMapBias = mipMapBias,
         antiAliasing = (int)msaaSamples,
         bindTextureMS = bindTextureMS,
         useDynamicScale = m_HardwareDynamicResRequested && useDynamicScale,
         memorylessMode = memoryless,
         vrUsage = vrUsage,
         name = CoreUtils.GetRenderTargetAutoName(width, height, slices, colorFormat, dimension, name, mips: useMipMap, enableMSAA: enableMSAA, msaaSamples: msaaSamples, dynamicRes: useDynamicScale)
     };

CoreUtils.cs

cs 复制代码
static string GetRenderTargetAutoName(int width, int height, int depth, string format, TextureDimension dim, string name, bool mips, bool enableMSAA, MSAASamples msaaSamples, bool dynamicRes)
{
    string result = string.Format("{0}_{1}x{2}", name, width, height);

    if (depth > 1)
        result = string.Format("{0}x{1}", result, depth);

    if (mips)
        result = string.Format("{0}_{1}", result, "Mips");

    result = string.Format("{0}_{1}", result, format);

    if (dim != TextureDimension.None)
        result = string.Format("{0}_{1}", result, dim);

    if (enableMSAA)
        result = string.Format("{0}_{1}", result, msaaSamples.ToString());

    if (dynamicRes)
        result = string.Format("{0}_{1}", result, "dynamic");

    return result;
}

3. 如何设置ColorGradingLUT的参数

  1. 正常配置入口是 URP Asset 的两个字段:

UniversalRenderPipelineAsset.cs

cs 复制代码
[SerializeField] ColorGradingMode m_ColorGradingMode = ColorGradingMode.LowDynamicRange;
[SerializeField] int m_ColorGradingLutSize = 32;

上面的面板对应的变量是m_ColorGradingModem_ColorGradingLutSize

  1. lutSize 允许范围是 16 到 65(会被 clamp):
    UniversalRenderPipelineAsset.cs
cs 复制代码
/// <summary>
/// The minimum size of the color grading LUT.
/// </summary>
public const int k_MinLutSize = 16;

/// <summary>
/// The maximum size of the color grading LUT.
/// </summary>
public const int k_MaxLutSize = 65;
...
public int colorGradingLutSize
{
    get { return m_ColorGradingLutSize; }
    set { m_ColorGradingLutSize = Mathf.Clamp(value, k_MinLutSize, k_MaxLutSize); }
}
  1. 这两个值会在管线初始化时写进 postProcessingData:
    UniversalRenderPipeline.cs
cs 复制代码
static void InitializePostProcessingData(UniversalRenderPipelineAsset settings, bool isHDROutputActive, out PostProcessingData postProcessingData)
{
    postProcessingData.gradingMode = settings.supportsHDR
        ? settings.colorGradingMode
        : ColorGradingMode.LowDynamicRange;

    if (isHDROutputActive)
        postProcessingData.gradingMode = ColorGradingMode.HighDynamicRange;

    postProcessingData.lutSize = settings.colorGradingLutSize;
    postProcessingData.useFastSRGBLinearConversion = settings.useFastSRGBLinearConversion;
    postProcessingData.supportDataDrivenLensFlare = settings.supportDataDrivenLensFlare;
}

4. 其他建议

  1. 尺寸可以改(16-65),改小省带宽,改大精度更好。
  2. 格式不能直接在 asset 里手工指定,它由 gradingMode + 硬件支持自动选择。
  3. 如果要强行固定格式/名字/执行时机,只能改 URP 包源码(ColorGradingLutPassUniversalRenderer/PostProcessPasses)。
相关推荐
风酥糖5 小时前
Godot游戏练习01-第27节-升级选项选择生效
游戏·游戏引擎·godot
郝学胜-神的一滴5 小时前
[简化版 GAMES 101] 计算机图形学 04:二维变换上
c++·算法·unity·godot·图形渲染·unreal engine·cesium
南無忘码至尊6 小时前
Unity学习90天-第2天-认识键盘 / 鼠标输入(PC)并实现WASD 移动,鼠标控制物体转向
学习·unity·c#·游戏开发
星夜泊客6 小时前
unity 海底海洋资源OceanEnviromentPackUrp材质丢失修正
unity·游戏引擎·材质
weixin_424294677 小时前
Unity 的Button Animator
unity·游戏引擎
UQ_rookie8 小时前
【Unity3D】在URP渲染管线下使用liltoon插件出现粉色无法渲染情况的解决方案
unity·游戏引擎·shader·urp·着色器·vrchat·liltoon
aqiu~19 小时前
VSCode编辑器用于Unity项目
vscode·unity
小贺儿开发1 天前
Unity3D 心理沙盘互动演示
unity·ai·pdf·人机交互·工具·互动·心理沙盘
CuPhoenix1 天前
【沧海拾昧】Unity 导入中文字体文字缺失的解决方法
unity