Unity URP瀑布Shader实战:从Shader Graph到性能优化的完整实现

Unity URP瀑布Shader实战:从Shader Graph到性能优化的完整实现 1. 项目概述为什么要在URP里手搓一个瀑布Shader做游戏场景尤其是自然风光瀑布几乎是绕不开的风景线。无论是潺潺溪流边的小落差还是悬崖峭壁上的磅礴水幕一个生动的瀑布能瞬间提升场景的氛围感和沉浸感。市面上有很多现成的瀑布粒子特效包效果华丽但往往伴随着高昂的性能开销和千篇一律的视觉感受。对于追求独特美术风格、或者对性能有严苛要求的项目比如移动端、开放大世界自己动手用Shader实现一个可控、高效、风格化的瀑布效果就成了一个非常实际且有趣的技术挑战。这次我们不依赖复杂的粒子系统堆叠而是聚焦于Unity的Universal Render Pipeline用Shader Graph和少量代码从零构建一个可深度定制的瀑布着色器。这个方案的核心优势在于它将瀑布的视觉表现“固化”在材质和模型上渲染开销稳定与场景融合度更高并且能通过参数实时调整水流形态、颜色、泡沫细节实现从写实到卡通的各种风格化表达。你会发现理解了背后的原理后制作一个瀑布本质上是在“欺骗”眼睛模拟水体的运动、折射、反射和泡沫消散的物理过程。2. 核心思路拆解瀑布的视觉魔法是如何炼成的一个看起来真实的瀑布可以分解为几个关键的视觉层。我们的Shader将逐一模拟这些层次。2.1 视觉层次分解第一层是基础水流。这是瀑布的主体我们需要让水面有流动感。最简单有效的方法就是使用一张或两张噪声纹理通过UV动画让纹理坐标随时间移动来模拟水波的流动。在URP Shader Graph中我们可以用Time节点驱动Tiling And Offset节点实现UV的平移。第二层是法线与折射。平静的水面像镜子流动的水面则会让透过它看到的背景景物扭曲。这种扭曲感可以通过法线贴图来实现。我们用另一套UV动画采样一张水波法线贴图将其法线信息输入到URP的Normal通道并影响场景颜色的折射Refraction。在Shader Graph中可以使用Scene Color节点获取背景像素然后用Normal Distortion节点对其进行偏移。第三层是泡沫与浪花。这是瀑布视觉的灵魂所在尤其是水流撞击底部岩石时产生的白色泡沫。我们需要在瀑布的特定区域如底部、与岩石交界处叠加泡沫纹理。关键是如何智能地确定这些区域。一个经典且高效的方法是基于顶点法线方向进行判断。在瀑布模型上水流垂直向下的面其顶点法线通常是水平方向的而在瀑布底部水花飞溅的区域我们可以将模型面片的法线调整为向上。这样在Shader中我们可以检查像素点的法线是否接近世界空间“向上”即(0,1,0)方向如果是则混合进泡沫纹理。第四层是边缘与深度。瀑布的边缘不应该像纸片一样单薄。我们需要根据深度信息让瀑布的边缘产生半透明的淡出效果模拟水雾。同时不同深度的水体颜色也应该有变化通常越深颜色越暗、越饱和。这可以通过与场景深度图进行交互来实现。2.2 技术方案选型Shader Graph vs HLSL在URP中我们有两种主要方式编写Shader可视化的Shader Graph和手写HLSL代码。对于这个瀑布效果我强烈推荐使用Shader Graph为主辅以少量Custom Function节点的方案。为什么选择Shader Graph直观高效流动的UV、纹理混合、法线计算等操作可以通过连线可视化完成迭代速度快美术同学也能参与调整。URP兼容性好Shader Graph天生与URP管线集成自动处理了光照模型如PBR或Simple Lit、渲染路径前向渲染和SRP Batcher兼容性减少底层错误。参数暴露方便任何中间计算结果都可以轻松地暴露为材质参数[MainTexture],[Normal],[HDR]等在Inspector面板中实时调节。需要Custom Function的情况 当遇到一些Shader Graph没有内置的复杂计算时比如需要根据模型顶点法线在世界空间中的方向来生成一个遮罩Mask或者进行一些自定义的噪声算法混合我们可以写一小段HLSL代码封装成Custom Function节点接入图中。这保持了主体逻辑的清晰又具备了代码的灵活性。我们的瀑布Shader将基于URP的Lit或UnlitMaster Node构建。如果不需要复杂的动态光影瀑布通常作为自发光或受环境光影响为主使用UnlitMaster Node性能更佳所有颜色由我们自己完全控制。3. 核心模块详解与Shader Graph构建接下来我们进入Shader Graph编辑器一步步搭建节点网络。假设我们已经创建了一个新的Unlit Graph并命名为Waterfall_URP。3.1 基础流动与颜色层首先解决水流的动态。我们需要两种纹理采样来制造层次感避免流动过于规律和虚假。创建属性在Blackboard中创建两个Texture2D属性命名为BaseWater_Ripples和BaseWater_Noise分别用于基础波纹和细节噪声。再创建两个Color属性ShallowColor浅水色和DeepColor深水色。UV动画将UV节点分别连接到两个Tiling And Offset节点。Offset的输入由Time节点的Time输出乘以一个可调节的Vector2速度参数如FlowSpeed1和FlowSpeed2来控制。这两组速度最好不同且非倍数关系例如(0.05, 0.1)和(0.07, -0.03)这样混合后的流动会更自然。纹理采样与混合用动画后的UV分别对两张纹理进行采样。然后将两个采样结果用Add或Multiply节点混合。这里我常用Add再乘以一个小于1的系数因为水的反光和高光叠加往往是加性的。深度颜色混合水流颜色不应是单一的。我们模拟水深变化。添加一个Position节点设置为World空间取其Y轴分量高度。通过Remap节点将其映射到0-1的范围作为Lerp线性插值的因子在ShallowColor和DeepColor之间进行插值。最后将这个颜色与上一步的纹理混合结果相乘。实操心得流动速度不宜过快尤其是用于背景或中景的瀑布。过快的UV动画会给人一种“电视雪花”或“故障”感而非水流。建议将速度参数默认值设得很小在场景中边预览边微调。3.2 法线生成与折射模拟即使是不透明/半透明的瀑布微妙的折射感也能极大增强体积感。法线纹理采样创建NormalMap纹理属性。使用另一套独立的UV动画速度与基础层不同对其进行采样。使用Normal Unpack节点解包出法线向量。扭曲场景颜色这是实现折射的关键。在Shader Graph中找到Scene Color节点。它默认需要屏幕UV坐标。我们将默认的Screen Position节点的XY分量除以W分量得到归一化的屏幕UV。然后将上一步解包的法线向量的XY分量或经过缩放的作为偏移量Offset加到这个屏幕UV上。用扭曲后的UV去采样Scene Color。混合将扭曲后的场景颜色与基础颜色层进行混合。混合模式可以使用Lerp用一个名为Refraction Strength的Float属性0-1作为混合系数。系数为0时完全显示基础色为1时完全显示折射色。通常设置为0.1-0.3之间效果比较自然。注意事项Scene Color节点在URP中属于“抓屏”操作在某些硬件或渲染设置下可能有额外开销或限制如不在透明队列后渲染可能抓不到正确背景。对于移动平台如果性能吃紧可以省略此步骤专注于泡沫和流动效果视觉损失相对可接受。3.3 智能泡沫区域生成这是瀑布效果的画龙点睛之笔。我们将实现基于法线的泡沫遮罩。世界空间法线获取添加Normal Vector节点将Space设置为World。这样我们得到的是顶点法线转换到世界空间后的方向。向上方向检测添加Vector3常量节点值为(0, 1, 0)即世界空间“上”方向。使用Dot Product点积节点计算世界法线与向上方向的点积。点积的几何意义是两者夹角的余弦值。当法线完全向上时点积为1完全水平时为0完全向下时为-1。生成遮罩我们的目标是让法线朝上的区域比如水花溅起的平面产生泡沫。因此将点积结果通过一个Saturate节点钳制到0-1然后使用一个Power节点进行幂次方计算例如0.5次方即开根号。幂运算可以让遮罩的过渡更柔和或更锐利。这个结果就是我们需要的Foam Mask。泡沫纹理与动画创建Foam Texture属性。同样为其制作UV动画但速度应该比水流更快、更随机模拟泡沫的涌动。用Foam Mask去控制泡沫纹理采样的显示强度通常使用Multiply节点。颜色与叠加泡沫通常是亮白色或带一点环境色的。将泡沫纹理采样结果与一个HDR Color属性可命名为Foam Color相乘提高其亮度。最后使用Add节点将泡沫颜色叠加到之前已经混合了折射效果的基础颜色上。为了让泡沫更突出可以基于Foam Mask对最终输出的Alpha值也进行微调让泡沫区域更不透明一些。3.4 边缘雾化与深度融合为了让瀑布更好地融入场景避免生硬的边界我们需要处理边缘。深度淡出添加Scene Depth节点和Screen Position节点。将屏幕位置用于采样场景深度得到背景像素的深度值。同时通过Position节点World和矩阵计算也能得到当前瀑布像素的深度。两者相减得到深度差。当瀑布像素与背景非常接近深度差很小时它可能在边缘。我们可以用这个深度差来控制边缘的透明度。使用Smoothstep函数设定一个深度差阈值范围在此范围内将Alpha值从1渐变到0。顶点偏移模拟体积纯片元着色器处理的瀑布始终是一个平面。我们可以通过顶点着色器让瀑布网格的顶点沿法线方向轻微随机偏移模拟水体的不规则表面和体积感。这需要在Shader Graph中创建一个Custom Function节点编写一小段HLSL代码对Position节点进行偏移。偏移量可以用顶点世界坐标的XZ值作为输入通过sin、cos和时间函数生成简单的波动。// 在Custom Function中函数体大致如下 void VertexOffset_float(float3 Position, float3 Normal, float Speed, float Strength, out float3 OutPosition) { float3 worldPos Position; // 使用世界坐标和时间的简单噪声模拟 float wave sin(worldPos.x * 0.5 _Time.y * Speed) * cos(worldPos.z * 0.3 _Time.y * Speed * 0.7); OutPosition Position Normal * wave * Strength; }将Position和NormalObject Space输入输出连接到Master Node的Vertex Position的Offset端口。注意强度Strength要调得非常小避免模型变形过大。4. 材质与模型准备实战Shader写好了还需要正确的模型和材质配置来配合。4.1 瀑布模型制作要点瀑布的模型不需要高模一个简单的面片Plane或由多个面片组成的带状网格即可。但建模时有几个关键点拓扑与流向网格的拓扑布线最好能顺着预设的水流方向。这对于后续顶点动画如果有和泡沫遮罩的生成有好处。底部扩展在瀑布撞击地面的区域将网格水平向外扩展出一圈并将这一圈顶点的法线朝上。这是我们之前Shader中Foam Mask能够正确生效的几何基础。你可以想象这是水花溅起形成的“水潭”表面。多层叠加为了增加丰富度可以使用2-3层透明度略有不同的相同Shader材质赋予错位摆放的相似模型并赋予略微不同的流动速度参数可以模拟出瀑布水幕的层次感和厚度。4.2 材质参数调节心法将Shader Graph保存并生成Shader文件然后创建一个新材质球使用它。面对一大堆参数按顺序调节先调颜色关闭泡沫和折射先调节ShallowColor和DeepColor找到一个符合场景光照的基色。通常深水区偏蓝绿浅水区偏青白。再调流动调节两张水流纹理的Tiling平铺和FlowSpeed。Tiling值大则纹理细节多水流显得细碎值小则水流图案大显得平静。让两层速度错开形成有机的扰动。然后加泡沫打开泡沫调节Foam Texture的平铺和速度应该比水流快。调节Foam Color通常给一个偏冷的亮白色如HDR颜色强度1.5-2.5。最关键的是观察泡沫是否出现在模型法线朝上的区域即你建模时扩展的底部。可以通过临时将Foam Mask直接输出为颜色来调试这个遮罩的范围。最后微调融合打开折射将Refraction Strength调到一个较低的值0.1-0.2。调节边缘深度淡出的阈值让瀑布的边缘与山体岩石自然融合不生硬。渲染设置在材质的Inspector面板确保Surface Type设置为TransparentBlending Mode选择Alpha或Additive后者用于特别明亮、发光的水体。Render Face设置为Both双面渲染。5. 性能优化与常见问题排查一个效果再好如果性能堪忧也无法上线。以下是针对移动端或高性能场景的优化建议和常见坑点。5.1 性能优化清单纹理压缩与尺寸所有使用的纹理水流、噪声、法线、泡沫应使用合适的压缩格式如ASTC尺寸不宜过大通常512x512或1024x1024足够。法线贴图尤为重要。简化计算如果不需要折射果断移除Scene Color节点及相关计算。这是显著的性能节省点。减少纹理采样评估是否真的需要两张独立的水流纹理。有时一张高质量噪声纹理配合两套UV动画也能达到不错效果。慎用顶点偏移顶点着色器中的复杂计算尤其是三角函数sin/cos对顶点数多的模型开销大。如果瀑布模型面数少几百个三角形以内可以保留如果面数多或性能瓶颈在CPU可以考虑关闭此功能。Shader变体管理Shader Graph会为每个启用的功能分支生成变体。在材质中保持参数稳定避免运行时频繁修改启用/禁用功能以减少变体数量。对于确定不用的功能最好在Shader Graph中直接删除相关分支而不是仅仅将参数设为0。批处理确保使用相同的材质实例或材质属性块MaterialPropertyBlock来渲染多个瀑布模型以促进GPU Instancing或SRP Batcher合批减少Draw Call。5.2 常见问题与解决方案实录问题1瀑布看起来像一张平移的贴图纸非常假。排查检查UV动画速度是否过快或过于规律。检查是否只使用了一张纹理缺乏多层次的细节混合。解决使用至少两层不同速度、不同缩放比例的噪声纹理进行混合。在纹理采样后加入基于世界坐标的顶点颜色或第二套UV的微扰动打破均匀性。问题2泡沫没有出现在预期的底部或者到处都是。排查首先在Shader Graph中将Foam Mask直接连接到Base Color查看遮罩的视觉化效果。它应该只在模型法线朝上的部分显示为白色。解决确认瀑布模型的法线方向是否正确。在3D建模软件中检查并修正底部面片的法线使其朝上。在Shader中调整点积计算后的Saturate和Power参数控制遮罩的阈值和过渡柔和度。问题3瀑布边缘有锯齿或闪烁。排查这可能是深度测试Z-fighting或透明排序问题。解决稍微增大瀑布模型与背后山体岩石之间的间隙。在材质中可以尝试轻微修改Render Queue的值调整渲染顺序。对于透明物体正确的排序至关重要。问题4在移动设备上帧率下降明显。排查使用Unity Profiler或Frame Debugger工具分析渲染阶段耗时。重点查看是否因使用了Scene Color抓屏导致昂贵的全屏纹理读取。解决如前所述移除折射效果。检查纹理尺寸和压缩格式。将瀑布的Shader切换为更简单的Unlit版本并关闭所有非核心功能如顶点偏移。问题5瀑布在特定角度或光照下变黑。排查如果使用了LitMaster Node检查光照设置。可能是法线贴图采样或转换错误导致表面法线指向错误方向使得光照计算异常。解决确保法线贴图纹理类型设置为Normal map。在Shader Graph中检查Normal Unpack节点是否正确连接。如果问题依旧尝试暂时断开法线贴图输入使用默认法线看是否是贴图本身的问题。实现一个令人信服的瀑布效果是理解Shader如何模拟自然现象的优秀练习。它涉及UV动画、法线映射、屏幕空间效果、基于几何信息的遮罩生成等多个核心技巧。通过Shader Graph我们可以将这些技巧模块化、可视化地组合起来并快速迭代出符合项目艺术风格的结果。记住所有参数都没有绝对的最优值最终效果需要在目标平台和场景光照下进行反复微调。当你看到自己亲手创造的虚拟水流奔腾而下时那种成就感正是图形编程的魅力所在。