ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Unity水体交互实战:深度图与交互纹理实现实时波纹泡沫

Unity水体交互实战:深度图与交互纹理实现实时波纹泡沫 1. 水体交互到底在做什么从“看着像水”到“摸起来像水”水面效果在实时渲染里一直是个分水岭。很多项目能把水做得“看着像水”——有反射、有折射、有波纹静态截图能唬人。但玩家一走进水里脚边没有涟漪角色穿过瀑布水帘帘子纹丝不动船划过湖面船尾连个像样的尾迹都留不下。这种割裂感就是水体交互要解决的问题。水体交互说白了就是让水面和场景里的物体产生双向的视觉反馈。物体动水面跟着动水面动物体周围的光影跟着变。它不是一个单一功能而是一整套围绕“接触”和“扰动”的渲染机制。核心关键词包括交互波纹、深度检测、折射偏移、岸边泡沫、尾迹生成、实时反射。这套东西做出来玩家不需要看说明书脚踩进水里那一瞬间画面自己会说话。这篇文章适合谁看如果你已经写过基础的Unity Shader知道顶点着色器和片元着色器大概在干什么但一到“让水面响应物体”就卡壳那这篇就是给你准备的。我会从整体设计思路讲起把深度图、波纹生成、泡沫遮罩、折射扰动这几个核心环节拆开揉碎再给出一套可以直接抄的实操流程。中间会穿插我踩过的坑和实测有效的参数尽量让你少走弯路。先说清楚一个前提水体交互的实现方式有很多种有基于物理模拟的有基于屏幕后处理的也有基于Shader Graph可视化连线的。我下面讲的主要是基于深度图和交互纹理的实时方案这套方案在移动端和PC端都能跑性能可控效果也够用。如果你追求的是流体力学级别的真实模拟那可能需要另一套思路但那个开销不是一般项目能承受的。2. 整体设计思路为什么是深度图加交互纹理2.1 核心架构的选型逻辑做水体交互第一个要决定的就是水面怎么知道有物体靠近了这个问题有三种常见解法。第一种是物理碰撞检测。给水面加碰撞体物体进入触发区域后CPU通知Shader。这种方式精确但开销大尤其是多个物体同时交互时物理查询和消息传递会成为瓶颈。而且水面通常是一个大平面碰撞体的形状很难贴合波浪起伏。第二种是屏幕空间深度比较。渲染水面时采样当前像素对应的场景深度和水面自身深度做比较。如果场景深度比水面深度近说明有物体挡在水面前面也就是物体和水面发生了重叠。这个重叠区域就是交互发生的地方。这种方式完全在GPU上完成不需要CPU参与性能极好而且天然支持任意形状的物体。第三种是交互纹理绘制。用一个额外的相机从上方俯拍或者用脚本把物体的位置、半径、强度画到一张RenderTexture上。水面Shader采样这张纹理就知道哪里被扰动了。这种方式适合做尾迹、涟漪扩散这类需要“记忆”的效果。我的方案是深度图为主交互纹理为辅。深度图负责实时检测物体和水面的接触区域交互纹理负责记录和扩散扰动。两者结合既能做到即时响应又能做出涟漪扩散的余韵。注意深度图方案有一个硬性前提——你的渲染管线必须支持深度纹理。内置管线里相机需要开启depthTextureModeURP里需要在管线设置里勾选Depth Texture。这个不打开后面所有东西都是空中楼阁。2.2 为什么不用纯屏幕后处理有人可能会问既然深度图是屏幕空间的那为什么不干脆把整个水体交互做成一个屏幕后处理效果答案很简单屏幕后处理拿不到水面的法线信息也没法区分“这是水面”和“这是地板”。你需要在渲染水面的那个Pass里同时拿到水面自身的深度、法线、以及场景深度才能算出正确的交互效果。屏幕后处理只能拿到颜色和深度信息不够。而且水体交互往往需要和水的其他效果反射、折射、波浪耦合。比如物体入水时折射偏移要跟着扰动变化泡沫要沿着交互边缘生成。这些都需要在水面的Shader内部统一处理拆到后处理里反而更麻烦。2.3 交互纹理的格式选择交互纹理我一般用ARGBHalf或者ARGB32。R通道存扰动强度G通道存泡沫强度B通道存法线扰动的X方向A通道存Y方向。如果显存紧张可以压缩成RGBAHalf但精度会掉一些。纹理分辨率不需要太高512x512或者1024x1024足够因为涟漪本身是低频的高频细节靠法线贴图补。纹理的投影方式有两种一种是世界空间俯投影把物体的世界坐标映射到纹理的UV上另一种是屏幕空间投影直接用屏幕UV。俯投影适合做尾迹和固定区域的交互屏幕空间投影适合做全屏的雨滴涟漪。我一般用俯投影因为可控性强而且不受相机旋转影响。3. 核心细节解析深度检测、波纹生成与泡沫遮罩3.1 深度检测的精度陷阱深度检测的原理很简单采样场景深度和水面深度比较差值小于某个阈值就认为是交互区域。但这里有几个坑。第一个坑是深度值的非线性。Unity的深度纹理存的是NDC空间下的深度经过投影矩阵变换后近处的精度高远处的精度低。如果你直接用差值做阈值判断近处可能很敏感远处就完全没反应。解决办法是把深度值转换到线性空间用线性深度做比较。// 把深度纹理的原始值转成线性深度 float sceneDepth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv)); float waterDepth LinearEyeDepth(i.depth); float depthDiff sceneDepth - waterDepth;第二个坑是水面自身的深度写入。如果水面Shader没有正确写入深度场景深度和水面深度比较时就会出错。确保水面的Pass里设置了ZWrite On或者在URP里用正确的DepthOnly Pass。第三个坑是透明物体的深度。如果场景里有其他透明物体挡在水面前面深度图里可能没有它们的信息导致交互检测失效。这个问题的根本解法是让透明物体也写入深度但那样又会影响渲染顺序。我的做法是接受这个限制因为大多数情况下和水面交互的物体都是不透明的。3.2 交互波纹的生成方式波纹的生成有两种主流方式基于法线的扰动和基于高度的位移。基于法线的扰动是在片元着色器里根据交互强度对水面的法线进行偏移。这种方式开销小适合做细微的涟漪。但缺点是只能改变光照不能改变水面的实际形状所以侧面看的时候会穿帮。基于高度的位移是在顶点着色器里根据交互纹理的强度把顶点往上抬。这种方式能做出真实的隆起但需要水面有足够的细分。如果水面是一个只有四个顶点的大平面顶点位移根本看不出效果。我的做法是两者结合顶点位移做大的隆起法线扰动做细节。顶点位移的幅度不要太大0.05到0.1个单位就够了否则水面会像果冻一样弹。法线扰动的强度可以大一些因为那是视觉上的“波纹感”的主要来源。// 顶点位移示例 float interactStrength tex2Dlod(_InteractTex, float4(uv, 0, 0)).r; vertex.y interactStrength * _WaveHeight;3.3 泡沫遮罩的生成逻辑泡沫是水体交互里最出效果的部分。物体入水时接触边缘会泛起一圈白沫这个细节能让交互的真实感提升一个档次。泡沫的生成逻辑是在深度差值的边缘区域也就是场景深度和水面深度接近的地方生成泡沫。具体来说当深度差小于某个阈值时泡沫强度逐渐增大当深度差为负物体在水面以下时泡沫强度达到最大。float foam saturate(1.0 - depthDiff / _FoamDistance); foam * step(0, depthDiff); // 只在水面以上生成但这样生成的泡沫是静态的物体不动的时候泡沫就一直贴在那里。为了让泡沫有动态感可以叠加一层噪声纹理用时间做偏移让泡沫看起来在流动。实操心得泡沫的阈值不要设得太死留一点过渡区间。我一般用smoothstep而不是step这样泡沫的边缘会更柔和不会出现硬边。3.4 折射偏移与交互的耦合水面的折射效果通常是用GrabPass或者URP的CameraOpaqueTexture来实现的。采样屏幕颜色时根据法线偏移UV就能做出折射的扭曲感。当物体和水面交互时折射偏移应该跟着扰动变化。具体来说交互强度越大折射偏移越明显。这样物体入水时水下的部分会因为折射扭曲而显得更“在水里”。float2 refractionOffset normal.xy * _RefractionStrength; refractionOffset interactStrength * _InteractRefraction; float3 sceneColor tex2D(_CameraOpaqueTexture, uv refractionOffset).rgb;这里要注意折射偏移的方向要和法线方向一致否则会出现反向扭曲的诡异效果。4. 实操过程从零搭建一套可用的水体交互4.1 场景准备与管线配置先确认你的渲染管线。内置管线的话在主相机上挂一个脚本设置depthTextureMode DepthTextureMode.Depth。URP的话在Universal Render Pipeline Asset里勾选Depth Texture和Opaque Texture。然后创建一个水面平面。不要用Unity自带的Plane那个只有121个顶点做顶点位移不够用。我一般用ProBuilder或者Blender做一个细分网格分辨率至少200x200。如果性能吃紧可以降到100x100但波浪的细节会损失一些。水面的材质新建一个Shader。我习惯从Unlit Shader开始改因为不需要默认的光照模型全部自己控制。4.2 深度图采样与交互区域计算在Shader里声明深度纹理和交互纹理sampler2D _CameraDepthTexture; sampler2D _InteractTex; float4 _InteractTex_ST;在顶点着色器里计算屏幕UV和深度struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float4 screenPos : TEXCOORD1; float depth : TEXCOORD2; }; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; o.screenPos ComputeScreenPos(o.pos); o.depth o.pos.z / o.pos.w; return o; }在片元着色器里采样深度并计算差值float2 screenUV i.screenPos.xy / i.screenPos.w; float sceneDepth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenUV)); float waterDepth LinearEyeDepth(i.depth); float depthDiff sceneDepth - waterDepth;这里LinearEyeDepth把非线性深度转成线性深度单位是世界单位。depthDiff为正表示物体在水面以上为负表示在水面以下。4.3 交互纹理的绘制与采样交互纹理的绘制我一般用一个正交相机从上方俯拍。相机的orthographicSize设成水面尺寸的一半nearClipPlane和farClipPlane覆盖水面的高度范围。然后给需要交互的物体挂一个脚本在OnWillRenderObject或者Update里把物体的位置和半径画到RenderTexture上。绘制的方式可以用Graphics.DrawMesh也可以用CommandBuffer。我习惯用CommandBuffer因为可以精确控制绘制时机。CommandBuffer cb new CommandBuffer(); cb.SetRenderTarget(interactRT); cb.ClearRenderTarget(false, true, Color.clear); cb.DrawMesh(quadMesh, Matrix4x4.identity, interactMaterial); Camera.main.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, cb);交互材质里用物体的世界坐标映射到UV然后画一个圆形渐变。圆心的强度为1边缘衰减到0。float2 uv (worldPos.xz - _InteractAreaMin) / (_InteractAreaMax - _InteractAreaMin); float dist distance(uv, _ObjectUV); float strength saturate(1.0 - dist / _Radius); return float4(strength, strength, 0, 0);水面Shader采样这张纹理时用世界坐标的XZ映射到UVfloat2 interactUV (worldPos.xz - _InteractAreaMin) / (_InteractAreaMax - _InteractAreaMin); float4 interact tex2D(_InteractTex, interactUV);4.4 波纹扩散与法线扰动交互纹理里的强度是静态的物体不动涟漪就不动。为了让涟漪扩散可以在采样时对UV做时间偏移或者用多个采样点做模糊。我的做法是用一个简单的衰减扩散在C#脚本里每帧对RenderTexture做一次模糊同时把强度乘以一个衰减系数。这样涟漪会自然扩散并消失。// 每帧调用 Graphics.Blit(interactRT, tempRT, blurMaterial); Graphics.Blit(tempRT, interactRT, fadeMaterial);模糊材质用高斯模糊或者简单的Box Blur衰减材质就是把颜色乘以0.98。法线扰动则是在水面Shader里根据交互强度对法线进行偏移float3 normal UnpackNormal(tex2D(_NormalMap, uv _Time.y * _FlowSpeed)); normal.xy interact.r * _InteractNormalStrength; normal normalize(normal);4.5 泡沫与折射的最终合成泡沫的合成是在深度差值的边缘区域叠加一层白色float foam saturate(1.0 - abs(depthDiff) / _FoamDistance); foam * interact.g; float3 finalColor lerp(waterColor, foamColor, foam);折射的合成是在采样屏幕颜色时根据法线和交互强度偏移UVfloat2 refractionUV screenUV normal.xy * _RefractionStrength interact.r * _InteractRefraction; float3 sceneColor tex2D(_CameraOpaqueTexture, refractionUV).rgb; finalColor lerp(finalColor, sceneColor, _RefractionBlend);最后加上反射、高光、菲涅尔一套完整的水体交互就出来了。5. 常见问题与排查技巧实录5.1 深度图采样全黑或全白这是最常见的问题。原因通常是深度纹理没有正确生成。检查步骤内置管线看相机的depthTextureMode是否设置URP看管线设置里Depth Texture是否勾选如果用了多个相机确认主相机的深度纹理是否被覆盖。另一个可能是SAMPLE_DEPTH_TEXTURE的宏在URP里不适用需要改用SampleSceneDepth。URP的Shader库和内置管线不一样这个要特别注意。5.2 交互区域偏移或错位交互纹理的UV映射和深度图的屏幕UV映射是两套坐标。如果交互区域和水面的实际接触点对不上大概率是UV映射的公式写错了。检查_InteractAreaMin和_InteractAreaMax是否覆盖了水面的实际范围以及世界坐标到UV的映射是否用了正确的轴。避坑技巧在Scene视图里画一个Gizmo把交互区域的范围可视化出来这样一眼就能看出对不对。5.3 波纹不扩散或扩散太快波纹的扩散速度取决于模糊的半径和衰减系数。模糊半径太大波纹会糊成一团太小又看不出扩散。衰减系数太大波纹消失太快太小又会一直残留。我实测下来模糊半径用2到4个像素衰减系数用0.96到0.98效果比较自然。具体数值要根据你的帧率和纹理分辨率调。5.4 移动端性能优化深度图采样和交互纹理绘制在移动端是有开销的。如果帧率吃紧可以降级深度图用半分辨率交互纹理用256x256模糊只做一次。泡沫和折射可以关掉只保留法线扰动。另外交互纹理的绘制不要每帧都做可以隔帧做或者只在物体移动时做。静止的物体不需要每帧重绘。问题现象可能原因排查方向深度图全黑深度纹理未生成检查相机设置和管线配置交互区域错位UV映射错误检查世界坐标到UV的转换公式波纹不扩散模糊或衰减参数不当调整模糊半径和衰减系数移动端卡顿深度图和交互纹理开销大降分辨率、隔帧绘制、关闭次要效果泡沫硬边阈值用了step改用smoothstep做过渡5.5 水面反射与交互的冲突如果水面用了反射探针或者平面反射交互波纹可能会被反射效果覆盖。解决办法是在反射采样之后再叠加交互效果。也就是说先算反射再算法线扰动和泡沫最后合成。顺序反了交互效果就会被反射盖住。6. 进阶扩展让水体交互更有层次感6.1 基于物理的尾迹生成船划过水面时尾迹不是简单的圆形涟漪而是V字形的扩散波。这个可以用交互纹理的绘制方式来实现不是画圆而是画一个V字形的遮罩随着时间逐渐扩散和衰减。具体做法是在C#脚本里根据船的速度和方向计算V字形的两个边然后用Graphics.DrawMesh画到交互纹理上。V字形的角度和船速相关速度越快角度越小。6.2 雨滴涟漪的随机分布下雨场景里水面需要随机分布的涟漪。这个可以用一个噪声纹理驱动交互纹理的绘制每帧在随机位置画一个小圆强度随机半径随机。为了性能可以预生成一批涟漪位置循环使用。6.3 水下视角的交互反馈当相机潜入水下时水面交互应该有不同的表现。水下看水面应该看到全反射和Snell窗效果。这个需要根据相机的深度切换水面的渲染模式。具体来说当相机在水面以下时菲涅尔项要反转折射要变成全反射。这个扩展比较复杂涉及到相机的深度检测和Shader的变体切换。如果你的项目有水下场景这个值得做。6.4 交互纹理的压缩与复用如果场景里有多个水面可以共用一张交互纹理。只要它们的交互区域不重叠或者重叠区域可以接受相互影响。这样可以省显存也省绘制开销。纹理的格式可以用R8只存强度泡沫和法线扰动用程序化生成。这样显存占用可以降到四分之一。7. 我个人在实际操作中的几点体会水体交互这个东西参数调起来很磨人。同样的Shader换一个场景参数就得重调。我的经验是先把深度检测和交互区域调准这是地基。地基不稳后面泡沫和折射怎么调都是歪的。泡沫的阈值我一般会留一个_FoamSoftness参数用来控制边缘的柔和度。这个参数在物体快速移动时特别有用硬边的泡沫会像贴纸一样假柔和的泡沫才有水花的感觉。交互纹理的衰减速度不要设得太快。我见过很多项目涟漪刚出来就没了看起来像水面在抽搐。衰减慢一点让涟漪有扩散的余韵真实感会强很多。最后移动端的话能省则省。深度图半分辨率交互纹理256模糊一次泡沫和折射二选一。先把法线扰动做出来那个是性价比最高的效果。等性能有余量了再加泡沫和折射。这套方案我在几个项目里都用过从PC到移动端都跑得动。核心就是深度图加交互纹理剩下的都是在这个骨架上添肉。你把骨架搭稳了想加什么效果都方便。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表