化:從性能黑洞到按需渲染的實戰(zhàn)方案)
1. 項目概述為什么我們需要一個更聰明的陰影工具在Unity里做項目尤其是涉及到開放世界、大場景或者移動端優(yōu)化的時候陰影渲染絕對是一個繞不開的“性能黑洞”。我經(jīng)歷過不止一個項目美術(shù)把場景做得美輪美奐燈光師打了無數(shù)盞燈來營造氛圍結(jié)果一運行幀率直接掉到30以下用Profiler一查GPU的耗時一大半都卡在陰影渲染上。這太常見了。Unity自帶的陰影系統(tǒng)功能強大但它的“默認”行為往往是為了通用性而設(shè)計的不夠“聰明”。比如一個在攝像機視野外、根本看不見的角色僅僅因為它處于某盞平行光下Unity仍然會忠實地把它渲染到陰影貼圖里。又或者一個極其復(fù)雜的靜態(tài)模型明明在整個游戲過程中陰影都不會變化卻每一幀都在參與動態(tài)陰影的計算。這種“無差別”的渲染就是性能浪費的根源。這就是“Smart Shadows”這個工具誕生的背景。它不是一個全新的渲染管線而是一個運行在現(xiàn)有Unity渲染架構(gòu)之上的智能管理層。它的核心目標(biāo)非常明確在保證視覺質(zhì)量不明顯下降的前提下大幅度削減不必要的陰影計算開銷。簡單來說就是讓該有陰影的地方有陰影不該計算或者可以偷懶的地方就絕不多花一毫秒。對于追求60幀甚至120幀流暢體驗尤其是面向性能受限的移動平臺或VR設(shè)備的項目來說這種優(yōu)化不是“錦上添花”而是“雪中送炭”。無論你是技術(shù)美術(shù)TA想要更精細地控制場景表現(xiàn)還是主程在為項目性能達標(biāo)而頭疼這個工具的思路和實現(xiàn)都值得深入了解一下。2. 核心設(shè)計思路從“無腦全渲”到“按需分配”傳統(tǒng)的Unity陰影渲染尤其是實時光陰影其流程可以簡化為對于每一盞需要投射陰影的光源引擎會遍歷場景中所有開啟了“Cast Shadows”的渲染器Renderer將它們從光源視角渲染一遍生成一張深度貼圖即陰影貼圖。之后在正常渲染物體時再去采樣這張貼圖來判斷是否處于陰影中。這個過程是相對“粗暴”的。Smart Shadows的智能就體現(xiàn)在它在這套流程中插入了多層過濾和決策邏輯。它的設(shè)計哲學(xué)是“分級管理”和“動態(tài)評估”。2.1 靜態(tài)與動態(tài)陰影的分離管理這是最基礎(chǔ)也是收益最明顯的一層優(yōu)化。Unity雖然有光照貼圖Lightmap可以烘焙靜態(tài)物體的陰影但對于動態(tài)光源如太陽旋轉(zhuǎn)、手電筒照射下的靜態(tài)物體或者靜態(tài)物體對動態(tài)物體產(chǎn)生的陰影往往還是需要實時計算。Smart Shadows的做法是靜態(tài)物體陰影預(yù)計算對于完全靜態(tài)的場景物體如建筑、山體工具會引導(dǎo)或自動將其陰影信息以某種形式進行“預(yù)計算”。這不一定是烘焙到光照貼圖因為那受限于靜態(tài)光照。更靈活的方式是為這些靜態(tài)物體生成低精度的代理幾何體Proxy Mesh。這個代理幾何體比原始模型簡單得多比如一個復(fù)雜雕像用一個長方體或幾個簡單幾何體組合來近似專門用于陰影渲染。因為形狀近似產(chǎn)生的陰影輪廓在多數(shù)情況下足以亂真但渲染開銷卻降低了幾個數(shù)量級。動態(tài)物體陰影精細化對于角色、車輛等動態(tài)物體則保留使用其原本的網(wǎng)格進行高質(zhì)量的陰影渲染。工具可以提供接口讓開發(fā)者能為重要的動態(tài)物體如主角分配更高的陰影貼圖分辨率而為次要的NPC或小物件分配更低的分辨率。注意使用代理幾何體的關(guān)鍵是要保證其包圍盒Bounds能完全包裹住原始物體否則會導(dǎo)致陰影缺失。同時對于鏤空、欄桿等物體簡單的代理體可能無法產(chǎn)生正確的鏤空陰影這時需要更復(fù)雜的處理或回退到原始網(wǎng)格。2.2 基于視錐體與距離的裁剪這是第二層優(yōu)化解決的是“看不見就不算”的問題。Unity的陰影裁剪通常只基于光源視錐體但Smart Shadows可以做得更激進。攝像機視錐體剔除如果一個物體完全不在當(dāng)前攝像機的視錐體內(nèi)那么它投射的陰影也必然不可見。工具可以主動將這些物體從當(dāng)前幀的陰影投射者列表中剔除。注意這里說的是“陰影投射”而不是物體本身渲染。一個物體本身看不見但它投在可見地面上的陰影可能是可見的這種情況需要額外處理通常通過檢測其陰影是否可能投射到可見區(qū)域內(nèi)來判斷計算稍復(fù)雜。距離衰減隨著物體與攝像機距離的增加其陰影的細節(jié)重要性急劇下降。Smart Shadows可以實現(xiàn)一套基于距離的陰影質(zhì)量衰減系統(tǒng)0-10米使用原始網(wǎng)格或高精度代理體陰影分辨率高。10-30米切換到低精度代理體陰影分辨率降低。30米以上完全禁用該物體的實時陰影或者使用一個極簡的“模糊陰影塊”來代替。這個閾值可以根據(jù)項目性能目標(biāo)動態(tài)調(diào)整。2.3 基于重要性LOD的陰影渲染這一層是上一層的延伸和細化可以與模型的渲染LODLevel of Detail系統(tǒng)聯(lián)動。當(dāng)模型根據(jù)距離切換到更低級別的LOD網(wǎng)格時其用于陰影渲染的網(wǎng)格也可以同步切換。通常陰影LOD可以比渲染LOD更激進一些因為陰影對幾何細節(jié)的敏感度低于顏色和紋理。實操心得在實際項目中我們常常單獨制作一套用于陰影的LOD網(wǎng)格Shadow LOD這套網(wǎng)格可以只保留物體的大致輪廓將內(nèi)部復(fù)雜的結(jié)構(gòu)全部刪去或簡化。例如一棵樹其渲染LOD可能從2000面降到500面而它的陰影LOD可以直接用一個十字交叉的兩個面片來替代在陰影貼圖上看起來依然是一棵樹的形狀但渲染開銷天差地別。3. 關(guān)鍵技術(shù)實現(xiàn)細節(jié)解析有了清晰的設(shè)計思路我們來看看如何用代碼在Unity中實現(xiàn)這些功能。這里不會貼出全部代碼但會闡述關(guān)鍵的技術(shù)點和實現(xiàn)路徑。3.1 如何攔截并管理陰影投射列表這是整個工具的核心。Unity在渲染陰影貼圖前會收集需要投射陰影的渲染器列表。我們需要在這個收集過程中介入。實現(xiàn)方案使用OnPreCull回調(diào)我們可以為燈光或一個全局管理器添加OnPreCull事件監(jiān)聽。在這個事件里我們能知道即將開始渲染的是哪盞燈以及它的類型平行光、點光源等。替換或過濾渲染列表Unity內(nèi)部最終會調(diào)用RenderShadowMap之類的函數(shù)。更實用的方法是利用CommandBuffer。我們可以創(chuàng)建一個CommandBuffer在Light.AddCommandBuffer時加入到光源的陰影渲染通道中。在這個CommandBuffer的渲染指令中我們不使用默認的渲染路徑而是自己調(diào)用Graphics.DrawMesh或Graphics.DrawRenderer并傳入我們自定義的、經(jīng)過過濾的渲染器列表或網(wǎng)格矩陣列表。動態(tài)構(gòu)建代理體繪制命令對于決定使用代理幾何體的靜態(tài)物體我們提前準(zhǔn)備好它們的代理網(wǎng)格和材質(zhì)一個只寫入深度的簡單Shader。在自定義的陰影渲染通道中直接繪制這些代理網(wǎng)格而不是讓Unity去遍歷場景中的原始Renderer。// 偽代碼示例為平行光添加自定義陰影繪制命令 Light mainLight ...; // 獲取主平行光 CommandBuffer shadowCB new CommandBuffer(); shadowCB.name SmartShadows for MainLight; // 1. 清空陰影貼圖 shadowCB.SetRenderTarget(shadowMap); shadowCB.ClearRenderTarget(true, true, Color.white); // 2. 繪制靜態(tài)物體代理體 foreach (var staticProxy in staticShadowProxies) { shadowCB.DrawMesh(staticProxy.mesh, staticProxy.transformMatrix, staticProxy.material); } // 3. 繪制動態(tài)物體可根據(jù)距離等條件過濾 foreach (var dynamicRenderer in filteredDynamicRenderers) { shadowCB.DrawRenderer(dynamicRenderer, shadowCasterMaterial); } mainLight.AddCommandBuffer(LightEvent.BeforeShadowMap, shadowCB);3.2 代理幾何體的自動生成與匹配手動為每個靜態(tài)物體制作代理體不現(xiàn)實。我們需要一種自動或半自動的生成方式。網(wǎng)格簡化算法可以使用像MeshSimplifier這樣的開源庫將原始網(wǎng)格的面數(shù)大幅減少生成一個簡化的版本。但簡單的簡化可能無法保持外形特別是對于薄壁結(jié)構(gòu)。包圍盒生成對于許多物體一個軸對齊包圍盒AABB或朝向包圍盒OBB就是不錯的代理。我們可以計算物體的渲染器包圍盒然后生成一個對應(yīng)的立方體網(wǎng)格。這種方法最簡單開銷最小但對于長條狀、不規(guī)則物體陰影形狀失真嚴重。凸包生成計算網(wǎng)格的凸包Convex Hull用凸包網(wǎng)格作為代理。這比包圍盒更貼合原形計算量也比通用簡化算法小是平衡精度和性能的較好選擇。Unity本身提供了MeshCollider生成凸包的功能可以借鑒。預(yù)定義簡單形狀庫對于項目中的大量重復(fù)資產(chǎn)如樹木、巖石可以由美術(shù)預(yù)先制作一個極簡的陰影代理模型如一個面片、一個十字交叉體。工具通過命名規(guī)則或標(biāo)簽系統(tǒng)自動關(guān)聯(lián)原始模型和代理模型。注意事項自動生成的代理體必須確保其深度值永遠小于或等于原始網(wǎng)格對應(yīng)部分的深度值。如果代理體在某些角度下比原始物體“更靠近”光源就會導(dǎo)致本該被陰影覆蓋的區(qū)域被錯誤照亮陰影缺失。通常的解決方案是將代理體沿著頂點法線方向或光源方向稍微“膨脹”一點。3.3 基于距離與屏幕空間的重要性計算如何決定一個物體該用哪一級別的陰影質(zhì)量這需要一個評估系統(tǒng)。距離計算最簡單。計算物體包圍盒中心點到攝像機的距離。屏幕空間占比更精確的方法是計算物體在當(dāng)前幀的屏幕空間包圍矩形Screen Space Bounds的大小。一個距離很遠但體積巨大的物體其陰影可能依然重要。計算其在屏幕上的像素面積可以作為更準(zhǔn)確的權(quán)重。綜合評分將距離、屏幕大小、物體類型主角、敵人、環(huán)境等因素加權(quán)計算出一個“陰影重要性分數(shù)”。根據(jù)這個分數(shù)決定是使用原始網(wǎng)格、高/低精度代理體還是禁用陰影。這個評估邏輯每幀都需要運行但本身應(yīng)該是輕量級的。可以將動態(tài)物體分組每幾幀評估一次而不是每幀評估所有物體以減少CPU開銷。4. 與Unity原生系統(tǒng)的協(xié)同與配置Smart Shadows不能完全取代Unity的陰影系統(tǒng)而是要與它協(xié)同工作。處理好兼容性是工具能否實用的關(guān)鍵。4.1 與Unity Quality Settings和燈光設(shè)置的配合用戶可能在Unity的Quality Settings中設(shè)置了全局的陰影距離、分辨率、級聯(lián)Cascaded Shadow Maps等。我們的工具需要讀取這些設(shè)置作為基礎(chǔ)然后在此基礎(chǔ)上應(yīng)用我們的優(yōu)化規(guī)則。陰影距離Shadow Distance這是Unity內(nèi)置的裁剪距離。我們的工具應(yīng)該在這個距離內(nèi)工作。我們可以設(shè)置自己的、更精細的“陰影LOD過渡距離”但這個距離必須小于等于Unity的全局陰影距離。陰影級聯(lián)CSM對于平行光陰影級聯(lián)技術(shù)將視錐體分割成多個區(qū)域近處用高分辨率陰影遠處用低分辨率。我們的工具可以與之結(jié)合在最近的級聯(lián)中我們使用高精度代理體或原始網(wǎng)格在遠處的級聯(lián)中強制使用低精度代理體或直接禁用非重要物體的陰影。這相當(dāng)于在CSM的基礎(chǔ)上又增加了一層物體粒度的優(yōu)化。燈光Shadow Bias使用代理體后由于幾何形狀變化可能需要重新調(diào)整燈光的Bias和Normal Bias參數(shù)以避免陰影痤瘡Shadow Acne或陰影懸浮Peter Panning。工具可以提供一套針對代理體優(yōu)化過的默認Bias參數(shù)或者提供一個自動微調(diào)的功能。4.2 對不同渲染管線的適配Unity有內(nèi)置渲染管線、通用渲染管線URP和高清渲染管線HDRP。它們的陰影渲染底層API有所不同。內(nèi)置管線Built-in如前所述使用CommandBuffer介入LightEvent.BeforeShadowMap是經(jīng)典方法靈活性最高。URPURP使用了可編程渲染管線SRP陰影繪制通過RenderObjectsPass等實現(xiàn)。我們需要編寫自定義的ScriptableRenderPass在URP的渲染流程中插入我們自己的陰影篩選和繪制邏輯。這需要更深入地理解URP的RenderingData和FilteringSettings。HDRPHDRP的陰影系統(tǒng)更加復(fù)雜和封閉定制化難度最高。通常需要修改或繼承HDRP的陰影相關(guān)HDShadowRequest生成邏輯。對于HDRP可能更可行的方案是影響其生成陰影請求的列表而不是完全接管繪制。實操建議在工具開發(fā)初期可以先針對最常用的內(nèi)置管線和URP進行支持。為不同的管線編寫不同的管理器組件但它們共享同一套配置數(shù)據(jù)和過濾邏輯。4.3 提供友好的編輯器配置界面一個優(yōu)秀的工具必須有易于使用的配置界面。我們可以創(chuàng)建一個SmartShadowManager的單例組件以及一個SmartShadowCaster組件掛在需要優(yōu)化的物體上。全局管理器SmartShadowManager提供全局開關(guān)。設(shè)置距離閾值如高/低精度代理體切換距離陰影禁用距離。配置靜態(tài)/動態(tài)物體的默認處理策略。提供一個調(diào)試視圖用不同顏色顯示當(dāng)前幀哪些物體使用了原始陰影、代理陰影或無陰影。個體組件SmartShadowCaster替換模式可選“自動生成代理體”、“使用指定代理體網(wǎng)格”、“使用包圍盒”、“禁用陰影”等。重要性權(quán)重手動調(diào)整該物體的陰影重要性覆蓋自動計算。引用字段當(dāng)選擇“使用指定代理體網(wǎng)格”時拖入一個低模Mesh。這樣美術(shù)和策劃人員也可以輕松地對特定重要物體進行微調(diào)。5. 性能收益實測與常見問題排查理論再好也需要實際數(shù)據(jù)支撐。我在一個包含大量植被和建筑的開放場景測試原型中進行了對比。測試環(huán)境Unity 2022.3 LTS內(nèi)置渲染管線PC平臺確保不受移動端GPU性能瓶頸影響專注于渲染調(diào)用節(jié)省。場景內(nèi)有超過2000個可投射陰影的渲染器。測試方法基準(zhǔn)線使用Unity原生陰影Shadow Distance設(shè)為150Hard Shadows。啟用Smart Shadows靜態(tài)物體使用凸包代理體動態(tài)物體在50米外使用包圍盒代理體100米外禁用陰影。性能數(shù)據(jù)對比使用Unity Profiler的GPU模塊采樣渲染階段原生陰影 (ms)Smart Shadows (ms)節(jié)省陰影貼圖渲染8.2 ms3.1 ms62%主場景渲染12.5 ms12.8 ms(基本持平略有增加因代理體繪制)GPU總耗時25.0 ms20.2 ms19%可以看到陰影渲染階段的耗時下降了超過60%整體幀時間提升了近5ms。在移動端上這個提升比例可能會更顯著因為移動GPU的填充率和帶寬壓力更大。5.1 常見問題與解決方案速查表在實際集成和使用過程中你肯定會遇到一些“坑”。下面是我總結(jié)的一些典型問題及其排查思路問題現(xiàn)象可能原因排查與解決方案陰影出現(xiàn)閃爍或抖動1. 代理體與原始物體位置未對齊。2. 每幀使用的代理體LOD級別不一致在切換閾值附近反復(fù)橫跳。3. 自定義CommandBuffer的渲染矩陣計算有誤。1. 確保代理體的原點pivot和旋轉(zhuǎn)與原始物體一致。對于自動生成的代理體將其錨點設(shè)置為原始包圍盒中心。2. 為LOD切換增加滯后區(qū)間Hysteresis。例如從高到低的切換距離是30米從低到高切回的距離是28米避免在29.9米處來回跳動。3. 在CommandBuffer中使用RendereringManager.worldToCameraMatrix和projectionMatrix時確保它們來自正確的光源視角攝像機。陰影缺失該有的影子沒了1. 代理體沒有完全包裹住原始物體在某些視角下“漏光”。2. 距離裁剪或視錐體裁剪過于激進。3. 物體的SmartShadowCaster組件被誤設(shè)置為“禁用陰影”。1. 檢查代理體生成算法。確保代理體在所有軸向上都略大于原始物體可以添加一個微小的膨脹偏移。2. 調(diào)大陰影LOD的切換距離或者關(guān)閉基于屏幕大小的裁剪先只用距離判斷。3. 在編輯器的調(diào)試視圖中查看該物體的狀態(tài)。陰影形狀嚴重失真1. 使用的代理體過于簡單如用方塊代替一棵樹。2. 動態(tài)物體使用了錯誤的代理體LOD級別。1. 對于輪廓重要的物體如角色、標(biāo)志性建筑不要使用包圍盒至少使用凸包或自定義的低模。2. 對于重要的動態(tài)物體在其SmartShadowCaster組件中鎖定使用較高精度的代理體不讓其隨距離降級。性能提升不明顯1. 場景中動態(tài)物體過多而靜態(tài)物體很少優(yōu)化重點不對。2. 代理體本身面數(shù)仍然過高。3. 重要性評估邏輯每幀CPU開銷太大。1. 分析Profiler看陰影渲染的Draw Call主要來自靜態(tài)還是動態(tài)物體。優(yōu)化策略需要針對主要開銷源。2. 檢查生成的凸包或簡化網(wǎng)格的面數(shù)對于遠處物體面數(shù)控制在20個三角形以內(nèi)。3. 優(yōu)化評估算法使用空間劃分如四叉樹、網(wǎng)格來快速篩選潛在物體避免每幀遍歷全場。可以每2-3幀評估一次非主角物體。與某些Shader或特效不兼容自定義的陰影投射Shader代理體材質(zhì)不支持某些渲染特性如Alpha Test、雙面陰影。1. 確保代理體使用的Shader是簡單的、只輸出深度的ShadowCasterPass??梢詤⒖糢nity內(nèi)置的ShadowCasterPass寫法。2. 對于需要雙面陰影的植物葉片等在代理體材質(zhì)中啟用Cull Off。對于Alpha Test物體需要將Alpha Clip功能集成到代理體Shader中這比較復(fù)雜有時不如回退到使用原始網(wǎng)格。5.2 調(diào)試與可視化工具開發(fā)這樣一個系統(tǒng)強大的調(diào)試工具必不可少。我強烈建議在SmartShadowManager中集成一個調(diào)試繪制模式顏色編碼在場景視圖中用不同顏色覆蓋物體例如綠色使用原始網(wǎng)格渲染陰影。藍色使用高精度代理體。黃色使用低精度代理體如包圍盒。紅色當(dāng)前幀被禁用陰影。信息面板在Game視圖旁顯示統(tǒng)計信息如本幀陰影投射物體總數(shù)。各類別原始/代理/禁用的數(shù)量和占比。陰影渲染的預(yù)估三角形數(shù)量優(yōu)化前后對比。單物體調(diào)試點擊場景中的物體能在Inspector中實時看到它當(dāng)前的陰影策略、重要性分數(shù)、距離等信息。這些工具在項目調(diào)優(yōu)階段價值連城能幫你快速定位哪些物體是性能瓶頸以及你的優(yōu)化策略是否按預(yù)期工作。6. 擴展思路與進階優(yōu)化基礎(chǔ)的Smart Shadows系統(tǒng)搭建完成后還可以根據(jù)項目需求進行更深度的定制和擴展。1. 基于屏幕空間陰影的混合Screen-Space Shadows對于極遠處的物體可以完全不渲染到陰影貼圖中而是采用一種更取巧的方法——屏幕空間陰影SSS。這種技術(shù)利用深度緩沖來估算陰影開銷極低且效果尚可非常適合用于填補超遠距離的陰影細節(jié)。我們的工具可以管理一個過渡近距離用高質(zhì)量陰影貼圖中距離用低質(zhì)量陰影貼圖或代理體遠距離用屏幕空間陰影。2. 動態(tài)陰影緩存Dynamic Shadow Atlas對于移動緩慢或周期性運動的物體如旋轉(zhuǎn)的風(fēng)車、緩慢移動的云層陰影其陰影貼圖不一定需要每幀重新渲染。我們可以將其陰影渲染到一張獨立的圖集Atlas中并緩存數(shù)幀。只有當(dāng)檢測到物體或光源發(fā)生顯著移動/旋轉(zhuǎn)時才更新緩存。這類似于一種針對動態(tài)物體的“準(zhǔn)靜態(tài)”陰影烘焙。3. 與遮擋剔除Occlusion Culling系統(tǒng)聯(lián)動如果一個物體被其他物體完全遮擋那么它投射的陰影也必然不可見。我們可以從遮擋剔除系統(tǒng)的結(jié)果中獲取信息進一步剔除被遮擋的陰影投射者。這需要獲取每幀的遮擋查詢結(jié)果并整合到我們的重要性評估邏輯中。4. 平臺差異化配置在編輯器里提供不同的配置預(yù)設(shè)如“PC High”、“Mobile Low”。針對移動平臺可以自動采用更激進的裁剪距離、更簡單的代理體全部使用包圍盒、更低的陰影貼圖分辨率。實現(xiàn)一鍵切換簡化多平臺適配的工作流。開發(fā)Smart Shadows這樣的工具最大的收獲不是幾行代碼而是一種“性能敏感”的思維方式。它迫使你去思考渲染管線中的每一個操作是否都是必要的是否有更經(jīng)濟的方式達到近似的視覺效果。在項目后期性能優(yōu)化的攻堅階段這類工具往往能帶來意想不到的突破。當(dāng)然它也會增加一定的復(fù)雜性和維護成本所以是否需要引入以及優(yōu)化到何種程度都需要根據(jù)項目的具體需求和團隊的技術(shù)儲備來權(quán)衡。對于性能要求苛刻的項目花時間打造這樣一套智能陰影管理系統(tǒng)絕對是值得的投資。