關于multi_compile的使用知乎的這篇和這篇講的很好,本文轉載結合一下。
一.什么是ShaderVariant(Shader變體)
在寫shader時,往往會在shader中定義多個宏,并在shader代碼中控制開啟宏或關閉宏時物體的渲染過程。最終編譯的時候也是根據(jù)這些不同的宏來編譯生成多種組合形式的shader源碼。其中每一種組合就是這個shader的一個變體(Variant)。
Material ShaderKeywords與ShaderVariant
Material所包含的Shader Keywords表示啟用shader中對應的宏,Unity會調用當前宏組合所對應的變體來為Material進行渲染。
在Editor下,可以通過將material的inspector調成Debug模式來查看當前material定義的Keywords,也可在此模式下直接定義Keywords,用空格分隔Keyword。如圖1

圖1:設置keyword
在程序中,可用Material.EnableKeyword()、Material.DisableKeyword()、Shader.EnableKeyword()、Shader.DisableKeyword()來啟用/禁用相應的宏。
Enable函數(shù)應與Disable函數(shù)相對應。若一個宏由Material.EnableKeyword()開啟,則應由Material.DisableKeyword()關閉,Shader.DisableKeyword()無法關閉這個宏。Material中定義的Keywords由Material的函數(shù)進行設置。
二.multi_complie 還是 shader_feature
shader_feature 和 multi_complie 是兩個很相似的預編譯指令,在Editor模式下,他們是幾乎沒有區(qū)別的。
共同點是:
聲明Keyword,用來產生Shader的變體(Variant)
#pragma multi_compile A B
//OR #pragma shader_feature A B
//-----------------------A模塊----------------------
#if A
return fixed4(1,1,1,1);
#endif
//---------------------------------------------------
//-----------------------B模塊-----------------------
#if B
return fixed4(0,0,0,1);
#endif
//---------------------------------------------------
- 這個Shader會被編譯成兩個變體:一是只包含A模塊代碼的變體A;二是只包含B模塊代碼的變體B;
指定的第一個關鍵字是默認生效的,即默認使用變體A; - 在腳本里用Material.EnableKeyword或Shader.EnableKeyword來控制運行時具體使用變體A還是變體B;
- 它們聲明的Keyword是全局的,可以對全局的包含該Keyword的不同Shader起作用;
- 全局最多只能聲明256個這樣的Keyword;
- 請注意Keyword的數(shù)量和變體的數(shù)量之間的關系,并可能由此導致的性能開銷,比如聲明#pragma
multi_compile A B和#pragma multi_compile D E 這樣的兩組Keyword會產生 2x2=4 個Shader變體,但若聲明10組這樣的keyword,則該Shader會產生1024個變體;
區(qū)別在于:
如果使用shader_feature,build時沒有用到的變體會被刪除,不會打出來。也就是說,在build以后環(huán)境里,運行代碼Material.EnableKeyword("B")可能不起作用,因為沒有Material在使用變體B,所以變體B沒有被build出來,運行時也找不到變體B。
如果想解決這個問題,可以采取以下辦法中的其中一種:
使用multi_complie 代替 shader_feature,multi_complie 會把所有變體build出來;
把這個Shader加入“always included shaders”中 (Project Settings -> Graphic);
創(chuàng)造一個使用變體B的Material,強行說明變體B有用;
三.__
上文已經提到了,最多聲明256個全局Keyword,因此我們要盡量節(jié)省Keyword的使用數(shù)量。其中一個技巧是使用 __(兩條下劃線),如:
#pragma multi_compile __ A
//OR #pragma shader_feature __ A
//-----------------------A模塊----------------------
#if A
return fixed4(1,1,1,1);
#endif
//---------------------------------------------------
return fixed4(0,0,0,1);
- 此方式相比#pragma multi_compile A B 的方式,我們可以減少使用一個Keyword。
- 此方式仍會編譯成兩個變體:一是不包含A模塊代碼的變體非A;二是包含A模塊代碼的變體A;
默認為 __ ,即變體非A生效。
四.multi_complie_local
全局的Keyword只能有256個(而且Unity自身已經使用掉了一部分,所以實際可用的更少),這或許會最終對我們造成限制,而且大部分Keyword并不需要進行全局聲明。
因此,我們可以使用multi_complie_local來聲明局部的、只在該Shader內部起作用的Keyword,用法相似:
#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A
//-----------------------A模塊----------------------
#if A
return fixed4(1,1,1,1);
#endif
//---------------------------------------------------
return fixed4(0,0,0,1);
但需要注意:
local Keyword仍有數(shù)量限制,每個Shader最多可以包含64個local Keyword
因為這種Keyword是局部的,Material.EnableKeyword仍是有效的,但對Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword這種全局開關說拜拜
當你既聲明了一個全局的Keyword A ,同時又聲明了一個同名的、局部的Keyword A,那么優(yōu)先認為Keyword A是局部的。
五.Shader編寫規(guī)范
1.建議使用shader_feature時將定義語句寫成完整模式,并且不要在一個語句中定義多個宏。
完整模式:#pragma shader_feature _ A,不建議寫成#pragma shader_feature A。
不建議在一個語句中定義多個宏,如: #pragma shader_feature _ A B C,若一定要定義多個宏,請務必將其寫成完整模式,不使用完整模式在切換shader時可能會與想要的效果不一致,具體原因尚未測得。
2.若在shader中使用shader_feature,請為這個shader指定一個CustomEditor
每個使用shader_feature來定義Keyword的shader都需要再末尾加個 CusomEditor “xxxx”,并在代碼中實現(xiàn)類xxxx(需繼承自UnityEditor.ShaderGUI),用來對Keywords定義進行設定。
這么做是因為Material中的部分Keyword是由shader中的屬性(Properties)所控制的。比如shader中含有_NormalMap的屬性并且定義了與_NormalMap相關的Keyword,這個Keyword需要在Material含有NormalMap時添加,不含NormalMap時移除。這個功能可由自定義的CustomEidtor實現(xiàn)。
具體如何寫這個CustomEditor類可參考Unity builtin_shaders\Editor\StandardShaderGUI.cs。該文件可去Unity官網下載,下載時選擇內置著色器即可。如圖

3.其他減少變體數(shù)量的方案&注意事項
1.內存中ShaderLab的大小和變體成正比關系。從減少內存方面應該盡量減少變體數(shù)量,可以使用 #pragma skip_variants。
2.在使用ShaderVariantCollection收集變體打包時,只對shader_feature定義的宏有意義,multi_compile的變體不用收集也會被全部打進包體。
3.2018.2新功能OnProcessShader可以移除無用的shader變體。比#pragma skip_variants更合理。
4.項目前期介入美術效果制作流程,規(guī)范shader宏定義使用,防止TA為了美術效果過度使用宏定義的情況,以過往項目經驗來看,到后期進行此項工作導致的資源浪費非常之大。
5.ShaderLab在相關shader加入內存時就已經產生,但如果沒有被渲染的話不會觸發(fā)CreateGPUProgram操作,如果提前在ShaderVariantCollection中收集了相關變體并執(zhí)行了warmup的話,第一次渲染時就不會再CreateGPUProgram,對卡頓會有一定好處。
六.Unity自身還內建的一些multi_compile,自建的multi_compile會產生較多的變體,實際開發(fā)中如果沒有能力把控好變體建議慎用:
multi_compile_fwdbase:為基本前向渲染通道編譯多個變體,不同的變體可能需要處理不同的光照貼圖類型,或者一些使用了主平行光的陰影,而另一些禁用了。(參考Unity渲染管線)
multi_compile_fwdadd:為附加前向渲染通道編譯多個變體,不同的變體可能需要處理不同類型的光源——平行光、聚光燈或者點光源,亦或者它們附帶cookie紋理的版本。
multi_compile_fwdadd_fullshadows:同上,不過包含了可以讓光源擁有實時陰影的功能。
multi_complie_fog:為處理不同的霧效類型(off/linear/exp/exp2)擴展了多個變體。
multi_compile_particles:主要包含了軟陰影粒子的宏定義,可以在質量設置界面開關軟陰影粒子。