天天看點

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一緻而出現的問題

前言

相信讀者對透明效果都不陌生,因為透明效果是遊戲中經常使用的一種效果。要實作透明效果,通常會在渲染模型時控制它的透明通道。而其透明度則控制是其是否會顯示,0 表示完全不顯示,1 表示完全顯示。

Unity 中通常使用兩種方法來實作透明效果:透明度測試(Alpha Test)和 透明度混合(Alpha Blending)。

  • 透明度測試。透明度測試是一種十分 “簡單粗暴” 的機制,當有一個片元的透明度不符合條件時,就直接舍棄,不再任何處理(不會對顔色緩沖有影響);如果符合,就進行正常的處理(深度測試,深度寫入等);是以這帶來的效果也是兩極分化的,要麼完全透明,要麼完全不透明。
  • 透明度混合。透明度混合可以得到真正的半透明效果。它會使用目前片元的透明度作為混合因子,與顔色緩沖中的值進行混合,得到新的顔色。需要注意的是,此方法需要關閉深度寫入,而是以帶來的問題就是要 十分十分十分 地注意渲染順序。
為了友善讀者了解,先解釋一下深度緩沖,深度測試和深度寫入
  • 深度緩沖。用于解決可見性問題的強大存在。決定了哪個物體的哪些部分會被渲染,哪些部分會被遮擋
  • 深度測試。開啟後,當渲染一個片元時,根據它的深度值判斷該片元距離錄影機的距離,然後将它的深度值和深度緩沖中的值進行比較
  • 深度寫入。開啟後,當一個片元進行了深度測試後,如果它的值距離更遠,則說明有物體擋在了它前面,那麼它就不會被渲染,如果更近,那麼這個片元就應該覆寫掉顔色緩沖中的值,并把它的深度值更新到深度緩沖中。
可能會有讀者會問:為什麼透明度混合需要關閉深度寫入呢?我們可以同過一張圖來解釋
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
平面 1 和 平面 2 都是在錄影機視線上,平面 1 是透明的而平面 2 是不透明的且平面 1 擋住了平面 2。理論上我們應該可以透過平面 1 來看到平面 2。事實上,如果沒有關閉深度寫入,平面 1 和 平面 2在渲染時進行深度測試,測試結果為平面 2更遠,是以平面 2不會被渲染到螢幕上,即我們看不到平面 2。這很顯然是不符合我們所要的。

一. 渲染順序

1.1 渲染順序的重要性

前文說過,關閉了深度寫入後,渲染順序就變得十分重要,為什麼這麼說呢
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
如圖,A 和 B渲染順序不一樣。有兩種情況
    • 先渲染 B 再 渲染 A。此時深度緩沖中沒有資料,B 直接寫入它的顔色緩沖和深度緩沖;然後渲染 A,A進行深度測試,結果為 A 更近,是以此時會用 A 的透明度與顔色緩沖中值進行混合,得到正确的半透明效果。
    • 先渲染 A 再 渲染 B。此時深度緩沖中并沒有資料,A 會寫入顔色緩沖,但不會寫入深度緩沖(因為關閉了深度寫入);然後渲染 B ,B 進行深度測試,而此時深度緩沖中并沒有資料,是以  B 會直接寫進顔色緩沖和深度緩沖,就會覆寫掉顔色緩沖中 A 的顔色,是以最終渲染出來,從視覺上是 B 在 A 的前面

1.2 渲染隊列

Unity 中提供了 渲染隊列,并用整數索引表示渲染隊列,索引越小,越早渲染
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
 也可以使用 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上找到它的定義
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

I. 建立一個場景,去掉天空盒;建立一個 Material 和 shader ,命名為 Alpha Test;建立一個 Cube;準備一張不同區域透明度不同的透明紋理(讀者可以在本文最下方下載下傳)。

II. 定義 Propreties 語義塊

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

 Properites 語義塊并沒有什麼特别的屬性,_Cutoff 屬性用來控制透明度,範圍為【0,1】,因為紋理像素的透明度範圍就在此範圍。

III.  指定渲染隊列

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

在 SubShader 中定義一個 Tags,IgnoreProjector 決定 shader 是否會受投影器的影響,RenderType 可以讓 shader 歸入提前定義的組(這裡是 TransparentCutout)。

IV. 定義與 Properties 中相比對的變量

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
V. 定義輸入輸出結構體
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
VI. 定義頂點着色器
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

 TRANSFORM_TEX 函數我們在之前已經解釋過了,如果讀者對此不太了解,可以翻看我的上一篇文章

VII. 定義片元着色器

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

 這些代碼相信讀者都不陌生,這裡 clip 函數對不符合條件的片元舍棄了,即不渲染了。

VIII. 最後設定 FallBack

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
完整代碼:
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 的效果:

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
我們可以看到通過透明度測試實作的效果在邊界處并不理想,有鋸齒,而為了解決這個問題,我們就應該使用透明度混合,來得到更柔和的效果。

三. 透明度混合

3.1 透明度混合的實作

回想一下,我們前面所說的透明度混合的原理:把自身的顔色和顔色緩沖中的顔色進行混合,得到新的顔色。既然要混合,那就需要混合指令 Blend。混合語義有許多,我們稍後會具體地介紹,在這裡,我們使用 Blend SrcFactor DstFactor 這條語義,其中 Blend 是操作,SrcFactor,DstFactor 是因子;我們把 SrcFactor 設為 SrcAlpha,DstFactor 設為 OneMinusSrcAlpha。即我們即将使用的混合語義代碼為 Blend SrcAlpha OneMinusSrcAlpha,這相當于,混合後顔色為:
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

不明白這條公式的讀者不用着急,我們稍後會具體解釋,這裡先知道我們即将使用這條式子便可。

 代碼和透明度測試類似,是以這裡隻列出需要注意的修改的地方。

I. 建立一個 Material 和 shader ,命名為 Alpha Blend;建立一個 Cube;使用同一張透明紋理。

II.修改 Properties 語義塊

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

其中 _AlphaScale 用來控制整體的透明度。當然也要在CG代碼片中定義與其對應的變量。

III. 修改 Tags

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
IV. 關閉深度寫入和開啟混合
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
V.修改片元着色器
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

 我們用透明紋理的透明通道和 _AlphaScale 來控制整體透明度

VI. 修改 FallBack

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
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 的效果:

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
 對比透明度測試,我們可以看到透明度混合更加柔和平滑。

 3.2 混合指令

Blend SrcFactor DstFactor 開啟混合,設定因子。源顔色 x ScrFacor  + 目标顔色 x DstFactor,結構存入顔色緩沖
Blend SrcFactor DstFactor,SrcFactorA DstFactorA 和上面類似,隻是混合透明通道的因子不同
 混合有兩個操作數:源顔色(source color)和 目标顔色(destination color)
  • 源顔色。指片元着色器産生的顔色值,用 S 表示。
  • 目标顔色。指顔色緩沖中的值,用 D 表示。
  • 兩者混合後,得到的新顔色用 O 表示。

而上面三者都包含了 RGBA 通道。

除了 Blend Off 以外,使用Blend 指令,Unity 會為我們開啟混合,因為隻有開啟了混合,混合指令才起效。

混合指令由 操作 和 因子 組成,操作預設是使用 加操作,而為了混合RGB 通道 和 A通道,是以我們需要 4 個因子

以混合指令 Blend SrcFactor DstFactor 為例,預設為加操作,SrcFactor 為源顔色, DstFactor 為目标顔色,然後計算

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
 下面是 ShaderLab 支援的一些混合因子:
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
讀者可以自行選擇因子來試試效果

四. 雙面渲染

一般來說,如果一個物體是透明的,要麼我們應該可以看到它的内部和它的任一個面,但前面我們實作的透明中并沒有實作這個效果,因為 Unity 在預設引擎下剔除了物體背面(是相對于錄影機方向的背面,而不是世界坐标中前後左右的背面),不渲染。而剔除的指令為

Cull Back | Front | Off

為了實作雙面渲染,我們可以這樣實作:設定兩個 Pass ,一個隻渲染前面,一個隻渲染背面。不過需要注意的是,由于開啟了深度測試,是以要注意渲染順序,要先渲染背面,再渲染正面,這樣就能確定背面會被渲染出來。

【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理
兩個 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】(五) ------ 透明效果之半透明效果的實作及原理
 現在我們可以清楚地看到物體的内部了。

五. 總結

透明效果是十分常見且有用的一種實作,我們可以利用它來實作很多有趣的效果。要實作透明,更多地是對渲染的一種了解。本文隻是對Unity中渲染的一些基礎解釋,希望能對讀者有所幫助。 本文所用透明紋理及shader
【Unity Shader】(五) ------ 透明效果之半透明效果的實作及原理

繼續閱讀