最近在准备一个卡通渲染风格的原型项目,其中水面是最难啃的一块。翻了很多资料后发现,网上关于风格化水面的内容要么只讲原理不给操作,要么直接丢一个写好的 Shader 让你复制,完全没讲透为什么这么连节点。本文就以《塞尔达传说:风之杖》的水面为目标,完整拆解用 Unity Shader Graph 复刻这套风格化水面特效的整个过程,包含 URP 工程配置、双层噪声波浪、顶点起伏、卡通高光、Fresnel 边缘光和浪花白沫的完整实现思路。
无论你是刚接触 Shader Graph 的新手,还是已经写过不少 Shader 想尝试卡通风格的老手,这篇文章都会有一定的参考价值。文中所有节点连接都给出了具体参数,也把每个节点“为什么这样连”解释了一遍,方便你在自己的项目里二次改造。
1. 先从“风之杖的水面”说起:它为什么值得复刻
《塞尔达传说:风之杖》是 2003 年发售的 GameCube 游戏,它最标志性的视觉风格就是用卡通渲染(Toon Shading / NPR,即 Non-Photorealistic Rendering)营造出“可游玩动画电影”的感觉。整个海拉鲁世界被大片海洋包围,玩家大部分时间都在海面上航行,所以水面几乎成了游戏里出现频率最高的材质。
用现在的技术眼光看,它其实并没有使用多么复杂的物理模型:没有真实光线追踪,没有超精细法线贴图,也没有逐像素海浪模拟。水面主要由几层动画噪声驱动顶点起伏,配合色阶化着色和卡通形状的高光,让人一眼就能认出“这是塞尔达的水”。它的高明之处在于:
- 大面积波浪节奏缓慢,视觉上舒服不刺眼。
- 细碎波纹让水面有“流动感”,而不是一张静止贴图。
- 颜色分区干净利落,深浅蓝过渡明确,不追求真实反射。
- 卡通高光非常简洁,不会像 PBR 高光那样出现大量微细亮点。
从工程角度来看,这套效果非常适合用 Unity Shader Graph 实现。因为 Shader Graph 是可视化节点编辑工具,你可以非常直观地把“UV 流动”“噪声叠加”“颜色阈值”等概念用节点表达出来。后面排查问题时,把节点连线看一遍基本就清楚问题出在哪里。
本文的最终目标是在 URP 渲染管线下,用 Shader Graph 制作一个可以放到场景里直接使用的风格化水面材质,包含以下能力:
- 水面会持续波动,不是静态贴图。
- 有深蓝和浅蓝两层颜色过渡。
- 高光是卡通的,有明确阈值边界。
- 水面边缘有类似浪花泡沫的亮色效果。
- 所有核心参数都暴露在 Material 面板上,方便调参。
2. 开始之前:环境准备与工程配置
2.1 推荐的 Unity 与渲染管线组合
先说明一点:Shader Graph 不是独立运行的插件,它依赖具体的渲染管线。内置渲染管线(Built-in RP)、URP(Universal Render Pipeline,通用渲染管线)、HDRP(High Definition Render Pipeline,高清渲染管线)都可以使用 Shader Graph,但同一套节点在不同管线下的光照变量名称和输出节点会有差别。
本文选择 URP,原因很简单:
- URP 是当前 Unity 新项目的主流默认管线。
- 它对移动端和 PC 端支持都比较好,风格化游戏项目里非常常用。
- Shader Graph 在 URP 下的文档和社区案例最多,踩坑时容易搜到解决方案。
版本方面没有必须锁定的要求,建议使用 Unity 2021.3 LTS 或更高的 LTS 版本。URP 包可以直接在 Package Manager 中搜索 “Universal RP” 安装,Unity 会自动选择与当前版本兼容的 URP 版本。如果你的项目已经创建,可以打开Window > Package Manager,在左上角包来源里选择 “Unity Registry”,搜索 “Universal RP” 后点击 Install。
2.2 创建 URP 工程并调整管线配置
如果你是从零开始,最省事的方式是直接在 Unity Hub 里新建工程时选择 “Universal 3D” 模板。这样 Unity 会自动帮你创建好 URP 管线资源,并且已经在 Graphics Settings 中配置完成。
如果要在现有工程中接入 URP,则需要到Project Settings > Graphics中,把 Scriptable Render Pipeline Settings 指定为你创建好的 URP Asset(UniversalRenderPipelineAsset)。这里容易漏掉的是:如果场景里原有的材质使用的是内置管线的 Standard Shader,切换 URP 后它们往往会变成紫红色或无法正常显示,需要替换为 URP 管线下的材质。
创建好工程后,在 Project 窗口右键选择Create > Rendering > URP Asset(with Universal Renderer),可以生成一套最基本的 URP 配置资源,后面如果需要调抗锯齿、阴影距离、颜色分级等效果,直接在这个资源上修改即可。
2.3 准备水面测试场景
测试水面不需要复杂的场景,简单一点反而方便观察:
- 在 Hierarchy 中新建 Cube,Scale 设置为(10, 1, 10),作为水面下面的“海底”或地面。
- 新建 Plane,Scale 设置为(10, 1, 10),这个 Plane 就是我们的水面。
- 把场景默认的平行光保留,再创建一个 Directional Light 作为补光,强度控制在 0.3 左右。
需要注意的是,Shader Graph 水面并不一定要求物体是 Plane,只要是带有 UV 的网格都可以。水面的顶点数量会影响波浪的细腻程度,后面做顶点波浪时,建议把 Plane 的网格段数调高一些。如果是 Unity 自带的 Plane,它的默认分段数比较少,做顶点起伏后棱角会比较明显。你可以在建模软件中生成一个 20×20 分段的平面导出 FBX,也可以直接在场景里叠加多个不同大小和旋转的水面片来模拟层次。
3. Shader Graph 基础:从创建到第一个能用的水面材质
3.1 认识 Shader Graph 主界面
在 Project 窗口右键选择Create > Shader Graph > URP > Lit Shader Graph,创建一个 Lit 类型 Shader Graph。名字可以命名为Water_WindWaker.shadergraph,方便和普通材质区分。
打开后会看到几个核心区域:
- Main Preview:右上角的实时预览窗口,可以旋转场景观察材质效果。
- Graph Inspector:右侧属性面板,可以设置 Surface、Blend Mode 等渲染状态。
- Blackboard:左侧活动列表,用来创建暴露在材质面板上的参数(Property)。
- Node 搜索窗口:在空白处右键可以搜索节点,或者直接按空格键呼出搜索框。
一开始建议先熟悉这几个面板的位置,因为后面调参数会频繁用到。
3.2 设置 Surface Options
在 Graph Inspector 中,选中最顶部的 Shader Graph 文件节点,把 Surface 设置为 Transparent。
这个设置决定了材质的渲染方式。Transparent 会让水面拥有半透明能力,可以从侧面透出一些下方海底的颜色,这是风格化水面“通透感”的重要来源。如果保持默认的 Opaque,后面给 Alpha 通道赋值也不会生效。
同时把 Blend Mode 选择为 Alpha 或 Premultiply。Alpha 模式比较常用,适合普通的半透明水面。如果你希望水面边缘更干净,可以测试 Premultiply,具体差异可以在预览窗口里直观看到。
3.3 构建最基础的水面颜色输出
目前 Shader Graph 默认的 Fragment 输出中,可以看到 Base Color、Normal、Metallic、Smoothness、Emission 等端口。我们需要做的是:
- 把 Base Color 连到一个简单的颜色节点上,比如深蓝色 RGB(0.1, 0.3, 0.55)。
- 把 Smoothness 设置为 0.6,这个值影响后面卡通高光的锐利程度。
- 把 Metallic 设置为 0,风格化水面不需要金属感。
- Alpha 端口先连一个 Float 值 0.9,等后面实现浪花白沫时再替换。
这样你就得到了第一版“能看的水面”。不过它还是静止的,下一节开始加入动画和噪声。
4. 让水面动起来:UV 流动与双层噪声波浪
4.1 UV 流动是什么
在 Shader 中,模型的每个顶点都带有一组 UV 坐标,取值范围通常是 0 到 1。纹理采样就是通过 UV 坐标去查贴图颜色。如果我们在采样噪声之前,先让 UV 坐标随着时间持续改变,那么采样结果就会产生一种“贴图在流动”的视觉效果。
最常用的做法是把 Time 节点输出的秒数乘以一个速度系数,然后加到 UV 上。Shader Graph 中通过 Tiling And Offset 节点可以同时处理“缩放”和“偏移”。其中 Offset 接收二维向量,我们就用(Time * speed, Time * speed2)这样的形式驱动。
从视觉原理上讲,UV 流动等同于让整张噪声贴图沿着某方向移动。水面波浪往往不是单一方向直线漂移,所以我们会让两层噪声分别沿着不同方向流动,再叠加起来,看起来就会更自然。
4.2 第一层:大波浪(Gradient Noise)
在 Shader Graph 节点库中搜索Gradient Noise,它的输入有两个关键端口:
- UV:采样的坐标。
- Scale:噪声的整体缩放,数值越小噪声范围越大。
Gradient Noise(梯度噪声)的特点是变化连续、起伏圆滑,没有明显的方格块状感,非常适合做水面大面积波浪。相比之下,Value Noise 会出现明显网格状走势,做水面时要谨慎使用。
连线思路如下:
- 新建 UV 节点。
- 将 UV 连入 Tiling And Offset 节点,Tiling 设为 (0.3, 0.3),Offset 仍为 (0, 0),这个 Tiling 值越小,波浪幅度跨度越大。
- 再新建一个 Tiling And Offset,接收上一个 Tiling And Offset 的输出,Offset 连接
Time × 0.05。 - 将结果输入 Gradient Noise 的 UV,Scale 设置为 0.5 左右。
- 为了调节振幅,在 Gradient Noise 后接一个 Multiply 节点,乘以振幅值 0.2。
这一层的效果是水面缓慢的大范围起伏,视觉节奏比较慢。你可以在 Main Preview 里播放预览,观察大浪是否在移动。
4.3 第二层:细碎波纹
大波浪只能提供基础形态,真正让水面生动起来的是第二层“碎波”。它的思路和第一层完全一致,只是 Tiling 更大、速度更快、振幅更小:
- 新建第二个 UV 节点。
- 通过一个 Tiling And Offset 把 UV 放大到 (2.5, 2.5)。
- Offset 连接
Time × 0.3,让碎波快速流动。 - 采样 Gradient Noise,Scale 设置为 2.0。
- 输出乘以振幅 0.03。
这一层相当于在大波浪上叠加高频细节。两层噪声最终相加,可以得到一个带有多层次细节的高度值。这个高度值有两个作用:
- 叠加到模型顶点位置的 Y 分量上,让网格顶点真实起伏。
- 驱动颜色分区和浪花泡沫的阈值,形成风格化颜色变化。
4.4 顶点波浪:让网格本身起伏
如果只改变颜色不改变顶点位置,水面看起来就是在平面上流动的“油漆”,缺少立体感。要让水面真正凹凸不平,需要在 Vertex 阶段处理 Position。
在 Shader Graph 中,选中 Fragment 模块下方的 Vertex 选项,展开后可以看到 Position、Normal、Tangent 三个输出端口。我们需要修改的是 Position。
具体做法:
- 获取 Object 空间下的 Position 节点,类型选择 Object。
- 使用 Split 节点把 Position 拆成 R、G、B 三个分量,分别代表 X、Y、Z。
- 把 R 和 B 重新组合成二维向量,作为水面平面上的输入坐标。
- 用这个坐标参与多层噪声计算(也就是前面大波浪和细碎波纹的计算)。
- 把最终噪声高度值加到 Y 分量上。
- 使用 Combine 节点重新组合 XYZ,连到 Vertex Position。
这里有个容易被忽略的细节:顶点位置修改必须发生在 Vertex 阶段,而不是 Fragment 阶段。Shader Graph 里需要在顶部切换到 Vertex 模式。如果你只在 Fragment 阶段修改位置,预览窗口里水波形体不会发生任何变化。
实际制作中,水面网格的顶点密度决定了波浪的形状精度。Unity 默认 Plane 只有 10×10 的分段,做顶点位移后会出现明显的“折纸感”。建议使用高分段平面,或者在 Maya/Blender 中导出 50×50 分段的平面 FBX 使用。
5. 风格化着色:卡通高光与边缘光
5.1 色阶渐变代替物理 PBR 颜色
《风之杖》水面颜色最重要的特征是“分区明确”。它不像真实水面那样有连续的环境反射渐变,而是很直接地分成了深蓝、中蓝和亮蓝几个区域。这种阶梯状颜色过渡就是典型的 Toon Ramp 思路。
在 Shader Graph 中实现方式有很多,最简单也最容易理解的是用Smooth Step做阈值处理:
- 使用前面计算好的噪声高度值。
- 用 Smooth Step 节点设置最小值 0.45、最大值 0.55,将高度值映射为 0 到 1。
- 用 Lerp 节点在深蓝色 RGB(0.06, 0.25, 0.5) 和浅蓝色 RGB(0.2, 0.6, 0.85) 之间插值。
- 把 Lerp 结果连到 Base Color。
Smooth Step 的 Min 和 Max 控制的其实就是色阶过渡的软硬度。如果想让风格更硬朗,可以把 Min 和 Max 拉近,比如 0.48 和 0.52,这样过渡边缘更锐利。如果想让水面柔和一些,可以把 Min/Max 拉开,比如 0.4 和 0.6。
实际项目中,我建议用Sample Gradient节点配合 Gradient 参数来做颜色分区。Gradient 可以定义多个颜色标点,用同一个高度值采样,能实现深蓝、浅蓝、白沫三层甚至四层颜色,比单次 Lerp 更灵活。例如在高度 0 到 0.3 时取深蓝,0.3 到 0.7 时取中蓝,0.7 到 1.0 时取带青白色的亮蓝。
5.2 卡通高光
真实 PBR 高光往往是一个或大或小的连续高光斑,而卡通渲染的高光通常是边界清晰的亮面。在 Shader Graph 中,最常用的做法是:
- 使用
Main Light节点获取场景主光源的方向。 - 使用
Normal Vector节点获取当前像素法线。 - 使用
Dot Product计算主光方向和法线的点积,得到光照强度。 - 使用
Smooth Step设置一个较高阈值,例如 0.85 到 0.95,让点积结果二值化。 - 二值化结果乘以高光颜色(通常是白色或淡青色),加到 Base Color 上。
这里使用的是经典的卡通光照模型,也就是 NdotL。它与 PBR 的 Blinn-Phong 高光不同,不会依赖视角方向,所以无论玩家从哪个角度看,高光都稳定出现在同一个光照区域内。
如果你希望高光形状更集中在某个范围,可以在二值化后接一个Power节点,把指数调大。例如把二值化输出提高到 8 次方以上,高光范围会明显缩小,边缘更硬。
5.3 Fresnel 边缘光
水面有一种很自然的视觉现象:视线与水面夹角越接近水平,我们看到的水面反射越强,颜色越亮;垂直向下看时则相对较暗。这个效果可以用 Fresnel 节点模拟。
在 Shader Graph 中:
- 搜索并创建
Fresnel Effect节点。 - 把 Power 设置为 2.0 左右,控制边缘光的衰减速度。
- 把 Fresnel 输出乘以一个亮度系数,例如 0.6。
- 将结果叠加到 Base Color 上。
Fresnel Effect 节点内部会根据法线与视线方向的夹角计算强度,所以我们不需要额外连接法线和视线方向节点。它默认使用 Fragment 阶段的 Normal Vector 和 View Direction。
加上 Fresnel 后,当相机高度较低时,水面边缘会呈现一圈明显的亮蓝色,很接近《风之杖》里海平线附近水面反射阳光的感觉。这个值和上面卡通高光叠加时,需要注意总亮度不要超过 1,否则会丢失颜色层次。
5.4 浪花白沫
浪花是水面最具识别度的元素之一。它不需要所有波浪都变成白色,只需要在高振幅区域出现一些“泡沫”一样的亮色块。实现方法很简单:用噪声高度值做一个阈值判断。
- 取最终噪声高度值(两层噪声相加后的结果)。
- 使用
Step节点,设置阈值 0.75,高度超过 0.75 的部分输出 1,否则输出 0。 - Step 结果乘以白色 RGB(1, 1, 1)。
- 将结果叠加到 Base Color,同时把结果连到 Alpha 端口,让泡沫区域更不透明。
为了不让白沫边缘太硬,可以用 Smooth Step 替代 Step,例如 Min 0.7、Max 0.75,这样泡沫边缘会有一个小小的过渡带。白沫过多时,把阈值调高;白沫过少时,降低噪声振幅或调低阈值。
如果你打算做成更高级版本,还可以加入“水下深度”因素:在水面比较浅的区域,泡沫更明显;深海区域则很少泡沫。这需要额外采样场景深度,暂不作为本文的基础内容。
6. 完整 Shader Graph 连线结构与参数清单
6.1 核心连接逻辑参考
把前面的节点组合起来,整个 Shader Graph 的连线逻辑可以总结成以下过程:
UV → TilingAndOffset(波浪UV) → GradientNoise → 大波浪高度 UV → TilingAndOffset(碎波UV) → GradientNoise → 碎波高度 大波浪高度 + 碎波高度 = 最终高度 最终高度 → Vertex Position.y(顶点起伏) 最终高度 → SmoothStep → Lerp(深蓝, 浅蓝) → Base Color 最终高度 → SmoothStep(硬阈值) → 白色 → Base Color叠加 + Alpha MainLight方向 · Normal → SmoothStep → 卡通高光 → Base Color叠加 Fresnel → 系数 → Base Color叠加实际连节点时,建议从下往上搭:先做顶点位移,再做颜色,再做高光和边缘光,最后加入泡沫。这样每一步都能在预览窗口看到清晰结果,定位问题也容易。
6.2 参数暴露与命名建议
Shader Graph 左侧 Blackboard 中创建的 Property 会自动显示在材质 Inspector 上。为了后续调整方便,建议要把这些核心参数暴露出来,并设置合理的默认值:
WaveStrength(Float,默认 0.2):大波浪振幅。WaveSpeed(Float,默认 0.05):大波浪流动速度。WaveTiling(Float,默认 0.3):大波浪尺度。DetailStrength(Float,默认 0.03):碎波振幅。DetailSpeed(Float,默认 0.3):碎波速度。DeepColor(Color,默认 RGB(0.06, 0.25, 0.5)):深水颜色。ShallowColor(Color,默认 RGB(0.2, 0.6, 0.85)):浅水颜色。FoamThreshold(Float,默认 0.75):浪花白沫阈值。SpecularPower(Float,默认 8):卡通高光指数。FresnelPower(Float,默认 2):边缘光强度控制。
养成暴露参数的习惯很重要。如果你把所有数值都写成固定节点常量,后面美术同学调整时会非常痛苦。Shader Graph 的一大优势就是可以让美术在不打开 Shader 的情况下直接调材质参数。
6.3 在 Inspector 中调节材质参数
创建好 Shader Graph 后,在 Project 窗口中找到它,右键选择Create > Material,然后把材质拖到场景中的 Plane 上。选中材质,在 Inspector 中可以看到刚才暴露的参数。
一个比较实用的调参顺序是:
- 先调
WaveTiling和WaveStrength,找到你想要的波浪尺度。 - 再调
DeepColor和ShallowColor,确定整体色调。 - 之后调
FoamThreshold,控制泡沫范围。 - 最后调
FresnelPower和SpecularPower,补足高光和边缘光的强度。
在调整时建议打开 Scene 视图的“材质预览”模式,让灯光从侧面打来,方便观察高光形状。
7. 常见问题与排查思路
7.1 材质全部变成紫红色
这是使用 URP 后最常见的现象。紫红色通常意味着 Shader 没有成功编译,或者当前 Shader 与渲染管线不兼容。
可能的原因有:
- 工程还没有切换为 URP,但 Shader Graph 创建的是 URP Lit Shader。
- Shader Graph 文件存在节点报错,导致编译失败。
- 当前 GPU 不支持 Shader 所需的特性。
解决办法是:先确认Project Settings > Graphics是否已经指定 URP Asset;再打开 Shader Graph 看是否有红色报错节点;最后检查 Console 窗口的具体报错信息。如果 Shader Graph 里某些节点版本不兼容,Unity 通常会提示你升级或替换。
7.2 打开工程出现 D3D11 GPU 错误
在很多 Unity 新版本中,启动时会看到类似 “A D3D11-compatible GPU (Feature Level 11.0, Shader Model 5.0) is required to run the Unity Graphics” 的报错。这个报错并不一定说明你的显卡坏了,而是当前环境(比如虚拟机、远程桌面、老显卡)无法提供支持 Shader Model 5.0 的 D3D11 硬件能力。
常规排查思路:
- 更新显卡驱动。
- 在
Player Settings > Graphics APIs中尝试删掉 D3D12,只保留 D3D11,或者反过来尝试。 - 如果是虚拟机或远程桌面环境,考虑切换到本机物理显卡运行。
- 如果只是预览工程,也可以把渲染 API 改为 OpenGL 或 Vulkan 试试。
注意,这不是项目代码的问题,更多是环境兼容性问题。Shader Graph 的某些节点要求 SM5.0,在低端设备上可以改用 Simple Noise 替代 Gradient Noise,降低硬件要求。
7.3 水面波浪没有动画
如果材质已经正常显示,但水面完全静止,优先检查两处:
- Time 节点是否连接到了 Tiling And Offset 的 Offset 输入。很多人会忘记把 Time 放大或变小,这里如果直接连 Time,速度可能会太快,让人误以为没动。
- 顶点位移部分是否在 Vertex 阶段操作。如果你只在 Fragment 阶段修改了 Position,预览里颜色可能变了但网格不变。
另外,Shader Graph 的预览窗口依赖于编辑器运行态时间,在某些情况下需要点击播放按钮或手动移动场景相机才能看到动画更新。
7.4 水面过暗或过亮
水面过暗通常是因为场景中没有足够的光照,或者材质使用了 Lit Shader 但主光源被阴影遮挡。可以先用 Unlit Shader Graph 快速验证噪声和颜色本身是否正常,再切回 Lit 查看光照效果。
水面过亮则通常是因为泡沫和 Fresnel 叠加过了。颜色输出值一旦超过 1,就会进入 HDR 范围,产生刺眼的高光。建议把 Base Color 的最终结果控制在 0.6 到 0.9 之间,给高光预留余地。
7.5 浪花白沫块状感明显
泡沫阈值过于生硬时,白沫会像一块块白色瓷砖,而不是自然的碎浪。解决思路有两个:
- 把 Step 换成 Smooth Step,增加过渡宽度。
- 在泡沫阈值判断前,对噪声增加一次模糊或平滑,可以用 Posterize 节点降低灰度级,让高度变化更柔和。
另外,噪声 Scale 越大,白沫分布越碎;Scale 越小,白沫越呈片状。想要自然海面的泡沫,密度需要多试几次。
8. 最佳实践与渲染性能建议
8.1 节点数量与性能平衡
Shader Graph 用起来方便,但如果无节制地堆节点,编译出来的 Shader 性能可能会很差,尤其在移动平台上。对水面来说,两到三层噪声通常是性价比最高的方案:
- 第一层 Gradient Noise 负责大波浪。
- 第二层 Gradient Noise 负责碎波。
- 如果还不够,可以第三层用 Simple Noise 做细节扰动。
层数越多,码率越高,GPU 压力越大。手机上建议不超过三层,PC 上可以适当增加。如果你的目标机型是低端安卓,优先考虑用 Simple Noise 替换 Gradient Noise,虽然视觉效果稍微生硬一点,但性能改善明显。
8.2 移动端兼容性设置
发布到移动端时,要注意 Surface 设置为 Transparent 会带来额外的 Overdraw 和混合开销。水面面积大、半透明层级多时尤其明显。建议:
- 水面的 Render Queue 设置在 Transparent 范围内。
- 尽量减少水面的重叠层数,例如不要用多个半透明水面贴片叠在一起。
- 在 Player Settings 中开启 Multithreaded Rendering,对部分平台有帮助。
- 如果项目有前后景分层需求,尽量用深度写入技巧替代多张半透明网格。
8.3 与角色交互的水面涟漪
《风之杖》里林克的小船划过水面时,船尾会拉起一道白色的浪痕。这个效果严格来说不是水面材质本身能独立完成的,它需要与角色/小船进行交互检测。
常见实现方案有:
- UV 偏移法:当小船移动时,在船尾位置持续向水面 Shader 写入一组 UV 坐标,让泡沫噪声在指定位置增强。
- 世界位置法:传递小船的世界坐标到 Shader 中,在世界空间下计算一个距离场,距离小船越近的片元,泡沫阈值越低。
- Renderer Texture 法:把实时绘制的浪花图案渲染到 Render Texture,水面采样这张贴图。
这三种方法各有优缺点。第一种最简单,适合原型;第三种最灵活,但要多一个相机或绘制 pass。如果你准备做完整的航海玩法,建议从方案二开始,它的可控性和复用性都比较好。
8.4 规范化参数管理
风格化水面的参数很多,如果不集中管理,美术调起来会觉得“每个材质都要单独调一遍”。实际项目中可以用一个 C# 脚本把多个水面材质的相同参数统一驱动。
例如下面的脚本可以放在场景中的空物体上,用来按全局速度播放水面动画:
// 文件路径:Assets/Scripts/WaterFlowController.cs using UnityEngine; public class WaterFlowController : MonoBehaviour { [Range(0f, 5f)] public float globalSpeed = 1f; public Material[] waterMaterials; private readonly int waveSpeedID = Shader.PropertyToID("_WaveSpeed"); private readonly int detailSpeedID = Shader.PropertyToID("_DetailSpeed"); private void Update() { foreach (Material mat in waterMaterials) { if (mat != null) { mat.SetFloat(waveSpeedID, globalSpeed * 0.05f); mat.SetFloat(detailSpeedID, globalSpeed * 0.3f); } } } }这样项目里所有用到水面材质的地方,只需要改一个globalSpeed,全局水面动画速度都会同步变化。类似的思路也可以用于颜色、泡沫阈值等参数的批量管理。
9. 下一步学习路线
本文完成的水面只是塞尔达风格化渲染的一个起点。如果你想把这套水面真正放到一个完整的游戏场景中,还应该继续深入以下几个方面:
- 深度与水面交互:读取场景深度,让水面在水下地形较浅的地方颜色更浅,甚至可以模拟角色落水时溅起水花。
- 风格化天空与光照:《风之杖》的水面好看,很大程度依赖于它那套明亮干净的卡通天空盒和高光环境。URP 中的 Ambient 和 Reflection Probe 对水面影响明显,值得单独调优。
- 手写 Shader 对比学习:Shader Graph 对理解“效果由哪些节点组成”很有帮助,但想深入优化性能、理解底层逻辑,建议再用 HLSL 手写一遍简化版水面 Shader。例如下面这段代码展示的是高度噪声的核心思路:
// 文件路径:Assets/Shaders/WaterNoise.hlsl(示意片段,供理解思路) #ifndef WATER_NOISE_INCLUDED #define WATER_NOISE_INCLUDED float hash21(float2 p) { p = frac(p * float2(123.34, 456.21)); p += dot(p, p + 45.32); return frac(p.x * p.y); } float noise2D(float2 uv) { float2 i = floor(uv); float2 f = frac(uv); float2 u = f * f * (3.0 - 2.0 * f); float a = hash21(i); float b = hash21(i + float2(1.0, 0.0)); float c = hash21(i + float2(0.0, 1.0)); float d = hash21(i + float2(1.0, 1.0)); return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y); } void WaterHeight_float(float2 uv, float time, out float height) { float h1 = noise2D(uv * 0.5 + float2(time * 0.05, 0.0)); float h2 = noise2D(uv * 2.5 + float2(0.0, time * 0.3)); height = (h1 + h2 * 0.3) * 0.5; } #endif这段 HLSL 的函数可以放到 Shader Graph 的 Custom Function 节点中使用,也可以直接在手写 Shader 中调用。它的价值在于让你看清噪声和时间的组合方式。
下一步的实践任务可以这样安排:在本文水面基础上,添加一个简易的小船模型,实现“船尾拖浪花”的效果;然后尝试把颜色从两层改成四层 Gradient 色带,观察画面风格变化;最后把水面放到昼夜循环场景中,配合 URP 的灯光组件验证不同光照下的表现。
风格化渲染最有趣的地方就在于“少即是多”——不需要追求完全真实的物理计算,而是用几个关键节点组合出高识别度的视觉印象。塞尔达风之杖的水面正是这种思路的典型范例,希望这篇文章能帮你打开更多自定义风格的大门。如果本文对你有帮助,记得收藏备用,后面我会继续更新角色交互涟漪和风格化天空相关的内容。