news 2026/7/24 1:36:30

Unity URP动态水面Shader实现:从法线扰动到深度折射全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity URP动态水面Shader实现:从法线扰动到深度折射全解析

1. 项目概述:为什么要在URP里折腾水面?

做游戏或者数字孪生这类项目,场景里要是没片像样的水,总觉得差点意思。静态的水面贴图早就过时了,玩家和用户现在要的是那种波光粼粼、能倒映天空、甚至能互动起浪的真实感。Unity作为主流引擎,实现水面效果的路子很多,但自从Universal Render Pipeline(URP,通用渲染管线)成为官方主推的新标准后,很多老方法就得跟着变一变。

我最近刚在一个数字孪生项目里用URP重做了一遍水面,踩了不少坑,也总结了一套比较稳当的实现流程。你会发现,URP虽然简化了渲染设置,但在实现一些高级效果时,它的“脾气”和内置渲染管线不太一样,尤其是涉及到自定义Shader和后期处理的时候。网上很多教程是基于老版内置管线写的,直接照搬可能会遇到各种光照不正常、深度采样失败的问题。所以,这篇内容我就围绕“在URP下实现一套完整的动态水面效果”这个目标,从核心思路拆解到Shader代码细节,再到性能优化和常见坑点,给你完整捋一遍。无论你是想做个湖面、海面,还是一个小水池,这套方法都能给你一个扎实的起点。

2. 核心思路与方案选型:从“看起来像”到“动起来真”

实现动态水面,本质上是在模拟光与水的复杂交互。我们不需要完全复现物理公式,而是用一系列“视觉把戏”来欺骗眼睛。在URP框架下,我们需要组合使用几种关键技术:

2.1 核心视觉组件拆解

一个合格的水面,通常由以下几个视觉层叠加而成:

  1. 基础颜色与透明度:水的颜色不是单一的,通常岸边浅、深处蓝,并且具有半透明特性。
  2. 法线扰动(波浪):这是动态感的来源。通过一张或叠加多张法线贴图(Normal Map),并让它们随时间滚动,来模拟水面的波光粼粼。这是最关键的一步。
  3. 高光反射:水面对光源(如太阳、月亮)的镜面反射。在URP中,我们需要自己计算高光,因为URP的Lit Shader流程和内置管线的Standard Shader不同。
  4. 环境反射:反射天空盒或周围环境。可以用反射探针(Reflection Probe),但对于开阔水面,更常用的方法是使用屏幕空间反射(SSR)或简单的立方体贴图(Cubemap)采样。
  5. 折射:透过水面看到水底物体的扭曲效果。这需要用到相机的深度纹理和颜色纹理,进行扰动采样。
  6. 边缘泡沫与焦散:岸边或物体周围的白色泡沫,以及水底的光斑效果。这些属于提升细节的“加分项”。

2.2 URP下的方案决策

在URP中实现上述效果,你有几个选择,每个选择都意味着不同的工作量、效果上限和性能开销:

  • 方案A:使用URP Lit Shader为基础修改

    • 思路:从URP提供的Simple LitLitShader模板开始,在其基础上添加法线滚动、折射等计算。
    • 优点:可以最大程度复用URP的光照模型,确保与场景中其他URP物体的光照一致性(如阴影接收)。上手相对快。
    • 缺点:URP的Shader结构复杂,定制深度效果(如复杂的折射、边缘泡沫)时需要深入理解其代码框架,可能会束手束脚。
    • 适用场景:对光照一致性要求高,且水面效果需求中等(有波浪、基础反射折射即可)的项目。
  • 方案B:编写自定义Unlit Shader

    • 思路:完全自己控制输出颜色,抛开URP的PBR光照计算。自己实现菲涅尔效应(Fresnel)、法线、反射、折射。
    • 优点:绝对的控制权,可以实现任何天马行空的效果,代码结构清晰,便于调试和优化。
    • 缺点:需要自己处理与场景光源的交互(比如让水面能对方向光产生高光),失去了URP内置的阴影接收等便利功能。
    • 适用场景:追求独特风格化效果,或需要极致性能控制(可以砍掉很多计算)的场景。
  • 方案C:使用Asset Store插件

    • 思路:直接购买如AQUASWater System等成熟插件。
    • 优点:效果华丽,功能全面(通常包含水下效果、流动贴图等),节省大量开发时间。
    • 缺点:成本高,插件可能过于庞大,定制化修改需要研究插件代码,可能与项目特定的URP版本存在兼容性问题。
    • 适用场景:商业项目赶工期,或对水面效果有极高要求且预算充足。

对于大多数希望学习和掌控技术的中小型项目,我推荐从方案A开始。它平衡了效果、性能和可学习性。本次分享也将以“基于URP Lit Shader模板进行扩展”作为主线。当你完全吃透后,转向方案B也会非常轻松。

注意:无论选择哪种方案,在URP中获取深度和颜色纹理用于折射,都需要在URP Asset中显式开启Opaque TextureDepth Texture选项。这是第一个,也是最重要的一个坑点,后面会详细说。

3. 环境准备与核心资源制作

工欲善其事,必先利其器。在动手写代码前,先把环境和素材准备好。

3.1 URP项目配置检查

  1. 创建或升级项目:确保你使用的是Unity 2021 LTS或更新版本,创建项目时选择“Universal Render Pipeline”模板。如果是老项目,需要通过Package Manager安装Universal RP包,并创建URP Asset替换掉原有的渲染管线资产。
  2. 关键设置:在Project窗口中找到你的URP Asset文件(通常叫UniversalRP-HighQuality或类似),选中它,在Inspector面板中:
    • 勾选Opaque Texture:这将生成一张包含所有不透明物体颜色的屏幕纹理,是我们做折射的基础。
    • 勾选Depth Texture:生成深度纹理,用于计算水面像素的深度差,实现正确的折射和深度边缘泡沫。
    • 调整Opaque Downsampling:为了性能,可以设为2x Bilinear4x Box,这会在渲染折射时使用降采样的纹理,轻微牺牲质量换取性能,对于移动端尤其重要。

3.2 法线贴图资源准备

水面的动态波浪几乎完全依赖法线贴图。你需要至少一张高质量的水面法线贴图。可以去纹理资源网站搜索“Water Normal Map”,选择那种带有细腻波纹的。理想情况下,准备两张不同缩放和滚动速度的法线贴图,叠加使用可以打破重复感,让波浪看起来更自然。

  • 技巧一:自己生成法线。如果你有类似Substance Designer的工具,可以自己制作。一个简单的方法是:在Photoshop中创建灰度图,用云彩、纤维等滤镜模拟波浪高度,然后使用“滤镜->3D->生成法线图”功能来转换。
  • 技巧二:使用噪声叠加。在Shader中,除了采样法线贴图,还可以混合一些程序化噪声(如Value Noise),进一步增加细节和随机性,避免明显的贴图重复平铺。

3.3 创建自定义Shader与材质

  1. 在Project窗口中右键,选择Create -> Shader -> Universal Render Pipeline -> Lit Shader。给它起个名字,比如WaterURP.shader。这个模板就是我们改造的起点。
  2. 创建一个新材质球,将Shader选择为你刚刚创建的WaterURP。后续所有的属性调节都将在这个材质球上进行。

4. Shader核心逻辑解析与实现

这是最硬核的部分。我们将打开WaterURP.shader文件,一步步添加功能。我会先解释原理,再给出代码片段。请注意,以下代码需要插入到Shader的适当位置,主要是SurfaceInput.hlsl(输入结构)、Lighting.hlsl(光照计算)和片元着色器(fragment shader)部分。为了清晰,我会说明插入点。

4.1 扩展输入属性与变量

首先,在Shader文件的Properties块和对应的HLSL变量声明块中,添加我们需要的参数。

// 在 Properties 块中添加 _BaseColor("Water Color", Color) = (0.1, 0.3, 0.5, 0.8) _ShallowColor("Shallow Water Color", Color) = (0.2, 0.6, 0.8, 0.5) // 浅水区颜色 _DepthFactor("Depth Color Factor", Range(0, 5)) = 1.0 // 深度颜色过渡系数 _WaveNormalMap("Wave Normal Map", 2D) = "bump" {} _WaveNormalMap2("Wave Normal Map 2", 2D) = "bump" {} _NormalTiling("Normal Tiling", Vector) = (1,1,0,0) // 两张法线贴图的平铺 _NormalSpeed("Normal Speed", Vector) = (0.05, 0.03, 0.04, 0.02) // xy: 第一张速度, zw: 第二张速度 _NormalStrength("Normal Strength", Range(0, 2)) = 0.5 // 法线强度 _RefractionStrength("Refraction Strength", Range(0, 0.1)) = 0.02 // 折射强度 _FresnelPower("Fresnel Power", Range(1, 10)) = 5.0 // 菲涅尔效应强度 _Specular("Specular", Range(0, 1)) = 0.5 // 高光强度 _Smoothness("Smoothness", Range(0, 1)) = 0.9 // 光滑度 _FoamColor("Foam Color", Color) = (1,1,1,1) _FoamDistance("Foam Distance", Range(0, 5)) = 1.0 // 泡沫出现距离 _FoamNoise("Foam Noise", 2D) = "white" {} // 泡沫噪声图

4.2 实现动态法线采样

这是产生波光粼粼效果的核心。思路是:采样两张法线贴图,让它们的UV随时间偏移,然后混合两张法线向量。

在片元着色器中(通常在SurfaceData填充之后,光照计算之前):

// 计算随时间滚动的UV float2 uv1 = input.uv * _NormalTiling.xy + _Time.y * _NormalSpeed.xy; float2 uv2 = input.uv * _NormalTiling.zw + _Time.y * _NormalSpeed.zw; // 从法线贴图解码法线向量(记得将贴图设置为Normal Map格式) float3 normal1 = UnpackNormalScale(SAMPLE_TEXTURE2D(_WaveNormalMap, sampler_WaveNormalMap, uv1), _NormalStrength); float3 normal2 = UnpackNormalScale(SAMPLE_TEXTURE2D(_WaveNormalMap2, sampler_WaveNormalMap2, uv2), _NormalStrength); // 混合两张法线。简单的加法后归一化,或者使用更复杂的混合方式 float3 waveNormal = normalize(float3(normal1.xy + normal2.xy, normal1.z * normal2.z)); // 保留z分量的乘积有助于保持法线“向上” // 将扰动后的法线应用到表面数据中,供URP光照计算使用 surfaceData.normalTS = waveNormal; // 注意,这里是在切线空间下的法线

实操心得_NormalSpeed的取值很关键。让两张法线图以不同速度、不同方向(比如一个x正方向,一个y负方向)滚动,能有效避免产生明显的定向流动图案,让波浪看起来更自然、无序。数值通常很小,比如(0.03, 0.02, -0.02, 0.01)。

4.3 实现基于深度的颜色与折射

水的颜色应该随深度变化,并且能看到水底物体的折射扭曲。

// 1. 获取屏幕空间位置和深度 float2 screenUV = input.screenPos.xy / input.screenPos.w; // 得到0-1范围的屏幕UV float sceneRawDepth = SampleSceneDepth(screenUV); // 采样场景深度 float waterRawDepth = input.screenPos.w; // 水面本身的深度(线性深度需要转换,这里简写) // 将深度值转换为线性深度(近似)。URP提供了`LinearEyeDepth`函数,但需要正确引入。 // 假设我们已经正确引入了`Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl` float sceneLinearDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams); float waterLinearDepth = LinearEyeDepth(waterRawDepth, _ZBufferParams); // 计算深度差 float depthDifference = sceneLinearDepth - waterLinearDepth; depthDifference = max(0, depthDifference); // 确保非负 // 2. 基于深度差混合深浅水颜色 float depthLerpFactor = saturate(depthDifference * _DepthFactor); float4 waterColor = lerp(_ShallowColor, _BaseColor, depthLerpFactor); // 3. 折射效果:用法线扰动屏幕UV,再去采样场景颜色 float2 refractionOffset = waveNormal.xy * _RefractionStrength * saturate(depthDifference); // 深度越大,折射越强 float2 refractedScreenUV = screenUV + refractionOffset; float3 sceneColor = SampleSceneColor(refractedScreenUV); // 采样扰动后的场景颜色 // 4. 将折射颜色与水的本色混合。简单做法是直接用水的透明度进行混合 float3 finalBaseColor = lerp(sceneColor, waterColor.rgb, waterColor.a);

关键点解析:这里用到了SampleSceneDepthSampleSceneColor。在URP中,你需要先在Shader顶部声明这些纹理。通常在HLSLINCLUDE块或Pass之前添加:

TEXTURE2D_X_FLOAT(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); TEXTURE2D_X(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture);

并且确保你的URP Asset中开启了Depth TextureOpaque Texture。这是最容易出错的地方,如果采样结果是黑的,首先检查这里。

4.4 实现菲涅尔反射与高光

菲涅尔效应是指视线与水面法线夹角越大(即看向水面边缘),反射越强;夹角越小(看向水面正下方),折射越强。

// 计算菲涅尔系数 (Fresnel) float3 viewDirWS = normalize(input.viewDirectionWS); // 世界空间视线方向 float3 normalWS = TransformTangentToWorld(waveNormal, input.tangentToWorld); // 将法线转到世界空间 float fresnel = pow(1.0 - saturate(dot(viewDirWS, normalWS)), _FresnelPower); // 采样环境反射(这里使用最简单的方法:采样天空盒) float3 reflection = GlossyEnvironmentReflection(reflect(-viewDirWS, normalWS), 0.0, _Smoothness); // 可以使用URP内置函数 // 混合反射颜色到最终颜色 float3 finalColor = lerp(finalBaseColor, reflection, fresnel); // URP Lit Shader的高光计算通常已经在光照函数中完成,我们只需要确保surfaceData.specular和smoothness设置正确。 // 我们在前面Properties中定义的_Specular和_Smoothness,需要传递给surfaceData。 surfaceData.specular = _Specular; surfaceData.smoothness = _Smoothness;

4.5 实现边缘泡沫

泡沫通常出现在水面与其他物体(如岸边、船体)交界处,也就是深度发生突变的地方。

// 计算深度差的变化率(梯度),模拟泡沫 float depthGradient = fwidth(depthDifference); // 使用fwidth求近似梯度 float foamMask = saturate(depthGradient * _FoamDistance * 10); // 放大并钳制 // 可以叠加一张噪声图来打破泡沫的均匀感 float2 foamUV = input.worldPos.xz * 0.1 + _Time.y * 0.1; // 世界坐标缩放后滚动 float foamNoise = SAMPLE_TEXTURE2D(_FoamNoise, sampler_FoamNoise, foamUV).r; // 结合梯度遮罩和噪声 foamMask *= foamNoise; // 将泡沫颜色混合到最终颜色 finalColor = lerp(finalColor, _FoamColor.rgb, foamMask * _FoamColor.a);

5. 材质调试与场景整合

Shader写完后,回到Unity编辑器,调整材质球上的参数,观察效果。

5.1 参数调节经验

  • _NormalStrength:从0.1开始慢慢调。太小没波浪,太大会显得水面像沸腾的油锅,不自然。
  • _DepthFactor:控制从浅水色过渡到深水色的速度。对于泳池或近岸水域,可以调大(如3-5),让颜色变化更明显;对于深海,可以调小(如0.5-1),颜色更均匀。
  • _RefractionStrength:典型的“少即是多”参数。0.01到0.03之间通常就能得到不错的扭曲效果,再大就容易让人眼晕了。
  • _FresnelPower:控制反射出现的范围。值越大,反射越集中在边缘(视角很斜时)。5是一个不错的起始值。

5.2 场景布置技巧

  1. 水面网格:用一个简单的平面(Plane)或细分更多的网格作为水面。对于大范围水域,建议使用一个低多边形网格,靠法线贴图提供细节。如果需要远处有海浪轮廓,可以专门做一个带有起伏的网格作为远景。
  2. 反射探针:在水面中心位置放置一个反射探针(Reflection Probe),选择类型为“Realtime”或“Baked”。如果是静态场景,烘焙一次即可。这对于反射周围静态环境至关重要。
  3. 后期处理:在URP的Volume中,可以添加一些后期效果来增强水面:
    • Bloom:让水面的高光部分“泛光”,更有梦幻感。
    • Color Adjustments:微调对比度和饱和度,让水的颜色更通透。
    • Vignette:轻微暗角可以引导视线聚焦到水面中心。

6. 性能优化与常见问题排查

一个好看但跑不动的效果是没有意义的。以下是针对URP水面的优化和排错指南。

6.1 性能优化要点

  1. 纹理与采样优化
    • 法线贴图尺寸不宜过大,1024x1024甚至512x512对于大多数情况足够,可以压缩为DXT5nm(PC)或ASTC(移动端)格式。
    • 在URP Asset中开启Opaque Downsampling,如前所述。
    • 考虑在移动端或低配平台上,只使用一张法线贴图,或者降低法线滚动的计算频率(比如在片元着色器中使用_Time.y * 0.5)。
  2. Shader复杂度
    • 边缘泡沫和复杂的焦散效果是性能杀手。如果帧率吃紧,优先考虑简化或移除这些效果。
    • 检查Shader的编译变体数量。过多的#pragma multi_compile会导致构建包体膨胀。可以使用Shader Variant Stripping工具或手动剔除不需要的特性。
  3. 渲染状态
    • 水面的材质应设置为TransparentOpaque(如果不需折射)。透明渲染开销更大,如果水完全不透明,可以设为Opaque并关闭折射计算。
    • 确保水面物体在相机的裁剪距离(Culling Distance)内是合理的,不要渲染看不见的远处水面。

6.2 常见问题与解决方案实录

问题现象可能原因排查与解决步骤
水面一片纯黑或纯白1. Shader编译错误。
2. 深度/颜色纹理未正确声明或采样。
1. 查看Console窗口是否有Shader错误(红色报错)。
2. 检查Shader中TEXTURE2D_X/_CameraDepthTexture等声明是否正确,拼写是否与URP内置变量名一致。
3.确认URP Asset中已勾选Depth TextureOpaque Texture。这是最常见的原因!
没有波浪效果1. 法线贴图未赋值或格式不对。
2. 法线贴图导入设置未标记为“Normal Map”。
3._NormalStrength为0。
1. 检查材质球上法线贴图槽位。
2. 在Project窗口选中法线贴图,在Inspector中确保Texture TypeDefault,并勾选Bypass sRGB Sampling,同时将Advanced下的Format设为Normal map。对于URP,更推荐使用Texture Importer中的Normal Map纹理类型。
3. 调整_NormalStrength参数。
折射效果错乱或闪烁1. 深度计算错误,特别是线性深度转换。
2. 折射偏移量_RefractionStrength过大。
3. 水面网格背面渲染问题。
1. 使用Visualize Depth工具(或写一个简单的Debug Shader输出深度值)检查深度纹理是否正确。
2. 将_RefractionStrength调小至0.01左右。
3. 确保水面网格法线朝上。或在水面Shader的SubShader中设置Cull Off(双面渲染)试试。
与场景光源交互异常(无高光)1. 自定义Shader未正确输出或计算高光信息。
2. 表面数据(SurfaceData)中的smoothnessspecular设置不正确。
1. 如果基于URP Lit模板,确保SurfaceData结构体中的smoothnessspecular字段被正确赋值。
2. 检查场景中主光源(Directional Light)的强度是否足够,并启用了阴影。
在Game视图正常,打包后失效1. Shader变体被错误剥离。
2. 某些图形API(如OpenGL ES)支持特性不同。
1. 在Project Settings -> Graphics -> Shader Stripping中,尝试关闭一些剥离选项,或为你的Shader添加必要的multi_compile指令。
2. 在Player Settings中,检查目标图形API,并测试在较低版本(如GLES 2.0)下的表现。简化Shader中可能不被广泛支持的函数。

6.3 一个实用的调试技巧

当你对效果不满意又不知道是哪部分出问题时,可以创建一个“调试材质”。复制你的水Shader,然后修改片元着色器,让它只输出你关心的中间值。例如:

// 调试:只显示法线 return float4(waveNormal * 0.5 + 0.5, 1.0); // 将法线从(-1,1)映射到(0,1)显示 // 调试:只显示深度差 return float4(depthDifference.xxx * 0.1, 1.0); // 深度差通常很小,乘个系数便于观察 // 调试:只显示菲涅尔系数 return float4(fresnel.xxx, 1.0);

通过这种方式,你可以清晰地看到每一层效果的贡献,从而精准定位问题。

最后,别忘了性能分析。使用Unity的Profiler(特别是GPU模块)和Frame Debugger,观察绘制水面时的GPU耗时和Draw Call,确保它在你的目标平台上运行流畅。实现动态水面是一个平衡艺术与技术的过程,从基础的法线扰动开始,逐步叠加反射、折射、深度色等效果,每次只添加一个功能并充分测试,是最高效的策略。希望这份结合了URP特性的指南,能帮你少走弯路,做出既漂亮又高效的水面。

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

MSP430 ADC12_A模块:寄存器配置、中断机制与低功耗数据采集实战

1. 项目概述与核心价值在嵌入式系统开发,尤其是基于德州仪器(TI)MSP430系列微控制器的项目中,模数转换器(ADC)往往是连接物理世界与数字处理核心的咽喉要道。无论是读取温度传感器的微弱电压,还…

作者头像 李华
网站建设 2026/7/24 1:28:15

虾皮之家相关的工具有哪些?五大类Shopee数据分析工具全盘点

很多Shopee卖家在这款插件关闭之后,第一反应是"赶紧找替代品",但打开百度一搜,发现数据分析工具五花八门——有免费的、有付费的、有插件的、有网页端的、有做选品的、有做广告的。如果你需要一个能替代这款插件全局分析能力、同时…

作者头像 李华
网站建设 2026/7/24 1:27:00

电池弹片机构选用的优质材质探讨

在当今各类电子产品广泛普及的时代,电池弹片机构作为其中的关键组件,发挥着极为重要的作用。它能够连接电池与电子设备电路,保障电池电源稳定传输,其性能的优劣直接影响到电子产品的整体质量和可靠性。对于电池弹片机构而言&#…

作者头像 李华
网站建设 2026/7/24 1:25:52

职场能力突围:学历与能力的错位与应对策略

1. 职场现象观察:学历与能力的错位困境上周和几个老同事聚餐时,听到一个特别有意思的案例:某互联网公司一位大专学历的95后,凭借出色的项目交付能力月薪涨到近3万,结果团队里211毕业的领导处处给他穿小鞋。这种情况在技…

作者头像 李华
网站建设 2026/7/24 1:24:28

AI决策可追溯性:技术实现与行业应用解析

1. AI决策可追溯性的行业痛点与核心价值在金融风控系统中,我们曾遇到一个典型案例:某AI信贷审批模型突然将一位优质客户的信用评分从850分骤降至620分,导致200万贷款申请被拒。技术团队花了整整三周时间逆向排查,才发现是某个特征…

作者头像 李华
网站建设 2026/7/24 1:22:17

切问学术Agent:助力学术研究高效开展的智能专业辅助工具

对于科研人员来说,文献工作往往伴随着两个极端的痛苦:一是搜索时的大海捞针,为了几篇核心文献,不得不花费数小时翻阅成百上千条琐碎的摘要;二是阅读时的翻译折磨,在专业术语和复杂的 LaTeX 公式间反复推敲&…

作者头像 李华