減少著色器(管線)編譯造成的卡頓
管線編譯,也常被稱為著色器編譯,是引擎為了讓 GPU 能繪製各種內容所需的一個高成本運算步驟。
Godot 中的著色器與材質,在能夠由 GPU 執行前,需要經過多個步驟處理。
更精確地說, 著色器編譯 指的是將 Godot 產生的 GLSL 程式碼轉換為跨平台可共用的中介格式(如 Vulkan 下的 SPIR-V)。然而,這個中介格式並不能直接由 GPU 使用。
管線編譯 則是 GPU 驅動程式將這個中介格式轉換成 GPU 真正可以用來繪製畫面的格式。驅動程式通常會在系統中某處快取這些管線,以避免每次遊戲執行時都要重新編譯。這個快取在驅動程式更新時通常會被刪除。
管線所包含的資訊不僅只有著色器程式碼,因此每個著色器可能會對應數十種甚至更多的管線!這讓引擎很難事先預編譯所有管線,因為這會非常耗時且佔用大量記憶體。此外,這個步驟只能在使用者的系統上執行,除非硬體和驅動版本完全相同,否則結果也難以在不同使用者間共用。
在 Godot 4.4 之前,除了當物件首次進入攝影機視野時才產生管線外,沒有其他解決方式,這導致著名的 著色器卡頓 ,通常只會在第一次遊玩時發生。自 Godot 4.4 起,引擎加入了新的機制來降低管線編譯導致的卡頓。
Ubershaders(超級著色器):Godot 利用 specialization constant(特化常數)這個功能,讓驅動程式能以光照、陰影品質等一組參數來最佳化管線程式碼。特化常數可用來限制不必要的功能,藉此最佳化著色器。每當更改特化常數時,就需要重新編譯管線。Ubershaders 是一種特殊版本的著色器,能在渲染時動態變更這些常數,代表 Godot 可以僅預先編譯一個通用管線,並在遊戲運作時於背景中編譯更優化的版本,大幅減少需建立的管線數量。
管線預編譯:藉由使用超級著色器,引擎能在多個時機點預先編譯管線,例如載入網格(Mesh)或新增節點到場景時。將其納入資源載入流程後,這些管線甚至可以在載入畫面或遊戲進行時,由多個背景執行緒並行預編譯。
從 Godot 4.4 起,Godot 會自動偵測所需的管線並在載入時預編譯。這套偵測機制大多是自動化的,但需要 RenderingServer 在載入階段能看到所有著色器、網格或渲染功能。舉例來說,如果你在遊戲執行時才載入某個網格和著色器,那麼這組合的管線直到載入時才會被編譯。同樣地,在遊戲運作時啟用 MSAA 或建立 VoxelGI 節點,也會觸發管線的重新編譯。
管線預編譯監控工具
預先編譯管線是 Godot 降低著色器卡頓的主要手段,但這並非萬全之策。瞭解可能導致管線卡頓的情境會很有幫助,且比起舊版 Godot,現在的解決方法也更為直接。隨著 Godot 未來版本導入更多偵測技術,這些臨時解法的重要性也會逐步降低。
Godot 除錯器提供了監控工具,可追蹤遊戲所建立的管線數量,以及觸發編譯的步驟。你可以在遊戲執行時觀察這些監控項目,無需每次都清除驅動快取就能找出潛在的著色器卡頓來源。若這些數值在非載入畫面時突然增加,表示首次遊玩時可能會出現卡頓。建議你務必檢查這些監控數據,幫助玩家避免卡頓,因為你自己若未刪除驅動快取或未在較弱硬體上測試,可能無法發現這些問題。
其中一個範例專案的管線編譯結果。
備註
我們可以看到遊戲進行時所編譯的管線,並確認哪些步驟可能會導致卡頓。請注意,這些數值只會增加而不會減少,因為這些監控工具不會追蹤已刪除的管線,而管線也可能在遊戲中被刪除並重新建立。
Canvas:在繪製 2D 節點時編譯。目前引擎尚未支援 2D 元素的預編譯,因此首次繪製 2D 節點時仍可能出現卡頓。
Mesh:在載入 3D 網格時會編譯,並判斷哪些管線能根據其屬性預編譯。如果在遊戲進行時載入網格,可能會導致卡頓,但若在背景執行緒載入則可減輕。節點上的材質覆蓋等修飾器無法在此步驟預編譯。
Surface:在準備繪製畫面時,首次將 3D 物件實體化到場景樹時編譯。這也可能包括尚未顯示的節點。只有節點第一次加入場景的那一幀會發生卡頓,如果發生在載入畫面後,通常不會造成明顯卡頓。
Draw:當 3D 物件需要繪製且未事先預編譯超級著色器時,會隨需編譯。這代表引擎遇到尚未涵蓋的特殊情境或程式碼遭修改,導致無法預編譯管線,進而在遊戲中造成卡頓。這與 Godot 4.4 之前的版本相同。若你發現這裡有編譯發生,請 回報給開發者 <https://github.com/godotengine/godot/issues> ,因為在有超級著色器系統下不應出現這情況。回報時請附上最簡化的重現專案。
Specialization:於遊戲進行時於背景編譯,以最佳化影格率。這不會造成卡頓,但若每幀發生太多編譯,可能會降低 FPS。
管線預編譯功能
Godot 提供了許多渲染功能,但不一定每個遊戲都會用到。不過,管線預編譯無法預先判斷某功能是否會被專案使用。有些功能必須在使用者將節點加入場景或切換專案或環境設定時才會被偵測到。當這些功能首次被偵測到時,管線預編譯系統會記錄下來,往後建立的網格或表面就能預編譯支援這些功能的管線。
如果你的遊戲會用到這些功能,請務必在載入大部分資產之前,盡早有一個場景啟用這些功能。這個場景可以很簡單,只要涵蓋未來遊戲會用到的功能即可。如果需要,也可以只讓它在螢幕外被渲染一幀,例如用 ColorRect 節點遮住,或用置於視窗外的 SubViewport。
此外,遊戲進行時若變更這些功能,將會立即產生卡頓。請確保僅在設定畫面等必要時機才變更這些功能,並在套用變更時插入載入畫面與提示訊息。
MSAA 等級:當專案設定的 3D MSAA 等級變更時啟用。不幸的是,不同的視窗使用不同 MSAA 等級時會造成卡頓,因為引擎一次只追蹤一個等級以進行預編譯。
反射探針:在場景中加入 ReflectionProbe 節點時啟用。
獨立高光:當使用次表面散射或需直接從螢幕取樣高光的合成特效時啟用。
動態向量:當使用 TAA、FSR2 或需要動態向量(如動態模糊)的合成特效時啟用。
法線與粗糙度:當使用 SDFGI、VoxelGI、螢幕空間反射、SSAO、SSIL,或自訂著色器 / CompositorEffect 使用
normal_roughness_buffer時啟用。光照貼圖:當場景中加入 LightmapGI 節點並有節點使用烘焙光照貼圖時啟用。
VoxelGI:場景中加入 VoxelGI 節點時啟用。
SDFGI:WorldEnvironment 啟用 SDFGI 時啟用。
多重視角:XR 專案時啟用。
16/32 位元陰影:當專案設定更改陰影貼圖的深度精度時啟用。
全向光雙拋物面陰影:當全向光源啟用陰影並使用雙拋物面模式時啟用。
全向光立方體陰影:當全向光源啟用陰影並使用立方體貼圖模式(預設)時啟用。
如果你在遊戲進行時遇到卡頓,且監控工具顯示 Surface 步驟的編譯數量突然增加,很可能是某項功能在事前未啟用。請確保這些效果在遊戲載入時就被啟用,以減輕相關問題。
管線預編譯與實體化
遊戲中常見的卡頓來源之一,是某些效果只有在特定互動發生時才會動態實體化到場景。例如,若有一個粒子特效,只有玩家執行某個動作時才用腳本加到場景。即使該場景已預先載入,若效果從未實體化過,引擎仍可能無法預先編譯對應的管線。
所幸,從 Godot 4.4 起,只要場景被實體化過一次(即使完全不可見或在攝影機外),這些管線就能被預先編譯。
在某範例專案中,將隱藏的子彈節點掛在玩家物件下。這樣有助於引擎預先編譯該特效所需的管線。
如果你知道某些特效會在遊戲過程中動態加入場景,且發現這些特效出現時編譯數量會突然增加,一個解決方法是將這些特效的隱藏版本掛載在場景內某處,確保它們一定會被實體化一次。
舉例來說,若玩家角色能引發爆炸效果,你可以將該特效作為隱藏節點掛載在玩家物件下。請記得停用隱藏節點上的腳本,或隱藏可能造成問題的其他子節點,這可透過啟用節點的 可編輯子節點 選項來達成。