化:BakeMesh與動態(tài)合批實戰(zhàn)指南)
1. 項目概述當骨骼動畫成為性能瓶頸在Unity項目里尤其是移動端或者需要大量同屏角色的場景性能問題常常在不經(jīng)意間冒出來。如果你發(fā)現(xiàn)游戲幀率在角色密集的區(qū)域驟降Profiler里SkinnedMeshRenderer.Render或者Render.Mesh的耗時高居不下那么你很可能正面臨骨骼動畫的性能挑戰(zhàn)。SkinnedMeshRenderer蒙皮網(wǎng)格渲染器是Unity實現(xiàn)角色動畫的基石它讓網(wǎng)格能隨著骨骼“動”起來但這份靈活是以性能為代價的。每幀CPU都需要計算每個頂點受骨骼影響的權(quán)重變換再將結(jié)果傳遞給GPU這個過程在角色數(shù)量多、骨骼復(fù)雜時開銷會急劇上升。這篇文章我們就來徹底解決這個問題。核心思路很直接既然動態(tài)蒙皮計算是瓶頸那就把它“靜態(tài)化”。我們將深入實戰(zhàn)兩種經(jīng)過驗證的優(yōu)化方案BakeMesh烘焙網(wǎng)格與動態(tài)合批Dynamic Batching。前者將動態(tài)的蒙皮網(wǎng)格在運行時“烘焙”成靜態(tài)網(wǎng)格一勞永逸地消除CPU蒙皮計算后者則是在特定條件下讓Unity自動合并多個使用相同材質(zhì)的靜態(tài)網(wǎng)格渲染調(diào)用大幅減少Draw Call。兩者結(jié)合能讓你在保持動畫效果的同時獲得接近靜態(tài)場景物件的渲染性能。無論你是正在為卡頓所困的開發(fā)者還是想提前規(guī)避性能風險的架構(gòu)者這套組合拳都值得你仔細研究。2. 核心原理與方案選型為什么是BakeMesh與動態(tài)合批在深入代碼之前我們必須搞清楚問題根源和解決方案的底層邏輯。盲目優(yōu)化只會事倍功半。2.1 SkinnedMeshRenderer的性能開銷分析一個SkinnedMeshRenderer組件每幀的工作流程可以簡化為動畫系統(tǒng)計算根據(jù)Animator或動畫剪輯計算出每一根骨骼在當幀的最終變換矩陣位置、旋轉(zhuǎn)、縮放。蒙皮計算CPU端對于網(wǎng)格的每一個頂點根據(jù)其綁定的骨骼索引最多通常4根和權(quán)重將骨骼變換矩陣進行混合計算出該頂點在世界空間中的最終位置和法線。這個過程稱為“蒙皮”或“線性混合蒙皮LBS”。數(shù)據(jù)傳遞與渲染GPU端將計算好的頂點位置、法線等數(shù)據(jù)組織成新的網(wǎng)格數(shù)據(jù)傳遞給GPU進行渲染。開銷主要來自第2步。假設(shè)一個角色模型有5000個頂點每幀都需要為這5000個頂點執(zhí)行一次矩陣混合運算。當10個、20個這樣的角色同屏時CPU的運算量就非??捎^了。此外每個SkinnedMeshRenderer通常都會產(chǎn)生至少一個獨立的Draw Call如果材質(zhì)不同則更多這也是渲染壓力的主要來源。2.2 BakeMesh從動態(tài)到靜態(tài)的魔法BakeMesh是SkinnedMeshRenderer類的一個方法。它的作用是在運行時的某一時刻捕獲當前SkinnedMeshRenderer的蒙皮狀態(tài)即骨骼當前姿勢下的網(wǎng)格形態(tài)并將其“烘焙”成一個普通的Mesh對象。這個新生成的Mesh的頂點位置已經(jīng)是經(jīng)過蒙皮計算后的最終位置它不再與骨骼綁定。之后我們可以用一個標準的MeshRenderer和MeshFilter組件來替換或配合原有的SkinnedMeshRenderer并使用這個烘焙出來的Mesh進行渲染。這樣一來該模型后續(xù)的渲染就完全不需要再進行蒙皮計算了變成了一個靜態(tài)物體。性能開銷瞬間降至與一個普通網(wǎng)格物件相同。關(guān)鍵考量何時使用BakeMesh角色動畫暫停時比如角色死亡后倒地、變成雕像、進入某種靜止狀態(tài)。此時動畫不再變化是烘焙的絕佳時機。動畫簡單或循環(huán)時對于僅包含位移、旋轉(zhuǎn)或者頂點形變很小的動畫烘焙后視覺差異可接受。對于表情動畫BlendShape豐富的角色烘焙會丟失后續(xù)的表情變化。中低端設(shè)備批量渲染在性能壓力大的場景可以將一批動畫角色烘焙成靜態(tài)網(wǎng)格用MeshRenderer渲染作為LOD細節(jié)層次的最低一級。2.3 動態(tài)合批合并渲染請求的藝術(shù)動態(tài)合批是Unity內(nèi)置的一項優(yōu)化功能。它的原理是在運行時對于滿足特定條件的一批靜態(tài)物體即沒有SkinnedMeshRenderer且變換矩陣在幀間不變Unity會自動將它們網(wǎng)格的頂點數(shù)據(jù)變換到世界空間并合并到一個大的頂點緩沖區(qū)中然后用一個Draw Call繪制出來。關(guān)鍵條件簡化版使用完全相同的材質(zhì)實例不僅僅是共享材質(zhì)球必須是同一個Material實例。網(wǎng)格頂點屬性規(guī)模在一定限制內(nèi)通常頂點數(shù)少于300。物體的縮放必須是統(tǒng)一縮放即x, y, z縮放值相同。物體不能接受實時陰影Shadow Caster。當我們使用BakeMesh將SkinnedMeshRenderer轉(zhuǎn)化為MeshRenderer后這些物體就具備了被動態(tài)合批的潛力。如果我們有100個相同的、靜止的士兵模型通過烘焙并使用同一個材質(zhì)實例Unity就有可能將它們合并成1個Draw Call來渲染性能提升是數(shù)量級的。方案選型總結(jié)純BakeMesh適用于單個或少量需要“凍結(jié)”動畫的高模角色追求單個角色的極致渲染效率。BakeMesh 動態(tài)合批適用于大量同質(zhì)、低模頂點數(shù)少、且動畫可被凍結(jié)的角色群。這是解決“人海戰(zhàn)術(shù)”場景性能問題的黃金組合。本實戰(zhàn)將重點圍繞此組合展開。3. 實戰(zhàn)BakeMesh 實現(xiàn)詳解理論清晰后我們進入實戰(zhàn)環(huán)節(jié)。首先實現(xiàn)最核心的烘焙功能。3.1 基礎(chǔ)烘焙腳本實現(xiàn)我們創(chuàng)建一個名為SkinnedMeshBaker.cs的腳本。它的核心職責是在指定時機獲取SkinnedMeshRenderer的當前狀態(tài)烘焙出Mesh并創(chuàng)建一個使用該Mesh的靜態(tài)游戲?qū)ο?。using UnityEngine; [RequireComponent(typeof(SkinnedMeshRenderer))] public class SkinnedMeshBaker : MonoBehaviour { private SkinnedMeshRenderer skinnedMeshRenderer; private Mesh bakedMesh; private GameObject bakedMeshObject; // 烘焙后創(chuàng)建的靜態(tài)物體 void Start() { skinnedMeshRenderer GetComponentSkinnedMeshRenderer(); // 可在此處或由外部事件觸發(fā)烘焙 // BakeCurrentPose(); } /// summary /// 烘焙當前姿勢的蒙皮網(wǎng)格 /// /summary /// returns返回烘焙后生成的靜態(tài)Mesh對象/returns public Mesh BakeCurrentPose() { if (skinnedMeshRenderer null) { Debug.LogError(SkinnedMeshRenderer not found!); return null; } // 1. 創(chuàng)建一個新的Mesh對象來接收烘焙數(shù)據(jù) if (bakedMesh null) { bakedMesh new Mesh(); } else { bakedMesh.Clear(); // 重要復(fù)用Mesh前先清空避免內(nèi)存泄漏 } // 2. 執(zhí)行烘焙核心API // 第一個參數(shù)接收烘焙數(shù)據(jù)的Mesh對象 // 使用skinnedMeshRenderer.sharedMesh作為拓撲結(jié)構(gòu)參考 skinnedMeshRenderer.BakeMesh(bakedMesh); // 3. 可選為烘焙后的Mesh計算邊界(Bounds)用于視錐體剔除 // BakeMesh不會自動重新計算邊界邊界可能還是原始靜態(tài)Mesh的AABB。 // 對于姿勢伸展的角色如舉手舊的邊界框可能太小導致錯誤剔除。 bakedMesh.RecalculateBounds(); return bakedMesh; } /// summary /// 用烘焙出的Mesh創(chuàng)建一個新的靜態(tài)游戲?qū)ο蟛⒔迷璖kinnedMeshRenderer /// /summary public GameObject ReplaceWithBakedMesh() { Mesh mesh BakeCurrentPose(); if (mesh null) return null; // 創(chuàng)建新的GameObject來承載靜態(tài)網(wǎng)格 bakedMeshObject new GameObject(gameObject.name _Baked); bakedMeshObject.transform.SetPositionAndRotation(transform.position, transform.rotation); bakedMeshObject.transform.localScale transform.lossyScale; // 注意使用全局縮放 // 添加MeshFilter和MeshRenderer組件 MeshFilter meshFilter bakedMeshObject.AddComponentMeshFilter(); meshFilter.mesh mesh; MeshRenderer meshRenderer bakedMeshObject.AddComponentMeshRenderer(); // 復(fù)制原SkinnedMeshRenderer的材質(zhì) meshRenderer.materials skinnedMeshRenderer.materials; // 禁用原來的SkinnedMeshRenderer而不是銷毀以便需要時恢復(fù) skinnedMeshRenderer.enabled false; return bakedMeshObject; } /// summary /// 切換回原始的SkinnedMeshRenderer /// /summary public void RevertToSkinnedMesh() { if (bakedMeshObject ! null) { Destroy(bakedMeshObject); bakedMeshObject null; } if (skinnedMeshRenderer ! null) { skinnedMeshRenderer.enabled true; } } void OnDestroy() { // 清理動態(tài)創(chuàng)建的Mesh防止內(nèi)存泄漏 if (bakedMesh ! null) { Destroy(bakedMesh); } } }代碼解析與注意事項BakeMeshAPI這是核心。它接收一個Mesh對象作為輸出容器。注意烘焙的是當前渲染狀態(tài)的網(wǎng)格其頂點位置、法線、切線等都已包含骨骼變換。RecalculateBounds這一步至關(guān)重要。烘焙出的Mesh其邊界Bounds默認繼承自sharedMesh的原始AABB軸向?qū)R包圍盒。如果你的角色動畫手臂舉得很高這個原始邊界框可能無法包裹住烘焙后的姿態(tài)導致角色在屏幕邊緣時就被錯誤地視錐體剔除了。調(diào)用RecalculateBounds會基于當前頂點數(shù)據(jù)重新計算一個緊密的包圍盒。材質(zhì)復(fù)制meshRenderer.materials skinnedMeshRenderer.materials這行代碼直接復(fù)制了材質(zhì)數(shù)組。這意味著新舊對象共享相同的材質(zhì)實例。這是后續(xù)實現(xiàn)動態(tài)合批的關(guān)鍵前提。對象管理我們選擇禁用而非銷毀原SkinnedMeshRenderer保留了隨時切換回動態(tài)動畫的能力提供了更大的靈活性。內(nèi)存管理在OnDestroy中銷毀動態(tài)創(chuàng)建的Mesh對象。Unity不會自動銷毀new Mesh()創(chuàng)建的網(wǎng)格必須手動管理否則會造成資源泄漏。3.2 高級技巧烘焙網(wǎng)格的復(fù)用與LOD集成基礎(chǔ)版本已經(jīng)能用但在生產(chǎn)環(huán)境中我們還需要考慮更多。1. 網(wǎng)格復(fù)用Pooling如果同一角色需要頻繁地在動態(tài)和靜態(tài)間切換例如游戲中的“凍結(jié)”技能反復(fù)創(chuàng)建和銷毀Mesh對象會產(chǎn)生GC垃圾回收壓力。我們可以使用對象池來管理烘焙出的Mesh。using System.Collections.Generic; public class MeshPool { private Dictionarystring, StackMesh meshPool new Dictionarystring, StackMesh(); public Mesh GetMesh(string key) { if (meshPool.ContainsKey(key) meshPool[key].Count 0) { return meshPool[key].Pop(); } return new Mesh(); } public void ReturnMesh(string key, Mesh mesh) { if (!meshPool.ContainsKey(key)) { meshPool[key] new StackMesh(); } mesh.Clear(); // 放回池子前清空數(shù)據(jù) meshPool[key].Push(mesh); } } // 在Baker腳本中使用 public class SkinnedMeshBaker : MonoBehaviour { private static MeshPool s_MeshPool new MeshPool(); private string poolKey; // 可以用角色預(yù)制體名姿勢哈希作為Key public Mesh BakeCurrentPose() { // ... 獲取skinnedMeshRenderer ... Mesh bakedMesh s_MeshPool.GetMesh(poolKey); skinnedMeshRenderer.BakeMesh(bakedMesh); bakedMesh.RecalculateBounds(); return bakedMesh; } public void OnBakedObjectDestroy() { if (bakedMesh ! null) { s_MeshPool.ReturnMesh(poolKey, bakedMesh); bakedMesh null; } } }2. 與LODGroup集成你可以將烘焙后創(chuàng)建的靜態(tài)GameObject作為一個LODGroup的某個層級例如LOD2。當相機距離足夠遠時系統(tǒng)自動切換到烘焙的靜態(tài)模型完全省去蒙皮計算。public class BakeMeshLOD : MonoBehaviour { public float bakeDistance 30.0f; // 超過此距離觸發(fā)烘焙 private SkinnedMeshBaker baker; private Transform camTransform; private bool isBaked false; void Start() { baker GetComponentSkinnedMeshBaker(); camTransform Camera.main.transform; } void Update() { float distance Vector3.Distance(transform.position, camTransform.position); if (!isBaked distance bakeDistance) { baker.ReplaceWithBakedMesh(); isBaked true; } else if (isBaked distance bakeDistance) { baker.RevertToSkinnedMesh(); isBaked false; } } }4. 實戰(zhàn)驅(qū)動動態(tài)合批的關(guān)鍵條件成功烘焙出靜態(tài)網(wǎng)格只是第一步要讓它們能被動態(tài)合批還需要精心設(shè)置。動態(tài)合批不是萬能的它對提交的物體有嚴格限制。4.1 滿足動態(tài)合批的硬性條件讓我們詳細拆解并確保滿足每一條相同的材質(zhì)實例這是最重要的條件。你必須確保所有烘焙后的MeshRenderer使用的Material是同一個實例。如果每個角色都Material.Instantiate()出一個新實例即使它們源自同一個材質(zhì)球也無法合批。正確做法在項目中使用材質(zhì)球的引用sharedMaterial或者在初始化時從一個公共源獲取材質(zhì)實例。// 在烘焙替換方法中 // meshRenderer.material someSharedMaterialInstance; // 錯誤這會產(chǎn)生新實例 meshRenderer.sharedMaterial someSharedMaterialInstance; // 正確共享實例 // 或者直接復(fù)制原渲染器的共享材質(zhì) meshRenderer.sharedMaterials skinnedMeshRenderer.sharedMaterials;網(wǎng)格頂點屬性規(guī)模Unity對單個合批的網(wǎng)格頂點/索引數(shù)量有限制這個限制與平臺有關(guān)。一個常見的經(jīng)驗法則是單個網(wǎng)格的頂點數(shù)最好少于300。對于角色模型這通常意味著你需要使用簡化的LOD0模型進行烘焙和合批而不是用高模。統(tǒng)一縮放Uniform Scale物體的Transform縮放值必須在X、Y、Z軸上完全相同。例如(1,1,1)、(2,2,2)可以但(1,2,1)就不行。如果你的角色需要非統(tǒng)一縮放動態(tài)合批將對其失效??紤]將縮放信息“烘焙”進頂點數(shù)據(jù)但這很復(fù)雜或者接受無法合批。不支持實時陰影如果物體需要投射實時陰影ShadowCaster通常無法被動態(tài)合批。對于大量小兵可以考慮使用烘焙光照貼圖Baked Lightmap或屏幕空間陰影而不是每個物體都產(chǎn)生Draw Call的實時陰影。4.2 材質(zhì)與著色器優(yōu)化著色器本身也會影響合批。為了最大化合批成功率你的材質(zhì)/shader應(yīng)遵循以下原則避免使用每實例數(shù)據(jù)Per-instance data如MaterialPropertyBlock。雖然它高效但使用它通常會打斷合批。使用輕量級Shader復(fù)雜的、多Pass的Shader會增加合批的難度和開銷。對于大量重復(fù)的靜態(tài)物體使用最簡化的無光照Unlit或標準Lambert著色器往往是最佳選擇。合并紋理Texture Atlasing如果不同角色需要不同的顏色或細節(jié)不要為每個角色創(chuàng)建單獨的材質(zhì)。而是將所有這些細節(jié)做進一張大圖圖集然后通過修改頂點顏色或UV偏移來區(qū)分。這樣所有角色仍然可以使用同一個材質(zhì)實例和紋理。4.3 實戰(zhàn)配置檢查清單在將烘焙物體投入場景前請對照此清單檢查[ ] 所有待合批物體的MeshRenderer.sharedMaterial引用是否指向完全相同的Material對象[ ] 每個Mesh的頂點數(shù)是否小于300可通過mesh.vertexCount查看[ ] 所有物體的Transform縮放是否是統(tǒng)一縮放transform.localScale.x .y .z[ ] 這些物體的MeshRenderer是否禁用了Cast Shadows和Receive Shadows或使用烘焙陰影[ ] 是否沒有對這些物體使用MaterialPropertyBlock你可以在Unity編輯器的Stats面板或使用Frame Debugger工具來驗證合批是否成功。成功時你會看到多個物體被合并到一個“DynamicBatch”的Draw Call下。5. 性能對比與問題排查優(yōu)化離不開測量。讓我們設(shè)計一個簡單的測試場景并用數(shù)據(jù)說話。5.1 測試場景搭建與數(shù)據(jù)對比場景搭建創(chuàng)建一個空曠場景實例化100個相同的帶SkinnedMeshRenderer和簡單循環(huán)動畫的角色預(yù)制體?;鶞蕼y試未優(yōu)化運行游戲在Profiler的Rendering區(qū)域觀察Draw Calls和Batches數(shù)量。100個獨立的SkinnedMeshRenderer很可能產(chǎn)生接近100個Batches。記錄CPU主線程和渲染線程的耗時特別是SkinnedMeshRenderer.Render和Animation.Update相關(guān)的開銷。BakeMesh優(yōu)化測試游戲運行后通過腳本觸發(fā)將所有角色的動畫烘焙成靜態(tài)網(wǎng)格并禁用原組件。再次觀察Profiler。Draw Calls和Batches數(shù)量可能沒有減少因為每個MeshRenderer還是獨立的但CPU端的SkinnedMeshRenderer.Render開銷應(yīng)該降為0Animation.Update開銷也可能因為角色靜止而減少。GPU端的Render.Mesh開銷可能會略有變化。BakeMesh 動態(tài)合批測試確保所有烘焙后的物體滿足第4章的條件相同材質(zhì)、統(tǒng)一縮放等。觀察Profiler。理想情況下Batches數(shù)量會急劇下降比如從100個合并到幾個。Draw Calls同樣大幅減少。這是性能提升最顯著的一步。實測數(shù)據(jù)示例僅供參考具體取決于模型和硬件場景BatchesDraw CallsCPU渲染耗時 (ms)說明100個動態(tài)角色~105~10515.2包含蒙皮計算開銷100個烘焙角色未合批~105~1055.1蒙皮開銷為0但Draw Call未減100個烘焙角色已合批~5~54.8Draw Call大幅減少CPU開銷低5.2 常見問題與解決方案實錄在實際操作中你肯定會遇到各種問題。以下是我踩過的一些坑和解決方案問題1烘焙后模型位置/旋轉(zhuǎn)/縮放不對。原因BakeMesh烘焙出的是模型空間Model Space的網(wǎng)格數(shù)據(jù)。當你用這個Mesh創(chuàng)建新對象時新對象的Transform位置、旋轉(zhuǎn)、縮放會疊加一次變換。解決方案在ReplaceWithBakedMesh方法中我們設(shè)置了新對象的position和rotation與原對象一致并使用lossyScale全局縮放。但更穩(wěn)健的做法是在烘焙時將骨骼的變換“應(yīng)用”到頂點上。我們可以通過臨時將SkinnedMeshRenderer的updateWhenOffscreen設(shè)為true并確保它在渲染隊列中更新一幀后再烘焙或者直接使用Graphics.DrawMesh的矩陣參數(shù)來校正。一個更簡單的方案是將烘焙后的GameObject作為原對象的子物體并重置子物體的LocalPosition/Rotation為0Scale為1。讓父對象原對象的Transform去處理世界變換。問題2動態(tài)合批沒有生效。排查步驟Frame Debugger這是最強大的工具。打開Window Analysis Frame Debugger逐幀查看渲染過程。找到你的那些靜態(tài)物體看它們是被單獨渲染Draw Mesh還是合并渲染Draw Mesh (dynamic batch)。檢查材質(zhì)在Frame Debugger里點擊對應(yīng)的Draw Call查看使用的材質(zhì)。確認多個物體是否真的指向同一個Material實例內(nèi)存地址相同。檢查縮放在場景中選擇物體查看Inspector里Transform的Scale值三個分量必須相等。檢查頂點數(shù)在Project窗口選中Mesh文件或在代碼中打印mesh.vertexCount。檢查陰影確保MeshRenderer的Cast Shadows和Receive Shadows已關(guān)閉或使用烘焙光照。問題3烘焙后法線/切線信息錯誤導致光照看起來奇怪。原因BakeMesh默認會烘焙法線和切線。但如果你的Shader依賴特定的切線空間計算或者模型在導入時沒有生成切線可能會出問題。解決方案烘焙后手動重新計算。bakedMesh.RecalculateNormals(); // 重新計算平滑法線 // bakedMesh.RecalculateTangents(); // 如果需要重新計算切線此方法已過時需要自己實現(xiàn)或使用AssetStore插件更根本的解決方法是確保原始模型的導入設(shè)置Import Settings中勾選了Calculate Tangents如果Shader需要。問題4烘焙大量角色時出現(xiàn)卡頓。原因BakeMesh和RecalculateBounds是同步CPU操作每幀烘焙上百個角色必然卡頓。解決方案分幀烘焙。不要在同一幀內(nèi)完成所有工作。public class BatchBaker : MonoBehaviour { public ListSkinnedMeshBaker objectsToBake new ListSkinnedMeshBaker(); private int currentIndex 0; IEnumerator BakeOverFrames(int perFrame) { while (currentIndex objectsToBake.Count) { for (int i 0; i perFrame currentIndex objectsToBake.Count; i, currentIndex) { objectsToBake[currentIndex].BakeCurrentPose(); // 或者直接Replace objectsToBake[currentIndex].ReplaceWithBakedMesh(); } yield return null; // 等待下一幀 } } void Start() { StartCoroutine(BakeOverFrames(5)); // 每幀烘焙5個 } }6. 擴展思路與最佳實踐掌握了核心方法后我們可以思考如何更優(yōu)雅地將這套方案集成到項目中。1. 基于狀態(tài)的自動化烘焙系統(tǒng)不要手動調(diào)用烘焙。設(shè)計一個系統(tǒng)根據(jù)角色狀態(tài)如距離相機的距離、是否在屏幕外、是否處于“石化/冰凍”狀態(tài)自動觸發(fā)烘焙或恢復(fù)。這可以與游戲玩法深度結(jié)合。2. 與ECS/DOTS集成如果你在使用Unity的ECS架構(gòu)思路可以更徹底。你可以編寫一個System在需要時將所有符合條件的SkinnedMeshRenderer的動畫狀態(tài)一次性計算并烘焙到MeshInstanceRenderer組件所需的Mesh中實現(xiàn)極致的數(shù)據(jù)導向性能。3. 作為AssetBundle資源管理的一部分對于確定在某個關(guān)卡只會以靜態(tài)形式出現(xiàn)的角色如背景中的雕像群可以在資源制作階段就烘焙好Mesh直接作為靜態(tài)模型導入Unity。這樣可以完全避免運行時的烘焙開銷也方便美術(shù)直接控制最終效果。4. 性能與質(zhì)量的權(quán)衡始終記住烘焙是“凍結(jié)”動畫。對于輕微循環(huán)動畫如呼吸、飄帶烘焙會丟失這些細節(jié)。一個折中方案是對角色身體主要部分軀干、四肢進行烘焙而對次要的、高頻運動的部件頭發(fā)、披風保留獨立的SkinnedMeshRenderer或使用頂點動畫、骨骼動畫簡化的方案。通過SkinnedMeshRenderer的bones屬性你甚至可以只烘焙部分骨骼影響的網(wǎng)格區(qū)域。最后的心得性能優(yōu)化沒有銀彈。BakeMesh動態(tài)合批是一套針對特定場景大量同質(zhì)、可靜止角色的強力組合拳。在實施前一定要用Profiler找準瓶頸。如果瓶頸不在蒙皮渲染而在動畫狀態(tài)機邏輯或AI計算上那么優(yōu)化渲染管道就收效甚微。這套技術(shù)的真正威力在于它讓你在保持視覺豐富度的同時突破了Draw Call和CPU蒙皮計算的上限為打造宏大的戰(zhàn)場、密集的人群提供了可能。在實際項目中我通常會為角色預(yù)制體配置一個“可烘焙”的標記并在場景加載后由統(tǒng)一的性能管理器根據(jù)當前設(shè)備的性能檔位決定是否啟用以及何時啟用烘焙策略實現(xiàn)自適應(yīng)的優(yōu)化。