公司动态

Unity URP与Shader Graph实战:打造写实水面效果(含边缘泡沫与动态波浪)

📅 2026/8/6 15:43:05
Unity URP与Shader Graph实战:打造写实水面效果(含边缘泡沫与动态波浪)
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深水色之间进行插值。透明度控制同样基于归一化的深度值可以创建一个透明度曲线。例如深度很浅时完全透明alpha0深度达到某个阈值时变为半透明alpha0.5深度很大时基本不透明alpha0.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做运算实现滚动。法线生成流流程AUV Time * Speed_A - Sample Texture 2D (Normal Map A) - Normal Unpack。流程BUV 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端接_BaseColorB端接_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控制后的透明度值。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中定义的所有属性并可以实时调节。这里有几个关键设置渲染队列Render Queue水面通常需要半透明混合。在材质的Advanced Options中将Render Queue设置为Transparent3000或者自定义一个值确保水面在 opaque不透明物体之后渲染才能正确混合。混合模式Blend Mode选择Alpha BlendSrcAlpha, 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最大的魅力在于“即时反馈”你可以一边调节参数一边在场景中实时看到变化这种迭代效率是传统编码无法比拟的。最后别忘了多参考现实中的水体照片或视频观察光线、颜色和波浪的细节不断微调参数才能让你数字世界中的水面真正“活”起来。