news 2026/9/19 18:53:32

Unity Shader Graph 水体流动实战:Flow Map 原理与 UV 偏移避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity Shader Graph 水体流动实战:Flow Map 原理与 UV 偏移避坑指南

1. 水体流动效果的核心思路与方案选型

1.1 为什么选择 Flow Map 而不是滚动 UV

刚接触水体效果的朋友,第一反应往往是给水面贴图加一个随时间递增的 UV 偏移,也就是常说的UV Scroll。这个做法确实简单,一行Time节点乘上速度再AddUV上就完事了。但只要你真的把它放进场景里跑一圈,就会发现两个致命问题:一是水面看起来像一条匀速传送带,缺乏自然的缓急变化;二是当水面有弯曲、分叉或者遇到岸边障碍物时,贴图会直接穿模,完全没有“绕流”的感觉。

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 ScrollUV 匀速偏移节点极少,性能最好无方向变化,穿模严重直线河道、远景水面
双层 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,透明度。

这些属性建好之后,在材质面板上就能实时调,不用每次改代码重新编译,效率高很多。特别是LoopDurationFlowSpeed这两个,调的时候盯着 Scene 视图看,很快就能找到合适的值。

3.2 方向采样与循环时间节点连接

先处理方向层。拖一个Sample Texture 2D节点,把FlowMap接进去,输出取 RG 通道。然后接Multiply 2Subtract 1,得到 [-1,1] 的方向向量。这个向量再乘上FlowStrength,方向层的输出就准备好了。

接着处理时间层。拖一个Time节点,输出接Divide,另一个输入接LoopDurationDivide的输出接Fraction,再接一个Multiply,另一个输入还是LoopDuration。这样得到的循环时间就在 [0, LoopDuration] 之间来回跑。这个循环时间再乘上FlowSpeed,得到最终的偏移量。

把方向层的输出和偏移量相乘,就得到了每个像素的 UV 偏移向量。这个向量再Add到原始的UV上,就是流动后的 UV。到这里,Flow Map 的核心逻辑就完成了,节点数量其实不到十个。

3.3 噪声采样与颜色合成

有了流动 UV,接下来采样噪声图。拖两个Sample Texture 2D节点,都接NoiseTex,但 UV 输入不同。第一个直接接流动 UV,第二个接流动 UV 乘一个 Tiling 系数再加一个固定偏移。两个采样结果用Add叠加,得到最终的噪声值。

这个噪声值接下来要参与颜色计算。我一般用LerpBaseColor和噪声值混合,噪声亮的地方颜色浅一点,暗的地方深一点,模拟水面的高光变化。混合系数给 0.3 到 0.5 之间比较自然。最后把结果接到Master StackBase ColorAlpha接上透明度属性,一个基础的水体流动 Shader 就成型了。

提示:如果水面看起来太“平”,可以在噪声叠加之后再加一个Power节点,把噪声的对比度拉高,水面的明暗变化会更明显。Power的值给 1.5 到 2 之间就行。

3.4 完整节点清单速查

为了方便你对照检查,我把整个 Shader 用到的节点列成一张表:

节点数量作用
Sample Texture 2D3采样方向图和两层噪声
Time1提供时间驱动
Divide / Fraction / Multiply各 1构成循环时间
Multiply / Subtract各 1方向向量解码
Add2UV 偏移叠加、噪声叠加
Lerp1颜色混合
Property 节点7暴露可调参数

照着这张表连,基本不会漏节点。连完之后记得在材质面板上把FlowMapNoiseTex两张贴图拖进去,不然水面会是纯色。

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,LoopDurationFlowSpeed用常量而不是属性,这样能省下不少开销。

另外,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,跑起来总觉得差点意思。后来加了法线扰动和边缘泡沫,同一个场景,美术那边直接说“这水活了”。所以基础打牢之后,别急着收工,往上叠细节才是拉开差距的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 18:50:00

Proxmox VE装完Web界面打不开?网络配置排查与修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 18:49:44

Vue样式隔离实战:scoped原理、边界与工程化落地策略

1. 样式事故现场:一个数字角标引发的故事我接手一个中后台管理项目两周后,某天运营反馈说“数字角标跑到了侧边栏上”。那个本该死死在“待办列表”右上角的小红点,不知道什么时候开始出现在左侧导航栏的某些菜单项旁边,而且形态不…

作者头像 李华
网站建设 2026/9/19 18:49:29

SSM+Vue实战:NBA球队管理系统设计与实现全攻略

1. 选题思路与整体设计:为什么NBA球队管理系统适合做毕设1.1 选题背景与核心需求拆解如果你的毕设题目是"NBA球队管理系统",第一反应可能是:这不就是一个普通的增删改查项目吗?确实,从功能上看它绕不开登录、…

作者头像 李华
网站建设 2026/9/19 18:48:15

网页版MC的实现:基于Canvas 2D与原生JS的体素游戏开发

最近我折腾了一个挺有意思的小东西——网页版MC.html。这个名字说白了就是“用HTML写的《我的世界》网页版”,一个单文件就能跑起来的体素小游戏:随机生成一片方块地形,用鼠标控制视角、WASD移动,左键敲方块,右键放方块…

作者头像 李华
网站建设 2026/9/19 18:45:38

Vue 3 购物车数量控件:nextTick 与影子动画实现数字滚动反馈

1. 场景还原:购物车数量控件为什么值得“较真”如果你做过电商前台,大概率会觉得购物车数量控件是个不能再小的组件:左边一个减号,右边一个加号,中间一个数字,最多再处理一下输入框的非法值校验&#xff0c…

作者头像 李华