1. 水体流动效果的核心思路与方案选型
1.1 为什么选择 Flow Map 而不是滚动 UV
刚接触水体效果的朋友,第一反应往往是给水面贴图加一个随时间递增的 UV 偏移,也就是常说的UV Scroll。这个做法确实简单,一行Time节点乘上速度再Add到UV上就完事了。但只要你真的把它放进场景里跑一圈,就会发现两个致命问题:一是水面看起来像一条匀速传送带,缺乏自然的缓急变化;二是当水面有弯曲、分叉或者遇到岸边障碍物时,贴图会直接穿模,完全没有“绕流”的感觉。
Flow Map 解决的正是这个痛点。它的本质是一张方向贴图,用 R 通道存 U 方向的速度分量,G 通道存 V 方向的速度分量,每个像素告诉 Shader“这一点的水应该往哪个方向流、流多快”。这样一来,河道拐弯处的水会顺着弯道走,遇到石头会自然分流,静水区和急流区可以共存于同一张水面。这就是为什么但凡稍微讲究一点的水体效果,几乎都会上 Flow Map。
从实现成本看,Flow Map 在 Shader Graph 里的节点数量其实并不多,核心逻辑就是“采样方向图 → 转成偏移量 → 叠加到 UV → 采样噪声图 → 输出”。真正让人翻车的地方不在主逻辑,而在UV 偏移的相位处理。这也是我在标题里特意加上“UV 偏移避坑指南”的原因——后面会专门用一整节来讲这个坑。
1.2 整体节点架构拆解
在动手连节点之前,先把整个 Shader 的数据流在脑子里过一遍,比盲目拖节点效率高得多。我习惯把 Flow Map 水体拆成四层:
- 方向层:采样 Flow Map,得到每个像素的流动方向向量。
- 时间层:用
Time节点驱动相位,但要经过Frac处理形成循环。 - 扰动层:用两层不同缩放、不同速度的噪声图叠加,制造水面的细节波纹。
- 合成层:把扰动后的 UV 结果采样噪声,再乘上颜色、透明度,最终接到
Master Stack。
这四层里,方向层和时间层是 Flow Map 的骨架,扰动层决定水面的“质感”,合成层决定它和场景的融合度。很多人做出来的水像塑料布,问题基本都出在扰动层只用了一张噪声图,缺少双层叠加带来的视差感。
提示:Flow Map 的方向向量通常以 0.5 为中性值存储,也就是说贴图里 RGB(128,128,0) 代表“不流动”。采样后需要先做一次
Multiply 2 - 1的映射,把 [0,1] 还原成 [-1,1] 的方向向量,这一步千万别漏。
1.3 方案对比:三种常见水体流动实现
为了让你更清楚 Flow Map 的定位,我把常见的三种方案拉出来对比一下:
| 方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| UV Scroll | UV 匀速偏移 | 节点极少,性能最好 | 无方向变化,穿模严重 | 直线河道、远景水面 |
| 双层 UV Scroll | 两层不同速度叠加 | 有一定细节感 | 仍无方向控制 | 湖泊、海面远景 |
| Flow Map | 方向图驱动 UV | 方向可控,绕流自然 | 需要额外方向贴图 | 河道、溪流、近景水面 |
从表格能看出来,Flow Map 多出来的成本主要就是一张方向贴图。这张图可以用工具烘焙,也可以手绘,甚至用简单的渐变加噪声凑合。对于近景水体,这点成本完全值得。
2. Flow Map 原理与 UV 偏移的相位陷阱
2.1 Flow Map 的通道编码与采样映射
Flow Map 这张图,说白了就是把“水流方向”这个二维向量塞进一张贴图的两个通道里。业界通用的约定是 R 通道存 X 方向(对应 U),G 通道存 Y 方向(对应 V),B 通道一般留空或者存强度遮罩。因为贴图通道的取值范围是 [0,1],而方向向量需要 [-1,1],所以存储时做了一次(dir + 1) / 2的编码,读取时就要反向做dir * 2 - 1的解码。
在 Shader Graph 里,这个解码过程用一个Multiply加一个Subtract就能完成。具体来说,采样Sample Texture 2D得到 RGBA,取 RG 两个通道,先Multiply一个 2,再Subtract一个 1,得到的就是真正的方向向量。这一步如果忘了做,水流方向会全部偏到第一象限,看起来就是所有水都往右上角流,非常诡异。
方向向量拿到之后,还要乘上一个流动强度参数。这个参数控制整体流速,通常给个 0.1 到 0.5 之间的值比较合适。给太大水面会像开了倍速,给太小又看不出流动。我一般会把它暴露成Float属性,方便在材质面板上实时调。
2.2 相位循环:为什么你的水会“跳帧”
现在到了最关键的部分,也是标题里说的“UV 偏移避坑指南”的核心。很多人第一次做 Flow Map,节点连完发现水面每隔一两秒就“啪”地跳一下,像视频卡帧一样。这个问题困扰过我整整一个下午,最后才想明白是相位没有做循环处理。
原理是这样的:Flow Map 的流动逻辑是UV + 方向 * 时间 * 速度。时间Time是无限增长的,那么偏移量也会无限增长。当偏移量超过 1 的时候,UV 采样就会从贴图的另一端重新开始,但因为方向是任意的,这个“重新开始”的位置和原来的位置对不上,于是就产生了跳变。
解决办法是引入一个循环周期。具体做法是:把时间除以周期,取小数部分Frac,再乘回周期。这样时间就在 [0, 周期] 之间循环,偏移量永远不会超过一个周期对应的距离,跳变自然就消失了。用节点表达就是:
Time → Divide (周期) → Frac → Multiply (周期) → 得到循环时间这个循环时间再拿去乘方向和速度,就能得到平滑循环的偏移量。周期一般设 2 到 5 秒,太短会看出循环感,太长又浪费。
注意:
Frac节点在 Shader Graph 里叫Fraction,别找错了。另外周期值不能为 0,否则会除零,水面直接变黑。
2.3 双层噪声叠加制造视差感
单层噪声采样出来的水面,怎么看都像一张会动的壁纸,缺乏立体感。真正让水面“活”起来的,是两层噪声以不同速度、不同缩放叠加。第一层缩放小、速度快,负责大面积的流动趋势;第二层缩放大、速度慢,负责细碎的波纹细节。两层叠加之后,因为速度不同,会产生一种微妙的视差,看起来就像水在往深处流。
具体参数上,我一般这样配:第一层 Tiling 给 1 到 2,速度给 0.3;第二层 Tiling 给 3 到 5,速度给 0.15。两层都用同一张噪声图就行,没必要准备两张。叠加方式用Add或者Lerp都可以,Add出来的对比度更强,Lerp更柔和,看你要什么风格。
这里有个小技巧:第二层的 UV 可以在第一层的基础上再叠加一个固定的偏移,比如(0.5, 0.5),这样两层的图案不会完全重合,细节会更丰富。这个偏移量不用太大,0.1 到 0.5 之间就行。
3. Shader Graph 节点实操全流程
3.1 属性面板与基础设置
打开 Shader Graph,先建一个Blank Shader Graph,然后在 Graph Inspector 里把 Material 设成Unlit,因为水体效果通常不需要参与光照计算,用 Unlit 性能更好,也更容易控制最终颜色。如果你想让水接受场景光照,那就选Lit,但节点会复杂一些,新手建议先从 Unlit 入手。
接下来在 Blackboard 里建几个属性,这是让 Shader 可调的关键:
FlowMap:Texture2D,方向贴图。NoiseTex:Texture2D,噪声图。FlowSpeed:Float,默认 0.3,整体流速。FlowStrength:Float,默认 0.2,方向强度。LoopDuration:Float,默认 3,循环周期。BaseColor:Color,默认浅蓝,水体基色。Alpha:Float,默认 0.8,透明度。
这些属性建好之后,在材质面板上就能实时调,不用每次改代码重新编译,效率高很多。特别是LoopDuration和FlowSpeed这两个,调的时候盯着 Scene 视图看,很快就能找到合适的值。
3.2 方向采样与循环时间节点连接
先处理方向层。拖一个Sample Texture 2D节点,把FlowMap接进去,输出取 RG 通道。然后接Multiply 2再Subtract 1,得到 [-1,1] 的方向向量。这个向量再乘上FlowStrength,方向层的输出就准备好了。
接着处理时间层。拖一个Time节点,输出接Divide,另一个输入接LoopDuration。Divide的输出接Fraction,再接一个Multiply,另一个输入还是LoopDuration。这样得到的循环时间就在 [0, LoopDuration] 之间来回跑。这个循环时间再乘上FlowSpeed,得到最终的偏移量。
把方向层的输出和偏移量相乘,就得到了每个像素的 UV 偏移向量。这个向量再Add到原始的UV上,就是流动后的 UV。到这里,Flow Map 的核心逻辑就完成了,节点数量其实不到十个。
3.3 噪声采样与颜色合成
有了流动 UV,接下来采样噪声图。拖两个Sample Texture 2D节点,都接NoiseTex,但 UV 输入不同。第一个直接接流动 UV,第二个接流动 UV 乘一个 Tiling 系数再加一个固定偏移。两个采样结果用Add叠加,得到最终的噪声值。
这个噪声值接下来要参与颜色计算。我一般用Lerp把BaseColor和噪声值混合,噪声亮的地方颜色浅一点,暗的地方深一点,模拟水面的高光变化。混合系数给 0.3 到 0.5 之间比较自然。最后把结果接到Master Stack的Base Color,Alpha接上透明度属性,一个基础的水体流动 Shader 就成型了。
提示:如果水面看起来太“平”,可以在噪声叠加之后再加一个
Power节点,把噪声的对比度拉高,水面的明暗变化会更明显。Power的值给 1.5 到 2 之间就行。
3.4 完整节点清单速查
为了方便你对照检查,我把整个 Shader 用到的节点列成一张表:
| 节点 | 数量 | 作用 |
|---|---|---|
| Sample Texture 2D | 3 | 采样方向图和两层噪声 |
| Time | 1 | 提供时间驱动 |
| Divide / Fraction / Multiply | 各 1 | 构成循环时间 |
| Multiply / Subtract | 各 1 | 方向向量解码 |
| Add | 2 | UV 偏移叠加、噪声叠加 |
| Lerp | 1 | 颜色混合 |
| Property 节点 | 7 | 暴露可调参数 |
照着这张表连,基本不会漏节点。连完之后记得在材质面板上把FlowMap和NoiseTex两张贴图拖进去,不然水面会是纯色。
4. 常见问题排查与实战避坑技巧
4.1 水面跳帧、闪烁、方向错乱速查
实际做的时候,问题往往不是节点连错,而是参数没配对。我把踩过的坑整理成一张速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 水面每隔几秒跳一下 | 相位没做 Frac 循环 | 加 Fraction 节点,设 LoopDuration |
| 所有水往同一方向流 | 方向图没做 2-1 解码 | 补 Multiply 2 和 Subtract 1 |
| 水面闪烁 | 噪声图 Tiling 过大 | 把 Tiling 降到 5 以下 |
| 水面完全不动 | FlowSpeed 为 0 或贴图没接 | 检查属性值和贴图槽 |
| 水面发黑 | LoopDuration 为 0 导致除零 | 把 LoopDuration 设成大于 0 |
| 边缘穿模 | 方向图边缘没有过渡 | 在方向图边缘做渐变遮罩 |
这张表里的每一条,我基本都亲自踩过。特别是第一条“跳帧”,当时排查了半天以为是 Time 节点的问题,最后才发现是少了Fraction。所以你现在看到标题里的“避坑指南”,真不是凑字数。
4.2 性能优化与移动端适配
Flow Map 水体的性能开销主要来自三次贴图采样。在 PC 上这完全不是问题,但如果是移动端,就得注意了。我的经验是:移动端把两层噪声减成一层,方向图分辨率降到 256 甚至 128,LoopDuration和FlowSpeed用常量而不是属性,这样能省下不少开销。
另外,Sample Texture 2D节点默认是带 Mipmap 的,如果水面在远处,Mipmap 会自然降低采样精度,这其实是好事。但如果你发现远处水面糊成一片,可以在贴图导入设置里把 Mipmap 关掉,或者调高各向异性过滤。这个取舍看你的场景需求。
还有一个容易被忽略的点:水面的渲染队列。如果水面和场景其他透明物体有穿插,记得在 Master Stack 里把Render Queue设成Transparent,并且把Depth Write关掉,不然会出现奇怪的遮挡关系。
4.3 让水面更真实的三个加分项
基础效果跑通之后,想让水面再上一个档次,可以试试这三个方向。第一是加法线扰动,用噪声图采样出来的值去扰动法线,水面会有细碎的高光闪烁,特别适合阳光下的溪流。第二是加边缘泡沫,用深度图或者场景位置算出一个边缘遮罩,在靠近岸边的地方叠加白色泡沫,真实感立刻拉满。第三是加颜色渐变,根据水深或者流速改变水的颜色,浅处偏绿、深处偏蓝,视觉层次会丰富很多。
这三个加分项都不难,法线扰动就是在噪声采样后接一个Normal From Height节点;边缘泡沫需要一个Scene Depth节点做差;颜色渐变用Lerp根据一个遮罩混合两种颜色就行。等你把基础 Flow Map 玩熟了,再往上加这些,会发现整个水体的表现力完全不一样。
我在实际项目里做河道水体的时候,最开始就是裸的 Flow Map,跑起来总觉得差点意思。后来加了法线扰动和边缘泡沫,同一个场景,美术那边直接说“这水活了”。所以基础打牢之后,别急着收工,往上叠细节才是拉开差距的地方。