https://mp.weixin.qq.com/s/rtVJDt0m4aGbigWadEZgXA
對游戲開發者而言,着色器長久以來就是游戲開發中的重要部分,在Unity中編寫並實現着色器的過程直觀且高效,優秀的着色器還可以創造非常精美的游戲畫面,同時保證極高的性能。今天將由Unity的技術工程師張陳淵來分享如何對Unity Shader着色器進行優化。
我們在Unity中創建Shader着色器的時候,會有四個選項:
-
Unlit Shader(無光照着色器):它是最基本的Vertex Shader(頂點着色器)和Fragment Shader(片段着色器)。
-
Image Effect Shader(圖像特效着色器):適用於屏幕效果的Vertex Shader(頂點着色器)和Fragment Shader(片段着色器)。
-
Surface Shader(表面着色器):它更抽象一些,隱藏了Vertex和Fragment函數,暴露Surface函數作為替代,內部實現了一完整的光照模型。
-
Computer Shader(計算着色器):利用GPU的並行特性,為我們提供單純的數據計算,可以與渲染管線無關。
除了Computer Shader外,另外三種並沒有本質的區別。而在書寫Unity Shader時,有一些寫法會影響到執行效率。
[Gamma]與[Linear]指定,當顏色空間配制為Gamma時,對屬性變量指定為[Gamma]和[Linear]都是無效的,只有當顏色空間配制為Linear時,指定才有效。所以當我們我們需要將一個Vector做為顏色使用,在Linear顏色空間下,指定其為[Linear]是比[Gamma]要高效的,因為如果指定為[Gamma],Unity會在CPU端做一次轉換計算。
[NoScaleOffset]可以為我們省掉一個變量的分配,如果不需要用到紋理Tiling和Offset,請使用[NoScaleOffset]。
屬性里面還有一個PerRenderData,如果有一個物體,我們進行渲染一百次,是同一個材質,但是我們有時候又需要這一百個物體顯示為不同的顏色,那么怎么做?一種方法是頂點色這樣做,但是這種方法不是特別好,我們還有一種方法可能是直接在材質屬性里面加一個顏色,然后每個材質設不同的顏色。
如果要為每個材質設不同的顏色,Unity會幫助你創建一百個這樣的材質,相當於你渲染一百個物體,每個物體顏色不一樣,所以直接設置的時候,會幫助你創建一百個材質,這對於GPU來講會影響,但是更重要是內存方面,會有一百個材質的內存開銷,這是一個最大的瓶頸。所以我們引入了PerRenderData的這么一個屬性的控制,它可以幫你把這個數據。例如:Color顏色,我們不需要將其畫到材質里面,而是通過Render可以設置。
現在介紹一下Tags,Tags指定渲染順序,告訴引擎如何以及何時將其渲染。Unity提供給我們一些默認的渲染隊列,每一個對應一個唯一的值,來指導Unity繪制對象到屏幕上。這些內置的渲染隊列被稱為Background, Geometry, AlphaTest,這些隊列不是隨便創建的,它們是為了讓我們更容易地編寫Shader並處理實時渲染的。
下面是隊列的描述:
-
Background:隊列通常被最先渲染。
-
Geometry:默認的渲染隊列。它被用於絕大多數對象。不透明幾何體使用該隊列。
-
AlphaTest:通道檢查的幾何體使用該隊列。它和Geometry隊列不同,對於在所有立體物體繪制后渲染的通道檢查的對象,它更有效。
我們優化的時候對於不透明的物體不需要從后往前畫,例如:很遠的山或者背景,作為Background,前面作為Geometry,其實前景應該先畫,然后再畫后景。
有時候Unity處理的比較粗糙,更多時候需要大家自己進行控制。半透明必須從后往前畫,這里沒有太多辦法進行優化。DisableBatching建議大家不要開啟,默認的情況是不開啟的。
ForceNoShadowCasting,例如:當我們畫不透明的物體,需要替換半透明的物體,但是半透明的物體沒有陰影,你可以更改Shader代碼讓它不投射陰影,直接加上ForceNoShadowCasting就可以了。
GrabPass,我們需要抓緩沖圖有二種方式,如下圖所示。
這二種方式的區別是很明顯的,第一種方式是比較低效的,因為GrabPass調用的時候必定會進行抓取的操作,所以沒次都是不同的。但是下面比較高效,一幀里面最多只執行一次,就是第一次使用會執行,后面不會執行。這個根據大家的實際的應用進行選擇。
Render State,以前直接寫圖形程序的時候可能會比較關注怎么設置渲染狀態,當它有變化的時候我們需要設置,沒有變化時候該怎么處理?
Unity引擎是這樣處理的,它把渲染狀態緩存,緩存的作用就是為了不要頻繁的切換,因為會判斷當前這個狀態和上一次的狀態是不是相同?如果是相同,不需要調用圖形API的接口,因為接口調用也有一定的消耗。那么這里可以引出一個優化點,如果渲染狀態頻繁的切換,那么起不到優化的作用。
所以我們寫Shader的時候,盡量不要讓這些連續的,不要有太多的渲染狀態,盡量保證是少的,這樣優化作用是大的。例如:AAA連續和BBB連續畫,這樣的效率高於AB間插的渲染。
Alpha Testing,大家知道在移動端,這是限於硬件的機能限制,盡量不要開啟,在移動端開啟,它的性能會比較低。
Color Mask,我們在移動端也盡量不要開啟,這是固定的,大家一定要記住,因現在受於移動端的限制,PC端沒有這個限制。
Surface Shader優化是比較難的,這里統計了一下我們能夠做什么?我們基本可以做這些事情。默認情況下Surface Shader其實是開啟了所有的計算,我們需要關閉或者模擬一些計算來達到一個優化的目的。
-
首先是Approxview,它從View Direction Normalized移到Vertex Shader進行計算。
-
其次是Halfasview,是使用光照方向和視角方向的中間向量替代一個視角方向,如果大家對比效果,覺得二種比較下來差不多,就可以選擇優化過的效果。
-
Noforwarddadd,如果只有一盞像素光,你可以開啟這個選項。
-
最后是環境光,我們可以關掉一個環境光,因為關了之后有一些優化的計算,這里優化強度比較大,但是光照損失也是比較大,如果覺得效果可以接受,就可以關掉它。
變體優化是一個比較重要的點,因為很多人覺得Shader很占內存, 上百兆占用都有可能。小的話20多兆,達到上百兆肯定是不行的,那么我們需要查原因,到底什么東西導致我們的Shader變得這么大。
Unity提供另外一個關鍵字Shader Feature,它只會把使用的編譯到包里面,而不會把沒有用的編譯里面。Multi_compile則會產生所有的變體。收集變體時需要注意,使用Shader Feature的方式,我們需要創建材質去進行變體的收集,材質需要序列化到場景中。
最后講講Shader的代碼優化,我個人覺得它可優化的空間不大,空間大的地方在哪兒?我們思考優化這些最后的效果,思考這些數據是不是真的應該去進入了渲染管線?這是重點。進入渲染管線的數據優化之后,我們再做Shader優化就是錦上添花的事情。
首先我們CPU的軟件裁減是不是高效且正確?當然我們會使用Unity的裁減功能,或者可以自己寫。當有Over Draw時,渲染順序是不是可以再優化 。例如:遠處的陰影是不是真的需要開啟?分辨率可以再小一點嗎,最后一個全屏特效做的次數,大家講的Bloom次數非常多,可不可以減少一些?或者其它的全屏特效可不可以集中一次把它做完。這些事情我們思考之后已經優化了很多效率。
最后我們可能需要針對代碼去進行優化。那么代碼優化我們需要利用一些工具做,英偉達的工具,iOS我們會使用Xcode,安卓有很多或者高通提供的工具我們都可以使用。
我們在代碼優化的幾個重點:
-
我們要保證效果的前提下,盡量把計算放在Vertex Shader 。
-
我們盡量不要寫多pass SubShader。
-
我們善用LOD,我們Mesh有LOD,紋理有Mipmap,Shader也有LOD。GPU執行二個Float的乘法和執行二個Vector4的乘法效率是一樣的。
-
少用分支, 還有一些內置函數,不建議自定義實現,我們可以使用提供的函數,那些是經過優化的。
-
最后是精度問題,Fixed,Half、Float,不同的設備上它的性能不一樣,我們盡量移動端使用一些低精度的數據。PC端這三種是沒有什么區別的。
Unity Shader着色器優化需要注意的重點內容就為大家介紹到這里,希望每個開發者都能創建出高效果的Shader。
更多Unity着色器相關內容盡在Unity官方中文論壇(UnityChina.cn)!