:深度圖與交互紋理實現(xiàn)實時波紋泡沫)
1. 水體交互到底在做什么從“看著像水”到“摸起來像水”水面效果在實時渲染里一直是個分水嶺。很多項目能把水做得“看著像水”——有反射、有折射、有波紋靜態(tài)截圖能唬人。但玩家一走進水里腳邊沒有漣漪角色穿過瀑布水簾簾子紋絲不動船劃過湖面船尾連個像樣的尾跡都留不下。這種割裂感就是水體交互要解決的問題。水體交互說白了就是讓水面和場景里的物體產(chǎn)生雙向的視覺反饋。物體動水面跟著動水面動物體周圍的光影跟著變。它不是一個單一功能而是一整套圍繞“接觸”和“擾動”的渲染機制。核心關鍵詞包括交互波紋、深度檢測、折射偏移、岸邊泡沫、尾跡生成、實時反射。這套東西做出來玩家不需要看說明書腳踩進水里那一瞬間畫面自己會說話。這篇文章適合誰看如果你已經(jīng)寫過基礎的Unity Shader知道頂點著色器和片元著色器大概在干什么但一到“讓水面響應物體”就卡殼那這篇就是給你準備的。我會從整體設計思路講起把深度圖、波紋生成、泡沫遮罩、折射擾動這幾個核心環(huán)節(jié)拆開揉碎再給出一套可以直接抄的實操流程。中間會穿插我踩過的坑和實測有效的參數(shù)盡量讓你少走彎路。先說清楚一個前提水體交互的實現(xiàn)方式有很多種有基于物理模擬的有基于屏幕后處理的也有基于Shader Graph可視化連線的。我下面講的主要是基于深度圖和交互紋理的實時方案這套方案在移動端和PC端都能跑性能可控效果也夠用。如果你追求的是流體力學級別的真實模擬那可能需要另一套思路但那個開銷不是一般項目能承受的。2. 整體設計思路為什么是深度圖加交互紋理2.1 核心架構的選型邏輯做水體交互第一個要決定的就是水面怎么知道有物體靠近了這個問題有三種常見解法。第一種是物理碰撞檢測。給水面加碰撞體物體進入觸發(fā)區(qū)域后CPU通知Shader。這種方式精確但開銷大尤其是多個物體同時交互時物理查詢和消息傳遞會成為瓶頸。而且水面通常是一個大平面碰撞體的形狀很難貼合波浪起伏。第二種是屏幕空間深度比較。渲染水面時采樣當前像素對應的場景深度和水面自身深度做比較。如果場景深度比水面深度近說明有物體擋在水面前面也就是物體和水面發(fā)生了重疊。這個重疊區(qū)域就是交互發(fā)生的地方。這種方式完全在GPU上完成不需要CPU參與性能極好而且天然支持任意形狀的物體。第三種是交互紋理繪制。用一個額外的相機從上方俯拍或者用腳本把物體的位置、半徑、強度畫到一張RenderTexture上。水面Shader采樣這張紋理就知道哪里被擾動了。這種方式適合做尾跡、漣漪擴散這類需要“記憶”的效果。我的方案是深度圖為主交互紋理為輔。深度圖負責實時檢測物體和水面的接觸區(qū)域交互紋理負責記錄和擴散擾動。兩者結合既能做到即時響應又能做出漣漪擴散的余韻。注意深度圖方案有一個硬性前提——你的渲染管線必須支持深度紋理。內置管線里相機需要開啟depthTextureModeURP里需要在管線設置里勾選Depth Texture。這個不打開后面所有東西都是空中樓閣。2.2 為什么不用純屏幕后處理有人可能會問既然深度圖是屏幕空間的那為什么不干脆把整個水體交互做成一個屏幕后處理效果答案很簡單屏幕后處理拿不到水面的法線信息也沒法區(qū)分“這是水面”和“這是地板”。你需要在渲染水面的那個Pass里同時拿到水面自身的深度、法線、以及場景深度才能算出正確的交互效果。屏幕后處理只能拿到顏色和深度信息不夠。而且水體交互往往需要和水的其他效果反射、折射、波浪耦合。比如物體入水時折射偏移要跟著擾動變化泡沫要沿著交互邊緣生成。這些都需要在水面的Shader內部統(tǒng)一處理拆到后處理里反而更麻煩。2.3 交互紋理的格式選擇交互紋理我一般用ARGBHalf或者ARGB32。R通道存擾動強度G通道存泡沫強度B通道存法線擾動的X方向A通道存Y方向。如果顯存緊張可以壓縮成RGBAHalf但精度會掉一些。紋理分辨率不需要太高512x512或者1024x1024足夠因為漣漪本身是低頻的高頻細節(jié)靠法線貼圖補。紋理的投影方式有兩種一種是世界空間俯投影把物體的世界坐標映射到紋理的UV上另一種是屏幕空間投影直接用屏幕UV。俯投影適合做尾跡和固定區(qū)域的交互屏幕空間投影適合做全屏的雨滴漣漪。我一般用俯投影因為可控性強而且不受相機旋轉影響。3. 核心細節(jié)解析深度檢測、波紋生成與泡沫遮罩3.1 深度檢測的精度陷阱深度檢測的原理很簡單采樣場景深度和水面深度比較差值小于某個閾值就認為是交互區(qū)域。但這里有幾個坑。第一個坑是深度值的非線性。Unity的深度紋理存的是NDC空間下的深度經(jīng)過投影矩陣變換后近處的精度高遠處的精度低。如果你直接用差值做閾值判斷近處可能很敏感遠處就完全沒反應。解決辦法是把深度值轉換到線性空間用線性深度做比較。// 把深度紋理的原始值轉成線性深度 float sceneDepth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv)); float waterDepth LinearEyeDepth(i.depth); float depthDiff sceneDepth - waterDepth;第二個坑是水面自身的深度寫入。如果水面Shader沒有正確寫入深度場景深度和水面深度比較時就會出錯。確保水面的Pass里設置了ZWrite On或者在URP里用正確的DepthOnly Pass。第三個坑是透明物體的深度。如果場景里有其他透明物體擋在水面前面深度圖里可能沒有它們的信息導致交互檢測失效。這個問題的根本解法是讓透明物體也寫入深度但那樣又會影響渲染順序。我的做法是接受這個限制因為大多數(shù)情況下和水面交互的物體都是不透明的。3.2 交互波紋的生成方式波紋的生成有兩種主流方式基于法線的擾動和基于高度的位移?;诜ň€的擾動是在片元著色器里根據(jù)交互強度對水面的法線進行偏移。這種方式開銷小適合做細微的漣漪。但缺點是只能改變光照不能改變水面的實際形狀所以側面看的時候會穿幫?;诟叨鹊奈灰剖窃陧旤c著色器里根據(jù)交互紋理的強度把頂點往上抬。這種方式能做出真實的隆起但需要水面有足夠的細分。如果水面是一個只有四個頂點的大平面頂點位移根本看不出效果。我的做法是兩者結合頂點位移做大的隆起法線擾動做細節(jié)。頂點位移的幅度不要太大0.05到0.1個單位就夠了否則水面會像果凍一樣彈。法線擾動的強度可以大一些因為那是視覺上的“波紋感”的主要來源。// 頂點位移示例 float interactStrength tex2Dlod(_InteractTex, float4(uv, 0, 0)).r; vertex.y interactStrength * _WaveHeight;3.3 泡沫遮罩的生成邏輯泡沫是水體交互里最出效果的部分。物體入水時接觸邊緣會泛起一圈白沫這個細節(jié)能讓交互的真實感提升一個檔次。泡沫的生成邏輯是在深度差值的邊緣區(qū)域也就是場景深度和水面深度接近的地方生成泡沫。具體來說當深度差小于某個閾值時泡沫強度逐漸增大當深度差為負物體在水面以下時泡沫強度達到最大。float foam saturate(1.0 - depthDiff / _FoamDistance); foam * step(0, depthDiff); // 只在水面以上生成但這樣生成的泡沫是靜態(tài)的物體不動的時候泡沫就一直貼在那里。為了讓泡沫有動態(tài)感可以疊加一層噪聲紋理用時間做偏移讓泡沫看起來在流動。實操心得泡沫的閾值不要設得太死留一點過渡區(qū)間。我一般用smoothstep而不是step這樣泡沫的邊緣會更柔和不會出現(xiàn)硬邊。3.4 折射偏移與交互的耦合水面的折射效果通常是用GrabPass或者URP的CameraOpaqueTexture來實現(xiàn)的。采樣屏幕顏色時根據(jù)法線偏移UV就能做出折射的扭曲感。當物體和水面交互時折射偏移應該跟著擾動變化。具體來說交互強度越大折射偏移越明顯。這樣物體入水時水下的部分會因為折射扭曲而顯得更“在水里”。float2 refractionOffset normal.xy * _RefractionStrength; refractionOffset interactStrength * _InteractRefraction; float3 sceneColor tex2D(_CameraOpaqueTexture, uv refractionOffset).rgb;這里要注意折射偏移的方向要和法線方向一致否則會出現(xiàn)反向扭曲的詭異效果。4. 實操過程從零搭建一套可用的水體交互4.1 場景準備與管線配置先確認你的渲染管線。內置管線的話在主相機上掛一個腳本設置depthTextureMode DepthTextureMode.Depth。URP的話在Universal Render Pipeline Asset里勾選Depth Texture和Opaque Texture。然后創(chuàng)建一個水面平面。不要用Unity自帶的Plane那個只有121個頂點做頂點位移不夠用。我一般用ProBuilder或者Blender做一個細分網(wǎng)格分辨率至少200x200。如果性能吃緊可以降到100x100但波浪的細節(jié)會損失一些。水面的材質新建一個Shader。我習慣從Unlit Shader開始改因為不需要默認的光照模型全部自己控制。4.2 深度圖采樣與交互區(qū)域計算在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 波紋擴散與法線擾動交互紋理里的強度是靜態(tài)的物體不動漣漪就不動。為了讓漣漪擴散可以在采樣時對UV做時間偏移或者用多個采樣點做模糊。我的做法是用一個簡單的衰減擴散在C#腳本里每幀對RenderTexture做一次模糊同時把強度乘以一個衰減系數(shù)。這樣漣漪會自然擴散并消失。// 每幀調用 Graphics.Blit(interactRT, tempRT, blurMaterial); Graphics.Blit(tempRT, interactRT, fadeMaterial);模糊材質用高斯模糊或者簡單的Box Blur衰減材質就是把顏色乘以0.98。法線擾動則是在水面Shader里根據(jù)交互強度對法線進行偏移float3 normal UnpackNormal(tex2D(_NormalMap, uv _Time.y * _FlowSpeed)); normal.xy interact.r * _InteractNormalStrength; normal normalize(normal);4.5 泡沫與折射的最終合成泡沫的合成是在深度差值的邊緣區(qū)域疊加一層白色float foam saturate(1.0 - abs(depthDiff) / _FoamDistance); foam * interact.g; float3 finalColor lerp(waterColor, foamColor, foam);折射的合成是在采樣屏幕顏色時根據(jù)法線和交互強度偏移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 交互區(qū)域偏移或錯位交互紋理的UV映射和深度圖的屏幕UV映射是兩套坐標。如果交互區(qū)域和水面的實際接觸點對不上大概率是UV映射的公式寫錯了。檢查_InteractAreaMin和_InteractAreaMax是否覆蓋了水面的實際范圍以及世界坐標到UV的映射是否用了正確的軸。避坑技巧在Scene視圖里畫一個Gizmo把交互區(qū)域的范圍可視化出來這樣一眼就能看出對不對。5.3 波紋不擴散或擴散太快波紋的擴散速度取決于模糊的半徑和衰減系數(shù)。模糊半徑太大波紋會糊成一團太小又看不出擴散。衰減系數(shù)太大波紋消失太快太小又會一直殘留。我實測下來模糊半徑用2到4個像素衰減系數(shù)用0.96到0.98效果比較自然。具體數(shù)值要根據(jù)你的幀率和紋理分辨率調。5.4 移動端性能優(yōu)化深度圖采樣和交互紋理繪制在移動端是有開銷的。如果幀率吃緊可以降級深度圖用半分辨率交互紋理用256x256模糊只做一次。泡沫和折射可以關掉只保留法線擾動。另外交互紋理的繪制不要每幀都做可以隔幀做或者只在物體移動時做。靜止的物體不需要每幀重繪。問題現(xiàn)象可能原因排查方向深度圖全黑深度紋理未生成檢查相機設置和管線配置交互區(qū)域錯位UV映射錯誤檢查世界坐標到UV的轉換公式波紋不擴散模糊或衰減參數(shù)不當調整模糊半徑和衰減系數(shù)移動端卡頓深度圖和交互紋理開銷大降分辨率、隔幀繪制、關閉次要效果泡沫硬邊閾值用了step改用smoothstep做過渡5.5 水面反射與交互的沖突如果水面用了反射探針或者平面反射交互波紋可能會被反射效果覆蓋。解決辦法是在反射采樣之后再疊加交互效果。也就是說先算反射再算法線擾動和泡沫最后合成。順序反了交互效果就會被反射蓋住。6. 進階擴展讓水體交互更有層次感6.1 基于物理的尾跡生成船劃過水面時尾跡不是簡單的圓形漣漪而是V字形的擴散波。這個可以用交互紋理的繪制方式來實現(xiàn)不是畫圓而是畫一個V字形的遮罩隨著時間逐漸擴散和衰減。具體做法是在C#腳本里根據(jù)船的速度和方向計算V字形的兩個邊然后用Graphics.DrawMesh畫到交互紋理上。V字形的角度和船速相關速度越快角度越小。6.2 雨滴漣漪的隨機分布下雨場景里水面需要隨機分布的漣漪。這個可以用一個噪聲紋理驅動交互紋理的繪制每幀在隨機位置畫一個小圓強度隨機半徑隨機。為了性能可以預生成一批漣漪位置循環(huán)使用。6.3 水下視角的交互反饋當相機潛入水下時水面交互應該有不同的表現(xiàn)。水下看水面應該看到全反射和Snell窗效果。這個需要根據(jù)相機的深度切換水面的渲染模式。具體來說當相機在水面以下時菲涅爾項要反轉折射要變成全反射。這個擴展比較復雜涉及到相機的深度檢測和Shader的變體切換。如果你的項目有水下場景這個值得做。6.4 交互紋理的壓縮與復用如果場景里有多個水面可以共用一張交互紋理。只要它們的交互區(qū)域不重疊或者重疊區(qū)域可以接受相互影響。這樣可以省顯存也省繪制開銷。紋理的格式可以用R8只存強度泡沫和法線擾動用程序化生成。這樣顯存占用可以降到四分之一。7. 我個人在實際操作中的幾點體會水體交互這個東西參數(shù)調起來很磨人。同樣的Shader換一個場景參數(shù)就得重調。我的經(jīng)驗是先把深度檢測和交互區(qū)域調準這是地基。地基不穩(wěn)后面泡沫和折射怎么調都是歪的。泡沫的閾值我一般會留一個_FoamSoftness參數(shù)用來控制邊緣的柔和度。這個參數(shù)在物體快速移動時特別有用硬邊的泡沫會像貼紙一樣假柔和的泡沫才有水花的感覺。交互紋理的衰減速度不要設得太快。我見過很多項目漣漪剛出來就沒了看起來像水面在抽搐。衰減慢一點讓漣漪有擴散的余韻真實感會強很多。最后移動端的話能省則省。深度圖半分辨率交互紋理256模糊一次泡沫和折射二選一。先把法線擾動做出來那個是性價比最高的效果。等性能有余量了再加泡沫和折射。這套方案我在幾個項目里都用過從PC到移動端都跑得動。核心就是深度圖加交互紋理剩下的都是在這個骨架上添肉。你把骨架搭穩(wěn)了想加什么效果都方便。