Fixed Function Shader
在上一小節中我們已經了解到,Fixed Function Shader是固定功能的着色器,它的功能有限,但是編寫比較簡單,因為它總是通過一系列的命令達到我們對圖形着色的目的。其中我們已經了解了Properties(屬性)、Material(材質)、Lighting(光照),接下來來了解一下最為重要的一個命令settexture。
很多時候我們對三維物體的着色並不是簡單地去設置它的光照和顏色,我們還希望給它畫上貼圖,這個功能應該是最為常見的,接着來實現這個功能。
在上一小節固定管線着色器的基礎上來進一步編寫,創建一個新的shader,命名為FixedFunctionShader2,同時創建一個材質mat_fixed_function_shader_2,並將shader關聯到材質上:
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 } 11 12 SubShader { 13 pass{ 14 // color(1,0,0,1) // 分別代表了 r,g,b,a 15 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 16 material{ 17 diffuse[_Color] // 漫反射 18 ambient[_Amnient] // 環境光 19 specular[_Specular] // 高光 20 shininess[_Shininess] // 述specular強度 21 emission[_Emission] // 自發光 22 } 23 lighting on // 光照開關 24 separatespecular on // 鏡面高光開關 25 26 settexture[_MainTex]{ 27 combine texture 28 } 29 } 30 } 31 }
要使用貼圖采樣,我們需要命令settexture,在固定管線着色器中settexture是最重要的也是最復雜的一個命令。settexture需要一個紋理屬性參數“_MainTex("MainTex",2d)=""”,其中的值就是紋理的名稱,默認可以是空字符串,接着在settexture結構體中添加命令“combine texture”。回到Unity工程中,創建一個新的球體,將新建的材質拖放在球體上,在該球體材質的檢視面板中拖放一張貼圖,效果如下:
這時就可以看到在球體上的磚塊樣式的貼圖,但是使用這個貼圖以后,球體上原有的燈光照明和高光效果已經沒有了。原因是在着色器中的combine命令指的是合並,它的參數目前只是用了texture紋理,這個texture指的就是屬性中的_MainTex設置的貼圖,而這里只有貼圖而沒有應用先前已經計算好的光照數據,因此在這里我們需要為當前貼圖texture乘上primary,primary是Fixed Function Shader的關鍵字,代表了前面所有計算材質和光照后的顏色值,將貼圖和這個值相乘,就會得到一個混合的新的顏色值。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 } 11 12 SubShader { 13 pass{ 14 // color(1,0,0,1) // 分別代表了 r,g,b,a 15 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 16 material{ 17 diffuse[_Color] // 漫反射 18 ambient[_Amnient] // 環境光 19 specular[_Specular] // 高光 20 shininess[_Shininess] // 述specular強度 21 emission[_Emission] // 自發光 22 } 23 lighting on // 光照開關 24 separatespecular on // 鏡面高光開關 25 26 settexture[_MainTex]{ 27 combine texture * primary 28 } 29 } 30 } 31 }
需要注意的是texture紋理的顏色值rgba每一個分量都是0到1,primary值的每一個分量也是0到1,當一個小於1的浮點數與另一個小於1的浮點數相乘,就會得到一個更小的浮點數,因此得到的顏色會變深,效果如下:
考慮到這個問題,Fixed Function Shader當中有一個關鍵字“double”,表示對某個結果乘以2的運算,用法為“combine texture * primary double”。修改完shader,回到工程中,可以觀察到球體變亮了。
以上就是一個最基本的settexture的運用,當然在某些情況下需要為這個物體不只混合一張圖,如果要混合兩張或者兩張以上的紋理該怎么處理呢?這里我需要注意的是,一個settexture只能帶上一個參數,不能再添加第二個參數,因此需要再編寫一個settexture和一個紋理屬性參數_SecTex。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 pass{ 15 // color(1,0,0,1) // 分別代表了 r,g,b,a 16 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 17 material{ 18 diffuse[_Color] // 漫反射 19 ambient[_Amnient] // 環境光 20 specular[_Specular] // 高光 21 shininess[_Shininess] // 述specular強度 22 emission[_Emission] // 自發光 23 } 24 lighting on // 光照開關 25 separatespecular on // 鏡面高光開關 26 27 settexture[_MainTex]{ 28 combine texture * primary double 29 } 30 31 settexture[_SecTex]{ 32 combine texture * primary double 33 } 34 } 35 } 36 }
回到Unity場景中,給_SecTex拖上一張紋理,效果如下:
可以觀察到僅僅顯示了第二張貼圖,第一張貼圖沒有和第二張貼圖進行混合,原因這里雖然進行了兩次settexture計算,但是如果想讓第一次settexture和第二次settexture的結果進行混合就不能使用“combine texture * primary”語句,因為primary總是代表前面頂點光照計算的顏色值,這里還需要加上第一次settexture的結果。在Fixed Function Shader當中除了primary外,還有一個previous關鍵字,previous指的是先前的數據,將代碼改成“combine texture * previous”,這里的意思就是用當前紋理的值去乘上當前settexture操作之前所有計算和采樣過后的結果。回到Unity,可以看到效果如下:
可以觀察到已經有了需要的結果,球體已經被混合了兩張紋理貼圖。按照這個思路可以繼續去編寫多個settexture,但是紋理混合的次數不是無限的,Fixed Function Shader是基本對照於顯卡硬件的固定渲染部分,所以顯卡有一個混合紋理的最大個數,一般來講越好的硬件可以混合的紋理越多,越差的硬件可以混合的紋理越少,基本上兩張紋理的混合是目前所有顯卡都能夠支持的。
接下來來了解一下固定管線着色器透明的處理,首先把其中一個球體遮擋在另一個球體之前,如下圖:
希望前面這個球體能夠半透明顯示,要實現物體的透明化,需要借助alpha值,alpha值介於0到1之間,0為完全透明,1位不透明。
在當前的shader程序中,最后一個settexture使用了第二張紋理與先前所有計算的顏色值相乘,如果我們去改變先前計算過程中的alpha值,那么應該可以去影響這個球體的半透明度。在當前material材質計算當中,使用了四種顏色值,分別是diffuse、ambient、specular和emission,這四種顏色的alpha值基本都是1,我們可以在檢視面板中把它們的alpha值都相對降低,按理來說這些顏色的alpha值都很低了,這四個顏色去影響貼圖的時候,即使貼圖的alpha值為1,相乘以那些顏色的alpha后,也會得到一個比較低的alpha值,然后第二個settexture又去相乘比較低的alpha值,最后應該會得到一個很低的alpha值,但是為什么不能觀察到球體的半透明化呢?這個時候就需要去了解一下Unity Shader當中一個比較重要的命令,這個命令是ShaderLab當中的Blending。如圖:
在着色器渲染過后,會進入到alpha測試,alpha測試以后就會進入到Blending階段,Blending指的是混合,它可以用SrcFactor(源元素)和DstFactor(目標元素)去進行混合運算。其中,當我們正在渲染當前這個球體時,渲染這個球體得到的顏色值就是SrcFactor,而球體以外其他的物體包括天空盒等已經在這個球體渲染之前被渲染完成的值就是DstFactor。
查看Unity Manual中的ShaderLab: Blending,在Blend factors下可以看到很多關鍵詞,其中我們可以先關注"SrcAlpha"和“OneMinusSrcAlpha”,"SrcAlpha"指的就是當前已經渲染得到的alpha值,而“OneMinusSrcAlpha”是指用1減去"SrcAlpha"的值,我們可以將命令“Blend SrcAlpha OneMinusSrcAlpha”放在shader程序當前的渲染通道當中。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 pass{ 15 Blend SrcAlpha OneMinusSrcAlpha 16 // color(1,0,0,1) // 分別代表了 r,g,b,a 17 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 18 material{ 19 diffuse[_Color] // 漫反射 20 ambient[_Amnient] // 環境光 21 specular[_Specular] // 高光 22 shininess[_Shininess] // 述specular強度 23 emission[_Emission] // 自發光 24 } 25 lighting on // 光照開關 26 separatespecular on // 鏡面高光開關 27 28 settexture[_MainTex]{ 29 combine texture * primary double 30 } 31 32 settexture[_SecTex]{ 33 combine texture * previous double 34 } 35 } 36 } 37 }
這句指令的意義是用當前渲染的alpha值與1減去當前渲染的alpha值的比例去混合之前已經被渲染好的場景顏色值。回到工程中,可以查看到球體發生了變化。如圖,
雖然看起來不明顯,但是我們可以發現球體整體變藍,不過它沒有真正達到透明,原因是在Unity引擎當中,存在一種渲染順序的問題。圖形顯卡渲染某一幀圖像的時候,當場景中有很多物體,到先渲染哪一個,后渲染哪一個,就取決於渲染順序。在當前場景中,如果先渲染后面的球體,再渲染前面的球體,那么在渲染后面球體的時候是看不到前面這個球體的,因此在第一次渲染后面的球體時是可以看到整個球體的,如下圖:
當第二次渲染前面這個球體時,該球體由於深度(z次序)被放在了靠近攝像機的地方,它會被稍后渲染,因此它就被疊加到先前生成的圖像上,並且遮住了后面的物體,在這種情況下,我們就需要稍微去改變一下前面這個球體的渲染順序,要改變渲染順序,我們就需要了解使用ShaderLab: SubShader Tags。
在SubShader當中允許添加Tags標簽,語法格式為:
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
查看“Rendering Order - Queue tag”,這是渲染隊列的標簽,它包括以下這些值,分別是Background、Geometry、AlphaTest、Transparent、和Overlay。一個實實在在的物體,它不透明的時候是默認使用Geometry進行渲染的,如果要去渲染半透明的物體,要使用在默認值后的渲染隊列,其中我們可以使用Transparent,將“Tags { "Queue" = "Transparent" }”加到SubShader中。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 Tags { "Queue" = "Transparent" } 15 pass{ 16 Blend SrcAlpha OneMinusSrcAlpha 17 // color(1,0,0,1) // 分別代表了 r,g,b,a 18 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 19 material{ 20 diffuse[_Color] // 漫反射 21 ambient[_Amnient] // 環境光 22 specular[_Specular] // 高光 23 shininess[_Shininess] // 述specular強度 24 emission[_Emission] // 自發光 25 } 26 lighting on // 光照開關 27 separatespecular on // 鏡面高光開關 28 29 settexture[_MainTex]{ 30 combine texture * primary double 31 } 32 33 settexture[_SecTex]{ 34 combine texture * previous double 35 } 36 } 37 } 38 }
回到工程中,可以觀察到球體已經變半透明了,如圖:
假設一張貼圖里已經含有alpha通道,我們希望使用這張圖本身的alpha通道去進行透明化處理,如果貼圖的一部分alpha為0,一部分alpha為1,那么就會形成一種鏤空的效果,怎么去設置貼圖的alpha通道處理呢?我們可以在最后的settexture的combine命令中加入第二個參數texture,代碼如下:
combine texture * previous double,texture
其中,逗號后面的參數只是取了alpha通道值,我們期望用當前紋理的alpha通道去做當前alpha通道的設置。
回到場景中,發現球體已經不透明了,這是因為如果在settexture的combine命令的第二個位置填寫了alpha的參數,那么它只能針對這個部分去取alpha值運算,而之前所有的顏色alpha值都失效了,要想看到透明效果,我們可以把這張貼圖的灰度值編程alpha值,如圖:
回到場景中,可以發現球體已經半透明了,如圖:
另外,還有一個命令constantColor,它可帶一個參數,在properties中聲明一個參數"_ConstantColor("ConstantColor",color)=(1,1,1,0.3)",將其放在第二個settexture中,然后在combine命令中參數texture乘上constant,用constant的alpha值0.3與紋理顏色中的alpha值再相乘,那么運算結果的alpha值會變得更小。回到工程場景中后會發現一個編譯錯誤,但是沒發現代碼中有什么問題,這里需要注意的是先前在編寫“_MainTex”和“_SecTex”紋理屬性時直接用了一個空字符串,而在官方Shader教程中,紋理的值往往是有值的,比如“_MainTex("MainTex",2d)="white"()”,如果不願意去添加這些值,可以將語句“_ConstantColor("ConstantColor",color)=(1,1,1,0.3)”添加在紋理屬性之前,如:
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _ConstantColor("ConstantColor",color)=(1,1,1,1) 10 _MainTex("MainTex",2d)="" 11 _SecTex("MainTex",2d)="" 12 } 13 14 SubShader { 15 Tags { "Queue" = "Transparent" } 16 pass{ 17 Blend SrcAlpha OneMinusSrcAlpha 18 // color(1,0,0,1) // 分別代表了 r,g,b,a 19 // color[_Color] // 小括號內容表示固定值,中括號內容表示可變參數值 20 material{ 21 diffuse[_Color] // 漫反射 22 ambient[_Amnient] // 環境光 23 specular[_Specular] // 高光 24 shininess[_Shininess] // 述specular強度 25 emission[_Emission] // 自發光 26 } 27 lighting on // 光照開關 28 separatespecular on // 鏡面高光開關 29 30 settexture[_MainTex]{ 31 combine texture * primary double 32 } 33 34 settexture[_SecTex]{ 35 constantColor[_ConstantColor] 36 combine texture * previous double,texture*constant 37 } 38 } 39 } 40 }
回到場景中,着色器已經可以正常工作了,這個時候可以托送檢視面板中的“ContantColor”值來改變球體的透明程度,但是可以發現當“ContantColor”的值為1時,球體也是半透明的,原因就是這里使用紋理的灰度值來當透明度。
以上就是ShaderLab中的Fixed Function Shader的簡單應用,比較重要的命令就是“settexture”,並且有一點復雜,而“settexture”中比較重要的命令是“combine”,“combine”命令可以配合“constantColor”使用來控制透明度,使用“blend”指令來控制最后的混合,並且使用tags標簽來控制渲染順序。