本文建立在 混合實境的效能建議基礎上,但重點放在 Unity 專屬的改進。
我們最近推出了一款名為 Quality Fundamentals 的應用程式,涵蓋 HoloLens 2 應用程式常見的效能、設計與環境問題與解決方案。 這個應用程式是接下來內容的絕佳視覺示範。
請使用建議的 Unity 專案設定
在 Unity 中優化混合實境應用程式效能時,最重要的第一步是確保你使用的是 Unity 推薦的環境設定。 該文章包含了一些打造高效能 Mixed Reality 應用程式中最重要的場景配置內容。 以下部分重點介紹這些建議的設定。
如何使用 Unity 進行設定
Unity 內建了 Unity Profiler ,這是收集你特定應用程式寶貴效能洞察的絕佳資源。 雖然你可以在編輯器中執行分析器,但這些指標並不代表真實的執行環境,因此結果應謹慎使用。 在裝置上遠端分析您的應用程式,以獲得最準確且可行的洞察。
Unity 提供了很棒的文件:
- 如何遠端將 Unity 設定檔連接到 UWP 應用程式
- 如何有效 診斷使用 Unity Profiler 的效能問題
GPU 配置檔
Unity 性能分析器
連接 Unity Profiler 並新增 GPU 設定檔後, (右上角的) 顯示 Add Profiler ,可以看到 CPU & GPU 中間花了多少時間。 這讓開發者能快速估算他們的應用程式是受 CPU 還是 GPU 限制。
注意事項
要使用 GPU 設定檔,你需要在 Unity 播放器設定中關閉圖形作業。 更多細節請參考 Unity 的 GPU 使用分析器模組 。
Unity 框架除錯器
Unity 的 框架除錯 器也是一個強大且具洞察力的工具。 它能讓你清楚知道 GPU 每幀的狀況。 要注意的是其他渲染目標和複製它們之間的 blit 指令,因為這些在 HoloLens 上很貴。 理想狀況下,HoloLens 不應使用螢幕外的渲染目標。 這些功能會在啟用昂貴的渲染功能時加入, (例如 MSAA、HDR 或全螢幕特效如泛光) ,這些都應避免。
HoloLens 幀率疊加
裝置入口系統 效能 頁面有關於該裝置 CPU 和 GPU 效能的良好總結。 你可以在頭戴裝置中啟用顯示幀率計數器和顯示幀率圖表。 這些選項分別啟用 FPS 計數器和圖表,讓你在任何裝置上運行的應用程式都能即時獲得回饋。
PIX
PIX 也可以用來分析 Unity 應用程式。 書中也有詳細的使用與安裝 HoloLens 2 PIX 的說明。 在開發版本中,Unity Frame Debugger 中看到的相同範圍也會在 PIX 中顯示,並能更詳細地檢查與分析。
注意事項
Unity 提供透過 XRSettings.renderViewportScale 屬性,在執行時輕鬆修改應用程式的渲染目標解析度的能力。 最終在裝置上呈現的影像解析度固定。 該平台會取樣較低解析度的輸出,以建立高解析度影像以供顯示器渲染。
UnityEngine.XR.XRSettings.renderViewportScale = 0.7f;
CPU 效能建議
以下內容涵蓋更深入的效能實務,特別是針對 Unity & C# 開發。
快取參考
我們建議在初始化時快取所有相關元件與 GameObjects 的參考,因為重複呼叫如 GetComponent<T> () 和 Camera.main 相較於儲存指標的記憶體成本較高。 . Camera.main 只是在下面用 FindGameObjectsWithTag () ,這會花費大量成本搜尋你的場景圖,尋找帶有 「MainCamera」 標籤的相機物件。
using UnityEngine;
using System.Collections;
public class ExampleClass : MonoBehaviour
{
private Camera cam;
private CustomComponent comp;
void Start()
{
cam = Camera.main;
comp = GetComponent<CustomComponent>();
}
void Update()
{
// Good
this.transform.position = cam.transform.position + cam.transform.forward * 10.0f;
// Bad
this.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 10.0f;
// Good
comp.DoSomethingAwesome();
// Bad
GetComponent<CustomComponent>().DoSomethingAwesome();
}
}
注意事項
避免使用 GetComponent (字串)
使用 GetComponent () 時,有幾種不同的過載。 重要的是一定要使用基於類型的實作,而不是過載字串的搜尋。 在你的場景中用字串搜尋比用類型搜尋還要昂貴。
(好的) 元件 GetComponent (類型)
(好的) T,GetComponent<T () >
(壞) 元件 GetComponent (字串) >
避免昂貴的操作
避免使用 LINQ
雖然 LINQ 可以乾淨且易於閱讀和寫入,但通常比手動撰寫演算法需要更多的計算和記憶體。
// Example Code using System.Linq; List<int> data = new List<int>(); data.Any(x => x > 10); var result = from x in data where x > 10 select x;常見的 Unity API
某些 Unity API 雖然有用,但執行成本可能很高。 大多數這些工作都需要在整個場景圖中搜尋匹配的 GameObject 清單。 這些操作通常可以透過快取參考或實作 GameObjects 的管理元件來追蹤執行時的參考來避免。
GameObject.SendMessage() GameObject.BroadcastMessage() UnityEngine.Object.Find() UnityEngine.Object.FindWithTag() UnityEngine.Object.FindObjectOfType() UnityEngine.Object.FindObjectsOfType() UnityEngine.Object.FindGameObjectsWithTag() UnityEngine.Object.FindGameObjectsWithTag()
注意事項
SendMessage () 和 BroadcastMessage () 應該不惜一切代價被淘汰。 這些函式的運算速度可能比直接函式呼叫慢約 1000 倍。
小心拳擊
Boxing 是 C# 語言與執行環境的核心概念。 它是將值型別變數(如
char、int、bool、 等)包裝成參考型別變數的過程。 當值型別變數被「框住」時,它會被包裹在System.Object一個 中,並儲存在管理堆積中。 記憶體會被分配,最終丟棄後必須由垃圾回收器處理。 這些分配與分配會產生績效成本,且在許多情況下是不必要的,或可輕易被更便宜的替代方案取代。為避免盒裝,請確保你儲存數值型別和結構體的變數、欄位和屬性, (包含
Nullable<T>) ,都以特定型別int(如 、float?或MyStruct)型別,而非使用物件。 若將這些物件放入清單,務必使用強型別的列表,如 而List<int>非List<object>ArrayList或 。C語言拳擊範例#
// boolean value type is boxed into object boxedMyVar on the heap bool myVar = true; object boxedMyVar = myVar;
重複的程式碼路徑
任何重複的 Unity 回調函式 (例如更新) ,且每秒執行多次且/或幀數應謹慎撰寫。 這裡任何昂貴的操作都會對效能產生巨大且持續的影響。
空回調函式
雖然以下程式碼看似無害,尤其每個 Unity 腳本都會用更新方法自動初始化,但這些空回調可能會變得昂貴。 Unity 會在非管理程式碼與受管理程式碼邊界之間來回運作,也就是 UnityEngine 程式碼與你的應用程式程式碼之間。 即使沒有執行功能,透過這座橋接切換上下文也相當昂貴。 如果你的應用程式有數百個 GameObject,且元件中包含空的重複 Unity 回調,這會特別麻煩。
void Update() { }
注意事項
Update () 是這種效能問題最常見的表現形式,但其他重複出現的 Unity 回調,例如以下,也可能同樣糟糕甚至更糟:FixedUpdate () 、LateUpdate () 、OnPostRender“、OnPreRender () 、OnRender () 等等。
有利於每幀執行一次的操作
以下 Unity API 是許多全息應用程式常見的操作。 雖然不總是可行,但這些函數的結果通常可以計算一次,並在指定幀內於整個應用程式中重複利用。
a) 好的做法是有一個專用的單例類別或服務來處理你的凝視射線投射到場景中,然後在所有其他場景元件中重複使用這個結果,而不是每個元件重複且相同的射線運算。 有些應用可能需要來自不同來源或針對不同的 層遮罩進行射線投射。
UnityEngine.Physics.Raycast() UnityEngine.Physics.RaycastAll()b) 避免在重複的 Unity 回調(如 Update () )中使用 GetComponent () 操作,方法是將 參考快取 於 Start () 或 Awake ()
UnityEngine.Object.GetComponent()c) 如果可能,建議在初始化時實例化所有物件,並利用 物件池 在應用程式執行時回收與重用 GameObjects,這是良好的做法
UnityEngine.Object.Instantiate()避免介面與虛擬結構
透過介面調用函式呼叫,相較於直接物件或呼叫虛擬函式,通常比直接結構或直接函式呼叫更昂貴。 如果虛擬功能或介面是不必要的,則應該將其移除。 然而,若使用這些方法能簡化開發協作、程式碼可讀性與程式碼維護性,這些方法的效能損失是值得的。
一般建議是,除非明確預期該成員需要覆寫,否則不應將欄位和函式標記為虛擬。 對於每幀多次或每幀呼叫一次的高頻碼路徑,例如某個
UpdateUI()方法,應特別小心。避免以值傳遞結構
與類別不同,結構體是值型別,當直接傳入函式時,其內容會被複製到新建立的實例中。 此複製會增加 CPU 成本,並增加堆疊上的記憶體。 對於小型結構體,影響很小,因此可以接受。 然而,對於每個框架反覆呼叫的函式,以及採用大型結構的函式,若可能,請修改函式定義以通過參考。 在這裡了解更多
雜項
物理學
a) 一般來說,改善物理最簡單的方法是限制物理時間或每秒迭代次數。 這會降低模擬的準確度。 請參閱 Unity 中的 TimeManager
b) Unity 中碰撞器的類型在效能特性上差異很大。 以下順序依序列出從左到右的效能最高到最差的碰撞器。 避免使用網狀碰撞器很重要,因為它們比原始碰撞器昂貴許多。
球 < 體膠囊 < 盒 <<< 網格 (凸) < 網格 (非凸)
動畫
關閉 Animator 元件來關閉待機動畫, (關閉遊戲物件的效果) 。 避免動畫師在迴圈中設定相同值的設計模式。 這種技術有相當大的開銷,但對應用本身沒有影響。 請在此處深入了解。
複雜演算法
如果你的應用程式使用了像逆運動學、路徑尋找等複雜演算法,請尋找更簡單的方法或調整相關設定以符合效能
CPU 與 GPU 的效能建議
一般來說,CPU 對 GPU 的效能取決於提交給顯示卡的 繪製呼叫 。 為了提升效能,繪製呼叫需要策略性 地減少 a 個) 或 b) 重組 ,以達到最佳效果。 由於抽取呼叫本身就很耗資源,減少呼叫將減少整體所需工作量。 此外,繪製呼叫間的狀態變化需要在圖形驅動程式中進行昂貴的驗證與轉換步驟,因此重新結構應用程式的繪製呼叫以限制狀態變化 (例如不同材質等 ) 能提升效能。
Unity 有一篇很棒的文章,概述並深入探討他們平台的批次繪製呼叫。
單次實例渲染
Unity 中的單次實例渲染允許每個眼睛的繪製呼叫縮減為一個實例繪製呼叫。 因為兩個繪製呼叫之間的快取一致性,GPU 的效能也有所提升。
要在您的 Unity 專案中啟用此功能
- 打開 OpenXR 設定 (到編輯>專案設定>、XR 插件管理>、OpenXR) 。
- 從渲染模式下拉選單選擇單次實例化。
請閱讀 Unity 以下文章,了解這種渲染方法的詳細資訊。
注意事項
單次實例渲染常見的問題之一,是開發者已經有未為實例設計的自訂著色器。 啟用此功能後,開發者可能會注意到有些遊戲物件只在一隻眼睛中渲染。 這是因為相關的自訂著色器沒有適當的實例化屬性。
靜態批次
Unity 能夠批次處理許多靜態物件,以減少對 GPU 的繪製呼叫。 靜態批次對大多數 Unity 中相同材質且 2 個 Renderer 物件皆可運作) ) 皆標示為靜態 (在 Unity 中選取物件,並勾選檢查器右上角的勾選方塊) 。 標記為 靜態 的遊戲物件在應用程式執行期間無法移動。 因此,在幾乎每個物件都必須放置、移動、縮放等情況下,靜態批次操作在 HoloLens 上可能很困難。對於沉浸式耳機,靜態批次能大幅減少繪製呼叫,從而提升效能。
想了解更多細節,可以閱讀 Unity 的繪製呼叫批次(Draw Call Batching)中的靜態批次(Static Batching)。
動態批次
由於在 HoloLens 開發中標記物件為 靜態 存在問題,動態批次是彌補此功能不足的絕佳工具。 它在沉浸式耳機上也很有用。 然而,Unity 中的動態批次啟用起來可能較為困難,因為 GameObjects 必須 ) 共享相同的材質 , 且 b) 符合一長串其他條件。
完整清單請閱讀 Unity 的繪製呼叫批次(Draw Call Batching)中的動態批次。 最常見的是,GameObjects 無法動態批次處理,因為相關的網格資料最多只能有 300 個頂點。
其他技術
批次處理只有在多個 GameObject 能夠共享相同素材時才能進行。 通常,這會被 GameObjects 必須有其材質獨特材質的材質限制。 將多個貼圖合併成一個大型貼圖,這種方法稱為 貼圖繪圖(Texture Atlasing)。
此外,在可能且合理的情況下,最好將網格合併成一個 GameObject。 Unity 中的每個渲染器都有其對應的繪製呼叫,而不是在一個渲染器下提交合併的網格。
注意事項
在執行時修改 Renderer.material 的屬性會產生一份材質的副本,因此可能會破壞批次處理。 使用 Renderer.sharedMaterial 來修改 GameObject 間的共享材質屬性。
GPU 效能建議
頻寬與填充率
在 GPU 上渲染畫面時,應用程式會受到記憶體頻寬或填充率的限制。
-
記憶體頻寬 是指 GPU 從記憶體中讀取和寫入的速率
- 在 Unity 裡,在編輯>專案設定>的品質設定中更改貼圖品質。
-
填充率 指的是 GPU 每秒可繪製的像素數。
- 在 Unity 中,使用 XRSettings.renderViewportScale 屬性。
優化深度緩衝區共享
我們建議您 啟用 深度緩衝區共享 ,以優化 全息影像的穩定性。 在此設定啟用基於深度的後期重投影時,建議您選擇 16位元 深度格式,而非 24位元 深度格式。 16 位元深度緩衝區會大幅降低頻寬 (進而增加深度緩衝區流量相關的) 電力。 這在功率降低和性能上都能帶來重大提升。 然而,使用 16 位元深度格式可能帶來兩種負面結果。
Z-格鬥
深度範圍保真度降低,使得 16 位元比 24 位元更容易發生 z-fighting 。 為了避免這些瑕疵,請修改 Unity 相機 的近遠剪裁平面,以反映較低的精度。 對於基於 HoloLens 的應用,通常可以選擇 50 公尺的遠端剪裁平面,而不是 Unity 預設的 1000 公尺,這樣通常可以消除任何 Z 格鬥。
停用模板緩衝區
當 Unity 建立 16 位元深度的渲染貼圖時,並沒有產生模板緩衝區。 選擇 Unity 文件中描述的 24 位元深度格式,若裝置 () (32 位元,則會建立 24 位元的 z 緩 衝區和 8 位元的模板緩衝區 ,這通常是) 的情況。
避免全螢幕特效
全螢幕操作的技術成本較高,因為它們的數量級是每幀數百萬次運算。 建議避免 後製效果 ,如抗鋸齒、泛光等效果。
最佳光照設定
Unity 中的即時全域光照能提供卓越的視覺效果,但涉及昂貴的光照計算。 我們建議透過 視窗>渲染>光照設定> 關閉每個 Unity 場景檔案的即時全域光照,取消勾選 即時全域光照。
此外,建議關閉所有陰影投射,因為這也會讓 Unity 場景中增加昂貴的 GPU 通行證。 陰影可以依照燈光關閉,但也可以透過品質設定來全面控制。
編輯>專案設定,然後選擇 品質 類別 > ,選擇 UWP 平台的 低品質 。 也可以直接把陰影屬性設為「停用陰影」。
我們建議你在 Unity 裡為模型使用烘焙光照。
減少多邊形數量
多邊形數量可由以下以下方式減少
- 從場景中移除物件
- 資產減量,即減少給定網格的多邊形數量
- 在你的應用程式中實作 LOD) 系統 (細節層級 ,該系統會用相同幾何體的低多邊形版本渲染遠處物體
理解 Unity 中的著色器
比較著色器效能的一個簡單近似方法是找出每個著色器在執行時平均執行的操作次數。 這在 Unity 中可以輕鬆完成。
選擇你的著色器資產或選擇材質,然後在檢查器視窗的右上角。 選擇齒輪圖示,接著點 「選擇著色器」
選取著色器資產後,在檢查器視窗下方選擇 「編譯並顯示程式碼」 按鈕
編譯完成後,請在結果中尋找統計部分,列出頂點與像素著色器的不同操作數量 (注意:像素著色器通常也稱為片段著色器)
優化像素著色器
根據上述方法彙整統計結果,片段 著色器 平均執行的操作通常比 頂點著色器多。 片段著色器,也稱為像素著色器,會在螢幕輸出中每像素執行,而頂點著色器則僅在所有繪製到螢幕的網格中,每個頂點執行。
因此,片段著色器不僅因為所有光照計算而擁有比頂點著色器更多的指令,且片段著色器幾乎總是在較大的資料集中執行。 例如,如果螢幕輸出是 2k x 2k 的影像,那麼片段著色器可以執行 2,000*2,000 = 4,000,000 次。 如果要渲染兩個眼睛,這個數字會加倍,因為有兩個畫面。 如果混合實境應用程式有多次處理、全螢幕後製效果,或是將多個網格渲染到同一像素,這個數字會大幅增加。
因此,減少片段著色器的操作數量,通常能比頂點著色器的最佳化帶來更大的效能提升。
Unity Standard 著色器替代方案
與其使用物理基礎渲染 (PBR) 或其他高品質著色器,不如考慮使用效能更好且價格更低的著色器。 Mixed Reality Toolkit 提供了 MRTK 標準著色器,並針對混合實境專案進行優化。
Unity 也提供 unlit、vertex lit、diffuse 及其他簡化的著色器選項,速度比 Unity Standard 著色器更快。 欲了解更多詳細資訊,請參閱 內建著色器的使用與效能 。
著色器預載入
使用 著色器預載 入和其他技巧來優化 著色器載入時間。 特別是,著色器預載時不會因為執行時編譯而出現卡頓。
限額超支
在 Unity 中,玩家可以透過切換場景視圖左上角的繪圖模式選單並選擇「上繪」來顯示場景的重繪。
一般來說,過載可以透過事先剔除物件來減輕,避免它們送入 GPU。 Unity 提供了關於為其引擎實作遮 蔽剔除 的詳細資訊。
記憶體建議
過度的記憶體配置 & 釋放操作會對全息應用程式產生不良影響,導致效能不穩定、畫面凍結及其他不良行為。 在 Unity 開發時,了解記憶體考量尤其重要,因為記憶體管理是由垃圾回收器控制的。
廢棄項目收集
當GC啟動以分析執行過程中不再在範圍內的物件,且其記憶體需要釋放以便重複使用時,全息應用程式會失去處理計算時間, (GC) 。 持續的分配與取消通常會要求垃圾回收器更頻繁地執行,進而損害效能與使用者體驗。
導致過度垃圾回收的最常見做法之一,就是在 Unity 開發中快取元件和類別的引用。 任何參考應在啟動 () 或喚醒 () 中擷取,並在後續函式如更新 () 或晚更新 () 中重複使用。
其他快速建議:
- 使用 StringBuilder C# 類別在執行時動態建構複雜的字串
- 當不再需要時,請移除對 Debug.Log () 的呼叫,因為它們在所有建置版本的應用程式中仍然會執行
- 如果您的全息應用程式通常需要大量記憶體,建議在載入階段(如載入或過渡畫面時)呼叫 System.GC.Collect ()
物件池化
物件池是一種常用的技術,用以降低連續物件分配與釋放的成本。 這是透過分配大量相同物件,並重複利用這個物件池中未啟用的可用實例,而不是不斷地生成和摧毀物件來達成。 物件池對於在應用程式中壽命可變的可重複使用元件非常棒。
啟動效能
考慮先用較小的場景啟動你的應用程式,然後用 SceneManager.LoadSceneAsync 載入剩下的場景。 這讓你的應用程式能盡快進入互動狀態。 在新場景啟動時,可能會有大幅度的 CPU 突升,渲染內容可能會卡頓或卡頓。 一種解決方法是將 AsyncOperation.allowSceneActivation 屬性設定為「false」,等待場景載入完成,將畫面清空為黑,再將畫面設回「true」以完成場景啟動。
請記得,當啟動場景載入時,全息啟動畫面會顯示給使用者。