1. 水体流动效果的核心思路与方案选型
1.1 为什么选择Flow Map而不是顶点动画
做过水面效果的人大概都经历过这个阶段:一开始用顶点动画让水面上下起伏,看起来还行,但总觉得少了点“流动感”。后来尝试用UV滚动,简单粗暴地让贴图往一个方向平移,结果水面像传送带一样机械重复,一眼假。再后来接触到Flow Map,才算是找到了一个在效果和性能之间比较平衡的方案。
Flow Map的本质是一张记录了方向向量的贴图。它的RG通道分别存储了U方向和V方向的偏移量,取值范围通常在0到1之间,映射到实际偏移量时需要转换到-1到1的区间。这张图告诉Shader:每个像素应该往哪个方向流动,流动的强度是多少。相比全局统一的UV滚动,Flow Map能让水面不同区域有不同的流动方向,比如靠近岸边的地方流速慢、方向偏转,河道中央流速快、方向一致,这种空间变化的流动感才是真实水体的样子。
从性能角度看,Flow Map方案的核心开销就是一次额外的贴图采样和几次向量运算,对于移动端来说完全可以接受。相比之下,如果用顶点动画做波浪,顶点数不够效果就粗糙,顶点数上去了性能又吃不消。而Flow Map是在片元层面做文章,跟模型复杂度解耦,这也是它在实际项目中更受欢迎的原因。
1.2 Shader Graph在这个流程里的角色
Shader Graph是Unity的可视化Shader编辑工具,从2018版本开始逐渐成熟,到2022 LTS已经相当稳定了。用它来做Flow Map水体效果有几个明显的好处:一是节点连线直观,调试的时候可以实时看到每个节点的输出,不用反复改代码编译;二是参数暴露方便,美术同学可以直接在材质面板上调流速、调方向、调颜色,不需要程序介入;三是跨管线兼容,URP和HDRP都能用同一套逻辑,迁移成本低。
当然也有代价。Shader Graph生成的代码在极端情况下可能不如手写HLSL精简,但对于水体流动这种中等复杂度的效果来说,差异可以忽略不计。而且Shader Graph支持自定义函数节点,真遇到性能瓶颈也可以把关键计算用HLSL封装进去,灵活度是够的。
1.3 整体方案拆解
整个效果可以拆成四个层次来理解。最底层是基础水体着色,包括深浅颜色混合、菲涅尔边缘光、高光反射这些常规操作。往上一层是流动层,用Flow Map驱动两张噪声贴图的UV偏移,让水面产生流动的纹理变化。再往上是细节层,叠加一层法线贴图的流动来模拟水面的微小波纹。最上面是交互层,比如根据深度图做岸边泡沫、根据场景物体做折射扰动。
这次重点讲的是流动层和细节层的实现,因为这是Flow Map最核心的应用场景,也是UV偏移最容易踩坑的地方。基础着色和交互层会简单带过,保证整体效果的完整性。
2. Flow Map的核心原理与UV偏移避坑指南
2.1 Flow Map贴图的制作与导入设置
Flow Map的制作方式有好几种。最常用的是在Photoshop或者Substance Designer里手绘方向,用颜色表示流向:红色通道控制水平方向,绿色通道控制垂直方向,中间值0.5表示不偏移。比如纯红色(1,0.5,0.5)表示向右流动,纯绿色(0.5,1,0.5)表示向上流动,黄色(1,1,0.5)表示向右上流动。这种编码方式的好处是直观,美术同学画起来容易理解。
另一种方式是用工具生成,比如FlowMap Painter或者Houdini的Flow Map节点,可以根据模型形状或者噪声自动生成方向场。对于河流这种有明确流向的场景,手绘加模糊过渡通常效果更好;对于湖面这种方向比较随机的水体,用噪声生成会更自然。
导入Unity的时候有几个关键设置必须注意。首先Texture Type要选Default,不要选Normal Map,因为Flow Map不是法线贴图。其次sRGB要取消勾选,因为Flow Map存储的是方向数据不是颜色数据,如果走sRGB空间会导致方向偏移不准确。Wrap Mode选Repeat,这样UV超出0到1范围时能正确平铺。Filter Mode根据需求选Bilinear或者Trilinear,一般Bilinear就够了。最后记得把压缩格式设成高质量,因为方向数据的精度损失会直接导致流动方向偏差。
注意:很多人会把Flow Map和法线贴图搞混,两者都是RGB贴图但用途完全不同。法线贴图存储的是法线方向,需要sRGB勾选;Flow Map存储的是UV偏移方向,必须取消sRGB。这个设置搞错的话,流动方向会偏得离谱。
2.2 UV偏移的三种常见错误与正确做法
UV偏移这块我踩过的坑最多,总结下来有三种典型错误。
第一种是直接用Time节点乘以速度去加UV。看起来没问题,但时间久了UV值会越来越大,到了几万甚至几十万的时候,浮点精度就不够了,水面会出现明显的抖动和跳变。正确的做法是用Time乘以速度之后取小数部分,或者用Fract节点把UV限制在0到1之间。更稳妥的方式是用Time节点的小数输出,Shader Graph里Time节点有Sine Time、Cosine Time、Time、Delta Time几个输出,直接用Time然后Fract一下就行。
第二种是Flow Map的RG通道没有做重映射。前面说过Flow Map的RG范围是0到1,但实际偏移量需要-1到1。如果直接拿RG去乘速度加UV,相当于所有像素都在往正方向偏移,只是偏移量不同,这样水面会整体往一个方向跑,失去Flow Map的意义。正确的做法是用Remap节点把0到1映射到-1到1,或者用Multiply加Add节点手动做:先乘以2再减1。
第三种是两张噪声贴图的UV偏移方向搞反了。为了让流动更自然,通常会用两张噪声贴图以不同速度、不同方向流动,然后混合。如果两张图的偏移方向完全一致,混合出来还是单向流动;如果方向相反,混合出来会有拉扯感。比较合适的做法是让两张图有30度到60度的方向差,速度差在1.5倍到2倍之间,这样混合出来的流动既有层次感又不会互相抵消。
2.3 时间缩放与流速控制的参数化设计
流速控制看起来简单,但要做好用需要把参数拆细。我一般会暴露这几个参数:基础流速(Base Speed)、流向强度(Flow Strength)、噪声缩放(Noise Scale)、时间缩放(Time Scale)。基础流速控制整体快慢,流向强度控制Flow Map的影响权重,噪声缩放控制纹理大小,时间缩放用来做全局变速比如慢动作效果。
这里有个细节:Time Scale不要直接乘在Time上,而是乘在流速上。因为Time是全局的,如果多个材质共用同一个Time但Time Scale不同,直接乘Time会导致每个材质的时间基准不一致,动画会错位。正确做法是Time乘以Base Speed再乘以Time Scale,这样每个材质独立控制自己的速度,互不影响。
另外流速的数值范围建议控制在0.01到0.5之间。太小了看不出流动,太大了水面会像快进的录像。如果是河流,0.05到0.15比较合适;如果是海浪,0.1到0.3;如果是瀑布,可以到0.5以上。这些数值不是绝对的,要根据场景比例和相机距离来调。
2.4 噪声贴图的选择与混合策略
噪声贴图的选择直接影响水面的质感。Perlin噪声比较柔和,适合湖面或者缓慢流动的河水;Worley噪声有细胞感,适合模拟水面泡沫或者沸腾效果;Simplex噪声介于两者之间,通用性最好。Unity的Mathf.PerlinNoise可以在C#里生成噪声,但在Shader里一般用预先生成的噪声贴图,性能更好。
混合策略上,我习惯用两层噪声做Lerp,然后用第三层噪声做扰动。具体来说:第一层噪声用Flow Map驱动UV偏移,速度设为1倍;第二层噪声也用Flow Map驱动但速度设为1.7倍,方向偏转45度;两层用Lerp节点按0.5混合。第三层噪声不参与流动,只用来扰动前两层的混合权重,让混合过渡更自然。
混合的时候要注意,两层噪声的UV不能完全一样,否则混合出来还是同一张图。一般会让第二层噪声的UV缩放跟第一层差一个质数倍,比如第一层缩放是3,第二层就是3.7或者5.3,这样两层纹理不会周期性对齐,流动看起来更随机。
3. Shader Graph节点搭建全流程
3.1 基础水体着色的节点框架
打开Shader Graph,先建一个URP的Lit Shader Graph,或者Unlit也行,看项目需求。我一般用Unlit然后自己加光照计算,这样控制更灵活。基础着色部分需要这几个节点:Base Color、Normal、Emission、Alpha。
Base Color用深水和浅水两个颜色做Lerp,Lerp的权重用菲涅尔节点。菲涅尔节点在Shader Graph里叫Fresnel Effect,输入Normal Vector和View Direction,输出一个0到1的值,边缘接近1中间接近0。用这个值做Lerp,水面边缘会偏浅色,中间偏深色,符合真实水体的视觉规律。
Normal部分先留空,等流动层做完再接进来。Emission用来做水面的高光反射,可以用简单的Blinn-Phong模型,也可以用环境贴图采样。Alpha根据需求设成1或者根据深度做透明,如果是河流一般设1,如果是浅水池塘可以设0.8左右。
3.2 Flow Map驱动UV偏移的节点连线
这是核心部分。从Sample Texture 2D节点开始,把Flow Map贴图接进去,输出RG通道。然后用Split节点把RG拆开,分别做Remap:R通道从0到1映射到-1到1,G通道同样处理。Remap节点在Shader Graph里叫Remap,输入In、In Min Max、Out Min Max,设置In Min Max为0和1,Out Min Max为-1和1。
Remap之后得到的是方向向量,用Multiply节点乘以Flow Strength和Base Speed,再用Multiply乘以Time。Time节点直接用默认输出,不需要Fract,因为后面加UV的时候会自动处理。然后把这个偏移量加到噪声贴图的UV上。UV来源是UV节点,默认是UV0,如果模型有多个UV通道可以选UV1。
这里有个优化点:如果Flow Map的RG通道在导入时已经做了重映射(比如在Photoshop里把0.5设为基准),那Shader里就不需要再Remap了。但为了通用性,我建议还是在Shader里做Remap,这样Flow Map的制作标准统一,换图的时候不用改节点。
3.3 双层噪声混合与流动细节叠加
第一层噪声的UV偏移做完之后,复制一套同样的节点做第二层。第二层的区别在于:Flow Strength乘以1.7,Time乘以1.3,方向偏转用Rotate节点旋转45度。Rotate节点在Shader Graph里叫Rotate,输入UV和Center,输出旋转后的UV。旋转角度用Radians转换一下,45度就是0.785弧度。
两层噪声采样完之后用Lerp混合,权重设0.5。混合结果接到Normal From Texture节点生成法线,或者直接接到Normal输入。如果要做更精细的水面,可以在两层噪声之外再加一层高频噪声做细节法线,UV偏移速度设成第一层的2.5倍,混合权重0.3。
细节叠加的时候注意,法线的强度要控制好。太强了水面像塑料,太弱了看不出波纹。一般Normal Strength设在0.3到0.8之间,根据噪声贴图的对比度调整。如果噪声贴图本身对比度很高,Normal Strength就设小一点;反之设大一点。
3.4 深度图与岸边泡沫的交互处理
岸边泡沫是提升水体真实感的关键细节。原理是用场景深度图减去水面深度,得到水面到场景物体的距离,距离越小说明越靠近岸边,泡沫越明显。Shader Graph里用Scene Depth节点获取场景深度,用Screen Position节点的W分量获取水面深度,两者相减得到深度差。
深度差经过Smoothstep处理,映射到0到1的泡沫强度。然后用这个强度去Lerp泡沫颜色和水面颜色。泡沫纹理可以用噪声贴图加阈值,也可以用专门的泡沫贴图。泡沫的UV也要跟着水面流动,但速度要比水面慢一些,这样泡沫看起来是浮在水面上的而不是跟着水跑。
注意:Scene Depth节点在URP里需要勾选Depth Texture选项,在URP Asset的Inspector里找到Depth Texture勾上。不勾的话Scene Depth返回的是0,泡沫效果出不来。这个坑我踩过好几次,每次换项目都要重新检查一遍。
4. 常见问题排查与性能优化实录
4.1 水面抖动与UV精度问题的排查
水面抖动是最常见的问题,表现是水面流动到一定程度后突然跳一下,或者整体出现高频抖动。原因基本可以锁定在UV精度上。Unity的UV默认是float2,精度足够,但如果UV值累积到很大(比如Time跑了几小时),float精度就不够了。
解决方案有两个:一是用Fract节点把UV限制在0到1之间,但这样会导致纹理接缝处不连续;二是用Time节点的小数部分,Shader Graph的Time节点有个Fraction输出,直接用它就不会累积。我一般用第二种,简单可靠。
还有一种抖动是Flow Map贴图的压缩导致的。如果Flow Map被压缩成低质量格式,方向数据会有量化误差,流动到某些区域时方向会突变。解决方法是把Flow Map的压缩格式设成RG Compressed或者高质量,在Texture Import Settings里把Compression改成High Quality。
4.2 流动方向与预期不符的调试方法
流动方向不对通常有三个原因:Flow Map的sRGB设置错了、Remap范围搞反了、UV的V轴方向跟预期相反。排查的时候可以先把Flow Map直接接到Base Color上,看看颜色分布是否符合预期。如果Flow Map显示出来是偏红的,说明R通道值高,应该往右流;如果显示偏绿,应该往上流。
如果颜色对了但方向反了,检查Remap的Out Min Max是不是设成了1和-1。有些教程会写成-1和1,顺序反了方向就反了。另外Unity的UV原点在左下角,而有些贴图工具的原点在左上角,导入的时候V轴会翻转,导致上下方向相反。这种情况在导入设置里把Flip Green Channel勾上就行。
4.3 移动端性能优化要点
移动端做Flow Map水体,性能优化主要抓三个点:贴图数量、采样次数、计算复杂度。贴图数量上,Flow Map一张、噪声两张、法线一张,加起来四张,对于中高端移动设备可以接受。如果还要加泡沫贴图和深度图,就有点多了,可以考虑把泡沫信息打包到噪声贴图的Alpha通道里。
采样次数上,两层噪声就是两次采样,加上Flow Map一次,法线一次,总共四次。如果性能吃紧,可以把两层噪声合并成一张贴图的两个通道,用一次采样拿到两层信息,但这样两层噪声的UV必须一样,流动方向就统一了,效果会打折扣。
计算复杂度上,Remap和Multiply都是廉价操作,主要开销在采样和Lerp。如果还开了抗锯齿和HDR,移动端压力会比较大。建议在移动端把Normal Strength调低,或者干脆关掉法线只保留颜色流动,等性能有余量再加回来。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 水面整体往一个方向跑 | Flow Map未做Remap | 检查Remap节点是否连接 | 添加Remap节点,0到1映射到-1到1 |
| 流动方向与预期相反 | sRGB设置错误或V轴翻转 | 把Flow Map接到Base Color看颜色 | 取消sRGB勾选,或勾选Flip Green Channel |
| 水面抖动跳变 | UV精度不足 | 检查Time是否直接乘速度加UV | 用Time的Fraction输出或Fract节点 |
| 两层噪声混合后无层次 | 两层UV缩放相同 | 检查两层噪声的Tiling值 | 第二层Tiling设为第一层的质数倍 |
| 岸边泡沫不显示 | Depth Texture未开启 | 检查URP Asset的Depth Texture选项 | 勾选Depth Texture |
| 移动端帧率骤降 | 采样次数过多 | 用Profiler查看GPU耗时 | 合并噪声贴图通道,降低Normal Strength |
| 水面边缘有硬边 | 菲涅尔强度过高 | 检查Fresnel Effect的Power值 | 降低Power值或改用Smoothstep过渡 |
| 流动速度不均匀 | Flow Map方向数据压缩损失 | 检查Flow Map压缩格式 | 设为高质量压缩或RG Compressed |
4.5 实操心得与避坑建议
第一个心得:Flow Map的模糊过渡比精确方向更重要。很多人画Flow Map的时候追求每个像素方向都精确,结果流动起来很生硬。实际上Flow Map需要一定的模糊,让相邻像素的方向平滑过渡,这样水面流动才有连贯性。在Photoshop里画完之后加个高斯模糊,半径2到5像素,效果会好很多。
第二个心得:噪声贴图的Tiling不要设整数。整数Tiling会导致纹理在UV边界处周期性对齐,流动看起来有重复感。用3.7、5.3、7.1这种质数或者带小数的值,能有效打破周期性。这个技巧在做任何平铺纹理的时候都适用。
第三个心得:Time Scale和Base Speed要分开控制。我见过很多项目把这两个合并成一个参数,结果做全局变速的时候所有水体的相对速度都乱了。分开控制的话,全局变速只改Time Scale,每个水体的相对速度保持不变,调整起来灵活得多。
第四个心得:在Scene视图里用Gizmos画Flow Map的方向。虽然Shader Graph不能直接画Gizmos,但可以写个简单的Editor脚本,在OnDrawGizmos里根据Flow Map贴图采样方向画箭头。这样调方向的时候直观很多,不用反复进Play模式看效果。
第五个心得:移动端慎用Scene Depth。Scene Depth在移动端需要额外的Depth Prepass,开销不小。如果项目对性能要求高,可以考虑用顶点颜色或者UV通道来标记岸边区域,绕过深度图计算。虽然精度差一点,但性能收益很明显。
5. 效果扩展与进阶玩法
5.1 加入折射与反射提升真实感
基础流动做完之后,可以加折射和反射让水面更真实。折射用Grab Pass或者URP的Camera Opaque Texture,采样屏幕空间纹理然后根据法线做UV扰动。扰动强度跟法线强度挂钩,法线越强扰动越大。反射用Reflection Probe或者Planar Reflection,前者性能好但精度低,后者效果好但开销大,根据项目需求选。
折射有个细节:扰动UV的时候要用屏幕空间的UV,不是模型的UV。Screen Position节点输出屏幕UV,加上法线乘以扰动强度,再去采样Opaque Texture。扰动强度一般设0.01到0.05,太大了画面会扭曲得看不清。
5.2 交互式水面的实现思路
交互式水面比如角色走过水面产生涟漪,原理是用一张Render Texture记录交互位置,然后在Shader里采样这张RT,根据距离计算涟漪强度。具体做法是角色脚下挂一个相机朝下拍,或者用Command Buffer把交互位置画到RT上。Shader里用RT的UV去采样,得到交互强度,叠加到法线或者高度上。
这个方案在PC上很成熟,移动端要谨慎,因为Render Texture的更新和采样都有开销。如果只是简单的脚步涟漪,可以用粒子系统代替,性能更好但效果没那么精细。
5.3 不同水体类型的参数预设
河流、湖泊、海洋、瀑布这几种水体的参数差异很大。河流流速快、方向明确,Flow Strength设0.8到1.0,Base Speed设0.1到0.2,噪声Tiling设2到4。湖泊流速慢、方向随机,Flow Strength设0.3到0.5,Base Speed设0.02到0.05,噪声Tiling设1到2。海洋有波浪,需要叠加顶点动画或者高度图,Flow Map只负责表面细节。瀑布是垂直流动,Flow Map的V方向要统一朝下,速度设0.3到0.5。
我一般会做几个材质预设,美术同学直接拖到模型上就能用,不用每次从头调参数。预设存在Project里,用Material Variant或者Prefab管理,换项目的时候直接拷贝过去。
5.4 与Crest或其它水体方案的对比
Crest是Unity社区里比较成熟的开源水体方案,功能很全,包括波浪模拟、泡沫、折射、反射、水下效果等等。相比之下,Shader Graph手搓Flow Map水体的优势在于轻量和可控。Crest适合做海洋这种大场景,功能多但开销也大;Flow Map适合做河流、池塘这种小场景,性能好、调参直观。
如果项目里已经有Crest,也可以把Flow Map作为补充,用在Crest覆盖不到的小水面上。两者不冲突,Crest管大水面,Flow Map管小水面,视觉风格统一一下就行。
5.5 后续可以继续折腾的方向
Flow Map水体做完之后,还可以往几个方向扩展。一是加水下效果,用后处理做水下的颜色偏移和扭曲;二是加天气系统,下雨的时候水面涟漪变多、流速变快;三是加季节变化,冬天水面结冰、流动变慢甚至停止。这些扩展都是在现有框架上加节点,不用推翻重来。
另外可以研究一下用Compute Shader做Flow Map的实时生成,比如根据地形高度和重力方向自动计算流向,这样美术不用手绘Flow Map,程序化生成更高效。不过这个复杂度比较高,适合有Compute Shader基础的同学折腾。
我个人在实际操作中的体会是,Flow Map水体效果的核心不在于节点多复杂,而在于参数调得细不细。同样的节点连线,流速差0.01、噪声Tiling差0.5,效果可能天差地别。所以做完之后不要急着收工,在Scene视图里多转转相机,从不同角度、不同距离看效果,调到一个自己觉得舒服的状态再保存。另外记得把参数暴露成Material Property,这样同一个Shader可以复用到不同水体上,不用每个水面都复制一份Shader Graph。