Unity风格化水面着色器全攻略:从Shader Graph到性能优化

Unity风格化水面着色器全攻略:从Shader Graph到性能优化

1. 项目概述:为什么我们需要风格化水面?

在Unity里做水面效果,大家第一时间想到的可能是那些追求物理真实感的PBR水体,波浪、反射、折射、焦散一应俱全。但很多时候,尤其是在卡通渲染、低多边形风格或者独立游戏中,这种“真实”反而会破坏整体的美术统一性。想象一下,一个色彩明快、造型简约的卡通世界里,突然出现一片写实到能看清海底礁石纹理的海水,是不是感觉特别出戏?

这就是“风格化水面着色器”存在的意义。它不追求物理正确,而是追求视觉正确风格统一。它的核心目标是:用可控、高效且富有表现力的方式,模拟出水体的“感觉”,而不是其物理本质。比如《塞尔达传说:旷野之息》里那片波光粼粼又清澈见底的湖泊,或者《原神》中那些色彩梦幻、边界清晰的水域,都是风格化水体的典范。

我接手过不少项目,从休闲手游到风格化Demo,几乎都绕不开对水体的定制。很多新手,甚至是有经验的开发者,一提到写Shader就头疼,觉得深不可测。其实,拆解开来,一个让人眼前一亮的风格化水面,无非是几个核心视觉元素的组合:基础颜色、波浪动画、边缘泡沫、镜面高光。本指南的目的,就是带你一步步,用Shader Graph(可视化着色器编辑工具)或者手写Shader代码,把这些元素像搭积木一样组合起来,打造出专属于你项目的惊艳卡通水体。无论你是美术想更好地控制效果,还是程序想深入理解渲染管线,这篇指南都会提供从思路到实现的完整路径。

2. 核心视觉元素拆解与设计思路

一个成功的风格化水面,绝不是一堆复杂技术的堆砌,而是有明确的设计目标。在动手写任何一行代码或连接任何一个节点之前,我们必须先想清楚:我们想要的水,看起来是什么样的?

2.1 基础色与深度渐变:营造水体通透感

真实的水之所以看起来是蓝色或绿色,是因为它对不同波长光线的吸收。在风格化处理中,我们简化这个物理过程,用基于深度的颜色渐变来模拟。

核心思路:离水面越近(深度值小),颜色越浅,可能带点泡沫的白色或浅绿色;离水面越远(深度值大),颜色越深,过渡到深蓝或墨绿色。这个“深度”,在Shader里通常通过场景深度纹理(Camera Depth Texture)水面像素的世界空间Y坐标做差来得到。

注意:在URP(通用渲染管线)或HDRP(高清渲染管线)中,获取深度纹理需要先在Renderer Asset中勾选相应选项。这是新手最容易卡住的第一步,配置不对,后续所有基于深度的效果都无法实现。

在Shader Graph中,你可以通过Scene Depth节点获取当前像素的深度,然后与通过Position节点(设置为World空间)得到的Y值进行计算。用一个SmoothstepRemap节点来控制深度到颜色的映射关系,再连接到一个Lerp(线性插值)节点,在两个颜色之间进行平滑过渡。这里的关键在于控制渐变区域的宽度和过渡的平滑度,太生硬会像油漆,太模糊则缺乏质感。

实操心得:不要只用简单的两种颜色做线性渐变。我通常会准备一个至少包含3-4个关键色的渐变条(Gradient),分别对应极浅水区、浅水区、深水区和非常深的区域。用Sample Gradient节点根据归一化的深度值采样,这样得到的水色层次感会丰富得多,也更接近手绘的感觉。

2.2 波浪动画:赋予水面生命力

静止的水面是死板的。风格化水面的波浪通常有两种做法:基于顶点动画和基于法线贴图扰动。

1. 顶点动画:直接修改水面网格顶点的Y轴坐标,模拟波浪起伏。这种方法性能消耗相对较高(顶点数多时影响更大),但效果直接,适合需要较大幅度波浪的场景。

  • 常用函数:使用sincos函数叠加,并赋予不同的频率、振幅和方向,可以创造出复杂的波谱。公式类似:offset.y = sin(vertex.x * frequency + time * speed) * amplitude
  • Shader Graph实现:使用Time节点获取时间,结合Position节点的XZ坐标,输入到SineCosine节点,再乘以控制参数,最后通过Add节点加到顶点位置的Y分量上。记得要在节点前勾选“Vertex Position”端口。

2. 法线贴图扰动:这是更主流、性能更好的方式。我们使用一张或两张(用于流动混合)波浪法线贴图,通过UV动画让其在水面流动,从而影响光照计算,产生波光粼粼的视觉效果,而网格本身并不变形。

  • 贴图选择:风格化水面通常使用特制的、对比度较高、细节概括的法线贴图,而不是写实的那种细腻噪波。
  • UV动画:用时间和一个速度参数对UV坐标进行偏移(Tiling And Offset节点)。使用两张法线贴图,以不同速度和方向滚动,然后混合(Blend Normal节点),可以避免产生明显的重复图案。
  • 影响范围:通过深度控制法线强度。浅水区波浪扰动应该更明显,深水区则减弱,这样更符合视觉规律。

个人建议:对于移动端或性能敏感的项目,优先使用法线贴图扰动。顶点动画留给那些需要表现巨浪或特殊互动(如船体划过)的局部效果。两者也可以结合使用,用微弱的顶点动画做基础起伏,再加法线贴图丰富细节。

2.3 边缘泡沫与海岸线:定义水域边界

这是风格化水体的“灵魂笔触”。一条干净利落、带有泡沫的岸边,能立刻让水体“立”在场景中,而不是漂浮在空中。

实现原理:边缘泡沫同样高度依赖深度信息。在岸边,即深度非常小(接近0)的区域,我们让水面呈现泡沫的颜色(通常是白色或米黄色)。

  1. 计算边缘因子:用saturate(1 - depth / foamRange)这样的公式。depth是计算出的水深,foamRange控制泡沫出现的范围。当深度小于foamRange时,因子大于0。
  2. 添加噪波:直接使用平滑的边缘会显得很假。我们需要引入噪波纹理来打破边缘的规则性。用一张噪声图(或使用程序化噪声)对边缘因子进行扰动。
  3. 模拟涌浪:更高级的效果是让泡沫有一种“涌上来又退下去”的动画。这可以通过将噪声图的UV加上一个随时间变化的偏移来实现,并且让偏移量在靠近岸边时更大。

在Shader Graph中的节点流:先通过深度计算出一个基础的边缘遮罩(Mask),然后用Noise节点采样,并通过Time节点驱动其UV偏移。将噪声与基础遮罩相乘或相加(取决于想要的效果),最后用一个Power节点来锐化或柔化泡沫的边缘形状。结果用于混合水体基础色和泡沫色。

避坑技巧:泡沫的颜色不要用纯白色(255,255,255),会显得很“扎眼”且没有体积感。尝试用(220,230,240)这样的淡蓝色白色,或者(245,240,230)这样的米白色,并赋予一点点非常微弱的蓝色或环境色影响,会自然得多。

2.4 高光与反射:捕捉那一抹亮光

风格化水面的高光通常比较“夸张”和“风格化”,可能是一个明亮的光斑,或者一条闪烁的光带,用以指示光源方向和增加视觉趣味。

1. 镜面高光(Specular):使用Blinn-Phong或更简单的计算模型。核心是计算视角方向与反射光方向的点积。在风格化中,我们经常对这个结果进行“硬切”处理。

  • 操作:计算出的高光强度通过一个Power节点(控制光斑大小和硬度)后,再用StepSmoothstep节点进行阈值化处理,得到一个边界清晰的高光区域,而不是真实渲染中那种柔和的衰减。
  • 动画:可以让高光强度随时间发生微弱脉动,或者根据波浪法线进行扰动,使其有闪烁感。

2. 平面反射(Planar Reflection):对于平静的湖面或水池,简单的屏幕空间反射(SSR)或立方体贴图反射可能不够,或者性能开销大。风格化项目常用一种取巧的“假反射”:采样一张渲染好的场景纹理(可能是上一帧的,也可能是一个专门的低分辨率渲染相机捕捉的),经过适当的扭曲(用法线贴图扰动UV)后叠加在水面上。这种方法可控性强,性能好,虽然物理上不准确,但视觉上足以“骗过”眼睛。

设计选择:如果你的游戏是俯视角或固定视角,可以精心调整一张静态的环境贴图作为反射源,效果既好又省性能。动态开放世界则需要更动态的方案,如使用反射探针(Reflection Probe)结合简单的UV扰动。

3. 在URP中使用Shader Graph完整实现

理论说再多,不如动手做一遍。下面我们以Unity URP管线为例,使用Shader Graph从零搭建一个包含上述所有元素的风格化水面着色器。我假设你已经创建了一个URP项目,并安装了Shader Graph包。

3.1 创建Shader Graph与基础设置

  1. 在Project窗口中右键 -> Create -> Shader -> Universal Render Pipeline -> Shader Graph。命名为“StylizedWater”。
  2. 双击打开。首先设置Graph的精度,对于移动端,可以考虑用Half精度以节省带宽,PC端可以用Float。在Graph Inspector面板中设置。
  3. 关键步骤:启用深度纹理。这是很多效果的基础。你需要确保:
    • 你的URP Asset(通常在Settings文件夹)中,Renderer列表下使用的Renderer Asset(如Universal Renderer Data)的配置里,勾选了Opaque Texture(它包含了深度信息)。
    • 在Shader Graph的Graph Settings中,勾选Camera Depth Texture选项。

3.2 构建基础网络:深度、颜色与泡沫

我们先从主颜色开始搭建。

  1. 获取深度与计算水深

    • 添加一个Scene Depth节点。它的默认输出是眼睛空间深度。我们需要将其转换为线性深度,或者直接与水面高度比较。
    • 更常用的方法是获取像素的世界位置Y值,与场景深度对应的世界位置Y值做差。这需要一点计算:
      • Scene Depth节点连接一个Linear Eye Depth节点(如需),然后使用Depth节点提供的Eye深度,通过View Direction(需要转换为世界空间)等节点可以重建世界位置,但过程较复杂。
      • 更实用的简化方法:对于平静的水面(网格是平面),我们可以用一个Position节点(空间设置为World),取其Y分量,作为水面高度。然后,用Scene Depth节点减去这个水面高度,就得到了近似的水深。虽然不完全精确,但对于风格化效果足够用,且性能极佳。
    • 公式节点示例:Depth = SceneDepth - WorldPos.y。将结果通过SaturateClamp限制在合理范围。
  2. 基于深度的颜色渐变

    • 创建一个Gradient属性,定义你的水色变化。例如,从浅青(#a8e6cf)到中蓝(#64b5f6)再到深蓝(#1a237e)。
    • 添加一个Sample Gradient节点。将上一步计算出的、经过归一化(除以最大可视深度)的Depth值,连接到Time端口(它在这里充当采样位置)。
    • 将这个颜色输出作为水体的Base Color
  3. 添加边缘泡沫

    • 创建一个Foam Color属性(浅白色)和Foam Range属性(Float,约0.05-0.2)。
    • 计算泡沫遮罩:FoamMask = 1 - saturate(Depth / FoamRange)。这样在深度为0时遮罩为1,深度大于FoamRange时遮罩为0。
    • 添加一个Noise节点(如Simple Noise),将其UV端口连接上水面世界位置的XZ坐标(可能需要乘以一个Tiling参数),并加上Time乘以Speed的偏移量。
    • Noise的输出与FoamMask相乘,用于打破边缘。可以用Power节点调整噪声的影响强度。
    • 最后,用这个处理后的遮罩,在Base ColorFoam Color之间进行线性插值(Lerp)。遮罩为1的地方显示泡沫色,为0的地方显示基础水色。

3.3 加入波浪法线与高光

  1. 法线贴图与流动

    • 创建两个Texture 2D属性,用于两张风格化的法线贴图。再创建两个Vector2属性作为它们的滚动速度(如(0.05, 0.03)和(-0.03, 0.04))。
    • 分别对两张法线贴图使用Sample Texture 2D节点。在采样前,对UV使用Tiling And Offset节点,其Offset输入连接Time乘以各自的Speed
    • 使用Blend Normal节点混合两张采样得到的法线。将混合后的法线连接到Master节点的Normal端口。
    • 深度控制法线强度:在深水区减弱法线扰动。可以创建一个Normal Intensity属性,然后与归一化的深度(反转:1-深度)进行相乘或插值,再应用到法线向量的强度上。这需要将法线从切线空间转换,处理后再转回去,或者直接对法线贴图的采样结果(RG通道)进行缩放。
  2. 风格化高光

    • 添加Specular ColorSpecular Power属性。
    • 使用Blinn-Phong光照模型需要视角方向和半角向量。Shader Graph提供了Specular节点(在PBR Master下是Specular,在Unlit Master下需自己计算)。
    • 更简单的风格化高光:直接计算视角方向与法线反射方向的点积。使用Reflection Vector节点和View Direction节点。
    • 对点积结果使用Power节点(输入Specular Power),得到一个锐利的高光核心。
    • 使用StepSmoothstep节点,设定一个阈值(如0.9),将高于阈值部分切出来作为高光区域。
    • 将这个高光遮罩与Specular Color相乘,然后加到最终的颜色输出上(注意是加法混合,Add)。

3.4 整合与主节点配置

将上面计算出的Base Color(已混合泡沫)、NormalSpecular输出分别连接到Master节点(通常是PBR MasterUnlit Master)的对应端口。

  • 使用PBR Master:更标准,可以接受场景光照。将颜色连接到Base Color,法线连接到Normal,高光颜色可以连接到Emission(来自发光)来模拟强烈的风格化高光,或者连接到Specular并调整平滑度。
  • 使用Unlit Master:完全自定义光照,更轻量,适合完全手绘风格或性能极端受限的情况。你需要自己计算所有颜色叠加。

最后的关键设置

  • 渲染队列(Queue):设置为Transparent(3000)或Geometry+1,以确保水面在大多数不透明物体之后渲染,正确处理透明和深度。
  • 混合模式(Blend):如果需要半透明效果(如水下模糊),使用SrcAlpha OneMinusSrcAlpha。对于大多数卡通水体,表面可以是不透明的(Opaque),边缘泡沫部分通过颜色混合实现透明感,所以也可以保持Opaque,通过Alpha Clip来处理边缘。
  • 深度写入(ZWrite):通常设置为On,确保深度缓冲正确,避免后续物体错误地渲染在水面之上。
  • 面剔除(Cull):设置为Back(剔除背面)或Off(双面渲染,如果你能看到水底)。

完成这些后,保存Shader Graph,创建一个材质球应用它,然后赋给一个平面网格,拖到场景里,你就能看到初步的效果了。接下来就是漫长的参数微调阶段。

4. 性能优化与进阶技巧

一个好看但跑不动的Shader是没有意义的。风格化Shader的优势之一就是性能可控。以下是一些优化和提升质量的技巧。

4.1 针对移动平台与WebGL的优化策略

  1. 精度选择:在Shader Graph的Graph Settings中,将Precision设置为Half。对于颜色和范围在0-1之间的参数,Half精度足够,且能显著提升移动端和WebGL的性能。
  2. 纹理压缩与尺寸:法线贴图和噪声图尽量使用压缩格式(如ASTC、ETC2),并控制尺寸(512x512或256x256通常足够)。避免使用超大纹理。
  3. 计算简化
    • 减少复杂的数学运算,如sin/cos。如果必须用,考虑使用纹理查找(Texture Lookup)来模拟简单的周期函数。
    • 将可以预先计算的值放在顶点着色器中计算,而不是片元着色器。例如,波浪的顶点位移。
    • 对于深度计算,如果水面是平坦的,使用前面提到的简化版世界高度差法,避免昂贵的重建世界位置计算。
  4. 减少纹理采样:尽可能复用纹理采样。例如,用一张纹理的R、G、B通道存储不同的噪声,或者用法线贴图的Alpha通道存储高度信息用于泡沫。
  5. LOD与剔除:对于远处的水面,可以使用更简化的Shader变体(通过Shader LOD实现),或者直接使用一个更简单的平面着色。

4.2 提升视觉质量的“黑科技”

  1. 水下扭曲(折射):模拟水面下物体看起来的扭曲效果。这可以通过抓取屏幕纹理(GrabPass或在URP中使用Scene Color节点),然后用法线贴图去扰动采样UV来实现。在Shader Graph中,使用Scene Color节点,并将其UV输入加上法线向量的XY分量乘以一个强度系数。注意,这个效果在移动端开销较大,慎用。
  2. 交互涟漪:让角色走入水中或物体落入水中时产生涟漪。这通常需要将交互信息(位置、强度)通过脚本传递到Shader中。在Shader中,计算每个像素到交互点的距离,根据距离和衰减函数,生成一个波浪位移或法线扰动,并随时间淡出。这需要一定的编码能力,将数据写入材质属性或全局Shader变量(如Shader.SetGlobalVector)。
  3. 焦散效果(风格化):真实的焦散模拟非常昂贵。风格化项目中,可以用一张流动的、高对比度的光斑纹理,投影到水底(通过世界XZ坐标采样),并随着水面法线轻微扰动,来模拟这种光影效果。
  4. 多级细节(LOD)融合:在岸边使用细节丰富的泡沫和波浪,在远离镜头的地方,逐渐淡出这些细节,过渡到平静的、颜色单一的水面。这可以通过相机距离来控制相关参数的强度。

4.3 材质参数化与美术控制

一个好的风格化水面Shader,应该将控制权最大限度地交给美术。这意味着所有重要的视觉参数都应该暴露为材质属性。

建议暴露的参数列表:

参数组参数名类型说明
颜色Deep Color/Shallow ColorColor或直接使用一个Gradient
Foam ColorColor泡沫颜色
Specular ColorColor高光颜色
波浪Wave Speed 1/2Vector2两张法线贴图的滚动速度
Wave Tiling 1/2Float法线贴图平铺度
Normal IntensityFloat法线整体强度
Wave Height/FrequencyFloat(如果用了顶点动画)
泡沫Foam RangeFloat泡沫出现的水深范围
Foam Noise TilingFloat泡沫噪声纹理大小
Foam Noise SpeedFloat泡沫噪声流动速度
Foam HardnessFloat泡沫边缘的软硬程度(Power)
高光Specular PowerFloat高光集中度
Specular ThresholdFloat高光显示阈值

将这些参数组织在材质面板上,美术就可以像调节Photoshop滑块一样,快速调出沙漠绿洲、寒冰湖泊、熔岩河流等不同氛围的水体,而无需程序员每次都修改Shader代码。

5. 常见问题排查与实战心得

即使按照步骤操作,你也可能会遇到各种“坑”。这里记录了一些我反复遇到的问题和解决方法。

5.1 水面闪烁或深度冲突(Z-fighting)

这是最常见的问题,表现为水面与地面或其他物体交界处出现闪烁的像素。

  • 原因:水面网格与地面网格在深度缓冲中的值过于接近,由于浮点数精度限制,GPU无法确定谁在前谁在后。
  • 解决方案
    1. 轻微抬高水面:在建模时,让水面网格比实际视觉位置稍微抬高一点点(例如0.001个单位)。这是最简单有效的方法。
    2. 调整渲染队列:确保水面在Transparent队列渲染,并且所有不透明物体(如地面)已经渲染完毕。
    3. 使用深度偏移(Depth Offset):在Shader Graph的Master节点设置中,或手写Shader中使用Offset指令。给水面一个微小的深度偏移,强制它“浮”在其他物体之上。但需谨慎使用,过度偏移会导致渲染顺序问题。

5.2 边缘泡沫在特定角度消失或异常

  • 原因:深度计算依赖于从相机到水底的交点。当视线与水面夹角非常小(掠射角)时,深度纹理采样可能失败或得到错误值。
  • 解决方案
    1. 钳制深度值:对计算出的深度进行最大最小值钳制(Clamp),避免出现极端负值或超大正值。
    2. 使用线性眼深度:确保使用的Scene Depth是线性化的,而非原始的硬件深度。在Shader Graph中,Scene Depth节点默认输出非线性深度,可以连接Linear Eye Depth节点转换。
    3. 添加视角衰减:计算视角方向与水面上法线的点积。当夹角过小时,逐步减弱甚至关闭泡沫效果。虽然物理上不正确,但能避免视觉瑕疵。

5.3 性能热点分析与定位

如果游戏运行时帧率下降,怀疑是水面Shader导致的:

  1. 使用Unity Profiler:在GPU模块下,查看最耗时的渲染函数。如果看到你的Shader名称排在前面,并且每个实例的耗时很高,就需要优化。
  2. 检查Draw Call:一个巨大的水面网格可能只是一个Draw Call,但如果水面被分割成很多小块,或者使用了大量重叠的透明效果,可能会增加Draw Call。
  3. 简化片元着色器:片元着色器(Fragment Shader)的计算量是性能的关键。回顾你的Shader Graph网络,检查是否有特别复杂的节点组合,尤其是循环、大量条件判断或高频率的噪声计算。尝试简化或寻找近似替代方案。
  4. 减少纹理采样:如前所述,合并纹理。使用Texture2D ArrayTexture Atlas来减少纹理切换。

5.4 与后期处理(Post Processing)的配合

风格化水面经常需要和屏幕空间后效配合,比如泛光(Bloom)来强化高光,或者颜色分级(Color Grading)来统一色调。

  • 泛光(Bloom):水面的高光区域(特别是风格化处理后的硬边高光)是触发泛光的理想来源。确保你的高光输出到了正确的通道(如Emission),并且在URP的Bloom后处理设置中,阈值(Threshold)设置得当,能捕捉到这些高光。
  • 环境光遮蔽(SSAO):屏幕空间环境光遮蔽可能会在水面与岸边交接处产生不正确的暗边。如果遇到此问题,可以考虑让水面Shader不参与SSAO计算(这通常需要修改或使用特定的Shader变体)。
  • 动态分辨率与FSR/DLSS:在适配这些技术时,要确保你的Shader中所有基于屏幕坐标(如用于折射的Screen Position)或屏幕纹理采样的计算,能正确应对渲染目标分辨率的变化。通常使用ComputeScreenPos等函数来处理会比直接使用UV更稳健。

打造一个令人满意的风格化水面,是一个不断迭代和调参的过程。没有一劳永逸的参数,最好的效果总是诞生于对项目视觉风格的深刻理解,以及美术与程序之间的紧密配合。从最基础的深度色开始,逐步叠加波浪、泡沫、高光,每次只调整一两个参数,观察变化,你就能逐渐掌控这片“数字水域”的每一个细节。记住,最终目标是服务于整体画面,而不是炫技。当你的水面能够完美地融入世界,不再引人注目却又不可或缺时,它就真正成功了。