Unity URP与Shader Graph实战:打造写实水面效果(含边缘泡沫与动态波浪)
2026/8/6 15:43:04 网站建设 项目流程

1. 项目概述:从游戏场景到技术实现

看到《原神》里璃月港波光粼粼的水面,那种写实感与氛围感,是不是让你也想在自己的Unity项目里复刻一个?无论是开放世界的地图水体,还是庭院场景中的一池静水,写实的水面效果都是提升沉浸感的关键。今天,我们不谈复杂的底层Shader代码,就用Unity的Universal Render Pipeline和可视化工具Shader Graph,来一步步拆解如何实现一个包含边缘泡沫动态波浪的写实水面。

这个方案的核心优势在于“可视化”和“管线适配”。URP是Unity当前及未来的主流渲染管线,而Shader Graph让你无需编写HLSL代码,通过连线就能构建着色器,大大降低了图形编程的门槛。我们将实现的效果包括:基于法线贴图的基础波浪起伏、随深度变化的透明与颜色、模拟岸边撞击的边缘泡沫,以及可配置的风力波浪参数。无论你是想为自己的独立游戏增添一抹亮色,还是为数字孪生或虚拟场景构建逼真环境,这套方法都能提供一个扎实的起点。

2. 核心思路与Shader Graph框架搭建

2.1 为什么选择URP + Shader Graph组合?

在动手之前,先理清选型逻辑。Unity的Built-in渲染管线虽然经典,但在现代图形效果和跨平台优化上已显乏力。URP作为轻量级、可编程的脚本化渲染管线,不仅性能更优,而且与Shader Graph深度集成。Shader Graph将着色器算法节点化,其最大价值并非取代代码,而是提供了一种直观的“原型设计”和“逻辑可视化”能力。对于水面这种涉及多重纹理混合、顶点偏移、菲涅尔效应的复杂效果,用节点连线来构建和调试,比反复修改代码并编译要高效得多,尤其适合美术和技术美术同学快速迭代。

我们的目标是创建一个URP下的Unlit Shader Graph(实际上会使用PBR Lit或自定义光照模型以获得更好效果,但Unlit更易于理解基础结构),并为其构建几个核心的功能模块。

2.2 Shader Graph资产创建与基础设置

首先,在Unity项目中确保已安装URP和Shader Graph包。在Project窗口右键,选择Create > Shader Graph > URP,然后根据是否需要复杂光照,选择Lit Shader GraphUnlit Shader Graph。对于写实水面,我强烈建议从Lit Shader Graph开始,因为它内置了与URP光照系统的交互,能更自然地反射环境光、天空盒,实现更真实的镜面高光。

创建后,双击打开Shader Graph编辑器。我们先搭建一个最基础的框架:

  1. Master Stack(主堆栈):这是输出最终颜色的地方。我们会主要操作Vertex Position(顶点位置,用于波浪)、Base Color(基础颜色)、Normal(法线)、Smoothness(光滑度)、Emission(自发光,可用于泡沫高光)等端口。
  2. 创建Properties(属性):在Blackboard(黑板)区域,创建一系列可在材质球上调节的参数。这是实现可配置性的关键。初期必备的属性包括:
    • _BaseColor(Color): 水体基础色。
    • _DepthColor(Color): 深水区颜色。
    • _DepthMaxDistance(Float): 颜色过渡的最大深度。
    • _WaveNormalMap(Texture 2D): 用于模拟波浪细节的法线贴图。
    • _WaveSpeed(Vector2): 法线贴图UV滚动的速度。
    • _WaveStrength(Float): 波浪的强度系数。
    • _FoamTexture(Texture 2D): 泡沫纹理。
    • _FoamDepth(Float): 泡沫出现的深度阈值。

这些属性将成为我们整个效果的调节旋钮。

3. 核心效果模块深度解析

3.1 动态波浪起伏的实现原理

静态的水面是没有灵魂的。波浪起伏通过两种方式结合实现:顶点偏移法线扰动

顶点偏移模拟的是水体表面大范围的起伏形状,比如涌动的海浪。在Shader Graph中,我们通过修改Position节点的输出到Master StackVertex Position来实现。一个经典的方法是使用正弦波(Sine)或噪声图(Noise)来驱动Y轴(高度)的偏移。例如,我们可以用Time节点结合Position节点的XZ坐标,输入到Sine节点,再乘以一个控制波浪强度和频率的参数,最后叠加到原始顶点Y坐标上。为了模拟更自然的多重波浪,通常会使用两套不同频率和速度的正弦波进行叠加。

法线扰动则负责模拟水面细腻的波纹、涟漪等高频细节,它不改变网格形状,但通过改变法线方向极大地影响光照计算,从而产生波光粼粼的视觉效果。这是通过滚动(Scrolling)一张或两张法线贴图(Normal Map)的UV坐标来实现的。具体步骤:

  1. 使用Tiling And Offset节点对UV进行平铺,控制波纹密度。
  2. 使用Time节点和Multiply节点生成一个随时间变化的偏移向量,与UV相加,实现滚动。
  3. 将滚动后的UV采样_WaveNormalMap,得到法线信息。
  4. 通常我们会使用两张法线贴图,以不同的速度和方向滚动,然后通过Blend Normals节点混合,这样能打破重复感,创造出无限变化的复杂波纹。

实操心得:顶点偏移不宜过强,尤其是对于大面积水面,过强的顶点位移可能导致远处裁剪或视觉失真。通常将主要视觉变化交给法线扰动。两张法线贴图的速度向量最好不是倍数关系,例如一个用 (0.05, 0.03),另一个用 (-0.03, 0.04),这样混合出的动态效果更随机、自然。

3.2 深度渐变与透明度控制

真实的水体不是单一颜色,靠近岸边浅水区清澈见底,到深水区逐渐变为深蓝或深绿。同时,透明度也随深度增加而降低。这个效果的核心是获取水面某一点到水底(或某个深度阈值)的距离。

在URP Shader Graph中,我们可以利用Scene Depth节点。该节点能获取当前像素对应的场景深度值(即到相机的距离)。但我们需要的是水面点到水底物体的垂直深度。一个常用技巧是:

  1. 在渲染水面时,通过一个Position节点(设置为World空间)获取水面当前点的世界坐标。
  2. 从该点垂直向下(-Y轴)发射一条射线,理论上其终点坐标的Y值应设为水底平面高度。但更通用的方法是:利用相机视角下的深度差。
  3. 更实用的方案是:使用Scene Depth节点获取当前像素处的场景深度(可能是水底或水下物体),同时用Screen Position节点和矩阵变换反算出水面当前像素的世界空间深度,两者相减,得到一个近似的“垂直深度差”。虽然这在非垂直视角下是斜边长度而非垂直深度,但视觉上通常可以接受,且计算高效。

得到深度值(我们记为depth)后,流程就简单了:

  1. 颜色混合:使用SmoothstepLerp(线性插值)节点。将depth除以_DepthMaxDistance进行归一化(并钳制在0-1),用这个值作为插值系数,在_BaseColor(浅水色)和_DepthColor(深水色)之间进行插值。
  2. 透明度控制:同样基于归一化的深度值,可以创建一个透明度曲线。例如,深度很浅时完全透明(alpha=0),深度达到某个阈值时变为半透明(alpha=0.5),深度很大时基本不透明(alpha=0.8)。这可以通过一个Remap节点或自定义的Curve节点来控制。

3.3 边缘泡沫效果的技术拆解

岸边泡沫是写实水面的点睛之笔,它模拟了波浪拍岸或水体与物体交界处,由于湍流和空气卷入产生的白色泡沫。其实现原理同样是基于深度,但逻辑相反:在深度非常浅(即水面几乎接触水底或岸边模型)的区域,显示泡沫纹理。

实现步骤:

  1. 深度检测:使用上述方法计算出的depth值。
  2. 泡沫遮罩生成:用_FoamDepth作为一个阈值。使用Smoothstep节点,当depth小于_FoamDepth时,输出从0到1的平滑过渡值,作为泡沫的强度遮罩(foamMask)。Smoothstep比简单的Step节点效果更柔和,边缘不生硬。
  3. 添加细节与动态:直接用foamMask显示白色会显得很假。我们需要引入泡沫纹理_FoamTexture。将世界坐标的XZ分量(或滚动后的UV)用于采样泡沫纹理,得到一个泡沫噪声图(foamNoise)。然后,将foamMaskfoamNoise相乘,这样泡沫只出现在浅水区,并且具有纹理细节。
  4. 融入最终颜色:将上一步得到的泡沫强度值,用于混合水体颜色和泡沫颜色(通常是白色或浅灰色)。可以叠加到Base Color上,或者更常见的是添加到Emission(自发光)通道,让泡沫看起来更亮、更醒目,不受光照影响。为了增加动态感,可以让泡沫纹理的UV也随时间缓慢滚动。

注意事项:边缘泡沫的效果高度依赖场景的碰撞体(Collider)或网格的几何形状。如果岸边是一个陡峭的垂直面,计算出的深度可能不会很小,导致泡沫不出现。因此,水底的模型需要尽可能贴合你希望出现泡沫的区域。有时,美术需要专门为水体制作一个简化的、略微倾斜的“水底碰撞面”来驱动泡沫效果。

4. 完整Shader Graph节点搭建实操

4.1 节点网络布局与信号流

让我们将上述模块用节点连接起来。建议在Shader Graph编辑器中分区域布局,例如左上角放置属性参数,左中区域处理UV和Time,中间主要区域搭建颜色和法线逻辑,右侧连接Master Stack。

信号流主干道如下:

  1. Time & UV流:创建Time节点,输出Time端口。为法线贴图和泡沫纹理创建各自的UV流,分别与Time做运算实现滚动。
  2. 法线生成流
    • 流程A:UV + Time * Speed_A -> Sample Texture 2D (Normal Map A) -> Normal Unpack。
    • 流程B:UV + Time * Speed_B -> Sample Texture 2D (Normal Map B) -> Normal Unpack。
    • 将两个解包后的法线通过Blend Normals节点混合,输出到Master Stack的Normal端口。
  3. 深度计算流
    • Scene Depth节点 -> 输出Eye深度。
    • Screen Position节点(设置为World空间)可辅助计算,或使用自定义节点计算水面深度。简单起见,可以假设水面是平面,用相机高度差估算。更精确的方法需要编写自定义节点函数。
    • 两者相减得到rawDepth
  4. 颜色混合流
    • rawDepth-> 除以_DepthMaxDistance->Saturate(钳制)得到depthFactor
    • depthFactor输入Lerp节点,A端接_BaseColor,B端接_DepthColor,输出基础水色waterColor
  5. 泡沫生成流
    • rawDepth_FoamDepth输入Smoothstep节点,得到foamMask
    • 采样_FoamTexture(使用另一套滚动的UV)得到foamNoise
    • foamMask*foamNoise->Power节点(可选,用于调整对比度)得到finalFoam
    • finalFoam与白色相乘,输出到Master Stack的Emission端口。同时,也可以用Lerp将泡沫颜色混合进waterColor
  6. Master Stack连接
    • Base Color: 连接最终混合了泡沫颜色的waterColor
    • Normal: 连接混合后的法线。
    • Emission: 连接finalFoam(或泡沫颜色)。
    • Smoothness: 可以连接一个较高的常量(如0.9),让水面光滑。
    • Alpha: 连接由depthFactor通过RemapCurve控制后的透明度值。

4.2 关键参数详解与调节指南

参数调节是让效果从“能用”到“好看”的关键。以下是一份参考配置及调节逻辑:

参数名建议初始值调节逻辑与影响
_BaseColorRGBA(0.1, 0.3, 0.5, 0.8)控制浅水区颜色。偏向青蓝色调更显清澈。Alpha值影响基础透明度。
_DepthColorRGBA(0.0, 0.1, 0.2, 0.9)控制深水区颜色。通常比基础色更深、饱和度更低。
_DepthMaxDistance10颜色从浅到深过渡的深度范围。值越小,颜色变化越剧烈,显得水很浅;值越大,过渡越平缓,显得水深。
_WaveNormalMap一张蓝紫色法线贴图建议使用无缝平铺的、细节丰富的法线贴图。可以准备两套不同尺度的贴图混合。
_WaveSpeed(0.03, 0.05), (-0.02, 0.04)X和Y分量分别控制U和V方向的滚动速度。正负号控制方向。两套速度差异越大,波纹越复杂。
_WaveStrength1.0控制法线扰动的强度。值太大会导致水面看起来像沸腾的油,太小则没有波澜。通常0.5-2.0之间调节。
_FoamTexture一张黑白噪声或泡沫细节图白色代表泡沫。使用Voronoi或Perlin噪声图效果不错。
_FoamDepth0.5泡沫开始出现的深度阈值。这是世界空间单位(米)。值越小,泡沫越集中在极浅的区域。通常0.1-1.0之间。
_FoamSpeed(0.01, 0.0)泡沫纹理的滚动速度。通常比波浪速度慢,模拟泡沫随波逐流的感觉。

调节顺序建议:先关闭泡沫,调节好波浪法线的速度和强度,让水面有基本的动态。然后调节深度颜色和过渡,确定水体的视觉深度。最后开启泡沫,精细调节泡沫深度和纹理,使其自然出现在岸边和岩石周围。

5. 材质与场景配置实战

5.1 材质球设置与性能考量

Shader Graph编译完成后,会生成一个.shadergraph文件和一个对应的.shader文件。右键.shadergraph文件,选择Create > Material,即可创建使用该着色器的材质球。将材质球拖拽到场景中的水面模型(通常是一个Plane或自定义的网格)上。

在材质球Inspector面板,你可以看到之前在Blackboard中定义的所有属性,并可以实时调节。这里有几个关键设置:

  1. 渲染队列(Render Queue):水面通常需要半透明混合。在材质的Advanced Options中,将Render Queue设置为Transparent(3000),或者自定义一个值,确保水面在 opaque(不透明)物体之后渲染,才能正确混合。
  2. 混合模式(Blend Mode):选择Alpha Blend(SrcAlpha, OneMinusSrcAlpha)来实现标准的半透明叠加。
  3. 深度写入(Depth Write):通常设置为Off。因为半透明物体不写入深度,可以避免后续透明物体之间的深度排序问题。但这可能导致水面内部重叠渲染顺序错误,如果出现奇怪的黑斑,可以尝试开启。
  4. 双面渲染(Double Sided):建议开启,因为玩家有可能从水下往上看。

性能提示:Shader Graph虽然方便,但节点过多、采样次数过多也会增加GPU负担。我们的实现中,采样了2次法线贴图、1次泡沫纹理、1次场景深度(这是一个全屏后处理采样,开销较大)。在移动端或低端设备上,可以考虑简化:只使用一张法线贴图,或者用顶点颜色代替深度计算来模拟简单的岸边渐变。

5.2 场景灯光与环境反射配置

写实水面极度依赖环境光照和反射。在URP中,你需要:

  1. 配置URP Asset:确保场景使用的URP Asset中,开启了Screen Space Ambient Occlusion (SSAO)Screen Space Reflections (SSR)。SSR能让水面反射场景中的其他物体,这是真实感的关键。虽然性能开销大,但对于静止或缓慢移动的水面,可以带来质的提升。
  2. 使用反射探针(Reflection Probe):对于动态物体或SSR无法覆盖的区域(如天空盒),反射探针是必不可少的。在水面中央或重要区域放置一个反射探针,并将其类型设置为RealtimeBaked。Realtime探针每帧更新,效果最好但性能最差;Baked探针需要烘焙光照贴图,适合静态场景。
  3. 设置天空盒(Skybox):一个高质量的HDRI天空盒能为水面提供丰富的环境光和反射源。在Lighting窗口的Environment选项卡中,指定一个合适的天空盒材质。
  4. 添加方向光(Directional Light):模拟太阳光。调整光的方向和强度,观察水面高光的变化。在Shader Graph中,我们使用了Lit Master,所以高光会自动根据场景灯光计算。

6. 常见问题排查与优化技巧

6.1 视觉效果问题与修复

在实际操作中,你可能会遇到以下典型问题:

问题现象可能原因解决方案
水面一片纯黑或不显示1. Shader编译错误。
2. 渲染队列设置错误,被其他物体遮挡。
3. 深度计算错误,导致像素被丢弃。
1. 检查Shader Graph是否有红色报错节点。
2. 将材质Render Queue设为Transparent,并检查模型缩放是否为负值。
3. 简化Shader,先屏蔽深度和泡沫相关节点,检查基础颜色和法线是否正常显示。
边缘泡沫不出现或全屏都是泡沫1._FoamDepth阈值设置不当。
2. 深度计算值异常(过大或过小)。
3. 水底模型缺失或位置不对。
1. 将_FoamDepth值调小(如0.1)观察泡沫是否集中在边缘。
2. 创建一个简单的Debug模式,将计算出的rawDepth直接输出为颜色,检查其数值范围是否合理(浅水区接近0,深水区增大)。
3. 确保水面模型下方有碰撞体或地形模型。
水面闪烁或Z-fighting水面模型与地面模型完全共面或极其接近。将水面模型在Y轴上略微抬高(如0.01个单位),使其略高于地面。
反射效果很弱或奇怪1. 未启用SSR或反射探针。
2. 水面Smoothness值太低。
3. 环境光太暗。
1. 在URP Asset中检查SSR设置,并确保场景中有激活的反射探针。
2. 将材质或Shader中的Smoothness参数调高(接近1.0)。
3. 增强方向光强度,或使用更亮的天空盒。
波浪看起来不自然、有重复感1. 法线贴图平铺次数太少。
2. 只使用了一张法线贴图滚动。
1. 在Tiling And Offset节点中增加UV的Tiling值(如从1改为5)。
2. 务必使用两张法线贴图,以不同速度和方向混合。

6.2 性能优化与进阶思路

当效果基本满意后,可以考虑以下优化和增强:

  1. LOD(多层次细节):为远处的水面使用简化版的Shader(例如,去掉泡沫、使用更简单的波浪计算、降低纹理采样次数)。这可以通过编写多个不同复杂度的Shader Graph,或在一个Shader中使用#ifdef指令配合Quality Settings来实现。
  2. 水面交互涟漪:通过渲染到纹理(Render Texture)的方式,实现物体(如船、角色)与水面的交互涟漪。这是一个相对高级的主题,需要在项目中创建一个摄像机渲染高度图到Render Texture,然后在Shader Graph中采样这个纹理来动态偏移顶点或法线。
  3. 焦散(Caustics):模拟水底的光斑效果。可以通过在Shader中,基于法线信息投影和滚动一张焦散纹理到水底物体上来近似实现。
  4. 水下效果:当相机进入水下时,切换后处理效果(如颜色偏移、模糊、边缘暗角)。这通常通过触发器(Trigger)检测相机位置,并启用不同的Volume来实现。

从《原神》璃月港的惊艳水面到你自己项目中的一池春水,距离并没有想象中遥远。通过URP和Shader Graph这套组合拳,我们系统地拆解了写实水面的构建过程:从动态波浪、深度渐变到边缘泡沫。整个过程的关键在于理解每个视觉效果背后的物理或视觉原理,并用节点化的思维去实现它。我个人的体会是,Shader Graph最大的魅力在于“即时反馈”,你可以一边调节参数,一边在场景中实时看到变化,这种迭代效率是传统编码无法比拟的。最后,别忘了多参考现实中的水体照片或视频,观察光线、颜色和波浪的细节,不断微调参数,才能让你数字世界中的水面真正“活”起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询