1. 项目概述:为什么我们需要白平衡节点?
在ShaderGraph里鼓捣过一阵子颜色调整的朋友,大概率都见过或者用过Color Adjustment分类下的那些节点:对比度、饱和度、色相,这些概念都比较直观。但“白平衡”(White Balance)这个节点,乍一看可能会让人有点懵——这不是摄影和后期处理里的概念吗?怎么跑到实时渲染的着色器里来了?
其实,这正是ShaderGraph强大和贴近实际美术工作流的一个体现。简单来说,白平衡节点的核心任务,就是模拟真实世界中“光源颜色”对物体固有色的影响,并允许我们对其进行校正。想象一下,你在钨丝灯(暖黄光)下看一张白纸,纸会显得偏黄;在阴天(冷蓝光)下看,纸又会偏蓝。你的大脑会自动进行“白平衡”,告诉你“这是一张白纸”。在CG渲染中,我们需要手动或自动地完成这个校正过程,以确保颜色在不同光照环境下看起来是“正确”的,或者说,是符合我们艺术预期的。
Unity的ShaderGraph提供了这个内置节点,让我们能在Shader层面,实时、动态地调整画面的色温和色调。这对于营造特定氛围(如温暖的黄昏、冷峻的月夜)、匹配不同场景的灯光,或者简单地修正因环境光设置不当导致的颜色偏差,都极其有用。它不是一个简单的颜色叠加,而是基于色彩科学模型进行的转换,理解其背后的原理,能让你从“会调参数”进阶到“懂得为何这么调”。
2. 节点接口与参数深度解析
打开ShaderGraph,找到White Balance节点,你会看到它非常简洁,只有三个输入端口和一个输出端口。但简洁的背后,每个参数都大有讲究。
2.1 核心输入端口:In,Temperature,Tint
In (Vector 3):这是待处理的原始颜色输入。通常,我们会将主纹理的采样颜色(例如Sample Texture 2D节点的RGBA输出中的RGB部分)连接到这里。它接受一个三维向量,代表RGB颜色空间中的一个颜色。
Temperature (Float):色温这是白平衡调整的第一个核心参数。它的命名和效果直接来源于物理学中的色温概念,单位是开尔文(K)。但在ShaderGraph的这个节点中,它是一个归一化、无单位的浮点数。
- 参数范围与效果:官方文档建议的最佳操作范围是
[-1.67, 1.67]。这个范围是怎么来的?我们稍后在原理部分会揭秘。你需要记住的是:- 负值(-1.67 ~ 0):为画面增加蓝色调,模拟高色温(冷光)环境,比如阴天、阴影、荧光灯。
- 正值(0 ~ 1.67):为画面增加黄色/红色调,模拟低色温(暖光)环境,比如日出日落、钨丝灯、烛光。
- 值为0:不进行色温偏移。
实操心得:在实际调节时,你很少需要用到±1.67这样的极值。对于大多数自然场景的微调,
[-0.5, 0.5]这个区间的变化已经非常明显和够用。直接从0开始,缓慢拖动滑块,观察画面中原本应该是白色或灰色的区域(如墙壁、衬衫),你会直观地感受到色温变化。
Tint (Float):色调这是白平衡调整的第二个维度,用于补偿色温调整可能无法完全校正的绿色或洋红色偏。
- 参数范围与效果:同样,最佳范围是
[-1.67, 1.67]。- 负值(-1.67 ~ 0):为画面增加绿色调。常用于校正某些荧光灯造成的洋红色偏。
- 正值(0 ~ 1.67):为画面增加洋红色(粉红色)调。
- 值为0:不进行色调偏移。
Out (Vector 3):处理后的颜色输出。直接连接到主着色器的Base Color或相应的颜色输入上即可。
2.2 参数联动与视觉影响
Temperature和Tint并不是完全独立的。在物理上,改变光源色温时,其光谱分布变化也可能轻微影响绿-品红轴。因此,这个节点在内部计算时,两者是协同工作的。当你大幅调整Temperature后,有时会觉得颜色还是有点“怪”,这时候微调一下Tint,往往能获得更自然、更“干净”的白色。
一个常见的用法是:先用Temperature滑块确定画面整体的冷暖基调,然后再用Tint进行精细的“去杂色”处理,让中性色(白、灰)显得更纯粹。
3. 背后的色彩科学:从RGB到LMS
如果只是调调参数,那这部分可以跳过。但如果你想真正驾驭这个节点,甚至未来自己魔改或实现类似功能,理解其原理至关重要。Unity官方在节点描述里隐藏了生成的代码,这段代码正是关键。
白平衡的核心思想是:将颜色从设备相关的RGB空间,转换到与人类视觉锥体细胞响应更相关的色彩空间(通常是LMS空间)进行校正,然后再转换回RGB空间。
1. 什么是LMS空间?LMS代表人类视网膜上三种锥体细胞对不同波长光的敏感度:长波(L, 对红色敏感)、中波(M, 对绿色敏感)、短波(S, 对蓝色敏感)。这是一个基于人眼生理特性的色彩空间。在白平衡计算中,使用LMS空间比直接在RGB空间做乘法更符合视觉感知,能产生更物理正确的结果。
2. 节点的计算步骤拆解:我们结合官方提供的Unity_WhiteBalance_float函数来一步步看:
步骤一:参数预处理
float t1 = Temperature * 10 / 6; float t2 = Tint * 10 / 6;这里将用户输入的[-1.67, 1.67]范围内的参数,映射到了大约[-2.78, 2.78]的范围。这个缩放因子(10/6)很可能是为了适配后续色度坐标计算的敏感度而经验设定的。
步骤二:计算目标白点的色度坐标(xy)
float x = 0.31271 - t1 * (t1 < 0 ? 0.1 : 0.05); float standardIlluminantY = 2.87 * x - 3 * x * x - 0.27509507; float y = standardIlluminantY + t2 * 0.05;这是算法的精髓所在。
0.31271是标准光源D65(近似平均日光)在CIE xy色度图上的x坐标。D65是许多色彩标准(如sRGB)的基准白点。- 根据
t1(处理后的色温)的正负,以不同的斜率(0.1或0.05)调整x坐标。这模拟了色温变化时,白点在色度图上沿黑体辐射轨迹的移动。暖光(正t1)x减小(向红色区域),冷光(负t1)x增大(向蓝色区域)。 standardIlluminantY是通过一个二次多项式从x计算出的对应y坐标,这个多项式描述了黑体轨迹。- 最后,用
t2(处理后的色调)在y方向上进行微调,模拟绿-品红偏移。
通过这几行代码,我们得到了一个目标白点的xy色度坐标(x, y)。
步骤三:将目标白点转换到LMS空间
float Y = 1; // 假设亮度为1 float X = Y * x / y; float Z = Y * (1 - x - y) / y; float L = 0.7328 * X + 0.4296 * Y - 0.1624 * Z; float M = -0.7036 * X + 1.6975 * Y + 0.0061 * Z; float S = 0.0030 * X + 0.0136 * Y + 0.9834 * Z; float3 w2 = float3(L, M, S);这里先将xyY色度坐标转换为CIE XYZ色彩空间,然后再通过一个转换矩阵CIEXYZToLMS(矩阵系数为0.7328, 0.4296等)得到目标白点在LMS空间的值w2。 同时,定义了一个参考白点w1 = float3(0.949237, 1.03542, 1.08728),这就是D65白点在LMS空间中的值。
步骤四:计算平衡系数并应用
float3 balance = float3(w1.x / w2.x, w1.y / w2.y, w1.z / w2.z);计算平衡系数balance。这个系数是关键!它的意义是:为了将“目标白点w2”校正回“参考白点w1”,需要在LMS空间的三个通道上分别乘以的系数。如果目标白点偏黄(L、M相对较高),那么balance的B通道值就会大于1,以增强蓝色来抵消黄色。
步骤五:颜色空间转换与校正
float3x3 LIN_2_LMS_MAT = { ... }; // 线性RGB 转 LMS 矩阵 float3x3 LMS_2_LIN_MAT = { ... }; // LMS 转 线性RGB 矩阵 float3 lms = mul(LIN_2_LMS_MAT, In); // 1. 输入颜色转LMS lms *= balance; // 2. 在LMS空间应用白平衡系数 Out = mul(LMS_2_LIN_MAT, lms); // 3. 转回线性RGB- 将输入的线性RGB颜色
In,通过矩阵LIN_2_LMS_MAT转换到LMS空间。 - 在LMS空间,将每个通道乘以对应的
balance系数。这一步直接完成了基于人眼感知的颜色校正。 - 将校正后的LMS颜色,通过逆矩阵
LMS_2_LIN_MAT转换回线性RGB空间并输出。
整个过程可以概括为:RGB -> LMS -> (平衡校正) -> LMS -> RGB。这个流程确保了校正过程符合人眼视觉特性,而不仅仅是简单的颜色滤镜。
4. 在ShaderGraph中的实战应用与技巧
理解了原理,我们来看看怎么在项目中用好它。白平衡节点通常不单独使用,而是作为颜色处理管线中的一环。
4.1 基础连接与场景氛围塑造
最直接的用法,是将它连在纹理采样之后,Fragment着色阶段之前。
[Texture Property] -> [Sample Texture 2D] (RGB) -> [White Balance Node] -> [Base Color]通过一个Vector1类型的属性(或通过脚本控制)来驱动Temperature和Tint参数,你就能轻松实现:
- 昼夜循环:白天使用偏冷的色温(如-0.3),黄昏使用暖色温(如0.7)。
- 室内外差异:室内钨丝灯用暖黄(0.5),室外阴天用冷蓝(-0.4)。
- 特殊效果:恐怖场景可以施加轻微的绿色调(Tint为负),梦幻场景可以施加粉色调(Tint为正)。
4.2 高级用法:与后处理栈配合
虽然节点可用于物体单独着色,但其威力更体现在屏幕后处理(Post-processing)中。你可以创建一个全屏后处理Shader Graph,使用Scene Color节点作为In的输入,对整个画面进行白平衡调整。
技巧:基于画面内容的自动白平衡模拟完全自动的白平衡算法很复杂,但我们可以模拟一个简化版。思路是:假设画面中央或高亮区域应该是中性色(灰白色)。
- 使用
Scene Color采样屏幕纹理。 - 使用一个
Blur节点对特定区域(如中心一小块)进行模糊,得到区域的平均颜色。 - 将这个平均颜色的RGB分量转换到某种亮度-色度空间(如简化版的
RGB to YCbCr),分析其色度(Cb, Cr)偏离中性值(0.5 for某些范围)的程度。 - 将这个偏离程度,映射为
Temperature和Tint的调整值,反馈给白平衡节点。
这样,当镜头对准一个蓝色物体时,Shader会认为画面偏蓝,并自动增加黄色来补偿,虽然不一定完全准确,但能实现动态的、基于内容的色彩调整效果。
4.3 性能考量与参数优化
白平衡节点涉及两次3x3矩阵乘法(RGB<->LMS转换)和一些标量计算,属于中等开销的操作。在移动端或对大量物体使用时需注意:
- 避免每帧动态剧烈变化:除非必要,否则将
Temperature和Tint设置为静态值或缓慢插值。 - 精度选择:在Shader Graph节点设置中,可以考虑将精度(Precision)从
Float改为Half,对于颜色计算,半精度在大多数情况下已经足够,能提升性能。 - 后处理优于逐物体:如果需要对整个场景调色,强烈建议使用后处理方案。一个全屏Pass应用一次白平衡,远比每个物体单独计算要高效。
5. 常见问题排查与效果调试指南
在实际使用中,你可能会遇到一些困惑或效果不如预期的情况。
5.1 为什么调整参数感觉没效果或效果不对?
可能原因1:颜色空间不正确。白平衡节点必须在线性颜色空间(Linear Color Space)下工作才能得到正确结果。请确保:
- Player Settings -> Player -> Other Settings -> Color Space 设置为Linear。
- 如果你在使用Gamma空间,节点的计算将基于错误的颜色响应曲线,导致校正失效或产生奇怪的颜色。
可能原因2:输入颜色值异常。检查连接到In端口的颜色值。确保其范围大致在[0, 1]的合理区间。如果输入是HDR颜色(值远大于1),白平衡系数的乘法可能会产生非常夸张的结果。可以考虑先对HDR颜色进行色调映射(Tone Mapping)或压缩,再进行白平衡调整。
可能原因3:参数超出合理范围。虽然节点允许输入任意浮点数,但官方建议的[-1.67, 1.67]是最佳范围。超出此范围,内部计算的色度坐标(x, y)可能变得无效(如y为负或x+y>1),导致后续计算出现NaN或不可预测的颜色。始终将参数控制在这个范围内。
5.2 如何获得“参考白”?
这是白平衡调试的核心技巧。在场景中放置一个纯白色或中性灰色的物体(例如,一个Unlit材质、颜色为(0.9, 0.9, 0.9)的球体),作为你的“白卡”。
- 将白平衡节点的参数归零。
- 在目标光照下观察这个“白卡”在游戏视图中的颜色。
- 调节
Temperature和Tint,直到这个“白卡”在屏幕上显示为你想要的、纯净的白色或灰色。 - 此时,白平衡参数就是针对当前光照环境的校正值。你可以将这个值应用到整个场景的后处理或相关材质上。
5.3 白平衡与Color Lookup(LUT)的区别
这是另一个常见困惑。两者都用于调色,但有本质区别:
- 白平衡节点:基于物理模型进行色彩校正。目标是让颜色在不同光照下“还原”或“匹配”到标准观察状态。它是全局性的、算法驱动的调整。
- Color Lookup (LUT):是一种色彩分级工具。它通过一个预制的颜色映射表,将输入颜色替换为输出颜色,以实现特定的艺术风格(如电影感、复古、赛博朋克)。它是整体性的、艺术驱动的映射。
通常,工作流是:先使用白平衡进行色彩校正(让颜色看起来“正确”),然后再使用LUT或其它工具进行色彩分级(让颜色看起来“好看”或“有风格”)。
5.4 参数动画与平滑过渡
如果你需要通过脚本动态控制白平衡(例如,从室内走到室外),直接每帧设置新的Temperature值可能会导致颜色跳跃。建议使用插值方法进行平滑过渡:
// C# 脚本示例 public float targetTemperature = 0.0f; public float currentTemperature = 0.0f; public float transitionSpeed = 2.0f; void Update() { currentTemperature = Mathf.Lerp(currentTemperature, targetTemperature, Time.deltaTime * transitionSpeed); // 将 currentTemperature 传递给Shader }在Shader中,使用这个平滑变化的值来驱动节点参数,可以获得非常自然的色温过渡效果。
掌握白平衡节点,意味着你掌握了在实时渲染中控制画面“光色”基础的一把钥匙。它连接了物理光学、色彩科学与艺术表现,是ShaderGraph工具链中一个兼具实用性和深度的模块。下次当你想让场景的灯光感觉更真实,或者想快速统一不同光源下的色调时,不妨优先考虑一下这个强大的节点。