Unity Shader Graph 水面效果制作:从波动、折射到菲涅尔效应

📅 2026/7/26 14:31:07
Unity Shader Graph 水面效果制作:从波动、折射到菲涅尔效应
1. 项目概述从零构建一个简单的水面Shader在Unity里做游戏尤其是涉及到自然场景水面效果几乎是绕不开的一环。一个灵动、真实的水面能瞬间提升场景的氛围感和沉浸感。很多刚接触Shader的新手一听到“水面Shader”就觉得头大觉得这玩意儿肯定涉及复杂的物理模拟和数学计算。其实不然一个视觉效果不错、性能友好的基础水面用Unity的Shader Graph完全可以轻松实现。这个项目我们就来动手做一个“简单的水面Shader Graph”。它不追求物理级的精确模拟而是聚焦于用最直观、最可控的节点组合出视觉上可信的水面核心特征波动、折射、高光反射和菲涅尔效应。通过这个案例你不仅能得到一个可以直接用在项目里的Shader更重要的是你能彻底理解这些视觉效果背后的图形学原理和实现逻辑以后再看到复杂的水面效果你也能一眼看穿它的“骨架”。这个Shader非常适合用于手机游戏、风格化游戏或者作为大型水体的远景简化版本。我们会从Shader Graph的基本布局讲起一步步连接节点解释每个参数的意义并分享我在实际项目中调整水面效果的实用心得和避坑指南。2. 核心视觉原理与Shader Graph设计思路在动手连线之前我们必须先想清楚一个让人感觉“像水”的表面到底有哪些视觉特征理解了这些我们的节点连线才会有方向而不是盲目地试错。2.1 水面四大核心视觉特征解析波动Waves这是水面动态感的灵魂。波动不是简单的上下起伏它通常由多种不同频率、不同振幅的波纹叠加而成形成一种有机的、非重复的复杂运动。在Shader中我们通常使用正弦Sine或噪声Noise函数来模拟这种波动。折射Refraction光从空气进入水中会发生偏折导致我们看到水下的物体位置发生偏移和扭曲。这是水面真实感的关键。在实时渲染中我们通常采用一种“欺骗”视觉的方法根据水面法线扰动屏幕背后物体的UV坐标来模拟这种扭曲效果。高光反射Specular Reflection平静的水面像镜子一样反射天空和周围环境有波纹的水面则会将高光打散成一片片闪烁的亮斑。这部分通常用基于法线和视角方向计算的高光模型如Blinn-Phong来实现。菲涅尔效应Fresnel Effect这是一个非常重要的光学现象。当我们的视线垂直于水面看向水底时更容易看到水下的折射内容当视线与水面夹角很小看向远处的水面时则更容易看到水面的反射内容。这个效应决定了水面何处更透明折射主导何处更像镜子反射主导。在Shader中我们用一个基于视角与法线夹角的函数来控制反射和折射的混合权重。2.2 基于Shader Graph的实现策略Shader Graph是一种基于节点的可视化着色器编辑工具它的核心思想是“数据流”。我们的目标就是构建一个处理流程将模型的顶点位置、UV、法线、视角方向等输入数据经过一系列计算节点最终输出到片元着色器的颜色上。我们的策略是模块化构建波动模块输入世界空间XZ坐标和时间通过噪声和正弦函数生成高度偏移并据此计算新的法线。折射模块使用波动模块生成的法线去扰动屏幕空间深度图或抓取纹理Grab Pass的UV模拟水下扭曲。反射模块采样环境立方体贴图Cubemap或平面反射探针Reflection Probe并结合高光计算。菲涅尔与混合模块计算菲涅尔系数用它作为权重将折射颜色、反射颜色以及一个基础水色进行混合。输出模块将最终颜色、法线、深度等数据输出到对应的Master Node端口。这个设计思路清晰地将复杂问题分解每个模块都可以独立调试非常符合Shader Graph的工作流。3. 创建与配置Shader Graph环境工欲善其事必先利其器。在开始连线之前确保你的Unity工程和Shader Graph环境已经准备就绪。3.1 工程准备与渲染管线选择首先你需要一个Unity项目建议使用2021 LTS或更新版本。Shader Graph并非独立存在它依赖于Unity的可编程渲染管线SRP。目前主要有两种选择通用渲染管线URP轻量、高效对移动平台和性能受限的项目非常友好。这是我们本案例的首选。高清渲染管线HDRP面向高端PC和主机提供电影级的画质但配置更复杂性能开销大。对于我们的简单水面URP完全足够。如果你新建项目时没有选择URP模板也可以在项目中通过Package Manager安装“Universal RP”包然后创建URP Asset并分配给Graphics Settings。3.2 创建第一个Shader Graph资源在Project窗口中右键选择Create - Shader - Universal Render Pipeline - Shader Graph。如果你使用的是HDRP则选择对应的HDRP选项。将其命名为“Water_Simple”。双击打开这个文件你会进入Shader Graph编辑器窗口。界面主要分为主图Master Stack位于右侧这是最终输出节点。URP中通常是Universal PBR或UnlitMaster Node。对于水面我们通常从Unlit开始因为它不参与引擎的标准光照计算我们完全自己控制颜色更直观。黑板Blackboard位于左侧用于定义和管理整个Shader的属性Properties这些属性会暴露在材质球Material的Inspector面板上供美术或策划调节。节点图区域中间最大的区域就是我们进行连线创作的地方。注意在开始连线前我强烈建议先将Master Node的Surface Type设置为TransparentBlend Mode设置为Alpha如SrcAlpha OneMinusSrcAlpha。因为水面是半透明的这能确保它正确地与背景混合。如果你希望水面有深度写入和测试防止透明物体排序错误可以再深入研究Depth Write和Depth Test的配置但作为入门我们先从简单的透明混合开始。4. 构建波动效果赋予水面生命波动是动态水面的基础。我们将创建一个可以控制频率、速度和幅度的波动系统。4.1 使用正弦函数创建基础波纹我们首先从最简单的正弦波开始。思路是将模型在世界XZ平面上的位置作为输入加上时间变量输入到正弦函数中输出一个在[-1, 1]之间周期变化的值作为顶点Y方向的偏移。获取顶点世界坐标添加一个Position节点将其Space设置为World。我们只需要X和Z分量来驱动波动所以后面连接一个Split节点将其分开。引入时间添加一个Time节点它提供了自游戏开始以来的时间值。构建波动UV将世界XZ坐标分别乘以一个系数我们将其定义为属性叫Wave Scale然后加上时间乘以另一个系数Wave Speed。这步操作相当于创建了一组随着空间和时间变化的UV坐标。公式可以理解为UV (WorldPos.xz * WaveScale) (Time * WaveSpeed)应用正弦函数将上一步得到的某个方向比如X方向的UV值输入到一个Sine节点中。Sine节点的输出范围是[-1, 1]。控制波动幅度将Sine的输出乘以一个属性Wave Amplitude得到最终的Y轴高度偏移量。影响顶点将这个高度偏移量与原始世界坐标的Y分量相加然后通过一个Combine节点与未变动的XZ分量重新组合成新的世界坐标。但是直接修改世界坐标并输出给Master Node是不常规的。更标准的做法是我们通过修改顶点法线来“欺骗”光照和反射计算让平面“看起来”在波动而不是真的移动顶点除非你需要精确的物理交互。对于简单水面修改法线通常就够了。4.2 叠加噪声实现自然波纹单一频率的正弦波看起来非常人工化。真实的水面波纹是多种频率叠加的。我们可以引入噪声Noise来打破这种规律性。添加梯度噪声在Blackboard上创建一个Sample Gradient Noise节点。噪声节点需要UV输入我们可以直接使用上一步处理过的世界XZ坐标乘以另一个较小的Noise Scale属性作为UV。噪声与正弦波混合将噪声的输出范围通常在[0,1]或[-1,1]与正弦波的输出通过一个Add或Multiply节点混合。Add会让波形更复杂Multiply会让噪声成为正弦波的振幅调制器产生更自然的变化。你可以尝试两种方式看看哪种效果更符合你的预期。计算波动后的法线这才是关键步骤。我们波动的高度最终会体现在法线上。我们可以利用Shader Graph自带的DDX和DDY节点来近似计算高度场即我们计算出的Y偏移在屏幕空间X和Y方向的变化率然后用这两个变化率来构造一个切向量和副切向量进而通过叉积得到新的法线向量。更简单的方法是使用一个Normal From Height节点如果Shader Graph版本支持或者自己用DDX和DDY构建。手动计算法线原理float3 worldNormal normalize(float3(-ddy(height), 1.0, -ddx(height)));这里ddx和ddy是获取相邻像素的高度差。这个公式基于一个近似高度场在X方向的变化主要影响法线的Z分量在Z方向的变化主要影响法线的X分量。实操心得波动参数调节是个艺术活。Wave Scale控制波纹的密度值越小波纹越大越稀疏值越大波纹越小越密集。Wave Speed不宜过快否则会显得不自然。Wave Amplitude决定了波纹的陡峭程度。我通常的做法是创建两到三组不同频率和速度的正弦波用不同的Scale和Speed然后叠加在一起这样得到的水面动态会丰富很多。噪声的Scale通常要设得比主波纹的Scale大很多让它产生一些低频的、缓慢的基底起伏。5. 模拟折射效果让水面“透”过去折射效果能让玩家感觉到水的存在和深度。在实时渲染中我们常用一种叫做“屏幕空间折射”的技巧。5.1 理解屏幕空间折射原理我们并不真正去计算光线的物理偏折。思路是假设水面是一层扭曲的玻璃我们透过它去看它后面的东西即场景中位于水面之后的像素。在Shader中我们可以获取到当前像素所对应的屏幕后缓冲区的颜色即不绘制水面时该位置的场景颜色然后根据水面法线对这个颜色的采样坐标进行一个微小的偏移从而产生扭曲感。在URP Shader Graph中我们可以通过Scene Color节点来获取屏幕后缓冲区的颜色。这个节点需要屏幕空间UV坐标作为输入。5.2 在Shader Graph中实现折射扭曲获取屏幕UV添加一个Screen Position节点将其模式设置为Default这会输出一个范围在[0,1]之间的屏幕空间UV坐标。用法线扰动UV使用我们之前计算出的波动后法线。由于法线是三维向量而UV是二维的我们通常取法线的X和Z分量或者根据效果需要调整来对屏幕UV进行偏移。创建一个Normalize节点确保法线长度为单位1。使用一个Split节点分离出法线的X和Z分量。将X和Z分量分别乘以一个很小的系数如0.01到0.05这个系数我们定义为Refraction Strength属性。这个值控制扭曲的强度太大看起来会像哈哈镜太小则没有效果。将计算出的偏移量float2(offsetX, offsetZ)与屏幕UV相加得到扰动后的UV。采样场景颜色将扰动后的UV输入到Scene Color节点。Scene Color节点会返回在扰动后的屏幕位置处场景本身的颜色。这就是我们模拟的折射颜色。处理边缘问题这里有一个常见的坑。当扰动后的UV坐标超出屏幕范围比如小于0或大于1时Scene Color可能会采样到黑边或者无效数据。一个简单的处理方法是使用Clamp节点将UV限制在[0,1]范围内但这在屏幕边缘会产生不自然的“吸附”效果。更好的做法是在Blackboard上定义一个Refraction Color属性当采样越界时用这个颜色进行混合或替代模拟深水区的颜色。注意事项Scene Color节点的使用是有性能开销的因为它通常意味着需要一次额外的全屏纹理采样对应着Grab Pass。在移动平台上尤其是低端设备上需要谨慎使用。对于性能敏感的项目可以考虑用一张预设的、代表水下环境的静态纹理来替代虽然动态感会下降但性能更好。另外折射效果的真实感高度依赖于场景本身的复杂度和颜色在空旷或纯色背景下折射效果可能不明显。6. 实现反射与高光捕捉天光与环境水面不仅透明还会反射周围的世界。我们将结合环境反射和镜面高光来模拟这一点。6.1 利用反射探针捕捉环境Unity提供了反射探针Reflection Probe来捕获场景的环境信息并生成一个立方体贴图Cubemap。我们的水面Shader可以采样这个贴图来获得环境反射颜色。采样环境立方体贴图在Shader Graph中添加一个Sample Cubemap节点。你需要将一个Cubemap资源拖到它的Cube输入端口。更动态的做法是使用Scene Sampling类别下的Reflection Probe节点它会自动使用当前影响水面的反射探针数据。计算反射方向反射方向需要根据视角方向和法线来计算。Shader Graph提供了Reflection Vector节点它需要View Direction视角方向和Normal法线作为输入。View Direction节点添加一个View Direction节点Space设置为World。Normal使用我们之前计算出的波动后法线。连接并采样将计算出的反射向量输入到Sample Cubemap或Reflection Probe节点的Vector输入端口即可得到该方向的反射颜色。6.2 添加镜面高光Specular细节即使有环境反射水面在直接光源如太阳照射下还会产生明亮的、集中的高光点。我们可以用Blinn-Phong模型来模拟。计算半角向量Blinn-Phong模型的关键是半角向量Half Vector即视角方向V和光源方向L的中间向量。H normalize(V L)。在Shader Graph中我们可以用Add和Normalize节点来计算。获取光源方向添加一个Main Light Direction节点在URP中它可以提供场景主光源通常是方向光的世界空间方向。计算高光强度高光强度 pow(max(0, dot(N, H)), Gloss)。这里N是法线dot(N, H)是法线与半角向量的点积Gloss是高光指数控制高光的集中程度值越大高光点越小越亮。我们可以用Power节点来实现pow运算。合成高光颜色将计算出的高光强度乘以一个Specular Color属性通常设为白色或光源颜色再乘以一个Specular Intensity属性来控制强度。实操心得反射探针的更新频率和分辨率对效果和性能影响很大。对于静态场景使用烘焙Baked的反射探针即可。对于动态场景可能需要设置为实时Realtime更新但要注意性能。高光部分Gloss值我一般设置在100到500之间具体看水面的粗糙程度。平静的湖面可以用更高的值如300以上而有风浪的海面则用较低的值如100左右。另外不要忘记将高光颜色与之前采样的环境反射颜色相加Add因为高光是额外的亮度贡献。7. 融合与调色菲涅尔效应与最终合成现在我们有折射颜色、反射颜色含高光和一个基础水色。如何将它们自然地混合在一起这就需要菲涅尔效应来控制。7.1 菲涅尔效应的原理与计算菲涅尔效应描述了光线在介质交界处反射比例随入射角变化的规律。在Shader中我们用一个简化的近似公式来计算菲涅尔系数FF F0 (1 - F0) * pow(1 - dot(V, N), 5)其中F0是法线入射垂直看向水面时的基础反射率。对于水这个值很小约0.02。V是视角方向。N是表面法线。dot(V, N)是视角与法线的点积。当垂直看向水面时V与N平行点积接近1(1-dot(V,N))接近0所以F接近F0反射弱折射强。当掠射看向水面时V与N垂直点积接近0(1-dot(V,N))接近1所以F接近1反射强折射弱。在Shader Graph中我们可以用Fresnel Effect节点来方便地计算这个值。它通常有Power对应公式中的指数5和Bias可以理解为F0两个参数。7.2 最终颜色混合与输出计算菲涅尔系数添加Fresnel Effect节点输入Normal波动法线和View Direction。调整Power和Bias参数。对于水Bias可以设得很低如0.02Power在3到8之间调节控制菲涅尔过渡的锐利程度。线性插值Lerp混合菲涅尔系数F是一个在[0,1]之间的值完美的混合权重。第一次Lerp折射 vs 基础色用F作为权重在折射颜色和基础水色之间插值。当F小垂直看时折射色权重大F大掠射看时基础水色权重大。这模拟了垂直看水更透明掠射看水更不透明显出本身颜色。第二次Lerp加入反射将上一步的结果与反射颜色含高光再进行一次插值。但这次我们可以直接用F作为反射颜色的叠加系数。因为菲涅尔效应直接决定了反射的强度。更常见的做法是最终颜色 折射与基础色的混合结果 反射颜色 * F。这里用加法因为反射是额外的亮度贡献。透明度控制水面的透明度也应该随视角变化。通常透明度可以与菲涅尔系数F反向关联或者用一个独立的属性控制。将计算好的透明度值连接到Master Node的Alpha端口。输出法线将我们计算出的波动后法线连接到Master Node的Normal端口如果你的Master Node是PBR类型且有Normal输入。这会影响引擎后续的光照计算让水面与其他物体的交互光照更准确。注意事项颜色混合是艺术性很强的一步。基础水色通常是一个偏蓝或偏绿的深色。直接使用Scene Color作为折射色有时会显得太“干”缺乏水下感。一个技巧是将Scene Color与一个淡淡的水下色调如蓝绿色进行Multiply混合。反射颜色也常常需要乘以一个灰度系数来降低其饱和度使其更融入环境。多参考真实照片或优秀的游戏截图进行调色。8. 参数调节心得与性能优化指南Shader写好了但看起来可能还是有点“假”。别急最后的调节阶段才是点睛之笔。这里分享一些我的参数调节清单和性能考量。8.1 关键参数调节清单与视觉目标把Blackboard上暴露的属性整理一下这里是一个典型的调节清单和它们影响的视觉目标属性组属性名典型值/范围视觉影响调节技巧波动Wave Scale0.5 - 5波纹大小。值越小单个波纹越大。远景用大波纹小值近景用小波纹大值。Wave Speed0.1 - 1波纹移动速度。与场景风格匹配。卡通风格可稍快写实风格宜慢。Wave Amplitude0.01 - 0.2波纹高度/陡峭度。过大看起来会像沸腾的水而不是自然波动。Noise Scale10 - 50叠加噪声的尺度。值越大噪声特征越大。用于打破正弦波的规律性产生随机基底起伏。折射Refraction Strength0.005 - 0.03水下景象的扭曲程度。非常敏感从0.01开始微调。透明水体用小值浑浊水体可稍大。Refraction Color深蓝/深绿当折射采样失效如边缘或用于染色。用于统一水下色调增加沉浸感。反射/高光Specular Intensity0.5 - 2高光亮度。晴天正午可调高阴天或夜晚调低。Gloss100 - 500高光集中度。平静水面 300 波涛水面 150。菲涅尔Fresnel Bias (F0)0.01 - 0.05垂直视角下的基础反射率。水约0.02 越像金属的材料此值越高。Fresnel Power3 - 8菲涅尔效应过渡的锐利度。值越大反射到折射的过渡越突然。颜色Base Color中蓝/湖绿水体的基础颜色掠射角时最明显。决定水体的整体色调。Opacity0.6 - 0.9整体透明度。可与菲涅尔关联实现边缘更透明。调节时遵循“从大到小”的原则先调波动让动态感觉对再调菲涅尔和基础色确定大关系最后微调折射强度和反射高光增加细节。8.2 针对移动平台与性能的优化策略一个好看的Shader如果跑不动等于零。对于水面这种全屏或大范围效果性能尤为重要。简化计算波动计算检查你的波动公式。如果用了多组如3组正弦波叠加可以考虑在移动平台减少到1-2组。噪声计算尤其是复杂的Perlin噪声开销较大可以尝试用更简单的Simple Noise或Gradient Noise甚至用一张低频率的噪声贴图Texture配合UV动画来替代实时噪声计算。反射采样避免使用实时更新的反射探针。尽量使用烘焙Baked的立方体贴图。如果场景是静态的一个全局的烘焙探针就足够了。折射采样Scene ColorGrab Pass是性能大户。如果不需要精确的动态折射可以考虑用一张模糊的、代表水下颜色的静态纹理来代替或者只对靠近摄像头的部分水面启用折射。精度优化在Shader Graph的节点设置或Master Node的Precision选项中将精度从Float改为Half。对于颜色和大部分计算Half精度16位浮点数在移动GPU上更快且视觉质量损失通常可接受。渲染状态优化深度写入ZWrite对于透明水面通常设置为Off但这可能导致透明排序问题水面与水下物体交错闪烁。一个折中方案是将不透明物体的渲染队列Queue设为Geometry将水面的渲染队列设为Transparent值更大并确保水面材质在所有不透明物体之后渲染。对于复杂情况可能需要更细致的深度测试ZTest设置。渲染队列Queue确保你的水面Shader在正确的队列中。透明物体Transparent应在所有不透明物体Geometry之后渲染。LOD细节层次为水面材质制作多个不同复杂度的Shader变体或使用Shader的LOD功能。当摄像机远离时自动切换到更简单的版本例如去掉折射、简化波动计算。调试性能可以使用Unity的Frame Debugger和Profiler重点关注SetPass Calls和GPU耗时。确保你的水面Shader不会成为瓶颈。9. 常见问题排查与进阶技巧即使按照步骤操作你也可能会遇到一些奇怪的问题。这里记录了几个我踩过的坑和对应的解决方法。9.1 常见渲染问题速查表问题现象可能原因解决方案水面全黑或不显示Master Node的Surface Type未设置为Transparent或Blend Mode错误。检查Master Node设置确保为Transparent和正确的混合模式如Alpha。折射效果扭曲方向不对或过强用于扰动UV的法线分量取错或Refraction Strength值太大。检查法线向量的哪个分量对应屏幕UV的哪个方向。通常法线X扰动UV.x法线Z扰动UV.y。将强度值调小一个数量级试试。水面边缘有硬边或黑边折射采样UV越界或透明混合与深度测试冲突。对UV进行Clamp或定义边缘颜色。调整材质的Render Queue和ZWrite/ZTest模式。反射内容错乱或为粉色反射探针未正确设置或未烘焙Sample Cubemap节点未连接有效的Cubemap纹理。在场景中放置反射探针Reflection Probe并确保其类型Baked/Realtime正确且已烘焙Bake。检查Shader中Cubemap资源引用。高光点位置奇怪或不随视角移动计算高光时使用的光源方向或视角方向空间不一致。确保所有向量法线、视角、光源方向都在同一坐标系下通常是世界空间World Space进行计算。水面在移动设备上帧率骤降Shader计算过于复杂特别是使用了实时噪声、多重Scene Color采样或实时反射探针。按照第8.2节的优化策略进行简化。使用Shader LOD在移动端关闭或简化折射和复杂波动。9.2 从“简单”到“出色”的进阶技巧掌握了基础之后你可以尝试加入以下效果让水面质感更上一层楼边缘泡沫在水面与岸边或其他物体交界处通常会有白色的泡沫。可以通过一张泡沫噪声贴图根据深度差水面与水下场景的深度差来混合实现。使用Depth节点获取场景深度与水面自身的深度进行比较在差值大的地方即浅水或岸边显示泡沫贴图。焦散Caustics阳光透过波动的水面在水底投射出动态的光斑这就是焦散。可以用一张动态的、扭曲的焦散贴图投影到水下的物体表面上。实现起来需要额外的Pass或渲染到纹理Render Texture的技术。深度色散靠近岸边的浅水区域颜色更浅更透明深处的海水颜色更深。可以通过采样场景深度用Smoothstep函数根据深度值来混合两种不同的水色。法线贴图细节除了程序化生成的法线还可以叠加一张细节法线贴图Detail Normal Map用于模拟水面微小的涟漪或细节纹理让近处观察时细节更丰富。实现这些效果需要更深入的Shader知识和更多的渲染指令。我的建议是先把本文这个基础水面的每一个节点、每一个参数都彻底弄懂做到能随心所欲地调出你想要的感觉。之后再以这个Shader为基底像搭积木一样把边缘泡沫、深度色散这些功能一个个作为独立的模块加进去每加一个都充分测试和优化。这样你就能逐步构建出属于自己的、适用于特定项目需求的强大水面Shader了。