筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一緻而出現的問題
【Unity Shader學習筆記】(三) ---------------- 光照模型原理及漫反射和高光反射的實作 【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實作 前言
相信讀者對透明效果都不陌生,因為透明效果是遊戲中經常使用的一種效果。要實作透明效果,通常會在渲染模型時控制它的透明通道。而其透明度則控制是其是否會顯示,0 表示完全不顯示,1 表示完全顯示。
Unity 中通常使用兩種方法來實作透明效果:透明度測試(Alpha Test)和 透明度混合(Alpha Blending)。
為了友善讀者了解,先解釋一下深度緩沖,深度測試和深度寫入
- 透明度測試。透明度測試是一種十分 “簡單粗暴” 的機制,當有一個片元的透明度不符合條件時,就直接舍棄,不再任何處理(不會對顔色緩沖有影響);如果符合,就進行正常的處理(深度測試,深度寫入等);是以這帶來的效果也是兩極分化的,要麼完全透明,要麼完全不透明。
- 透明度混合。透明度混合可以得到真正的半透明效果。它會使用目前片元的透明度作為混合因子,與顔色緩沖中的值進行混合,得到新的顔色。需要注意的是,此方法需要關閉深度寫入,而是以帶來的問題就是要 十分十分十分 地注意渲染順序。
可能會有讀者會問:為什麼透明度混合需要關閉深度寫入呢?我們可以同過一張圖來解釋 平面 1 和 平面 2 都是在錄影機視線上,平面 1 是透明的而平面 2 是不透明的且平面 1 擋住了平面 2。理論上我們應該可以透過平面 1 來看到平面 2。事實上,如果沒有關閉深度寫入,平面 1 和 平面 2在渲染時進行深度測試,測試結果為平面 2更遠,是以平面 2不會被渲染到螢幕上,即我們看不到平面 2。這很顯然是不符合我們所要的。
- 深度緩沖。用于解決可見性問題的強大存在。決定了哪個物體的哪些部分會被渲染,哪些部分會被遮擋
- 深度測試。開啟後,當渲染一個片元時,根據它的深度值判斷該片元距離錄影機的距離,然後将它的深度值和深度緩沖中的值進行比較
- 深度寫入。開啟後,當一個片元進行了深度測試後,如果它的值距離更遠,則說明有物體擋在了它前面,那麼它就不會被渲染,如果更近,那麼這個片元就應該覆寫掉顔色緩沖中的值,并把它的深度值更新到深度緩沖中。
一. 渲染順序
1.1 渲染順序的重要性
前文說過,關閉了深度寫入後,渲染順序就變得十分重要,為什麼這麼說呢 如圖,A 和 B渲染順序不一樣。有兩種情況
- 先渲染 B 再 渲染 A。此時深度緩沖中沒有資料,B 直接寫入它的顔色緩沖和深度緩沖;然後渲染 A,A進行深度測試,結果為 A 更近,是以此時會用 A 的透明度與顔色緩沖中值進行混合,得到正确的半透明效果。
- 先渲染 A 再 渲染 B。此時深度緩沖中并沒有資料,A 會寫入顔色緩沖,但不會寫入深度緩沖(因為關閉了深度寫入);然後渲染 B ,B 進行深度測試,而此時深度緩沖中并沒有資料,是以 B 會直接寫進顔色緩沖和深度緩沖,就會覆寫掉顔色緩沖中 A 的顔色,是以最終渲染出來,從視覺上是 B 在 A 的前面
1.2 渲染隊列
Unity 中提供了 渲染隊列,并用整數索引表示渲染隊列,索引越小,越早渲染 也可以使用 SubShader 中的 Queue 标簽來決定該模型屬于哪個渲染隊列
名稱 隊列索引号 描述 Background 1000 這個隊列會在任何隊列 之前被渲染,通常用來渲染繪制在背景的物體 Geometry 2000 預設的隊列,非透明物體使用此隊列 Alpha Test 2450 進行透明度測試的物體使用的隊列 Overlay 3000 按後往前順序渲染,使用透明度混合的物體應該使用此隊列 4000 在最後渲染的物體使用此隊列 二. 透明度測試
建立一個工程,去掉天空盒;建立一個Material 和 shader ,命名為 Alpha Test;建立一個 cube
需要提前了解的是:
- ZWrite Off 用于關閉深度寫入,可以寫在 Pass裡面,也可以寫在 SubShader 裡,如果是後者,那麼就會對所有的 Pass 産生效果,即所有的 Pass 都會關閉深度寫入。
- 我們在後面的代碼将會使用 clip 函數進行透明度測試。參數為裁剪時使用的标量或矢量,如果參數的任一分量為負數,就舍棄目前像素的輸出顔色。我們同樣可以在MSDN上找到它的定義
I. 建立一個場景,去掉天空盒;建立一個 Material 和 shader ,命名為 Alpha Test;建立一個 Cube;準備一張不同區域透明度不同的透明紋理(讀者可以在本文最下方下載下傳)。
II. 定義 Propreties 語義塊
Properites 語義塊并沒有什麼特别的屬性,_Cutoff 屬性用來控制透明度,範圍為【0,1】,因為紋理像素的透明度範圍就在此範圍。
III. 指定渲染隊列
在 SubShader 中定義一個 Tags,IgnoreProjector 決定 shader 是否會受投影器的影響,RenderType 可以讓 shader 歸入提前定義的組(這裡是 TransparentCutout)。
IV. 定義與 Properties 中相比對的變量
V. 定義輸入輸出結構體 VI. 定義頂點着色器TRANSFORM_TEX 函數我們在之前已經解釋過了,如果讀者對此不太了解,可以翻看我的上一篇文章
VII. 定義片元着色器
這些代碼相信讀者都不陌生,這裡 clip 函數對不符合條件的片元舍棄了,即不渲染了。
VIII. 最後設定 FallBack
完整代碼:1 Shader "Unity/01-AlphaTest" { 2 Properties { 3 _Color ("Main Tint", Color) = (1,1,1,1) 4 _MainTex ("Main Tex", 2D) = "white" {} 5 _Cutoff("Alpha Cutoff",Range(0,1)) = 0.5 6 } 7 SubShader { 8 Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"} 9 Cull Off 10 Pass 11 { 12 Tags{"LightMode" = "ForwardBase"} 13 14 CGPROGRAM 15 #pragma vertex vert 16 #pragma fragment frag 17 #include "Lighting.cginc" 18 #include "UnityCG.cginc" 19 20 fixed4 _Color; 21 sampler2D _MainTex; 22 float4 _MainTex_ST; 23 fixed _Cutoff; 24 25 struct a2v 26 { 27 float4 vertex : POSITION; 28 float3 normal : NORMAL; 29 float4 texcoord : TEXCOORD0; 30 }; 31 32 struct v2f 33 { 34 float4 pos : SV_POSITION; 35 float3 worldNormal : TEXCOORD0; 36 float3 worldPos : TEXCOORD1; 37 float2 uv : TEXCOORD2; 38 }; 39 40 v2f vert(a2v v) 41 { 42 v2f o; 43 o.pos = UnityObjectToClipPos(v.vertex); 44 o.worldNormal = UnityObjectToWorldNormal(v.normal); 45 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; 46 47 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 48 49 return o; 50 51 } 52 53 fixed4 frag(v2f i) : SV_TARGET0 54 { 55 fixed3 worldNormal = normalize(i.worldNormal); 56 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); 57 58 fixed4 texcolor = tex2D(_MainTex,i.uv); 59 60 clip(texcolor.a - _Cutoff); 61 62 fixed3 albedo = texcolor.rgb * _Color.rgb; 63 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 64 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir)); 65 66 return fixed4(ambient + diffues,1.0); 67 } 68 ENDCG 69 } 70 } 71 FallBack "Transparent/Cutout/VertexLit" 72 73 }
IX. 儲存,回到Unity,檢視效果
不同 Cutoff 的效果:
我們可以看到通過透明度測試實作的效果在邊界處并不理想,有鋸齒,而為了解決這個問題,我們就應該使用透明度混合,來得到更柔和的效果。三. 透明度混合
3.1 透明度混合的實作
回想一下,我們前面所說的透明度混合的原理:把自身的顔色和顔色緩沖中的顔色進行混合,得到新的顔色。既然要混合,那就需要混合指令 Blend。混合語義有許多,我們稍後會具體地介紹,在這裡,我們使用 Blend SrcFactor DstFactor 這條語義,其中 Blend 是操作,SrcFactor,DstFactor 是因子;我們把 SrcFactor 設為 SrcAlpha,DstFactor 設為 OneMinusSrcAlpha。即我們即将使用的混合語義代碼為 Blend SrcAlpha OneMinusSrcAlpha,這相當于,混合後顔色為:不明白這條公式的讀者不用着急,我們稍後會具體解釋,這裡先知道我們即将使用這條式子便可。
代碼和透明度測試類似,是以這裡隻列出需要注意的修改的地方。
I. 建立一個 Material 和 shader ,命名為 Alpha Blend;建立一個 Cube;使用同一張透明紋理。
II.修改 Properties 語義塊
其中 _AlphaScale 用來控制整體的透明度。當然也要在CG代碼片中定義與其對應的變量。
III. 修改 Tags
IV. 關閉深度寫入和開啟混合 V.修改片元着色器我們用透明紋理的透明通道和 _AlphaScale 來控制整體透明度
VI. 修改 FallBack
1 Shader "Unity/02-AlphaBlend" { 2 Properties { 3 _Color ("Main Tint", Color) = (1,1,1,1) 4 _MainTex ("Main Tex", 2D) = "white" {} 5 _AlphaScale("Alpha Scale",Range(0,1)) = 1 6 } 7 SubShader { 8 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 9 10 Pass 11 { 12 Tags{"LightMode" = "ForwardBase"} 13 ZWrite Off 14 Blend SrcAlpha OneMinusSrcAlpha 15 16 17 CGPROGRAM 18 #pragma vertex vert 19 #pragma fragment frag 20 #include "Lighting.cginc" 21 #include "UnityCG.cginc" 22 23 fixed4 _Color; 24 sampler2D _MainTex; 25 float4 _MainTex_ST; 26 fixed _AlphaScale; 27 28 struct a2v 29 { 30 float4 vertex : POSITION; 31 float3 normal : NORMAL; 32 float4 texcoord : TEXCOORD0; 33 }; 34 35 struct v2f 36 { 37 float4 pos : SV_POSITION; 38 float3 worldNormal : TEXCOORD0; 39 float3 worldPos : TEXCOORD1; 40 float2 uv : TEXCOORD2; 41 }; 42 43 v2f vert(a2v v) 44 { 45 v2f o; 46 o.pos = UnityObjectToClipPos(v.vertex); 47 o.worldNormal = UnityObjectToWorldNormal(v.normal); 48 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; 49 50 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 51 52 return o; 53 54 } 55 56 fixed4 frag(v2f i) : SV_TARGET0 57 { 58 fixed3 worldNormal = normalize(i.worldNormal); 59 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); 60 61 fixed4 texcolor = tex2D(_MainTex,i.uv); 62 63 fixed3 albedo = texcolor.rgb * _Color.rgb; 64 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 65 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir)); 66 67 return fixed4(ambient + diffues,texcolor.a * _AlphaScale); 68 } 69 ENDCG 70 } 71 } 72 FallBack "Transparent/VertexLit" 73 74 }
VII. 儲存,回到Unity,檢視效果
不同 _AlphaScale 的效果:
對比透明度測試,我們可以看到透明度混合更加柔和平滑。3.2 混合指令
混合有兩個操作數:源顔色(source color)和 目标顔色(destination color)
Blend SrcFactor DstFactor 開啟混合,設定因子。源顔色 x ScrFacor + 目标顔色 x DstFactor,結構存入顔色緩沖 Blend SrcFactor DstFactor,SrcFactorA DstFactorA 和上面類似,隻是混合透明通道的因子不同
- 源顔色。指片元着色器産生的顔色值,用 S 表示。
- 目标顔色。指顔色緩沖中的值,用 D 表示。
- 兩者混合後,得到的新顔色用 O 表示。
而上面三者都包含了 RGBA 通道。
除了 Blend Off 以外,使用Blend 指令,Unity 會為我們開啟混合,因為隻有開啟了混合,混合指令才起效。
混合指令由 操作 和 因子 組成,操作預設是使用 加操作,而為了混合RGB 通道 和 A通道,是以我們需要 4 個因子
以混合指令 Blend SrcFactor DstFactor 為例,預設為加操作,SrcFactor 為源顔色, DstFactor 為目标顔色,然後計算
下面是 ShaderLab 支援的一些混合因子:
讀者可以自行選擇因子來試試效果
四. 雙面渲染
一般來說,如果一個物體是透明的,要麼我們應該可以看到它的内部和它的任一個面,但前面我們實作的透明中并沒有實作這個效果,因為 Unity 在預設引擎下剔除了物體背面(是相對于錄影機方向的背面,而不是世界坐标中前後左右的背面),不渲染。而剔除的指令為
Cull Back | Front | Off
為了實作雙面渲染,我們可以這樣實作:設定兩個 Pass ,一個隻渲染前面,一個隻渲染背面。不過需要注意的是,由于開啟了深度測試,是以要注意渲染順序,要先渲染背面,再渲染正面,這樣就能確定背面會被渲染出來。
兩個 Pass 中,除了 Cull 指令不一樣外,其餘代碼都是和透明度混合中的代碼一樣,是以,這裡直接給出完整代碼現在來檢視下效果: 現在我們可以清楚地看到物體的内部了。1 // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' 2 3 Shader "Unity/04-AlphaBlendBothSide" { 4 Properties { 5 _Color ("Main Tint", Color) = (1,1,1,1) 6 _MainTex ("Main Tex", 2D) = "white" {} 7 _AlphaScale("Alpha Scale",Range(0,1)) = 1 8 } 9 SubShader { 10 Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 11 12 Pass 13 { 14 Tags{"LightMode" = "ForwardBase"} 15 16 Cull Front 17 18 ZWrite Off 19 Blend SrcAlpha OneMinusSrcAlpha 20 21 22 CGPROGRAM 23 #pragma vertex vert 24 #pragma fragment frag 25 #include "Lighting.cginc" 26 #include "UnityCG.cginc" 27 28 fixed4 _Color; 29 sampler2D _MainTex; 30 float4 _MainTex_ST; 31 fixed _AlphaScale; 32 33 struct a2v 34 { 35 float4 vertex : POSITION; 36 float3 normal : NORMAL; 37 float4 texcoord : TEXCOORD0; 38 }; 39 40 struct v2f 41 { 42 float4 pos : SV_POSITION; 43 float3 worldNormal : TEXCOORD0; 44 float3 worldPos : TEXCOORD1; 45 float2 uv : TEXCOORD2; 46 }; 47 48 v2f vert(a2v v) 49 { 50 v2f o; 51 o.pos = UnityObjectToClipPos(v.vertex); 52 o.worldNormal = UnityObjectToWorldNormal(v.normal); 53 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; 54 55 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 56 57 return o; 58 59 } 60 61 fixed4 frag(v2f i) : SV_TARGET0 62 { 63 fixed3 worldNormal = normalize(i.worldNormal); 64 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); 65 66 fixed4 texcolor = tex2D(_MainTex,i.uv); 67 68 fixed3 albedo = texcolor.rgb * _Color.rgb; 69 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 70 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir)); 71 72 return fixed4(ambient + diffues,texcolor.a * _AlphaScale); 73 } 74 75 76 77 78 79 ENDCG 80 } 81 82 83 Pass 84 { 85 Tags{"LightMode" = "ForwardBase"} 86 87 Cull Back 88 89 ZWrite Off 90 Blend SrcAlpha OneMinusSrcAlpha 91 92 93 CGPROGRAM 94 #pragma vertex vert 95 #pragma fragment frag 96 #include "Lighting.cginc" 97 #include "UnityCG.cginc" 98 99 fixed4 _Color; 100 sampler2D _MainTex; 101 float4 _MainTex_ST; 102 fixed _AlphaScale; 103 104 struct a2v 105 { 106 float4 vertex : POSITION; 107 float3 normal : NORMAL; 108 float4 texcoord : TEXCOORD0; 109 }; 110 111 struct v2f 112 { 113 float4 pos : SV_POSITION; 114 float3 worldNormal : TEXCOORD0; 115 float3 worldPos : TEXCOORD1; 116 float2 uv : TEXCOORD2; 117 }; 118 119 v2f vert(a2v v) 120 { 121 v2f o; 122 o.pos = UnityObjectToClipPos(v.vertex); 123 o.worldNormal = UnityObjectToWorldNormal(v.normal); 124 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; 125 126 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 127 128 return o; 129 130 } 131 132 fixed4 frag(v2f i) : SV_TARGET0 133 { 134 fixed3 worldNormal = normalize(i.worldNormal); 135 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); 136 137 fixed4 texcolor = tex2D(_MainTex,i.uv); 138 139 fixed3 albedo = texcolor.rgb * _Color.rgb; 140 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 141 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir)); 142 143 return fixed4(ambient + diffues,texcolor.a * _AlphaScale); 144 } 145 146 ENDCG 147 148 } 149 } 150 FallBack "Transparent/VertexLit" 151 152 }
五. 總結
透明效果是十分常見且有用的一種實作,我們可以利用它來實作很多有趣的效果。要實作透明,更多地是對渲染的一種了解。本文隻是對Unity中渲染的一些基礎解釋,希望能對讀者有所幫助。 本文所用透明紋理及shader