AssetBundle中的shader變體丟失問題

我們通過assetbundle加載材質球或shader時常遇到一個問題:在電腦上測試ok的shader,在手機上顯示一片粉紅。
出現這種情況的原因有很多,但是其中可能性最大的就是shader變體丟失導致的。

ab資源加載中的shader是如何被引用到的?

加載ab資源中的材質時,會主動索引并加載使用的shader。
“被包含在構建包體中”,指的這幾種情況:

  1. shader文件在Resources文件夾下;
  2. shader文件被設置在Setting => Graphics => Always Included Shaders中;
  3. shader被構建包含的場景中使用;這幾種情況下shader資源會被打進apk包中。

基于shader是否在構建包體中,有以下2種情況:

  • 如果shader被包含在構建包體內,則可以直接加載使用。
  • 如果shader未被包含在構建包體內,需要通過ab資源加載使用時,使用前shader需要首先加載shader所在的ab資源。 如果ab資源尚未加載,此時使用shader的材質就會出問題。

為什么ab中的shader變體會丟失?

multi_compile和shader_feature是著色器中定義宏的關鍵字,不同的關鍵字會生成不同的變體。
相比于multi_compile指令,使用 shader_feature 指令定義的著色器在構建時,沒有在材質球中使用到的變體將不會包含在最終的構建中。
所以 shader_feature 用于材質中設置的關鍵字,而 multi_compile 更適合通過代碼來全局設置的關鍵字。

在Assetbundle打包材質與其引用的shader時:

  • 如果shader和材質在同一個ab包中,在打包時會自動檢索到使用的keyword,并將其對應的變體編譯打包入ab包中。
  • 如果shader和使用到的材質不在同一個ab包中,則打包時無法檢索到使用到了哪些keyword,所以材質使用的變體可能丟失。
    這就是ab打包過程中的變體丟失的問題。

如何避免shader變體丟失?

第一種方法:將shader加入到ProjectSetting=>Graphics=>Always Included Shaders 中。
加在這里的shader不會剔除變體,所以適用于變體少且全部使用到的shader。
如果加入有很多變體的shader,會導致打包包體膨脹。
注意:shader_feature定義的關鍵字如果未使用,即使加入到了這里,變體也不會保留!

第二種方法:使用ShaderVirantCollection。
我們可以使用ShaderVirantCollection文件來定義使用到的變體,這樣打ab包時會自動將定義的變體編譯并打包,這樣就避免了變體丟失。
注意:ShaderVirantCollection也必須入包(與shader放入同一個ab包),否則不生效。

可以通過shader control插件來搜集使用到的shader變體,從而避免人工查找容易漏掉了某些變體。
當然shader control可以僅可以查詢到項目中材質球使用到的shader關鍵字,如果是通過代碼加載/設置shader keyword,則無法通過shader control查詢到。

assetbundle打包shader的最佳方案

下面是我當前項目中assetbundle打包shader的方案,暫時定義為我自己的最佳方案:

  • 把assetbundle資源中引用到的shader都放到一個ab包中,在進入游戲時首先加載此ab包,這樣可以保證后續所有使用的材質沒有問題。
  • 常用到且變體數量少的shader,可以將shader加入到Always Included Shaders 中,然后在打包ab資源時剔除這些shader。
  • 需要assetbundle打包的shader,使用ShaderVirantCollection定義需要打包的變體。可以通過shader control插件來搜集材質球使用到的shader變體,結合人工查找代碼中調用使用到的變體。

multip_compile 和shader_feature的區別

相同點

  • 聲明Keyword,用來產生Shader的變體(Variant)
#pragma multi_compile A B
//OR #pragma shader_feature A B
  • 這個Shader會被編譯成兩個變體:一是只包含A模塊代碼的變體A;二是只包含B模塊代碼的變體B;
  • 指定的第一個關鍵字是默認生效的,即默認使用變體A;
  • 在腳本里用Material.EnableKeyword或Shader.EnableKeyword來控制運行時具體使用變體A還是變體B;
  • 它們聲明的Keyword是全局的,可以對全局的包含該Keyword的不同Shader起作用;
  • 全局最多只能聲明256個(2020.3以后的全局keyword的數量限制是384)這樣的Keyword;
  • 請注意Keyword的數量和變體的數量之間的關系,并可能由此導致的性能開銷,比如聲明#pragma multi_compile A B和#pragma multi_compile D E 這樣的兩組Keyword會產生 2x2=4 個Shader變體,但若聲明10組這樣的keyword,則該Shader會產生1024個變體;

不同點
如果使用shader_feature,build時沒有用到的變體會被刪除,不會打出來。也就是說,在build以后環境里,運行代碼Material.EnableKeyword("B")可能不起作用,因為沒有Material在使用變體B,所以變體B沒有被build出來,運行時也找不到變體B。
如果想解決這個問題,可以采取以下辦法中的其中一種:

  1. 使用multi_complie 代替 shader_feature,multi_complie 會把所有變體build出來;
  2. 把這個Shader加入“always included shaders”中 (Project Settings -> Graphic);
  3. 創造一個使用變體B的Material,強行說明變體B有用;

局部keword

全局的Keyword只能有256個,這或許會最終對我們造成限制,而且大部分Keyword并不需要進行全局聲明。
因此,我們可以使用multi_complie_local來聲明局部的、只在該Shader內部起作用的Keyword,用法相似:

#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A

注意:

  • local Keyword仍有數量限制,每個Shader最多可以包含64個local Keyword;
  • 因為這種Keyword是局部的,Material.EnableKeyword仍是有效的,但對Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword這種全局開關來說無法使用;
  • 當你既聲明了一個全局的Keyword A ,同時又聲明了一個同名的、局部的Keyword A,那么優先認為Keyword A是局部的。

參考鏈接

Unity Manual - 著色器變體和關鍵字
Unity Blog - Stripping scriptable shader variants
【Unity游戲開發】馬三的游戲性能優化自留地

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容