
1. 项目概述为什么一张法线贴图就能“搅动”水面如果你刚接触Unity的ShaderGraph看到“动态水面”这个词可能会下意识地想到复杂的物理模拟、正弦波叠加、或者一大堆看不懂的数学节点。但今天我想分享一个能让你快速建立信心、看到即时效果的方法仅用一张法线贴图在5分钟内构建一个视觉上足够“唬人”的动态水面。这个方法的核心思路是“视觉欺骗”。我们并不去真实地模拟水的物理运动而是通过ShaderGraph巧妙地操控一张记录着水面凹凸信息的法线贴图让它动起来从而在视觉上模拟出水波荡漾、光线折射的效果。这就像在平静的湖面上投下一颗石子涟漪的扩散是复杂的但我们只需要一张表现涟漪纹理的图片让它移动和变化观众的大脑就会自动补全“这是一片动态的水”这个信息。对于游戏中的远景水面、魔法效果、或者性能受限的移动平台这种方案高效且实用。本教程将带你从零开始在Unity URP通用渲染管线环境下使用ShaderGraph完成这个效果。你会学到如何连接节点、理解每个参数的意义并最终获得一个可直接用于你项目的、带完整节点图的水面Shader。无论你是Shader新手想迈出第一步还是经验丰富的开发者寻找一个快速原型方案这篇内容都能给你带来直接的帮助。2. 核心原理拆解法线贴图与动态的奥秘在深入节点图之前我们必须先搞清楚两个核心概念法线贴图是什么以及我们如何让它“动”起来。2.1 法线贴图存储“朝向”的纹理法线贴图Normal Map是一张特殊的纹理它的每个像素颜色RGB并不代表视觉颜色而是编码了一个三维向量即该像素点所对应的表面法线方向。常规理解一个平面的法线是垂直向上的0, 1, 0。但在微观层面水面有波纹、石头有凹凸这些细节的法线方向各不相同。贴图编码法线方向X, Y, Z三个分量被映射到R, G, B三个颜色通道。通常Z分量垂直方向占主导所以法线贴图整体会呈现一种蓝紫色调。作用当光线照射到应用了法线贴图的表面上时引擎会根据每个像素“伪造”的法线方向计算光照从而在平滑的模型几何体上呈现出丰富的凹凸细节极大地节省了模型面数。对于水面一张好的法线贴图应该包含不同尺度和方向的波纹信息这是产生逼真水波光影的基础。2.2 让静态贴图“动”起来UV动画纹理是静止的如何让它产生动态的水波效果答案是让纹理的采样坐标UV随时间变化。UV坐标可以理解为纹理映射到模型表面的“经纬度”。如果我们让这个坐标系统缓慢地移动或旋转采样到的纹理像素就会发生变化从而在视觉上产生“流动”的效果。我们将采用两种最基本的UV变换来模拟水面的自然运动平移让法线贴图沿着某个方向如对角线缓慢移动模拟水流的整体趋势。缩放与叠加使用两套不同缩放比例和移动速度的UV对同一张法线贴图进行两次采样然后将结果混合。这能打破单一移动的规律性创造出更复杂、更自然的水波干涉图案。通过控制移动的速度、方向和混合强度我们就能轻松调节水面的“流速”和“汹涌程度”。2.3 ShaderGraph 中的关键节点Normal From Texture在手动计算法线并非不可行但ShaderGraph为我们提供了一个极其强大的内置节点Normal From Texture。这个节点是本次实战的“魔法核心”。它的作用是将一张高度图Height Map或灰度图实时转换为法线贴图。原理是计算纹理中相邻像素的亮度差根据差值来推算出表面的坡度即法线方向的变化。输入一张纹理Texture通常是一张表现水波起伏的灰度图。关键参数Offset采样偏移量。它决定了在计算法线时考察多远距离的像素差值。值越大生成的法线所表现的“波浪”尺度就越大。Strength强度乘数。它放大了亮度差对法线方向的影响。值越大凹凸感越强水面看起来就越“崎岖”。输出一个三维向量Vector 3即计算得到的法线信息。注意虽然这个节点叫“From Texture”但直接输入一张现成的、彩色的法线贴图给它通常得不到正确结果因为它预期的是高度信息。我们的技巧在于我们会用这个节点来处理经过UV动画采样后的灰度水波纹理动态生成法线而不是直接使用静态的法线贴图。这赋予了我们对水面形态的实时控制力。3. 实战构建5分钟节点图全流程现在我们进入Unity创建一个新的Unlit Shader Graph因为水体的颜色和光影我们将完全自定义控制。我将一步步拆解每个模块的构建。3.1 资源准备与图形创建首先你需要一张水波的高度图/灰度图。你可以在一些免费的纹理网站搜索“Water Normal”或“Water Height”但注意选择那种灰度对比明显、波纹细节丰富的图片。将其导入Unity。在Project窗口右键 - Create - Shader Graph - URP - Unlit Shader Graph命名为“SG_Water_Surface”。双击打开该Shader Graph。3.2 构建双层UV动画系统这是创造动态感的第一步。我们在Master Stack主堆栈之外的区域进行构建。创建基础UV与Time节点拖入一个Texture Coordinate节点获取模型默认的UV。我们使用其输出的“UV0”。拖入一个Time节点获取游戏运行时间。我们使用其输出的“Sine Time”或“Time”前者是带正弦周期的时间流动更柔和后者是线性增长的时间流动更持续。构建第一层UV变换创建一个Tiling And Offset节点。将Texture Coordinate的UV0连接到其UV端口。创建一个Vector2属性命名为FlowSpeed_1设置为(0.05, 0.05)。这代表第一层纹理在U和V方向上的移动速度。创建一个Multiply节点将Time节点的输出与FlowSpeed_1属性相乘得到随时间变化的偏移量。将上一步的乘积连接到Tiling And Offset节点的Offset端口。创建一个Float属性命名为Tiling_1设置为1。连接到Tiling And Offset节点的Tiling端口。这样我们就得到了第一层流动的UV坐标记作UV_Flow1。构建第二层UV变换完全复制上述步骤创建另一套独立的节点链。创建属性FlowSpeed_2设置为(-0.03, 0.04)即速度略慢且方向略有不同以产生差异。创建属性Tiling_2设置为2。这意味着第二层纹理被缩放得更小它将提供更细碎、高频的水波细节。最终得到第二层UV坐标记作UV_Flow2。实操心得两套UV的Tiling平铺值最好成倍数关系例如1和2或1和3Speed的值和方向也应不同。这样混合后的波纹才会自然、不重复避免产生明显的图案循环感。3.3 采样纹理与生成动态法线接下来我们用这两套动态的UV去采样同一张水波高度图并生成法线。纹理采样拖入一个Texture2D属性命名为WaveHeightMap并将你准备好的水波灰度图赋值给它。创建两个Sample Texture 2D节点。将WaveHeightMap属性同时连接到这两个采样节点的Texture输入。将之前构建好的UV_Flow1和UV_Flow2分别连接到这两个采样节点的UV输入。两个采样节点的采样器类型Sampler通常保持默认的Linear Repeat即可。应用Normal From Texture节点搜索并拖入Normal From Texture节点。注意我们需要两个。将第一个Sample Texture 2D节点的RGBA输出实际上我们只需要R通道的灰度值连接到第一个Normal From Texture节点的Texture输入。同样将第二个采样节点的输出连接到第二个Normal From Texture节点。创建两个Float属性分别命名为NormalStrength_1和NormalStrength_2默认值设为3和5。将它们分别连接到两个Normal From Texture节点的Strength端口。Offset端口可以暂时不连接使用其默认值或创建一个小的Float属性如0.5进行控制。混合两层法线水面的法线是两层运动叠加的结果。我们使用一个Lerp线性插值节点来混合它们但这不够“物理”。更常用的方法是使用Normal Blend节点或Add节点。这里使用一个简单的Add节点。将两个Normal From Texture节点的Out输出Vector3连接到一个Add节点的输入。然后使用一个Normalize节点对相加后的法线向量进行归一化使其长度恢复为1这是关键步骤否则光照计算会出错。最终得到的就是我们动态生成的混合法线。3.4 构建水面视觉外观有了动态法线我们就可以构建水面的颜色、透明度和高光了。基础颜色与深度色创建一个Color属性命名为ShallowColor浅水色设为淡蓝色或青绿色。创建另一个Color属性命名为DeepColor深水色设为深蓝色。水面颜色通常随深度或视角变化。我们可以做一个简化使用Fresnel Effect菲涅尔节点。将Position和Normal使用我们生成的动态法线连接到Fresnel Effect节点。将Fresnel Effect的输出一个在边缘为1、中心为0的渐变值作为系数连接到一个Lerp节点A端口接DeepColorB端口接ShallowColor。这样水面中心视角垂直向下显示浅色边缘视角倾斜显示深色。高光反射在URP中我们可以使用Specular或Simple Lit主节点来获得高光。但为了更直观的控制我们使用PBR Master节点如果你的Shader Graph模板支持或自定义计算。一个快速的方法是使用Reflection Probe或Scene Color节点获取环境反射然后与一个Color属性控制反射强度与色调相乘。更简单的方式是创建一个Smoothness属性浮点数0-1直接连接到主节点的Smoothness输入。引擎会自动根据法线和光滑度计算高光。将值设为0.8-0.95水面会显得光滑。透明度与折射模拟创建一个Float属性命名为Opacity设为0.8。连接到主节点的Alpha输入。确保在Graph Inspector中Graph Settings下的Surface Type设置为TransparentBlend Mode设置为Alpha。折射效果实现较复杂但我们可以做一个视觉模拟使用Screen Position节点和动态法线的XY分量对屏幕纹理进行轻微扰动模拟水下物体的扭曲感。这需要用到Scene Color节点和DDX/DDY节点属于进阶技巧本次为求快速可暂不实现。3.5 连接主节点与最终输出将我们计算好的所有数据连接到Master Stack或PBR Master的对应端口。Base Color连接之前Lerp出的水面颜色。Normal这是最关键的一步。将Normalize后的动态法线输出连接到一个Normal Vector节点如果需要然后连接到主节点的Normal输入端口。Smoothness连接你设置的高光滑度值。Alpha连接透明度属性。至此整个Shader Graph的主体部分已经构建完成。你可以点击Save Asset然后创建一个材质球并使用这个Shader。将其赋予一个Plane平面或自定义的水面网格你就能看到一个拥有动态波纹、基础颜色渐变和反光的水面了。4. 参数详解与效果微调指南节点连好了但水面可能看起来有点假或者运动不自然。别急通过调整以下参数你可以让它焕然一新。4.1 控制水流形态的核心参数下表总结了最关键的几个属性及其影响属性名类型默认值建议作用与调节技巧FlowSpeed_1/2Vector2(0.05,0.05) / (-0.03,0.04)控制两层水波的流动速度和方向。技巧让两层速度大小和方向均有差异避免同步。值越大流动越快。可以尝试让一层慢而横向一层快而斜向。Tiling_1/2Float1 / 2控制水波纹理的缩放。值越大纹理被拉伸得越小水波看起来越细碎、频率越高。技巧这是打破重复感的关键。让两层缩放比不是整数倍如1和2.3效果更随机。NormalStrength_1/2Float3 / 5控制由高度图生成法线时的凹凸强度。值越大波纹的“陡峭度”越高光影对比越强水面越“汹涌”。技巧通常第二层高频细节的强度可以略高于第一层基础波浪。WaveHeightMapTexture2D你的灰度图源纹理的质量决定上限。选择一张灰度对比清晰、波纹信息丰富的图。过于模糊或嘈杂的图效果不佳。4.2 视觉外观调节参数属性名类型默认值建议作用与调节技巧ShallowColorColor淡青蓝 (0.6,0.9,1.0,1)浅水区或水面中心颜色。受菲涅尔效应影响。调亮可使水面看起来更清澈、阳光明媚。DeepColorColor深蓝 (0.1,0.3,0.6,1)深水区或水面边缘颜色。调暗并增加饱和度能增加水体的深度感和神秘感。Fresnel PowerFloat3-5在菲涅尔节点中调节。值越大浅色区域中心范围越小颜色过渡越尖锐值越小过渡越平缓。SmoothnessFloat0.9水面光滑度。接近1时像镜子般反光降低到0.5以下会显得浑浊无光。晴朗水面用高值浑浊水面用低值。OpacityFloat0.8整体透明度。1为完全不透明0为完全透明。配合深度色使用可以营造深浅不一的水体透视感。实操心得调节时不要一次性改动太多参数。建议的流程是先调FlowSpeed和Tiling让流动感自然再调NormalStrength让波纹立体感合适最后再精细调整颜色和反光。在场景中放置一个方向光并旋转光源角度观察光影变化这是检验法线效果的最佳方式。5. 性能优化与进阶思路一个能在5分钟内实现的效果也必须考虑其在实际项目中的实用性。5.1 性能考量纹理采样我们采样了两次纹理。确保你的WaveHeightMap尺寸合理如512x512或1024x1024并启用Mipmaps。可以考虑将两张不同尺度的灰度图合并到一张RGBA纹理的两个通道中以减少一次采样。计算复杂度Normal From Texture节点内部有多次纹理采样和叉乘计算我们使用了两个。对于移动端如果性能吃紧可以尝试只使用一层或者寻找烘焙好的静态法线贴图配合UV动画的方案但动态感会减弱。渲染状态透明渲染Alpha Blending比不透明渲染更耗性能。如果水面不需要透视效果或永远被其他物体遮挡可以考虑设置为Opaque不透明。5.2 效果进阶方向当你掌握了基础版本后可以尝试以下扩展让水面更加逼真泡沫与浪花使用第二张纹理泡沫遮罩图根据水波法线的斜率可以通过计算法线Y分量的变化得到或UV动画的某种噪声来动态控制水面波峰处的泡沫显示Lerp一个白色到基础色。岸边浅水与深度结合场景的深度图Depth Texture或使用顶点颜色/世界位置来混合浅水色和深水色实现真实的岸边渐变效果而不是仅靠菲涅尔。折射失真如前所述使用Scene Color节点和动态法线的XY分量对屏幕坐标进行扰动模拟看到水下物体时的扭曲效果。这需要将Shader的Surface Type设置为Transparent并可能涉及 GrabPass 或 URP 的Opaque Texture。细节法线在现有两层宏观波浪的基础上可以添加第三层使用非常小的Tiling和快速Speed的细节法线用于模拟水面微小的皱波在特写镜头下提升质感。6. 常见问题与排查实录即使跟着步骤做你也可能会遇到一些“坑”。这里记录了几个最常见的问题和解决方法。问题现象可能原因排查与解决步骤水面一片纯色没有波纹1. 法线信息未正确连接到主节点。2.Normal Strength值太低或为0。3. 场景中没有方向光或光照设置有问题。1. 检查Normal节点输出是否最终连接到了主节点的Normal输入端口。2. 检查NormalStrength属性值尝试调大到5-10。3. 在场景中创建一个Directional Light旋转光源角度观察变化。水波流动速度过快或过慢FlowSpeed参数设置不当。FlowSpeed是一个每秒钟移动UV多少单位的量。默认值(0.05,0.05)意味着每秒移动纹理的5%。如果觉得慢尝试(0.1,0.1)如果觉得快尝试(0.02,0.02)。水波图案重复感很强不自然两层UV的Tiling和Speed太相似。确保Tiling_1和Tiling_2有显著差异如1和3。确保FlowSpeed_1和FlowSpeed_2的向量方向不同不要只是正负号相反大小也不同。水面边缘有硬边或闪烁1. 纹理Wrap Mode不是Repeat。2. 透明渲染排序问题。1. 在Unity Project窗口选中你的水波纹理在Inspector中确保Wrap Mode设置为Repeat。2. 确保水面材质球的渲染队列Render Queue设置正确如Transparent并且与其他透明物体的渲染顺序无冲突。可以尝试微调材质的Render Queue值。在Game视图正常但Scene视图无效果Scene视图的照明模式可能不是Shaded。在Scene视图左上角将照明模式从Wireframe或Shaded Wireframe切换为Shaded。法线看起来“扁平”缺乏立体感1. 源WaveHeightMap本身对比度不足。2.Normal From Texture节点的Offset值太小。1. 尝试用图像软件提高水波纹理的对比度让亮部和暗部更分明。2. 尝试增大Normal From Texture节点的Offset输入值例如从默认的0.5增加到1.0或2.0这会让它从更远的像素计算梯度产生更宏观的波浪感。最后别忘了Shader开发是一个迭代和艺术化的过程。本文提供的节点图和参数是一个坚实的起点但它不是你项目的终点。最出色的水面效果往往需要美术的介入——一张精心绘制或程序生成的高度图其作用远大于在Shader中盲目调整参数。多观察现实中的水面思考光线如何与波纹互动然后将这些观察转化为你的纹理和Shader参数这才是创造真实感的关键。