ShaderGraph饱和度节点深度解析:从算法原理到实战应用
2026/7/18 13:57:27 网站建设 项目流程

1. 项目概述:为什么我们需要一个“饱和度节点”?

在ShaderGraph里折腾过一阵子的朋友,肯定对“饱和度”这个概念不陌生。无论是做风格化的卡通渲染,还是调整场景氛围,甚至是修复某些材质看起来“灰蒙蒙”的问题,都离不开对颜色饱和度的控制。Unity的ShaderGraph内置了一个非常方便的Saturation Node,也就是我们今天要拆解的主角——饱和度节点。

简单来说,这个节点的功能很纯粹:它接收一个颜色(RGB向量)和一个浮点数值,然后输出一个调整了饱和度之后的颜色。当饱和度值为1时,原样输出;为0时,输出完全去色(也就是灰度)的结果;大于1时增强饱和度,让颜色更鲜艳;在0到1之间则降低饱和度。

听起来很简单对吧?但如果你只是把它当成一个“调色滑块”来用,那就太浪费了。这个节点背后,其实隐藏着图形学中一个非常经典且高效的颜色处理算法——基于亮度(Luma)的线性插值。理解这个算法,不仅能让你知道这个节点“怎么用”,更能让你明白它“为什么这么设计”,以及当它效果不理想时,你该如何变通甚至自己动手实现更符合需求的饱和度调整方案。这对于想从ShaderGraph使用者进阶为Shader创造者的你来说,是必经的一步。

2. 核心原理拆解:从“调色”到“数学”

很多教程会直接告诉你:“喏,连上线,调参数,效果就出来了。”但我们不满足于此。要真正掌握一个节点,必须扒开它的“外壳”,看看里面的“引擎”是怎么转的。根据官方文档提供的生成代码,Saturation节点的核心算法一目了然:

void Unity_Saturation_float(float3 In, float Saturation, out float3 Out) { float luma = dot(In, float3(0.2126729, 0.7151522, 0.0721750)); Out = luma.xxx + Saturation.xxx * (In - luma.xxx); }

这段简短的代码,就是整个节点的灵魂。我们来逐行解析。

2.1 第一步:计算亮度(Luma)

float luma = dot(In, float3(0.2126729, 0.7151522, 0.0721750));

这是整个操作的第一步,也是关键所在。它并不是简单地将RGB三个通道取平均值,而是做了一个加权平均。这个权重向量(0.2126, 0.7152, 0.0722)是ITU-R BT.709标准中定义的亮度系数,它反映了人眼对不同颜色光线的敏感程度。

  • 为什么是这些数字?人眼对绿色最敏感,对红色次之,对蓝色最不敏感。所以绿色(G)的权重最高(0.7152),红色(R)次之(0.2126),蓝色(B)最低(0.0722)。用这个权重计算出来的“亮度”(Luma),比简单的算术平均(各1/3)更符合人眼的感知,调整饱和度时视觉效果会更自然。
  • dot点积操作在做什么?点积在这里的本质是加权求和。In.r * 0.2126729 + In.g * 0.7151522 + In.b * 0.0721750。最终得到的luma是一个标量(float),它代表了输入颜色In在人眼感知下的灰度值。

实操心得:这个亮度系数是针对sRGB颜色空间的。如果你的纹理或颜色数据是在线性空间(Linear Space)下工作的,严格来说应该使用不同的系数(如BT.2020)。但在绝大多数游戏和实时渲染的sRGB工作流中,使用这个系数是标准且正确的做法。ShaderGraph默认处理sRGB纹理,所以直接用它没问题。

2.2 第二步:线性插值(Lerp)的变体

Out = luma.xxx + Saturation.xxx * (In - luma.xxx);

这一行是算法的核心计算。我们把它拆开看:

  1. luma.xxx:这是一个标量向向量的扩展。因为luma是一个float,而我们需要一个float3(RGB)来参与后续计算。luma.xxx就等同于float3(luma, luma, luma),也就是一个由亮度值构成的灰色。
  2. (In - luma.xxx):这是当前颜色与其对应灰度值的差值向量。这个差值代表了颜色的“彩色成分”或“色度”。一个完全去色的灰度图,这个差值就是0。
  3. Saturation.xxx * (In - luma.xxx):用饱和度参数去缩放这个“彩色成分”。当Saturation = 1时,彩色成分不变;当Saturation = 0时,彩色成分被完全归零;当Saturation = 2时,彩色成分被放大一倍。
  4. luma.xxx + ...:最后,将缩放后的“彩色成分”加回到原始的“亮度基础”上,得到最终输出颜色。

你可以把这个过程想象成调音台luma.xxx是音乐的“干声”(基础亮度),(In - luma.xxx)是音乐的“和声与色彩”(色度),Saturation就是控制“色彩声部”音量的推子。推子拉到最低,只剩干声(灰度);推子保持原位,原声输出;推子推高,色彩部分被放大,整体更“鲜艳”。

这个算法的精妙之处在于,它保证了颜色的亮度(Luma)在调整过程中保持不变。因为无论Saturation怎么变,最终结果中luma.xxx这一项是恒定加回去的。这意味着你增强或降低饱和度时,画面的明暗关系不会发生改变,这通常是我们想要的效果——只改变颜色的鲜艳程度,而不影响其明暗。

3. 节点接口与基础应用实战

理解了原理,我们回到ShaderGraph的界面,看看这个节点具体怎么用。

3.1 端口详解

Saturation节点通常有三个端口(有些版本可能端口名略有差异,但功能一致):

  • In (Vector 3):输入端口。接受一个float3类型的颜色值。这可以来自Texture Sample节点的RGB输出、Color节点的输出,或者是其他颜色运算的结果。
  • Saturation (Float):输入端口。接受一个浮点数,控制饱和度强度。这个值可以是常数,也可以来自一个Slider节点、Time节点,甚至是另一个复杂的计算流程,从而实现动态的饱和度变化。
  • Out (Vector 3):输出端口。输出调整后的float3颜色值,可以继续连接到主纹理颜色、自发光颜色或任何需要颜色的地方。

3.2 基础连接示例:制作一个可调节的“去色”效果

这是最直接的应用。假设我们有一个场景,希望玩家能通过一个UI滑块来让整个世界逐渐变成黑白。

  1. 准备基础颜色:使用一个Texture Sample节点采样场景的主贴图,将其RGB输出连接到Saturation节点的In端口。
  2. 创建控制参数
    • 在Blackboard中创建一个Float类型的属性,命名为_GlobalSaturation
    • 将其暴露出去(勾选Exposed),这样它就会出现在材质球的Inspector面板中。
    • 将这个属性节点拖到图上,其输出端口连接到Saturation节点的Saturation端口。
  3. 应用结果:将Saturation节点的Out连接到主着色器的Base Color输入上。
  4. 在材质中调节:现在,在材质Inspector里,你可以看到一个_GlobalSaturation的滑块。将其从1拖到0,就能看到材质颜色从彩色渐变为黑白。

注意事项:这里有一个常见的“坑”。如果你希望整个物体的饱和度统一变化,这样做没问题。但如果你希望物体的一部分(如高光、暗部)的饱和度变化程度不同,就需要更复杂的方案,比如将饱和度参数与一张遮罩(Mask)纹理相乘,再输入到节点。

3.3 进阶连接:实现“局部饱和度增强”

比如,你想让角色服装的花纹比布料本身更鲜艳一些。

  1. 准备两张图:一张是基础颜色贴图(Base Color),另一张是花纹区域的遮罩贴图(Mask,白色代表花纹区域,黑色代表布料区域)。
  2. 构建饱和度参数
    • 假设我们希望基础布料饱和度是0.8,花纹区域饱和度增强到1.5。
    • 我们可以使用一个Lerp(线性插值)节点。将LerpT输入连接遮罩贴图(单通道即可)。
    • LerpA输入设为0.8(常数),B输入设为1.5(常数)。
    • Lerp的输出就是一个根据遮罩变化的浮点数:在布料区域(Mask=0)输出0.8,在花纹区域(Mask=1)输出1.5,过渡区域平滑渐变。
  3. 连接饱和度节点:将基础颜色贴图的RGB输出连接到Saturation节点的In,将上一步Lerp的输出连接到Saturation节点的Saturation
  4. 输出结果:将Saturation节点的Out连接到Base Color

这样,你就实现了一个基于纹理遮罩的、非均匀的饱和度控制,比全局一个滑块要精细得多。

4. 内部算法模拟与自定义扩展

虽然ShaderGraph提供了现成的节点,但知其然更要知其所以然。我们完全可以自己用基础节点来“搭建”一个Saturation节点,这不仅能加深理解,还能在需要时进行自定义修改。

4.1 使用基础节点复现Saturation算法

我们根据之前的原理公式,在ShaderGraph中一步步搭建:

  1. 计算亮度(Luma)

    • In颜色(假设是float3)拆分成R,G,B三个通道(使用Split节点)。
    • 使用Multiply节点,分别将R通道乘以0.2126G通道乘以0.7152B通道乘以0.0722
    • 使用Add节点将这三个乘法的结果相加,得到luma(一个float)。更高效的做法是使用Dot Product节点,直接计算dot(In, float3(0.2126, 0.7152, 0.0722)),但ShaderGraph的基础节点库可能没有直接提供带常量的点积,需要自己用MultiplyAdd实现,或者使用Custom Function节点写入代码。
  2. 构造灰度颜色

    • 使用Combine节点,将上一步得到的luma(float)合并成一个float3,即(luma, luma, luma)。这对应代码中的luma.xxx
  3. 计算色度差并缩放

    • Subtract节点,计算In减去灰度颜色,得到色度差向量(In - luma.xxx)
    • Multiply节点,将色度差与输入的Saturation参数相乘。注意,Saturation是float,需要先通过Combine或直接与向量相乘(ShaderGraph会自动广播)来得到Saturation.xxx
  4. 合成最终颜色

    • Add节点,将灰度颜色缩放后的色度差相加,得到最终的Out颜色。

通过这个搭建过程,你会对每一步的计算有更直观的认识。在Graph中,这可能需要十几个节点的连接,远不如一个内置节点简洁,但作为学习练习非常有价值。

4.2 自定义变体:使用不同亮度系数的饱和度

内置节点固定使用了BT.709系数。但有些艺术风格可能需要不同的灰度感知。例如,在某些复古或高对比度风格中,你可能觉得绿色权重太高,想让去色后的结果更接近红蓝通道的混合。

这时,自定义的优势就体现出来了。在我们自己搭建的流程中,第一步计算亮度的乘法系数(0.2126, 0.7152, 0.0722)是可以替换的。

  1. 创建可调系数:在Blackboard中创建三个Float属性:_LumaWeightR,_LumaWeightG,_LumaWeightB。建议给它们设置默认值(如0.2126, 0.7152, 0.0722),并确保三者之和为1(虽然不是必须,但这样亮度范围更可控)。
  2. 修改亮度计算:将之前固定乘0.2126、0.7152、0.0722的地方,替换为乘以这三个属性。
  3. 后续步骤不变:后面的计算色差、缩放、合成的流程完全不变。

现在,你不仅可以通过Saturation参数控制饱和度强度,还能通过_LumaWeightRGB这三个参数,控制“以何种方式去色”。这为你实现特殊的艺术效果(比如模拟某种胶片或显示器的去色特性)提供了可能。

实操心得:自己搭建自定义节点时,务必将其封装成一个Sub Graph(子图)。将In(Vector3)、Saturation(Float)、以及可选的权重系数作为输入端口,将Out(Vector3)作为输出端口。这样,你就可以像使用内置Saturation Node一样,在任意着色器中拖拽使用你这个“增强版”饱和度节点了,整洁且可复用。

5. 实战场景与高级技巧

掌握了基础和原理后,我们来看看Saturation节点在真实项目中的一些巧妙用法和需要注意的“坑”。

5.1 场景一:动态环境氛围调节

在一天中的不同时间,或者进入不同区域(如毒气区、魔法结界)时,画面的整体饱和度需要变化。

  • 实现思路:将Saturation参数与游戏逻辑变量绑定。例如,可以定义一个全局的_WorldSaturation,由游戏代码(如Global Shader Property)根据时间或玩家状态进行驱动。
  • 进阶技巧:单纯改变全局饱和度可能很生硬。可以结合屏幕后处理(Post-processing)中的Color Grading(颜色分级)来使用。在ShaderGraph中(特别是URP的Fullscreen Shader Graph),你可以对渲染后的图像采样,然后应用Saturation节点。这样能实现更电影化的、非线性的饱和度调整曲线。

5.2 场景二:角色受击/死亡特效

角色受击时,除了闪白、变红,也可以快速降低角色自身饱和度然后恢复,营造一种“生命力被抽离”或“受到重击”的视觉感受。

  1. 制作脉冲曲线:使用Time节点和SineTriangle Wave节点,生成一个在0到1之间快速波动的值。或者使用一个简单的PingPong节点配合时间。
  2. 重映射饱和度:将脉冲曲线(比如范围0~1)通过Remap节点映射到目标饱和度范围(例如,从正常的1映射到最低的0.3)。这样,饱和度就会在0.3和1之间来回跳动。
  3. 应用:将此动态饱和度值连接到角色材质的Saturation节点。为了只影响角色,这个逻辑应该写在角色的局部材质中,而非全局后处理。

5.3 场景三:与其他颜色操作节点的组合

Saturation节点很少孤立使用,它通常是颜色处理流水线中的一环。

  • 顺序很重要Saturation->Hue Shift(色相偏移) 和Hue Shift->Saturation的效果是不同的。先调饱和度再调色相,颜色的变化会更“纯净”;先调色相再调饱和度,会因为色相移动而影响亮度感知,从而影响饱和度效果。通常,先调整色相/亮度,最后调整饱和度是更常见的流程。
  • MultiplyAdd结合:有时单纯调整饱和度无法达到理想的色彩强度。你可以将Saturation节点的输出再与一个颜色进行Multiply(正片叠底)来增强暗部色彩,或与一个颜色Add(相加)来增加整体色彩倾向。这比单纯把饱和度调到极高(>2)要更可控,因为极高的饱和度容易导致颜色溢出(超出显示范围,显得不自然)。

5.4 常见问题与排查技巧实录

即使理解了原理,在实际操作中还是会遇到一些意想不到的问题。下面是我踩过的一些坑和解决方法:

问题1:调整饱和度后,颜色看起来“脏”或“不自然”,特别是低饱和度时。

  • 原因分析:这很可能是因为输入的颜色值不在标准的sRGB范围内(比如是HDR颜色,值超过了1),或者亮度计算时出现了精度问题。当颜色值很大时,In - luma.xxx这个差值可能很大,乘以一个很小的饱和度值后,再与luma.xxx相加,可能会因为浮点数精度导致颜色出现偏差。
  • 解决方案
    1. 钳制输入:在将颜色输入Saturation节点之前,使用Clamp节点或Saturate节点将其范围限制在[0, 1]内(如果是LDR流程)。这能保证计算稳定性。
    2. 检查颜色空间:确保你的纹理导入设置和ShaderGraph中的颜色空间设置一致。如果纹理是sRGB,但你在线性空间下计算,颜色会变暗变灰,再调整饱和度就会很奇怪。
    3. 尝试自定义亮度系数:如4.2节所述,内置的亮度系数可能不适合你的特定艺术风格。尝试微调权重,找到最适合你画面感觉的灰度化方案。

问题2:我想只改变图像中特定颜色范围(如红色)的饱和度,怎么办?

  • 原因分析:内置的Saturation节点是全局操作,对所有颜色一视同仁。
  • 解决方案:你需要实现一个“选择性饱和度”调整。思路如下:
    1. 颜色识别:将输入颜色从RGB空间转换到HSV/HSL颜色空间。ShaderGraph有RGB to HSV节点。
    2. 创建遮罩:根据HSV中的H(色相)通道,使用RemapSmoothstep节点,生成一个针对目标色相范围(比如红色对应H值在0~0.1和0.9~1.0之间)的遮罩(0到1)。
    3. 混合饱和度:计算两个饱和度结果:一个是原始饱和度(比如1.0),一个是目标饱和度(比如0.5)。使用上一步的色相遮罩,对这两个饱和度值进行Lerp
    4. 应用:将混合后的饱和度值输入到Saturation节点。这样,只有红色区域的饱和度会被降低,其他颜色保持不变。这个方法常用于调整画面中过于抢眼的某种颜色。

问题3:在移动设备上,使用复杂的自定义饱和度网络导致性能下降。

  • 原因分析:自己用基础节点搭建的饱和度计算,尤其是包含多个MultiplyAdd的版本,指令数会比使用一个高度优化的内置Saturation节点多。
  • 解决方案
    1. 优先使用内置节点:在最终项目打包前,尽量用内置的Saturation Node替换自己搭建的复杂网络。内置节点通常由引擎底层提供,经过高度优化。
    2. 使用Custom Function封装:如果必须使用自定义算法(如改了亮度系数),不要用一堆基础节点连接,而是将其写在一个Custom Function节点的HLSL代码里。一段紧凑的HLSL代码通常比等价的节点网络生成更高效的Shader汇编指令。
    3. 性能分析:务必使用Unity的Frame Debugger或第三方GPU性能分析工具,对比使用内置节点和自定义节点时的Shader指令数(ALU操作)和耗时。在移动端,每一行指令都值得计较。

问题速查表:

问题现象可能原因排查步骤与解决方案
颜色变灰,但饱和度滑块无效节点连接错误,Saturation端口输入了固定值0检查Saturation端口的输入源,确保其值在变化(如连接了Slider)
调整饱和度时,画面明暗也变了可能错误地修改了亮度(Luma)计算部分,或输入颜色包含Alpha且被错误处理1. 检查自定义节点中亮度权重之和是否接近1。
2. 确保计算只针对RGB三通道,Alpha通道应直接 bypass。
高饱和度下颜色出现“荧光”感,不自然颜色值超出显示范围(>1),或饱和度值设置过高(>3)1. 在输出到最终颜色前,用Saturate节点钳制颜色到[0,1]。
2. 将饱和度参数限制在合理范围(如0~2)。
特定纹理饱和度调整效果与其他纹理不同纹理的导入颜色空间(sRGB vs Linear)不统一在Unity Project面板检查所有相关纹理的导入设置,确保“sRGB (Color Texture)”选项符合预期(颜色贴图勾选,非颜色贴图不勾选)。

6. 总结与思维延伸

通过这次对Saturation节点的深度解析,我们完成了一次从“使用者”到“理解者”再到“创造者”的跨越。我们不仅学会了如何连接这个节点,更剖析了其背后基于亮度加权平均的线性插值算法,甚至动手复现并扩展了它。

这个节点的设计哲学非常经典:分离亮度与色度,保持亮度恒定,仅对色度进行缩放。这种思想在图像处理中无处不在。理解了它,你再去看其他颜色调整节点,如Contrast(对比度)、Vibrance(自然饱和度),甚至是一些复杂的色彩查找表(LUT)应用,都会发现其核心思路有相通之处——都是在对颜色的不同属性进行解耦和独立控制。

我个人在项目中的体会是,饱和度调整是一把“双刃剑”。适度使用可以极大地提升画面的视觉冲击力和情绪表达;过度使用,尤其是全局无差别地拉高饱和度,会让画面显得艳俗、虚假,并加速视觉疲劳。更高级的做法永远是局部、有层次、有意图地使用它。结合遮罩、基于深度或法线的边缘检测、甚至屏幕空间信息,来让饱和度的变化服务于你的画面叙事和视觉引导,这才是Shader艺术真正的魅力所在。

最后一个小技巧:当你对默认的饱和度调整效果不满意时,不要只盯着Saturation一个参数。尝试在调整饱和度之前,先轻微调整一下色相(Hue)或明度(Value/Lightness),有时会得到意想不到的、更和谐的色彩效果。因为色彩是一个三维空间(HSV),单独在一个维度上猛调,不如在三个维度上协同微调。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询