Unity Shader 變體處理與預加載流程

一、什麼是Shader變體,它是怎麼出現的

當我們寫完一個shader以後,unity需要加載和編譯,這個過程由着色器的構建管線來完成,它的輸入是着色器,而它的輸出就是今天的主角---着色器變體;每一個着色器進入構建管線後會被解析,然後提取着色器片段(頂點着色器和片元着色器等),收集預處理指令,然後每一個着色器變體會有一個參數表;

爲什麼會有變體這個概念呢,其實它和我們使用宏的概念是一致的,就是一個全局的控制,同一個shader的變體之間的着色器代碼功能類似,只是有一些微小的差距,而如果我們爲每一種情況都寫一個shader,那可能有上千種情況就要上千個shader,這肯定是違反了設計原則的;但是shader變體的增加也會帶來一些問題,你需要知道的是,shader的構建過程是需要時間的,而且這個時間是和shader的變體數量呈正相關;

以下兩張圖截取自Unity Connect:Unity 2018.2新功能:可編程着色器變體移除,幫助我們更好的瞭解着色器構建管線的流程,想要仔細瞭解的,可以進去看一下;

二、着色器變體的使用

參考:Unity-Manual-製作多個着色器變體

#pragma multi_compile key_word1 key_word2

每一個pragma至少要有兩個變體的關鍵字,並且有且只有一個關鍵字被啓用;

#pragma shader_feature key_word1 key_word2

它和multi_compile類似,唯一區別在於shader_feature在最終版本中不包括未使用的着色器變體;

數量的計算

我們在一個subshader裏面,添加了如下代碼

 

着色器關鍵字的設置

shader_feature,最好使用Material.EnableKeyWord和Material.DisableKeyWord來完成,這個是針對某個材質來控制關鍵字使用的,範圍更小的,只針對某個材質的,它是一個public方法;

multi_compile,最好使用Shader.EnableKeyWord和Material.DisableKeysWord來完成,這個是做全局設置的,它是一個static方法;

關鍵字數量的限制

使用shader變體時,unity將關鍵字的數量限制爲256個,而unity內部已經使用了大約60個,所以開發者實際可用量更少,所以要注意不要超過限制;(unity對關鍵字的限制應該是防止開發者濫用關鍵字造成變體過多而制定的一個規範)

本地關鍵字

multi_compile和shader_feature聲明的關鍵字都會影響可用數量,unity提供了本地關鍵字來處理,也就是multi_compile_local與shader_feature_local,這樣着色器定義的關鍵字作用域將保留在該着色器內,而不是整個project;因此,我們應該儘可能使用local而非全局的聲明;

unity提供的常用內置的multi_compile

以下幾種都會產生相應的變體,如果可以確定不需要它們,就可以使用#pragma skip_variants來跳過;

三、優化Shader加載時間

首先,在Unity中Shader是如何加載的:

  • Editor中:修改shader並保存時立即編譯。
  • Runtime下,無論哪個平臺,都是在進入場景時加載shader object內容到內存,但是首次實際調用渲染時才編譯,編譯完成之後會cache下來。

我們所說的優化Shader加載時間指的通常是runtime下的加載;

1.優化shader加載時間

Shader程序是需要在GPU中的,將其從磁盤加載到顯存中當然需要一些時間,單個的GPU程序一般不會花費太多的加載時間,但是一個Shader通常會有大量的變體,也就是一個Shader可能會有很多個GPU程序,從而引起了加載時間過長的問題;

超着色器(Uber shader):超着色器是個可以生成多個着色器變體的着色器來源

在Unity中,超着色器由多個部分管理,包括ShaderLab子着色器、通道、着色器類型,以及#pragma multi_compile和#pragma shader_feature這兩個預處理指令。

舉個Standard Shader的例子,如果它被完全編譯,該Shader將會生成上千個有細微不同的着色器程序,它主要會產生兩個問題:

  • 從打包的角度考慮時間和空間:大量的着色器變體會增加遊戲的構建時間以及遊戲的包體數據
  • 從遊戲運行的角度考慮時間和空間:在遊戲中加載大量的着色器變體會導致遊戲卡頓而且會佔用大量內存

2.減少Shader構建時間

當構建遊戲時,Unity會檢測到一些遊戲中不會用到的內置着色器變體,然後在構建數據的時候跳過它們,這一選項稱爲Build-time stripping:

  • 單獨的Shader features,針對那些使用了#pragma shader_feature的着色器,如果沒有材質使用專門的變體,它就不會被構建;
  • 用於處理Fog和LightMapping模式的變體,如果它不被任何場景使用,它也不會被構建到遊戲數據中;這個可以在圖形設置中自己設置用於覆蓋默認選項;

3.默認的Shader加載過程

在默認設置下,Unity只會將ShaderLab程序資源加載到內存中,但並不會創建內置的着色器變體直到它們需要被使用;也就意味着這些包含在遊戲構建數據中的着色器變體不會佔用遊戲內存和加載時間,直到它們需要被使用;舉個例子:着色器使用一個變體來處理帶陰影的點光源,但是如果在遊戲中從來都沒有過帶陰影的點光源,那麼這個專門的變體永遠不會被加載到內存;

這種處理方式的一個缺點是:一些Shader變體在第一次需要使用時的加載問題,因爲一個新的GPU程序必須要加載到顯卡驅動中,這通常發生在unity運行時,而這也是我們不想看到的事情;Unity使用ShaderVariantCollection資源來解決該問題;

4.Shader Variant Collections

着色器變體收集是一種基於着色器列表的資源,而且包括對於每一個着色器,一系列的通道類型和需要加載的關鍵字組合;

爲了能夠幫助創建需要的asset,unity編輯器能夠自動追蹤這些實際使用的shader和相關變體,在圖形窗口我們可以創建一個新的ShaderVariantCollections資源來收集當前追蹤到的着色器或者清除當前追蹤的着色器列表;

一旦創建好一些SVC資源後,你可以在程序加載時將這些變體自動預加載或者可以通過腳本來加載單獨的變體;

我們傾向於預加載shader列表內包含的是頻繁使用的shader,因爲在預加載Shader列表中着色器變體將會被加載到內存中,而且生命週期將持續整個應用程序,也就是說這些預加載列表中的內容將會一直佔用大量的內存資源;爲了避免這種消耗,SVC資源需要被分隔成更小的個體,然後通過腳本來根據場景等按需加載;

四、Shader變體的移除

1.變體過多的弊端

shader變體過多,會不僅會造成加載時間的問題,它會導致包體過大,構建時間過長,以及內存佔用過多,加載時間變長等主要問題;

2.移除方式

基於項目設置:也就是可以在Project Settings去設置對一些功能是否支持,從而系統會決定是否剔除相關變體;

基於圖形設置:可以在圖形設置中的shader stripping來設置;

需要注意的是,unity對變體的自動移除會受到構建時間的約束,也就是它只會在構建時間內來判斷是否需要剔除,比如我們如果需要一個着色器變體來做光源着色,但是構建時並沒有光源,而是運行時腳本添加,這個時候就可能會出現變體被移除掉;

提升着色器代碼的設計

  • 首先要確定所有的關鍵字都是正在使用的,對於不用的關鍵字,一定要及時清理;
  • 之後要儘可能保證關鍵字的各種組合都是用到的,有可能產生了12中組合,但是隻能用到6種,這個時候就需要優化代碼了,要保證儘可能高效的代碼路徑

腳本移除着色器變體

Unity提供了IPreprocessShaders接口,我們只需要實現這個接口,unity在構建管線中會查找所有該接口的實現進行調用,從而移除相應的着色器變體(在unity的editor模式下是沒有用的,因爲不走構建流程);

如果要使用多個變體移除腳本,那麼需要將腳本按照用例分開,並且需要使用callbackOrder來設置腳本的執行順序,值越小越先執行;

編寫着色器變體移除功能的步驟如下

foreach shader in project{

    檢查shader所有的關鍵字,同時確定這些關鍵字是否需要;

    移除相應的關鍵字,並驗證構建版本的視覺效果;

}

其它技巧

  • 去除callback

五、Shader變體收集

1.ShaderVariantCollection

着色器變體集合類,記錄了每個shader中實際用到的着色器變體;它通常被用於預加載(warmup),也就是一個shader變體可以在遊戲開始就被加載而不是在遊戲中加載編譯;

一個着色器變體集合包含多個着色器變體,它是一個結構體(見下ShaderVariant);一個着色器變體集合的流程通常是記錄着色器變體然後保存成一個asset,然後手動加入一些預加載的變體,使用warmup可以完成一個着色器變體集合的預加載;

注意warmup函數的作用:加載一個着色器變體集合中所有的着色器;顯卡驅動不會去處理一個着色器,直到它實際被使用;然而,一些對象如果在遊戲運行中使用之前從來沒有使用的shader,就會因爲驅動編譯和優化shader導致的問題,在移動平臺上會更加嚴重;而warmup函數可以通過使用shader和變體渲染一個不可見的三角形來完成預加載,並且一個變體被預加載後,再調用warmup就不會再做任何工作;

2.ShaderVariant

它是一個結構體類型,有三個變量;

keywords:用於這個變體的關鍵字數組;

passType:用於這個變體的通道類型

shader:用於這個變體的shader

3.ShaderVariantCollection的創建和使用

一種是自己手動創建,然後手動添加變體集合;另一種是使用Unity內置功能進行創建,即在圖形設置中將當前場景遍歷到的變體保存;

啓動時加載和代碼加載,啓動時加載即在圖形設置內的shader preloading內設置需要預加載的shadervariantcollection;

代碼加載則是像加載其它資源一樣加載變體集,然後調用WarmUp()即可;

六、着色器變體的預加載流程

1.兩種預熱方式的比較

Shader.WarmupAllShaders

預熱當前所有已經加載的着色器,以防止將來出現性能問題或其它故障;但是,一次預熱過多會造成一些性能問題,最好還是需要使用更細粒度的着色器預熱方案,即ShaderVariantCollection;

ShaderVariantCollection.WarmUp,更細粒度的預熱方案,只會預熱該變體集中的所有着色器變體;

七、踩坑

1.shader構建處理的腳本要放在Editor目錄下

Editor目錄爲特殊文件夾,擴展IProcessShaders接口的腳本需要放置在Editor下,否則在進行構建的時候會報一堆找不到命名空間的錯誤;

參考

Unity Connect:Unity 2018.2新功能:可編程着色器變體移除(非常詳細)

UWA問答:一種Shader變體收集打包以及編譯優化的思路

Github-Unity變體剝離的編輯器工具

CatlikeCoding-Level Of Detail,以及中文翻譯版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章