現(xiàn):Shader UV重映射后處理全解析)
做Unity黑洞扭曲效果時(shí)我一開(kāi)始想得很天真用粒子在黑洞位置做個(gè)吸積盤再加個(gè)光暈貼圖看起來(lái)差不多就行。結(jié)果把粒子疊到真實(shí)場(chǎng)景里一測(cè)問(wèn)題立刻暴露——粒子只能影響自己周圍那一小塊畫面粒子背后的建筑、天空、地面全都是原封不動(dòng)的。黑洞的“吞噬感”完全立不住。后來(lái)我才想明白真正的扭曲必須作用在已經(jīng)渲染好的整張畫面上也就是要對(duì)屏幕圖像做二次采樣讓每個(gè)像素去重新決定“自己應(yīng)該顯示原來(lái)畫面里的哪個(gè)位置”。這正是扭曲著色器的核心價(jià)值。這篇就完整拆解這個(gè)實(shí)現(xiàn)從屏幕空間UV重映射的原理、Shader代碼、C#相機(jī)腳本到調(diào)參細(xì)節(jié)和踩坑記錄最后給出能直接跑起來(lái)的完整工程思路。1. 為什么黑洞扭曲必須交給屏幕空間著色器而不是粒子或模型變形1.1 三種方案放在一起比過(guò)之后結(jié)論很明確我最早踩的坑是用粒子方案。粒子系統(tǒng)對(duì)“吞噬”這種效果確實(shí)擅長(zhǎng)把粒子從四周吸過(guò)來(lái)、改變大小、疊加發(fā)光材質(zhì)視覺(jué)沖擊力很強(qiáng)。但粒子是場(chǎng)景里的獨(dú)立物體它渲染出來(lái)的內(nèi)容只能疊加在自己所在的片元上不可能對(duì)背景已經(jīng)畫好的像素產(chǎn)生“彎曲”。你想象一下黑洞下方有一堵墻墻不會(huì)因?yàn)榱W语w過(guò)就真的扭起來(lái)。所以粒子方案只能做氣氛做不了真正的空間扭曲。第二種思路是模型頂點(diǎn)變形。把場(chǎng)景里的Mesh做高細(xì)分然后在頂點(diǎn)著色器里根據(jù)距離旋轉(zhuǎn)頂點(diǎn)坐標(biāo)確實(shí)可以讓那一個(gè)模型扭起來(lái)。但問(wèn)題是天空盒、UI、粒子、大量沒(méi)有足夠細(xì)分的場(chǎng)景物件怎么辦你的黑洞效果不可能只作用于某一個(gè)模型它需要扭曲整個(gè)畫面。頂點(diǎn)變形天然解決不了“全屏扭曲”這個(gè)需求。第三種就是屏幕空間UV重映射也是最終采用的方案。它和前兩種有本質(zhì)區(qū)別它不修改任何場(chǎng)景物體而是等整幀畫面渲染完成之后把這張畫面當(dāng)作一張大紋理再去重新采樣。每個(gè)像素都問(wèn)自己一個(gè)問(wèn)題我不顯示自己原本的顏色我去采原畫面里另一個(gè)位置的顏色這樣看起來(lái)是不是就像畫面被擰了一下。這就是扭曲著色器的核心邏輯——屏幕空間UV重映射。它作用在整個(gè)畫面上所有已經(jīng)渲染出來(lái)的東西都會(huì)跟著扭曲。1.2 UV重映射的本質(zhì)改寫采樣地址而不是移動(dòng)物體用一個(gè)生活類比來(lái)說(shuō)明。把屏幕里已經(jīng)渲染好的完整畫面當(dāng)作一張打印出來(lái)的照片黑洞扭曲不是用PS里的“液化”一點(diǎn)點(diǎn)涂抹而是相當(dāng)于在照片上放了一塊哈哈鏡你眼睛看到的是鏡子里反射出來(lái)的照片局部鏡子在這個(gè)位置把畫面擰了一圈、拉近了一點(diǎn)。著色器做的就是這件事它不改變?cè)掌皇亲屆總€(gè)屏幕像素的采樣地址發(fā)生偏移。具體到黑洞場(chǎng)景采樣地址的偏移規(guī)律只有兩條第一越靠近黑洞中心的像素采樣的位置要旋轉(zhuǎn)得越多第二越靠近黑洞中心的像素采樣的位置要越往中心方向壓縮。旋轉(zhuǎn)產(chǎn)生“旋渦感”壓縮產(chǎn)生“吸入感”兩個(gè)疊加起來(lái)屏幕空間的黑洞扭曲效果就出來(lái)了。后面的Shader代碼本質(zhì)就是在實(shí)現(xiàn)這兩條規(guī)律。2. Shader核心屏幕取圖、漩渦旋轉(zhuǎn)與徑向壓縮的實(shí)現(xiàn)2.1 先搞清楚取屏幕畫面的兩條路OnRenderImage的source與GrabPass實(shí)現(xiàn)全屏扭曲第一步是拿到“已經(jīng)渲染好的畫面”。Unity里有兩種常見(jiàn)取圖方式這里必須分清楚因?yàn)樗鼪Q定了整個(gè)Shader怎么寫。第一種是后處理管線里的OnRenderImage。相機(jī)的渲染結(jié)果會(huì)作為source RenderTexture傳給C#函數(shù)然后在Shader里直接采樣_MainTex就能拿到當(dāng)前畫面。這種方式適合全屏后處理簡(jiǎn)單直接Unity會(huì)自動(dòng)處理平臺(tái)差異代碼里基本不用關(guān)心UV翻轉(zhuǎn)問(wèn)題。我們的主方案就走這條路。第二種是GrabPass。它在物體渲染之前抓取當(dāng)前屏幕內(nèi)容保存到一張紋理里之后任何Shader都可以通過(guò)GrabPass {}聲明并采樣這張紋理。這種方式適合“局部物體扭曲屏幕內(nèi)容”的場(chǎng)景比如在黑洞物體位置放一個(gè)Quad只處理Quad覆蓋范圍內(nèi)的畫面從而減少全屏后處理的開(kāi)銷。代價(jià)是GrabPass的開(kāi)銷本身不低而且在OpenGL和DirectX下紋理UV方向不一致需要手動(dòng)處理翻轉(zhuǎn)。對(duì)于這篇的Demo我優(yōu)先用OnRenderImage的后處理方式因?yàn)檫壿嬜钋逦⒆畈蝗菀撞绕脚_(tái)坑方便你把核心扭曲算法調(diào)明白。2.2 扭曲算法的推導(dǎo)距離衰減、旋轉(zhuǎn)量與壓縮Shader的fragment shader里屏幕UV坐標(biāo)范圍內(nèi)每個(gè)像素都有一段自己的邏輯。核心分四步第一步算當(dāng)前像素到黑洞中心的距離。這一步很簡(jiǎn)單UV相減取length得到dist。第二步根據(jù)距離算衰減系數(shù)t。當(dāng)像素剛好在黑洞邊緣時(shí)衰減應(yīng)該接近0越靠近黑洞中心衰減接近1。用t 1.0 - dist / _Radius就能得到從邊緣0到中心1的線性變化。但線性變化視覺(jué)上太均勻不夠“擰”所以后面還要加pow和smoothstep做衰減塑形。第三步根據(jù)衰減系數(shù)算旋轉(zhuǎn)角度。想讓畫面旋轉(zhuǎn)起來(lái)角度必須跟時(shí)間掛鉤用_Time.y乘以一個(gè)速度系數(shù)即可。同時(shí)為了讓中心區(qū)域比邊緣轉(zhuǎn)得更狠角度要再乘一個(gè)衰減系數(shù)。這樣距離中心越近的像素采樣點(diǎn)被旋轉(zhuǎn)的角度越大就形成“中心擰得最厲害”的旋渦效果。第四步做向心壓縮。把原本指向黑洞中心的方向向量旋轉(zhuǎn)angle角度之后再乘一個(gè)小于1的壓縮系數(shù)讓采樣點(diǎn)傾向于被拉向中心。這是“吸入感”的來(lái)源。2.3 可直接復(fù)制的Shader代碼下面是可以直接運(yùn)行的Shader代碼。使用方法新建Shader文件粘貼這段代碼再新建材質(zhì)把Shader指定成它其余參數(shù)保持默認(rèn)即可。Shader Custom/BlackHoleDistortion { Properties { _MainTex (Screen Texture, 2D) white {} _Center (Center (Viewport), Vector) (0.5, 0.5, 0, 0) _Radius (Radius, Range(0, 1)) 0.25 _SwirlStrength (Swirl Strength, Range(0, 5)) 1.5 _SwirlSpeed (Swirl Speed, Range(-5, 5)) 2.0 _PullStrength (Pull Strength, Range(0, 1)) 0.35 _FalloffPower (Falloff Power, Range(0.5, 4)) 2.0 } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_TexelSize; float2 _Center; float _Radius; float _SwirlStrength; float _SwirlSpeed; float _PullStrength; float _FalloffPower; fixed4 frag (v2f_img i) : SV_Target { float2 uv i.uv; // 計(jì)算當(dāng)前像素到黑洞中心的偏移和距離 float2 toCenter uv - _Center; float dist length(toCenter); // 半徑之外直接返回原始畫面 if (dist _Radius) return tex2D(_MainTex, uv); // t從邊緣0過(guò)渡到中心1 float t 1.0 - dist / _Radius; float falloff pow(t, _FalloffPower); falloff * smoothstep(0.0, 0.05, t); // 旋轉(zhuǎn)角度衰減系數(shù)乘以時(shí)間形成漩渦動(dòng)態(tài) float angle _SwirlStrength * falloff * _Time.y * _SwirlSpeed; float s, c; sincos(angle, s, c); // 對(duì)偏移向量做旋轉(zhuǎn) float2 rotated float2( c * toCenter.x - s * toCenter.y, s * toCenter.x c * toCenter.y ); // 向心壓縮越靠近中心壓縮得越狠 rotated * 1.0 - _PullStrength * falloff; // 重新映射采樣位置 float2 finalUv _Center rotated; // 防止采樣到屏幕外產(chǎn)生拉伸黑邊 float2 minUv _MainTex_TexelSize.xy; float2 maxUv 1.0 - _MainTex_TexelSize.xy; finalUv clamp(finalUv, minUv, maxUv); return tex2D(_MainTex, finalUv); } ENDCG } } }注意幾個(gè)關(guān)鍵點(diǎn)vert_img來(lái)自UnityCG.cginc是后處理Pass專用的頂點(diǎn)函數(shù)它會(huì)自動(dòng)填好v2f_img里的uv。_MainTex_TexelSize是Unity自動(dòng)提供的紋理尺寸信息這里用來(lái)做采樣邊界的clamp防止扭曲后采樣到屏幕外的區(qū)域。_Time.y是Unity內(nèi)置的時(shí)間變量單位是秒。2.4 每個(gè)參數(shù)控制什么Shader屬性速查表參數(shù)默認(rèn)值作用建議范圍_Center(0.5, 0.5)黑洞中心在屏幕上的位置視口坐標(biāo)0~1由C#每幀設(shè)置_Radius0.25黑洞扭曲的影響半徑占屏幕高度的比例0.05 ~ 0.45_SwirlStrength1.5旋渦旋轉(zhuǎn)的強(qiáng)度越大轉(zhuǎn)得越明顯0.5 ~ 5_SwirlSpeed2.0旋渦旋轉(zhuǎn)速度負(fù)數(shù)可反向旋轉(zhuǎn)-5 ~ 5_PullStrength0.35向心吸入強(qiáng)度越大畫面越被拽向中心0 ~ 0.8_FalloffPower2.0衰減曲線冪次越大邊緣越銳利、影響范圍越小0.5 ~ 4調(diào)參的時(shí)候最容易出現(xiàn)的問(wèn)題是_SwirlStrength和_SwirlSpeed同時(shí)調(diào)大中心區(qū)域每秒旋轉(zhuǎn)的圈數(shù)會(huì)瘋漲視覺(jué)上很容易暈甚至出現(xiàn)畫面倒轉(zhuǎn)的錯(cuò)覺(jué)。建議先固定速度從小到大推Strength找到最舒服的手感。3. C#腳本控制讓黑洞中心跟著場(chǎng)景物體走3.1 OnRenderImage的執(zhí)行時(shí)機(jī)與Blit的作用Shader寫完后要讓相機(jī)在渲染完正常畫面之后自動(dòng)執(zhí)行這段扭曲邏輯得把Shader掛到材質(zhì)上再讓材質(zhì)參與相機(jī)的后處理。Unity內(nèi)置的后處理入口就是OnRenderImage。這個(gè)函數(shù)的執(zhí)行時(shí)機(jī)在相機(jī)渲染完所有場(chǎng)景物體之后但在最終輸出到屏幕之前。你會(huì)拿到兩個(gè)RenderTexture參數(shù)source是已經(jīng)渲染好的完整場(chǎng)景圖destination是最終要輸出的目標(biāo)。通常用Graphics.Blit(source, destination, material)把source作為輸入紋理傳給Shader的_MainTex經(jīng)過(guò)材質(zhì)中的Pass處理后再寫入destination。這就是著一個(gè)后處理的全過(guò)程。如果材質(zhì)或控制參數(shù)不滿足條件直接Graphics.Blit(source, destination)做一次無(wú)處理的拷貝即可這樣黑洞腳本即使空轉(zhuǎn)也不會(huì)黑屏。3.2 世界坐標(biāo)到視口坐標(biāo)的換算Shader里需要的_Center是視口坐標(biāo)范圍0到1而你在場(chǎng)景里擺放的黑洞物體是一個(gè)世界坐標(biāo)。所以要先把世界坐標(biāo)轉(zhuǎn)換到屏幕Viewport坐標(biāo)。Unity里一個(gè)現(xiàn)成的API是Camera.WorldToViewportPoint。這個(gè)方法返回一個(gè)Vector3x和y就是0到1的視口坐標(biāo)z代表物體到相機(jī)的深度距離。當(dāng)z小于0時(shí)說(shuō)明物體在相機(jī)背后此時(shí)黑洞不在畫面內(nèi)應(yīng)該跳過(guò)扭曲直接拷貝。還要考慮一種情況物體雖然在相機(jī)前方但x或y跑出了0到1范圍此時(shí)黑洞不在屏幕內(nèi)同樣跳過(guò)。把viewport坐標(biāo)直接設(shè)置給Shader的_Center就能讓黑洞中心跟著場(chǎng)景里的物體實(shí)時(shí)移動(dòng)。3.3 完整C#腳本與使用步驟下面是可以直接用的控制腳本。掛到相機(jī)上后把材質(zhì)和場(chǎng)景中的黑洞錨點(diǎn)拖進(jìn)去即可運(yùn)行。using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class BlackHoleEffect : MonoBehaviour { public Material effectMaterial; public Transform blackHoleTransform; [Range(0f, 1f)] public float radius 0.25f; [Range(0f, 5f)] public float swirlStrength 1.5f; [Range(-5f, 5f)] public float swirlSpeed 2f; [Range(0f, 1f)] public float pullStrength 0.35f; [Range(0.5f, 4f)] public float falloffPower 2f; private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (effectMaterial null || blackHoleTransform null) { Graphics.Blit(source, destination); return; } Vector3 viewportPos GetComponentCamera().WorldToViewportPoint(blackHoleTransform.position); // 黑洞在相機(jī)背后時(shí)不處理 if (viewportPos.z 0f) { Graphics.Blit(source, destination); return; } effectMaterial.SetVector(_Center, new Vector4(viewportPos.x, viewportPos.y, 0f, 0f)); effectMaterial.SetFloat(_Radius, radius); effectMaterial.SetFloat(_SwirlStrength, swirlStrength); effectMaterial.SetFloat(_SwirlSpeed, swirlSpeed); effectMaterial.SetFloat(_PullStrength, pullStrength); effectMaterial.SetFloat(_FalloffPower, falloffPower); Graphics.Blit(source, destination, effectMaterial); } }使用步驟在場(chǎng)景中放置一個(gè)空物體命名為BlackHoleAnchor作為黑洞中心錨點(diǎn)。創(chuàng)建Shader和材質(zhì)材質(zhì)選擇Custom/BlackHoleDistortion。新建腳本將上面的代碼復(fù)制進(jìn)去掛到主相機(jī)上。把材質(zhì)拖到effectMaterial把BlackHoleAnchor拖到blackHoleTransform。運(yùn)行場(chǎng)景移動(dòng)BlackHoleAnchor就能看到扭曲效果跟隨物體移動(dòng)。[ExecuteInEditMode]加上之后在編輯器里不點(diǎn)擊Play也能預(yù)覽效果調(diào)參非常方便。但要注意編輯模式下如果相機(jī)不在當(dāng)前顯示狀態(tài)OnRenderImage也可能不會(huì)觸發(fā)。實(shí)踐中最穩(wěn)妥的做法是運(yùn)行時(shí)調(diào)整。4. 讓效果“像黑洞”的調(diào)參思路速度、衰減、徑向壓縮的配合4.1 為什么不能只轉(zhuǎn)圈要加徑向壓縮先做一個(gè)實(shí)驗(yàn)把_PullStrength調(diào)成0只保留旋轉(zhuǎn)。你會(huì)看到畫面雖然擰起來(lái)了但更像“攪拌機(jī)”而不是黑洞。原因很簡(jiǎn)單真實(shí)黑洞對(duì)周圍空間的拉扯是向心的物質(zhì)不僅繞中心轉(zhuǎn)也在不斷被拉向中心。只有旋轉(zhuǎn)沒(méi)有向心壓縮畫面缺乏“被吸進(jìn)去”的緊張感。把_PullStrength從0慢慢往上加觀察畫面上細(xì)節(jié)明顯的區(qū)域比如網(wǎng)格地面或貼了紋理的墻面。你會(huì)發(fā)現(xiàn)它們不僅被旋擰還逐漸向中心聚攏。這個(gè)視覺(jué)變化就是“吞噬感”的來(lái)源。但壓縮不能加過(guò)頭后面會(huì)說(shuō)為什么。4.2 衰減曲線決定“擰”的范圍和力度分布_FalloffPower這個(gè)參數(shù)控制的是衰減曲線形態(tài)很多人會(huì)忽略它。power1時(shí)從中心到邊緣的旋轉(zhuǎn)量是線性遞減的視覺(jué)上過(guò)度均勻但缺少焦點(diǎn)。power2或3時(shí)中心附近的旋轉(zhuǎn)量明顯更大邊緣更平緩畫面看起來(lái)“中心擰得很緊、邊緣自然散開(kāi)”更接近旋渦的視覺(jué)預(yù)期。power繼續(xù)加大到4以上扭曲區(qū)域會(huì)向中心收窄黑洞看起來(lái)更小、更有壓迫感。同時(shí)Shader里還乘了一個(gè)smoothstep(0, 0.05, t)它的作用是讓緊貼半徑邊緣的那一薄層過(guò)渡更柔和避免邊界出現(xiàn)明顯的“圓圈烙印”。如果覺(jué)得邊緣還是硬可以把這個(gè)smoothstep的范圍從0.05加到0.1代價(jià)是扭曲影響范圍會(huì)略微擴(kuò)大。4.3 徑向壓縮不能超過(guò)0.8的數(shù)學(xué)原因_PullStrength在Shader里的用法是rotated * 1.0 - _PullStrength * falloff;仔細(xì)看這個(gè)式子當(dāng)_PullStrength小于1時(shí)1.0 - _PullStrength * falloff始終大于0采樣地址始終在中心到原位置的連線上不會(huì)越過(guò)中心點(diǎn)。一旦_PullStrength大于1靠近中心的區(qū)域可能出現(xiàn)負(fù)系數(shù)采樣點(diǎn)會(huì)翻轉(zhuǎn)到中心的另一側(cè)畫面會(huì)出現(xiàn)類似“對(duì)折”的撕裂感看起來(lái)非常奇怪。所以這個(gè)參數(shù)的范圍限制在0到1之間實(shí)際操作中我建議最高只調(diào)到0.7。超過(guò)0.7后中心區(qū)域的畫面被壓縮得過(guò)于密集像素采樣的畫面內(nèi)容高度重復(fù)會(huì)出現(xiàn)臟兮兮的混色效果。想增強(qiáng)“吸入感”可以配合增加半徑或者降低FalloffPower都比硬拉壓縮系數(shù)穩(wěn)定。4.4 時(shí)間積分與長(zhǎng)時(shí)間運(yùn)行的角度精度Shader里的旋轉(zhuǎn)角度是_SwirlStrength * falloff * _Time.y * _SwirlSpeed意思是角度隨運(yùn)行時(shí)間線性增長(zhǎng)。如果游戲跑了幾十分鐘角度值會(huì)變得非常大此時(shí)使用sincos仍然能計(jì)算出正確結(jié)果但在某些移動(dòng)平臺(tái)的低精度浮點(diǎn)上大角度計(jì)算可能產(chǎn)生微小的抖動(dòng)。實(shí)際項(xiàng)目里我傾向于在C#側(cè)維護(hù)一個(gè)歸一化的角度值取模到360度或者2π以內(nèi)再傳入Shader避免這種潛在問(wèn)題。比如在C#里float angle Mathf.Repeat(_timeSinceStart * swirlSpeed, 360f); material.SetFloat(_AngleOffset, angle);然后把Shader里的角度改為angle _AngleOffset * falloff * _SwirlStrength。這樣既保證動(dòng)畫連續(xù)又讓角度值始終在一個(gè)安全范圍內(nèi)。5. 實(shí)測(cè)踩坑坐標(biāo)翻轉(zhuǎn)、硬邊緣、GrabPass開(kāi)銷與移動(dòng)端優(yōu)化5.1 屏幕UV翻轉(zhuǎn)問(wèn)題為什么同樣的代碼在兩個(gè)平臺(tái)表現(xiàn)不一樣后處理Shader里如果你直接使用OnRenderImage的sourceUnity內(nèi)置的vert_img已經(jīng)幫你處理了OpenGL和DirectX的UV方向差異。但如果你改用GrabPass方案就要自己處理翻轉(zhuǎn)。具體現(xiàn)象是同樣的GrabPass代碼在編輯器里正常打包到Android模擬器或某些OpenGL設(shè)備上扭曲中心偏移到了對(duì)稱位置像是把畫面翻了個(gè)面。根因是OpenGL的渲染紋理原點(diǎn)在左下角DirectX的原點(diǎn)位于左上角。Unity在大多數(shù)情況下會(huì)通過(guò)_MainTex_TexelSize.y的符號(hào)告訴你當(dāng)前平臺(tái)是否需要翻轉(zhuǎn)。使用GrabPass時(shí)一個(gè)常見(jiàn)的處理方式是#if UNITY_UV_STARTS_AT_TOP if (_GrabTexture_TexelSize.y 0) uv.y 1 - uv.y; #endif只有搞清楚紋理坐標(biāo)系的差異GrabPass方案才不會(huì)出問(wèn)題。這也是我推薦先學(xué)OnRenderImage方案的另一個(gè)原因它能讓你避開(kāi)干擾項(xiàng)專心調(diào)扭曲效果本身。5.2 扭曲邊緣的生硬斷層不只是衰減問(wèn)題全屏后處理實(shí)現(xiàn)的黑洞扭曲最影響觀感的問(wèn)題之一就是扭曲區(qū)域和正常區(qū)域之間出現(xiàn)明顯分界線。分界線的來(lái)源不只是衰減曲線還有采樣越界。如果扭曲后的finalUv超出了屏幕范圍tex2D會(huì)按Clamp模式采樣把邊緣像素拉長(zhǎng)成一條條色帶看起來(lái)像“光圈”一樣套在黑洞外面。解決方式是Shader里對(duì)finalUv做clamp并且clamp的邊界不是0和1而是留一個(gè)_MainTex_TexelSize的邊距。這樣即使理論采樣的UV越界也不會(huì)直接暴露為硬邊而是被柔和地限制在有效像素范圍內(nèi)。另一個(gè)常見(jiàn)坑是clamp后畫面出現(xiàn)“凍結(jié)”效果這是Clamp采樣本身的特點(diǎn)無(wú)法完全避免。如果追求極端效果可以考慮在邊緣追加一個(gè)遮蓋Pass把分界線藏在暗色漸變里。5.3 全屏后處理的性能代價(jià)與局部Quad方案全屏后處理意味著每一幀都要對(duì)屏幕上每個(gè)像素運(yùn)行一遍fragment shader。即使半徑之外的像素直接返回原始顏色也要執(zhí)行一次距離計(jì)算和一次紋理采樣。在1080p分辨率下這就是1920×1080約200萬(wàn)個(gè)像素的運(yùn)算量。對(duì)高端機(jī)不算什么但在低端移動(dòng)設(shè)備上如果在OnRenderImage里疊加多個(gè)后處理效果幀率會(huì)明顯下滑。一種常見(jiàn)的優(yōu)化思路是把扭曲效果從全屏改成局部即“局部Quad GrabPass”方案。具體做法是在黑洞位置放置一個(gè)始終面向相機(jī)的QuadQuad的Shader使用GrabPass抓取屏幕然后在Quad的fragment shader里執(zhí)行相同的扭曲算法。這樣Shader只處理Quad覆蓋范圍內(nèi)的像素范圍之外完全不跑。缺點(diǎn)是GrabPass抓屏本身有一定開(kāi)銷而且Quad需要遮擋剔除、屏幕尺寸動(dòng)態(tài)適配等額外邏輯。我的經(jīng)驗(yàn)是在老舊的移動(dòng)設(shè)備上局部Quad方案通常能比全屏方案快20%到30%但在HDRI、多相機(jī)場(chǎng)景下優(yōu)勢(shì)會(huì)被GrabPass的帶寬消耗抵消。具體取舍要結(jié)合項(xiàng)目跑分。5.4 移動(dòng)端與WebGL的適配建議Shader里的數(shù)學(xué)函數(shù)在移動(dòng)端都沒(méi)有問(wèn)題但要注意不要依賴半精度浮點(diǎn)來(lái)計(jì)算大范圍距離。length函數(shù)在不同精度下的表現(xiàn)不太一樣低精度下邊緣曲線可能抖動(dòng)。建議在項(xiàng)目里開(kāi)啟Metal/Vulkan的#pragma target 3.0并確保_FalloffPower的pow計(jì)算用的是float精度。WebGL方面這個(gè)效果的兼容性重點(diǎn)在紋理采樣格式。后處理source通常是RGBA Half FloatWebGL1.0不支持半浮點(diǎn)紋理的話可能出現(xiàn)畫面變紫或采樣異常。如果項(xiàng)目面向WebGL建議在Gradle或Player Settings里確認(rèn)Graphics API優(yōu)先WebGL2.0并將后處理紋理格式保持默認(rèn)的R8G8B8A8等效果跑通后再考慮更高質(zhì)量的HDR格式。6. 進(jìn)階玩法事件視界光暈、粒子配合與URP適配6.1 在同一個(gè)Pass里給黑洞疊加環(huán)形光暈黑洞畫面如果只有扭曲缺少一個(gè)明確的視覺(jué)錨點(diǎn)觀眾不容易一眼看清黑洞中心在哪。一個(gè)低成本的做法是在Shader片段里疊加一個(gè)事件視界光暈也就是圍繞黑洞中心畫一個(gè)發(fā)光環(huán)。不需要額外Pass在扭曲完成后直接修改顏色即可// 在return之前追加 fixed4 col tex2D(_MainTex, finalUv); // 事件視界光環(huán)距離中心約radius的一定比例處產(chǎn)生亮環(huán) float ringCenterDist _Radius * 0.35; float ringDist abs(dist - ringCenterDist); float ring 1.0 - smoothstep(0.0, _Radius * 0.06, ringDist); col.rgb ring * _RingColor.rgb * _RingIntensity; return col;注意dist是當(dāng)前像素到中心的距離ringCenterDist可以理解為光環(huán)落在扭曲范圍的哪個(gè)位置。調(diào)整_Radius * 0.35這個(gè)比例可以讓光環(huán)靠近中心或遠(yuǎn)離中心。光暈強(qiáng)度不要太高否則會(huì)把扭曲細(xì)節(jié)完全蓋住。6.2 粒子系統(tǒng)與黑洞效果聯(lián)動(dòng)Shader只能改畫面采樣位置無(wú)法憑空生成新的內(nèi)容。想讓黑洞周圍有吸積盤塵埃、光線被吸入的動(dòng)態(tài)細(xì)節(jié)最好配合粒子系統(tǒng)。做法是在BlackHoleAnchor下掛一個(gè)ParticleSystem粒子使用程序化噪聲的流動(dòng)貼圖從外圍向中心運(yùn)動(dòng)渲染模式選擇Billboard。粒子材質(zhì)不要用Additive建議用Alpha Blend配合暗色背景壓暗否則會(huì)像一團(tuán)白光浮在場(chǎng)景上。粒子發(fā)射方向始終不是真實(shí)黑洞的物理模擬但要的就是“視覺(jué)上被吸進(jìn)去”的感覺(jué)。把粒子速度曲線設(shè)置成越靠近中心越快同時(shí)在中心附近的粒子透明度快速降低造成被吞噬的錯(cuò)覺(jué)。再配合Bloom后處理整體觀感會(huì)非常接近電影里的黑洞表現(xiàn)。6.3 URP環(huán)境下的適配思路以上的OnRenderImage方案在Unity內(nèi)置渲染管線Built-in里可以跑通。如果你的項(xiàng)目用的是URPOnRenderImage不再被支持需要改用Renderer Feature方式。思路并不復(fù)雜創(chuàng)建一個(gè)ScriptableRendererFeature在其中注入一個(gè)自定義Pass在Execute中通過(guò)cmd.Blit把相機(jī)渲染紋理傳入同一個(gè)Shader執(zhí)行扭曲后再Blit回目標(biāo)。Shader本體基本不用改動(dòng)唯一需要注意的是URP下紋理獲取方式有所變化需要在CameraColorTarget中讀取當(dāng)前畫面的顏色緩沖。具體代碼改起來(lái)有一定篇幅網(wǎng)上也有大量現(xiàn)成的“URP Custom Blit Renderer Feature”模板可以參考。核心思想仍然是拿到相機(jī)畫面紋理喂給Custom/BlackHoleDistortion讓Shader做UV重映射。掌握了這層邏輯無(wú)論渲染管線怎么換實(shí)現(xiàn)路徑都很順暢。6.4 多黑洞疊加與參數(shù)隔離如果場(chǎng)景里同時(shí)有多個(gè)黑洞比如兩個(gè)扭曲中心互相靠近直接套同一套參數(shù)就會(huì)出現(xiàn)互相覆蓋的問(wèn)題。最基本的處理方式是給每個(gè)黑洞分配獨(dú)立的材質(zhì)防止SetVector和SetFloat互相污染。更復(fù)雜的做法是把黑洞中心列表傳入Shader在fragment階段遍歷每個(gè)黑洞中心計(jì)算最近中心的扭曲貢獻(xiàn)加權(quán)合成。遍歷能保證多個(gè)黑洞同時(shí)作用但性能開(kāi)銷隨黑洞數(shù)量線性增長(zhǎng)建議黑洞數(shù)量控制在3個(gè)以內(nèi)再多就考慮用Compute Shader處理。動(dòng)手做多個(gè)黑洞前先想清楚每個(gè)黑洞的半徑和壓縮強(qiáng)度是否可以互相疊加還是應(yīng)該用“最強(qiáng)者優(yōu)先”的策略。這類業(yè)務(wù)邏輯放到C#端會(huì)更清晰Shader只負(fù)責(zé)執(zhí)行單一公式即可。我個(gè)人在實(shí)際操作中的最大體會(huì)是這個(gè)效果調(diào)試起來(lái)并不難難的是視覺(jué)上“剛剛好”。扭曲太強(qiáng)畫面瞬間變成災(zāi)難扭曲太弱又像玻璃上沾了水完全沒(méi)有黑洞的壓迫感。我通常會(huì)先關(guān)掉所有后處理只留這個(gè)扭曲在網(wǎng)格地面上反復(fù)移動(dòng)黑洞錨點(diǎn)確認(rèn)旋轉(zhuǎn)、壓縮的配合穩(wěn)定后再疊加Bloom和粒子。最后再分享一個(gè)技巧在編輯器里開(kāi)著[ExecuteInEditMode]一邊拖動(dòng)場(chǎng)景里的黑洞錨點(diǎn)一邊看著Game視圖調(diào)參比一遍遍點(diǎn)Play高效得多這套工作流對(duì)這類視覺(jué)類Shader的調(diào)試幾乎都能用上。