
本系列结合Unity URP源码了解URP渲染背后的真相,今天讨论的是ColorGradingLUT 。
前言
ColorGradingLUT 是 Unity URP 中用于全局颜色风格统一的核心后处理技术,本质是颜色查找表(Lookup Table),通过预定义纹理将原始像素颜色替换为目标色,实现快速调色、风格化统一与性能优化。
- 查表换色:将每个像素的 RGB 值作为坐标,在 LUT 纹理中采样得到新颜色,批量改变画面色调与亮度。
- 风格统一:快速套入统一风格(如赛博朋克、复古胶片),避免逐物体调整。
- 影视级调色:可导出 Photoshop/DaVinci 等软件的 .cube 格式 LUT,还原专业调色效果。
- 性能高效:采样开销固定,比实时计算更省性能,适合移动端与批量场景。
1. ColorGradingLUT是在哪里设置的
- 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;
}
}
- UniversalRenderer 每帧判断是否需要生成 LUT(相机后处理开启且
PostProcessPasses已创建),参见UniversalRenderer.cs
cs
bool generateColorGradingLUT = cameraData.postProcessEnabled && m_PostProcessPasses.isCreated;
- 真正分配并入队参见
UniversalRenderer.cs的Setup函数:
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);
}
...
}
- 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);
}
- 具体的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 是怎么来的
- 基础名 _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);
}
...
}
- 尺寸来自
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。
- 格式来自 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
- 名字后缀中的
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的参数
- 正常配置入口是 URP Asset 的两个字段:

UniversalRenderPipelineAsset.cs
cs
[SerializeField] ColorGradingMode m_ColorGradingMode = ColorGradingMode.LowDynamicRange;
[SerializeField] int m_ColorGradingLutSize = 32;
上面的面板对应的变量是m_ColorGradingMode、m_ColorGradingLutSize。
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); }
}
- 这两个值会在管线初始化时写进 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. 其他建议
- 尺寸可以改(16-65),改小省带宽,改大精度更好。
- 格式不能直接在 asset 里手工指定,它由 gradingMode + 硬件支持自动选择。
- 如果要强行固定格式/名字/执行时机,只能改 URP 包源码(
ColorGradingLutPass与UniversalRenderer/PostProcessPasses)。