在閱讀本文之前我們首先需要弄清楚什么是 ShaderLab?
ShaderLab是由Unity發(fā)明或者說是由Unity首創(chuàng)的一種語言體系,,用來幫助大家做跨平臺(tái)Shading開發(fā),。它里面除了語言規(guī)則之外,還有很多其他的東西,。我們一個(gè)個(gè)來看,。
我們先來看看ShaderLab Text(就是它的文本)。
我們大家知道,,無論是寫代碼,,還是寫shading,實(shí)際上我們寫的都是一堆文本,。雖然在IDE 里看起來“花里胡哨”的,,但是對(duì)計(jì)算機(jī)來講就是文本。大家能看到,,這就是我們非常熟悉的Unity的Shader,,這個(gè)東西叫ShaderLab文本,是根據(jù)Unity定義的語法規(guī)則來寫的,。
我們可以看到,,在基礎(chǔ)文本里面會(huì)包含一些塊,比如說Shader的名字,,在Subshader里面也有它自己的屬性,比如Tag,、LOD等等,。在Subshader下面還有一個(gè)個(gè)Pass,在Pass里面我們還可以定義不同的pragma,、Vertex,、Fragment,而其中的每個(gè)都對(duì)應(yīng)著不同的代碼塊,。
這是ShaderLab第一個(gè)組成的部分,,我們稱之為ShaderLab文本。
但光有文本是不行的,,就如同你寫C++,,如果只是寫了一堆CPP文件,依然是無法被計(jì)算機(jī)認(rèn)可并執(zhí)行的,。中間需要有翻譯的過程,,這就是Shader Compiler的過程。如果大家在 Windows或者是Mac上有留意過的話,,就會(huì)發(fā)現(xiàn),,打開Unity之后再打開任務(wù)管理器,,或者在Mac上打開active monitor,你都會(huì)看到一個(gè)叫Unity shader Compiler的東西,。如果是早期的Unity版本,,你看到的就可能是cgbatch。
這個(gè)東西是干什么的呢,?實(shí)際上它類似一種服務(wù),,是Unity在后臺(tái)提供的一種服務(wù),用來幫助我們?nèi)グ褜懞玫腟haderLab語言翻譯為目標(biāo)機(jī)器能夠認(rèn)可并執(zhí)行的語言(我們會(huì)在下文中講解大概是怎么翻譯的),。
這是第二個(gè)組成模塊,。
第三個(gè)是一個(gè)體系,我們稱之為ShaderLab Asset,。我們寫的ShaderLab大部分是不會(huì)進(jìn)入到最終的運(yùn)行環(huán)境中去的,,需要經(jīng)過二次加工。ShaderLab里面有很多東西都是不能直接使用的,,需要進(jìn)行翻譯,。而加工之后的東西,我們叫它Asset(資產(chǎn)),,這個(gè)Asset比較常見的是這兩個(gè)地方,。
第一個(gè)地方,是我們打Assetbundle,,這是大家最常見的,,我們把shader 打成一個(gè)包,放到 bundle里,。
還有一個(gè)就是在我們打出來包的Resources下面,,會(huì)有l(wèi)evel或者是 SharedAssets 這樣的包,其實(shí)和Assetbundle的文件結(jié)構(gòu)是很類似的,,比如說場(chǎng)景里面直接引用的一些東西,,大家既愛又恨的Always include Shader也在這里面。
這是兩個(gè)Asset比較常見的地方,。
但是有一個(gè)地方,,大家不經(jīng)常會(huì)用到。當(dāng)我們?cè)?library 文件夾里打開一個(gè)工程,,就會(huì)看到一個(gè)叫 ShaderCache 的文件,。我們?cè)谧鲱A(yù)處理之后的中間產(chǎn)物,都會(huì)放到ShaderCache里面,。
我們都知道Unity特別喜歡使用GUID做索引的,,這個(gè)看起來和我們的library里面的data的東西很相似,也是一堆以數(shù)字或字母開頭的文件夾,然而我們現(xiàn)在看到的這個(gè)也是ID但不是GUID,。如果大家曾經(jīng)拆過Unity AssetBundle,,你會(huì)看到你寫的代碼。
你會(huì)經(jīng)??吹秸麄€(gè)從CGPROGRAM到ENDCG,,在你的Asset里看不到了,變成了一個(gè)名字叫 GPU program IDXXX的文件,,用一個(gè)ID索引了這一整段,。在里面我們可以很簡(jiǎn)單地認(rèn)為里面存的就是大家寫的CG program中間的東西,當(dāng)然不是直接存進(jìn)去的,,是經(jīng)過了一系列的加工,。這是我們存儲(chǔ)幾個(gè)shaderAsset的地方。
最后還有一個(gè)大家會(huì)經(jīng)常遇到但是往往被忽略掉的事情——ShaderLab是有Runtime的,。既然是一種有語法結(jié)構(gòu)的語言,,會(huì)包含很多的信息,Runtime能不能把這些信息利用起來就變得很重要,。不然的話寫了半天,,打上Asset也沒人用,就白白浪費(fèi)掉了,。
這個(gè)東西經(jīng)常在哪兒見到呢,?答案就是在Memory里面,如果你去使用一個(gè)Sample,,你會(huì)在ShaderLab里面看到它,。
這也是大家問題最集中的地方,為什么我ShaderLab這么大,,到底是哪個(gè)Shader大,?其實(shí)不是Shader大,而最有可能的是ShaderLab 整體很大,。
所以整個(gè)Unity的ShaderLab大致分為四塊,分別是由ShaderLab Text,、shaderLab Compiler,、shaderLab Asset以及shaderLab Runtime四個(gè)部分組合而成的。
我們了解了 ShaderLab 之后,,再簡(jiǎn)單地看一下 ShaderLab 的工作流,。
首先,當(dāng)我們?nèi)プ?Shader 的時(shí)候,,第一步就是去寫ShaderLab的Text,,寫完了之后干什么呢?寫完之后你會(huì)發(fā)現(xiàn),當(dāng)你回到Unity的時(shí)候,,Unity會(huì)開始有一個(gè)編譯的過程,,如果你第一次導(dǎo)入了很多的Shader,這個(gè)時(shí)候ShaderCompiler就開始工作了,。
Unity的Shader 不是一次性編譯到一個(gè)平臺(tái)上的,。
那么這個(gè)名為ShaderCache的東西,它是在什么時(shí)候產(chǎn)生的呢,?其實(shí)在 shader被import進(jìn)Unity系統(tǒng)的時(shí)候,,Unity 會(huì)把原始的shader文本發(fā)給shaderCompiler去做一次預(yù)處理,預(yù)處理的結(jié)果并不是針對(duì)某一個(gè)平臺(tái)最終的文本結(jié)構(gòu),,這個(gè)時(shí)候編譯出來的東西叫shader compilation info,,是一個(gè)中間狀態(tài)的一個(gè)信息集,這個(gè)信息集里面包含了很多重要的東西,。以下是其中比較重要的幾點(diǎn):
第一,,你的變體。我們知道 Unity 引入了 multi compile 和 shader feature 之后,,通過一次編碼就可以產(chǎn)生大量不同的 Shader,。第一次我們?nèi)ヌ幚沓鲎凅w的概念,是在我們做 Preprocess 的時(shí)候出現(xiàn)的,。經(jīng)過 Preprocess 第一次的處理,,在 shader compilation info 里面,就已經(jīng)把各個(gè)變體分開了,。
當(dāng)我們?nèi)グ?shader compilation info 編譯出來之后,,會(huì)把相關(guān)的信息序列化,并且寫到我們 ShaderCache 里面,,這就是大家所看到的 ShaderCache,。這個(gè)ShaderCache的信息會(huì)被用于我們后面的一些加速編譯,不需要每次進(jìn)入Unity都重新走一遍過程,。當(dāng)你的shader比較大,,變體比較多的時(shí)候,Preprocess的過程是相對(duì)比較慢的,。Preprocess的過程,,如果我們更細(xì)化地說,實(shí)際上做了以下幾件事情,。
第一個(gè)是做語法分析,,比如說我們解析語法數(shù),就會(huì)生成詞法解析器和語法解析器,。我們先去做了一次語法解析和詞法解析,,當(dāng)然在這個(gè)過程中Unity就會(huì)去檢查大家的shader寫得有沒有問題,如果有報(bào)錯(cuò),這個(gè)階段就完成了,。
當(dāng)我們解析完了之后,,Unity會(huì)把每一種不同的語言,從shader的文本中對(duì)應(yīng)的部分切割出來,。切割出來之后,,再用對(duì)應(yīng)的語言的Preprocess compile去做一遍對(duì)應(yīng)這個(gè)語言的解析檢查。通過這幾次檢查之后,,最終我們會(huì)得到完整的compilation info,,再把它寫到ShaderCache 里面。
如果大家在去做一個(gè)Shader的時(shí)候,,發(fā)現(xiàn)寫完的這個(gè)Shader好像不太對(duì),,或者有點(diǎn)問題,你感覺沒有進(jìn)行重新編譯,,最簡(jiǎn)單的方法就是把ShaderCache刪掉,,然后再強(qiáng)行導(dǎo)入一次,重新編譯一次,,這時(shí)候問題就迎刃而解了,。
我們把它編出來,放到ShaderCache 里之后,,這個(gè)時(shí)候只是Unity editor拿到了Compile 這個(gè)東西,,但并不能用于渲染,也不能打到最終的包里,。它只是Unity所使用的中間狀態(tài),,如果是編譯的話,大概就類似于IR的東西,。
我們?nèi)绾伟阉罱K編譯成可運(yùn)行的版本呢,?
我們可以從shader Compilation info,或者是ShaderCache里面找到相應(yīng)的文件,。這取決于你有沒有,,如果有的話,就能從shadercache里找到,;如果沒有的話,,就會(huì)走一遍Prepocess的過程,再重新產(chǎn)生shader Compilation info,。
拿到之后,我們會(huì)把這個(gè)東西再送到shader Compiler里面,,再做一些其他的事情,。這個(gè) shader Compiler里面包含了很多不同的服務(wù),剛才是Preprocess,這次我們要做的就是Binary Compile,。這個(gè)事情會(huì)在以下幾種情況下發(fā)生,。
第一,我們現(xiàn)在啟動(dòng)了Unity,。我們把資源都導(dǎo)入了,,點(diǎn)擊play。點(diǎn)的時(shí)候,,Unity 會(huì)做一件叫Unity Editor warmup all shader的事情(當(dāng)然在第一次導(dǎo)入的時(shí)候,,Unity 也會(huì)做)。這就是為什么2020年之前的版本,,大家在點(diǎn)開始的時(shí)候,,會(huì)經(jīng)常感覺到卡半天。實(shí)際上,,“卡”的過程會(huì)把你內(nèi)存里面,,或者是資源里面所有shader的變體都warmup。但是真機(jī)上不會(huì)卡,。
大家在去做一些性能檢查,,包括去研究原理的時(shí)候你會(huì)驚訝地發(fā)現(xiàn),Unity 實(shí)際上是兩個(gè)版本,,運(yùn)行時(shí)和編輯期是兩套完全不同的東西,。所以我們?cè)谧鲂阅芊治觯蛘呤莾?nèi)存,、CPU,、GPU 分析的時(shí)候,不要在編輯器里面做,。編輯器的設(shè)計(jì)目的是為了幫助大家以最流暢的速度去編輯,,所以有很多的東西,不會(huì)去考慮運(yùn)行時(shí)資源環(huán)境的占用,,比如CPU或是內(nèi)存的占用,。Unity會(huì)默認(rèn)認(rèn)為你的電腦非常棒,內(nèi)存不會(huì)爆,,CPU不會(huì)卡,,所以它可以盡情地?fù)]霍這些資源,盡量保證大家整體的編輯體驗(yàn)是好的,。但是在運(yùn)行時(shí),,Unity會(huì)考慮實(shí)際的運(yùn)行環(huán)境。比如手機(jī)和PC上的策略會(huì)有一些差異,。
在這個(gè)地方我們進(jìn)行 Binary Compile,。
第二種情況,,是真正開始打包了,比如說我們要給安卓打一個(gè)AssetBundle,,或者發(fā)一個(gè)安卓的APK,,這時(shí)候也會(huì)觸發(fā)這個(gè)過程??傊|發(fā)這個(gè)過程的必要前提是我的目標(biāo)平臺(tái)是明確的,,我知道要把中間的東西最終要翻譯成什么。BinaryCompile 的過程其實(shí)是一個(gè)非常神奇的過程,,Unity 實(shí)際上也不是直接把大家寫的,,比如說CG就直接翻譯到目標(biāo)平臺(tái)上,這個(gè)工作量其實(shí)是很大的,。
關(guān)于Unity的目標(biāo)平臺(tái)就非常多了,,比如說大家常見的手機(jī)平臺(tái)上有很多的API,加上主機(jī)平臺(tái),,他們都有自己整套的語言規(guī)范,。
大家可以腦補(bǔ)一下如果我們要是強(qiáng)行翻會(huì)怎樣。這是一個(gè)乘的關(guān)系,,左邊4個(gè),,假如說右邊是10個(gè)不同的平臺(tái),那就是40個(gè),,要寫40套不同的代碼,,代碼的路徑就非常的繚亂。其一代碼維護(hù)難度很大,,其二是也很難寫,。
Unity使用了第三方技術(shù),名為HLSLCC,,CC的意思是交叉編譯器,,大家可以搜到。這個(gè)最終幫助Unity做出了一些優(yōu)化和改變,,和Unity使用的版本不是完全一樣的(大家不要把網(wǎng)上的內(nèi)容改一下直接替進(jìn)來,,這樣行不通)。
Unity 實(shí)際上做了這樣的工作:Unity會(huì)先把前端的一些語言,,盡量地翻譯到DX那個(gè)級(jí)別上去,,通過DX的編輯器進(jìn)行編輯,編輯完了之后,,后端再走到HLSLCC,,再向目標(biāo)平臺(tái)去輸出,相當(dāng)于是一個(gè)兩步編譯的過程,。所以整體的難度降低了很多,,大部分的工作是由HLSLCC來做的,。
這個(gè)編譯過程也會(huì)導(dǎo)致一個(gè)問題,比如DX里面沒有,,翻譯不過去,中間要經(jīng)過一步,,其實(shí)就相當(dāng)于過路費(fèi)要交,,但是過路的時(shí)候沒有這個(gè)東西。因此Unity在2020以后的版本,,最早的時(shí)候是用的DXBC,,而現(xiàn)在用的則是DXRL,Unity也是基礎(chǔ)于DX的編譯器進(jìn)行了自己的擴(kuò)展,,以便盡可能地去支持一些新特性,。