Shader:最佳化破解變體的 “影分身” 之術

侑虎科技發表於2020-12-16

本期我們將剖析剛上新的Shader Analyzer中和Shader變體相關的規則“Build後生成變體數過多的Shader”、“專案中可能生成變體數過多的Shader”和“專案中全域性關鍵字過多的Shader”。我們將力圖以淺顯易懂的表達,讓職場萌新或優化萌新能夠深入理解。

首先我們來了解下相關的概念與意義。


1、什麼是Shader 和 變體(Variant)?

Shader從字面意義來講就是“著色器”;功能上來講就是用以實現圖形渲染的一種技術,更直白地說就是一段實現特定功能的程式碼程式。Unity工程中可以說所有物體的顏色、光照效果或質感等等,都和Shader有千絲萬縷的關聯。如圖,Unity 2019.3.7中在Project介面可以建立多種預設的不同型別的Shader。

Shader:最佳化破解變體的 “影分身” 之術

很多時候,不同效果之間只有一些微小的差距,為每一種渲染效果去專門寫一個Shader是很不現實的。從設計原則的角度講,我們應當儘可能共用重複的程式碼,而Shader的關鍵字(Keyword)就為我們提供了這個功能。

開發人員在寫Shader時,可以在Shader的程式碼段中去定義一些關鍵字,然後在程式碼中根據關鍵字開啟與否,去控制物體的渲染過程。如此一來同一份Shader原始碼就可以具備多種不同的功能。另外,我們可以在Runtime通過開啟或關閉關鍵字的方式動態改變渲染效果。

這樣在專案最終編譯的時候,引擎就會根據不同的關鍵字組合去生成多份Shader程式片段。每一種關鍵字組合對應生成的程式就是這個原始Shader的一個變體(Variant)。

2、什麼是關鍵字(Keyword)?

通俗地說,Shader中的關鍵字就是一個個標籤,方便材質在渲染時繫結不同的Shader變體,實現不同的效果。我們可以在Shader片段中使用編譯指令(compile directives)來定義Shader關鍵字。從變體生成特點上可分為“multi_compile”和“shader_feature”兩類,從作用範圍角度可分為區域性關鍵字和全域性關鍵字。

在Unity中multi_compile型別的關鍵字定義方式如下:

Shader:最佳化破解變體的 “影分身” 之術

該編譯指令會導致編譯時生成所有關鍵字組合的的變體,如下圖:

Shader:最佳化破解變體的 “影分身” 之術

而shader_feature類關鍵字定義方法如下:

Shader:最佳化破解變體的 “影分身” 之術

一般來講,帶有multi_compile類關鍵字的Shader,在Build時會把所有可能的關鍵字排列組合的變體全部生成,由此導致不必要的冗餘和包體體積增大;但好處是方便動態選擇Shader變體;

而對shader_feature而言,Unity在Build時,不會將未使用的shader_feature關鍵字生成的變體 包含入內,只有實際被材質使用到的關鍵字對應的變體才會被Build和打入包中,從而減少了記憶體佔用,精簡了包體體積。

但代價是自己要做額外的工作,舉例來說,有些shader_feature關鍵字對應的變體在Build時沒有被材質使用到,但是在執行時可能會通過程式碼開啟。這類變體實際需要使用,卻沒有被打入包中 ,就會導致理想中的效果無法生成。這時,就需要使用Shader Variant Collection,手動將這些體加入到變體收集器裡面。

需要說明的是,shader_feature 預編譯指令行至少有兩個關鍵字。如果只定義了一個關鍵字KW_X,則會預設生成一個下劃線關鍵字。以下兩行指令等價:

Shader:最佳化破解變體的 “影分身” 之術

一般Shader片段中multi_compile類關鍵字每增加一個,或者啟用的shader_feature類關鍵字增加一個,該Shader的變體數量就會增加一份。而對於變體數與記憶體、視訊記憶體的關係,UWA曾做過以下實驗:

使用#pragma multi_compile定義的一行關鍵字為一組,每組包含兩個關鍵字,對產生的記憶體進行統計,結果如下:

Shader:最佳化破解變體的 “影分身” 之術

由此可見變體數和ShaderLab的記憶體佔用基本成正比。而由於沒有使用Shader進行渲染,GfxDriver記憶體不會增加,沒有參與渲染的Shader變體是不會經歷CreateGPUProgram傳入GfxDriver記憶體中的。

然後我們來結合這次新功能中的相關規則進行具體說明變體對專案優化的意義。

3、可能生成變體數過多的Shader

Shader:最佳化破解變體的 “影分身” 之術

對Unity專案而言,Shader變體有其存在的積極意義。除了程式碼的共用與執行時渲染效果的動態改變之外,還增加了Shader程式在GPU上的執行效率。

對GPU來說,處理類似於“if-else”結構的分支語句不是它的強項,GPU的特點和功能決定了它更適合去並列地“執行”重複性的任務,而不是去“選擇”。所以Shader變體的存在就很好地解決了這個問題,GPU只需要根據關鍵字去執行對應的Variant內容就可以,避免了效能下降的可能。同時,專案在執行時,可以通過在程式碼中選擇不同的Shader變體,從而動態地改變著色器功能。

但是Shader變體是一把雙刃劍。在帶來以上便利的同時,也存在著各種問題:

1)在Build階段,過多的Shader變體數量會使得Build耗時明顯上升,而最終的專案包體體積也會變得臃腫。

2)在專案執行階段,Shader變體會以其龐大的數量產生可觀的記憶體佔用,同時也會導致專案載入時間的增加,也就是俗說的“卡頓”。

所以本條規則會掃描專案中的Shader指令碼,根據專案中Material上開啟的關鍵字情況去計算可能生成的變體數。開發團隊可以在找出這些可能生成過多變體數的Shader後,結合專案實際情況去進行相應的修改。

4、全域性關鍵字過多的Shader

Shader:最佳化破解變體的 “影分身” 之術

由於Unity支援的全域性關鍵字的總數有限(256個全域性關鍵字,64個區域性關鍵字),而Unity內部關鍵字已經佔用了約60個“名額”,所以我們建議開發團隊儘可能使用區域性關鍵字(shader_feature_local和multi_compile_local)。本條規則會對所有預編譯指令定義的關鍵字進行識別,找出那些全域性關鍵字過多的Shader以方便開發團隊進行進一步的檢查與修改。

5、Build後生成變體數過多的Shader

Shader:最佳化破解變體的 “影分身” 之術

專案進行打包(Build)的時候,會將專案實際使用的資源封裝到包裡面(如Scenes In Build中的場景依賴的所有資源等)。因此,並非所有的Shader資源都會被帶入包中。另外,本文介紹的第一條規則,僅會檢測目標路徑下的Shader指令碼檔案,對於專案使用的一些內建的(Built-in)Shader則無法檢測到。所以本條規則的意義,就在於統計打包後實實在在使用的Shader資源對應的變體。

我們模擬了專案的Build流程,將那些在Build後生成變體數過多的Shader統計出來,方便開發團隊根據專案的實際需求去進行進一步的檢查和修改。(此外需要說明的是,本規則只支援Unity2018.2及其以上的版本。)

希望以上這些知識點能伴隨本次的功能更新而在實際的開發過程中為大家帶來幫助。需要說明的是,每一項檢測規則的閾值都可以由開發團隊依據自身專案的實際需求去設定合適的閾值範圍,這也是本地資源檢測的一大特點。同時,也歡迎大家來使用UWA推出的本地資源檢測服務,可幫助大家儘早對專案建立科學的美術規範。

往期優化規則,我們也將持續更新。
《動畫優化:關於AnimationClip的三兩事》
《材質優化:如何正確處理紋理和材質的關係》
《紋理優化:讓你的紋理也“瘦”下來》
《紋理優化:不僅僅是一張圖片那麼簡單》

萬行程式碼屹立不倒,全靠基礎掌握得好!

效能黑榜相關閱讀

《那些年給效能埋過的坑,你跳了嗎?》
《那些年給效能埋過的坑,你跳了嗎?(第二彈)》
《掌握了這些規則,你已經戰勝了80%的對手!》

相關文章