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 Graph或Unlit Shader Graph。对于写实水面,我强烈建议从Lit Shader Graph开始,因为它内置了与URP光照系统的交互,能更自然地反射环境光、天空盒,实现更真实的镜面高光。
创建后,双击打开Shader Graph编辑器。我们先搭建一个最基础的框架:
- Master Stack(主堆栈):这是输出最终颜色的地方。我们会主要操作Vertex Position(顶点位置,用于波浪)、Base Color(基础颜色)、Normal(法线)、Smoothness(光滑度)、Emission(自发光,可用于泡沫高光)等端口。
- 创建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 Stack的Vertex Position来实现。一个经典的方法是使用正弦波(Sine)或噪声图(Noise)来驱动Y轴(高度)的偏移。例如,我们可以用Time节点结合Position节点的XZ坐标,输入到Sine节点,再乘以一个控制波浪强度和频率的参数,最后叠加到原始顶点Y坐标上。为了模拟更自然的多重波浪,通常会使用两套不同频率和速度的正弦波进行叠加。
法线扰动则负责模拟水面细腻的波纹、涟漪等高频细节,它不改变网格形状,但通过改变法线方向极大地影响光照计算,从而产生波光粼粼的视觉效果。这是通过滚动(Scrolling)一张或两张法线贴图(Normal Map)的UV坐标来实现的。具体步骤:
- 使用Tiling And Offset节点对UV进行平铺,控制波纹密度。
- 使用Time节点和Multiply节点生成一个随时间变化的偏移向量,与UV相加,实现滚动。
- 将滚动后的UV采样
_WaveNormalMap,得到法线信息。 - 通常我们会使用两张法线贴图,以不同的速度和方向滚动,然后通过Blend Normals节点混合,这样能打破重复感,创造出无限变化的复杂波纹。
实操心得:顶点偏移不宜过强,尤其是对于大面积水面,过强的顶点位移可能导致远处裁剪或视觉失真。通常将主要视觉变化交给法线扰动。两张法线贴图的速度向量最好不是倍数关系,例如一个用 (0.05, 0.03),另一个用 (-0.03, 0.04),这样混合出的动态效果更随机、自然。
3.2 深度渐变与透明度控制
真实的水体不是单一颜色,靠近岸边浅水区清澈见底,到深水区逐渐变为深蓝或深绿。同时,透明度也随深度增加而降低。这个效果的核心是获取水面某一点到水底(或某个深度阈值)的距离。
在URP Shader Graph中,我们可以利用Scene Depth节点。该节点能获取当前像素对应的场景深度值(即到相机的距离)。但我们需要的是水面点到水底物体的垂直深度。一个常用技巧是:
- 在渲染水面时,通过一个Position节点(设置为World空间)获取水面当前点的世界坐标。
- 从该点垂直向下(-Y轴)发射一条射线,理论上其终点坐标的Y值应设为水底平面高度。但更通用的方法是:利用相机视角下的深度差。
- 更实用的方案是:使用Scene Depth节点获取当前像素处的场景深度(可能是水底或水下物体),同时用Screen Position节点和矩阵变换反算出水面当前像素的世界空间深度,两者相减,得到一个近似的“垂直深度差”。虽然这在非垂直视角下是斜边长度而非垂直深度,但视觉上通常可以接受,且计算高效。
得到深度值(我们记为depth)后,流程就简单了:
- 颜色混合:使用
Smoothstep或Lerp(线性插值)节点。将depth除以_DepthMaxDistance进行归一化(并钳制在0-1),用这个值作为插值系数,在_BaseColor(浅水色)和_DepthColor(深水色)之间进行插值。 - 透明度控制:同样基于归一化的深度值,可以创建一个透明度曲线。例如,深度很浅时完全透明(alpha=0),深度达到某个阈值时变为半透明(alpha=0.5),深度很大时基本不透明(alpha=0.8)。这可以通过一个Remap节点或自定义的Curve节点来控制。
3.3 边缘泡沫效果的技术拆解
岸边泡沫是写实水面的点睛之笔,它模拟了波浪拍岸或水体与物体交界处,由于湍流和空气卷入产生的白色泡沫。其实现原理同样是基于深度,但逻辑相反:在深度非常浅(即水面几乎接触水底或岸边模型)的区域,显示泡沫纹理。
实现步骤:
- 深度检测:使用上述方法计算出的
depth值。 - 泡沫遮罩生成:用
_FoamDepth作为一个阈值。使用Smoothstep节点,当depth小于_FoamDepth时,输出从0到1的平滑过渡值,作为泡沫的强度遮罩(foamMask)。Smoothstep比简单的Step节点效果更柔和,边缘不生硬。 - 添加细节与动态:直接用
foamMask显示白色会显得很假。我们需要引入泡沫纹理_FoamTexture。将世界坐标的XZ分量(或滚动后的UV)用于采样泡沫纹理,得到一个泡沫噪声图(foamNoise)。然后,将foamMask与foamNoise相乘,这样泡沫只出现在浅水区,并且具有纹理细节。 - 融入最终颜色:将上一步得到的泡沫强度值,用于混合水体颜色和泡沫颜色(通常是白色或浅灰色)。可以叠加到Base Color上,或者更常见的是添加到Emission(自发光)通道,让泡沫看起来更亮、更醒目,不受光照影响。为了增加动态感,可以让泡沫纹理的UV也随时间缓慢滚动。
注意事项:边缘泡沫的效果高度依赖场景的碰撞体(Collider)或网格的几何形状。如果岸边是一个陡峭的垂直面,计算出的深度可能不会很小,导致泡沫不出现。因此,水底的模型需要尽可能贴合你希望出现泡沫的区域。有时,美术需要专门为水体制作一个简化的、略微倾斜的“水底碰撞面”来驱动泡沫效果。
4. 完整Shader Graph节点搭建实操
4.1 节点网络布局与信号流
让我们将上述模块用节点连接起来。建议在Shader Graph编辑器中分区域布局,例如左上角放置属性参数,左中区域处理UV和Time,中间主要区域搭建颜色和法线逻辑,右侧连接Master Stack。
信号流主干道如下:
- Time & UV流:创建Time节点,输出Time端口。为法线贴图和泡沫纹理创建各自的UV流,分别与Time做运算实现滚动。
- 法线生成流:
- 流程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端口。
- 深度计算流:
- Scene Depth节点 -> 输出Eye深度。
- Screen Position节点(设置为World空间)可辅助计算,或使用自定义节点计算水面深度。简单起见,可以假设水面是平面,用相机高度差估算。更精确的方法需要编写自定义节点函数。
- 两者相减得到
rawDepth。
- 颜色混合流:
rawDepth-> 除以_DepthMaxDistance->Saturate(钳制)得到depthFactor。depthFactor输入Lerp节点,A端接_BaseColor,B端接_DepthColor,输出基础水色waterColor。
- 泡沫生成流:
rawDepth与_FoamDepth输入Smoothstep节点,得到foamMask。- 采样
_FoamTexture(使用另一套滚动的UV)得到foamNoise。 foamMask*foamNoise->Power节点(可选,用于调整对比度)得到finalFoam。- 将
finalFoam与白色相乘,输出到Master Stack的Emission端口。同时,也可以用Lerp将泡沫颜色混合进waterColor。
- Master Stack连接:
- Base Color: 连接最终混合了泡沫颜色的
waterColor。 - Normal: 连接混合后的法线。
- Emission: 连接
finalFoam(或泡沫颜色)。 - Smoothness: 可以连接一个较高的常量(如0.9),让水面光滑。
- Alpha: 连接由
depthFactor通过Remap或Curve控制后的透明度值。
- Base Color: 连接最终混合了泡沫颜色的
4.2 关键参数详解与调节指南
参数调节是让效果从“能用”到“好看”的关键。以下是一份参考配置及调节逻辑:
| 参数名 | 建议初始值 | 调节逻辑与影响 |
|---|---|---|
_BaseColor | RGBA(0.1, 0.3, 0.5, 0.8) | 控制浅水区颜色。偏向青蓝色调更显清澈。Alpha值影响基础透明度。 |
_DepthColor | RGBA(0.0, 0.1, 0.2, 0.9) | 控制深水区颜色。通常比基础色更深、饱和度更低。 |
_DepthMaxDistance | 10 | 颜色从浅到深过渡的深度范围。值越小,颜色变化越剧烈,显得水很浅;值越大,过渡越平缓,显得水深。 |
_WaveNormalMap | 一张蓝紫色法线贴图 | 建议使用无缝平铺的、细节丰富的法线贴图。可以准备两套不同尺度的贴图混合。 |
_WaveSpeed | (0.03, 0.05), (-0.02, 0.04) | X和Y分量分别控制U和V方向的滚动速度。正负号控制方向。两套速度差异越大,波纹越复杂。 |
_WaveStrength | 1.0 | 控制法线扰动的强度。值太大会导致水面看起来像沸腾的油,太小则没有波澜。通常0.5-2.0之间调节。 |
_FoamTexture | 一张黑白噪声或泡沫细节图 | 白色代表泡沫。使用Voronoi或Perlin噪声图效果不错。 |
_FoamDepth | 0.5 | 泡沫开始出现的深度阈值。这是世界空间单位(米)。值越小,泡沫越集中在极浅的区域。通常0.1-1.0之间。 |
_FoamSpeed | (0.01, 0.0) | 泡沫纹理的滚动速度。通常比波浪速度慢,模拟泡沫随波逐流的感觉。 |
调节顺序建议:先关闭泡沫,调节好波浪法线的速度和强度,让水面有基本的动态。然后调节深度颜色和过渡,确定水体的视觉深度。最后开启泡沫,精细调节泡沫深度和纹理,使其自然出现在岸边和岩石周围。
5. 材质与场景配置实战
5.1 材质球设置与性能考量
Shader Graph编译完成后,会生成一个.shadergraph文件和一个对应的.shader文件。右键.shadergraph文件,选择Create > Material,即可创建使用该着色器的材质球。将材质球拖拽到场景中的水面模型(通常是一个Plane或自定义的网格)上。
在材质球Inspector面板,你可以看到之前在Blackboard中定义的所有属性,并可以实时调节。这里有几个关键设置:
- 渲染队列(Render Queue):水面通常需要半透明混合。在材质的Advanced Options中,将Render Queue设置为Transparent(3000),或者自定义一个值,确保水面在 opaque(不透明)物体之后渲染,才能正确混合。
- 混合模式(Blend Mode):选择Alpha Blend(SrcAlpha, OneMinusSrcAlpha)来实现标准的半透明叠加。
- 深度写入(Depth Write):通常设置为Off。因为半透明物体不写入深度,可以避免后续透明物体之间的深度排序问题。但这可能导致水面内部重叠渲染顺序错误,如果出现奇怪的黑斑,可以尝试开启。
- 双面渲染(Double Sided):建议开启,因为玩家有可能从水下往上看。
性能提示:Shader Graph虽然方便,但节点过多、采样次数过多也会增加GPU负担。我们的实现中,采样了2次法线贴图、1次泡沫纹理、1次场景深度(这是一个全屏后处理采样,开销较大)。在移动端或低端设备上,可以考虑简化:只使用一张法线贴图,或者用顶点颜色代替深度计算来模拟简单的岸边渐变。
5.2 场景灯光与环境反射配置
写实水面极度依赖环境光照和反射。在URP中,你需要:
- 配置URP Asset:确保场景使用的URP Asset中,开启了Screen Space Ambient Occlusion (SSAO)和Screen Space Reflections (SSR)。SSR能让水面反射场景中的其他物体,这是真实感的关键。虽然性能开销大,但对于静止或缓慢移动的水面,可以带来质的提升。
- 使用反射探针(Reflection Probe):对于动态物体或SSR无法覆盖的区域(如天空盒),反射探针是必不可少的。在水面中央或重要区域放置一个反射探针,并将其类型设置为Realtime或Baked。Realtime探针每帧更新,效果最好但性能最差;Baked探针需要烘焙光照贴图,适合静态场景。
- 设置天空盒(Skybox):一个高质量的HDRI天空盒能为水面提供丰富的环境光和反射源。在Lighting窗口的Environment选项卡中,指定一个合适的天空盒材质。
- 添加方向光(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 性能优化与进阶思路
当效果基本满意后,可以考虑以下优化和增强:
- LOD(多层次细节):为远处的水面使用简化版的Shader(例如,去掉泡沫、使用更简单的波浪计算、降低纹理采样次数)。这可以通过编写多个不同复杂度的Shader Graph,或在一个Shader中使用
#ifdef指令配合Quality Settings来实现。 - 水面交互涟漪:通过渲染到纹理(Render Texture)的方式,实现物体(如船、角色)与水面的交互涟漪。这是一个相对高级的主题,需要在项目中创建一个摄像机渲染高度图到Render Texture,然后在Shader Graph中采样这个纹理来动态偏移顶点或法线。
- 焦散(Caustics):模拟水底的光斑效果。可以通过在Shader中,基于法线信息投影和滚动一张焦散纹理到水底物体上来近似实现。
- 水下效果:当相机进入水下时,切换后处理效果(如颜色偏移、模糊、边缘暗角)。这通常通过触发器(Trigger)检测相机位置,并启用不同的Volume来实现。
从《原神》璃月港的惊艳水面到你自己项目中的一池春水,距离并没有想象中遥远。通过URP和Shader Graph这套组合拳,我们系统地拆解了写实水面的构建过程:从动态波浪、深度渐变到边缘泡沫。整个过程的关键在于理解每个视觉效果背后的物理或视觉原理,并用节点化的思维去实现它。我个人的体会是,Shader Graph最大的魅力在于“即时反馈”,你可以一边调节参数,一边在场景中实时看到变化,这种迭代效率是传统编码无法比拟的。最后,别忘了多参考现实中的水体照片或视频,观察光线、颜色和波浪的细节,不断微调参数,才能让你数字世界中的水面真正“活”起来。