本文引用 Unity Shader入門精要
開啟透明混合后,一個物體被渲染到屏幕上時,每個片元除了顏色值和深度值外,還有——透明度。透明度為1,則完全不透明,透明度為0,則完全不會顯示。
在Unity中我們有兩種方式實現透明度效果
- 透明度測試(Alpha Test):這種方式無法得到真正的半透明效果。只是0或1(完全透明和完全不透明)
- 透明度混合(Alpha Blending):使用當前的透明度作為混合因子,與已經存儲在顏色緩沖中的顏色值進行混合。
那讓我討論第一個問題:渲染順序
渲染順序
為什么先說渲染順序呢?
在之前的兩篇文章中,我並沒有涉獵到渲染順序。因為對於不透明的物體,渲染順序的決定是由深度緩沖決定的。
深度緩沖的基本思想為:根據深度緩沖中的值來判斷該片元距離攝像機的距離,當渲染一個片元時,需要把深度值和已經存在於深度緩沖中的值進行比較(前提:開啟深度測試),如果它的值距離攝像機更遠,說明該片元不應被渲染。
但是當我們使用透明度混合時,就得關閉深度寫入(ZWrite)。
為什么關閉深度寫入呢?
一個半透明的物體后面如果有物體的話,應該是可以被看到的,但是深度寫入會把它剔除掉。
所以對於渲染順序就得我們自行控制了。
上兩個圖分別是兩種渲染順序情況,
圖一:透明物體在前,不透明物體在后。
- 情況一:如果先渲染不透明物體(開啟深度寫入和深度測試),將不透明物體顏色寫入顏色緩存,深度寫入深度緩沖,然后渲染透明物體(關閉深度寫入,開啟深度測試),將透明物體的顏色與顏色緩沖中的顏色混合,得到正確結果。
- 情況二:先渲染透明物體(關閉深度寫入,開啟深度測試),透明物體顏色寫入顏色緩沖,然后渲染不透明物體(開啟深度寫入和深度測試),深度緩存中沒有內容,所以直接覆蓋顏色緩沖。得到錯誤結果。
圖二:兩個透明物體。
- 情況一:先渲染后方的透明物體,顏色寫入顏色緩沖,然后渲染前方透明物體,顏色和顏色緩沖中的顏色混合,得到正確結果。
- 情況二:先渲染前方的透明物體,顏色寫入顏色緩沖,然后渲染后方透明物體,顏色和顏色緩沖中的顏色混合,得到后方物體在前方物體前的畫面,得到錯誤結果。
基於這種情況Unity給我們一種解決方式(大多數引擎的解決方式):物體排序+分割網格。
- 物體排序:1.先渲染所有不透明物體,並開啟它們的深度測試和深度寫入。2.把半透明物體按它們距離攝像機的遠近進行排序,然后按照從后往前的順序渲染半透明物體(開啟深度測試,關閉深度寫入)。
- 分割網格:解決物體排序遺留問題:循環重疊(例:3個物體互相重疊),我們把網格分割,分別判斷分開后的網格的順序來進行渲染。
Unity Shader 的渲染順序
Unity提供了渲染隊列(render queue)解決渲染順序的問題。用SubShader的Queue標簽來設置我們的模型在哪個渲染隊列。索引越小越先渲染
Unity的5個渲染隊列:
- Background:索引:1000,這個隊列是最先渲染的。
- Geometry:索引:2000,默認渲染隊列。不透明物體使用這個隊列。
- Alpha Test:索引:2450,需要透明度測試的物體使用該隊列。
- Transparent:索引:3000,按照從后往前的順序渲染,使用透明度混合的物體都應該用該隊列。
- Overlay:索引:4000,該隊列用於實現一些疊加效果,最后渲染。
透明度測試
只要一個片元的透明度不滿足條件,那么這個片元就會被舍棄。用clip來進行透明度測試。
立方體的貼圖每個塊都是不同的透明度分別是50%、60%、70%、80%,下面是我把Alpha Cutoff設為0.7時的效果。你會發現50%和60%透明度的貼圖已經不見了。
透明度測試Shader代碼:
1 Shader "My Shader/AlphaShader" 2 { 3 Properties 4 { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Texture", 2D) = "white" {} 7 _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 8 } 9 SubShader 10 { 11 // 透明度測試隊列為AlphaTest,所以Queue=AlphaTest 12 // RenderType標簽讓Unity把這個Shader歸入提前定義的組中,以指明該Shader是一個使用了透明度測試的Shader 13 // IgonreProjector為True表明此Shader不受投影器(Projectors)影響 14 Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" } 15 16 Pass 17 { 18 Tags { "LightMode"="ForwardBase" } 19 20 CGPROGRAM 21 #pragma vertex vert 22 #pragma fragment frag 23 24 #include "UnityCG.cginc" 25 #include "Lighting.cginc" 26 27 struct a2v 28 { 29 float4 vertex : POSITION; 30 float3 normal : NORMAL; 31 float4 texcoord : TEXCOORD0; 32 }; 33 34 struct v2f 35 { 36 float4 pos : SV_POSITION; 37 float2 uv : TEXCOORD0; 38 float3 worldNormal : TEXCOORD1; 39 float3 worldPos : TEXCOORD2; 40 }; 41 42 sampler2D _MainTex; 43 float4 _MainTex_ST; 44 fixed4 _Color; 45 // 用於決定調用clip函數時進行的透明度測試使用的判斷條件 46 fixed _Cutoff; 47 48 v2f vert (a2v v) 49 { 50 v2f o; 51 52 o.pos = UnityObjectToClipPos(v.vertex); 53 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 54 o.worldNormal = UnityObjectToWorldNormal(v.normal); 55 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 56 57 return o; 58 } 59 60 fixed4 frag (v2f i) : SV_Target 61 { 62 fixed3 worldNormal = normalize(i.worldNormal); 63 fixed3 worldPos = normalize(i.worldPos); 64 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 65 // 紋素值 66 fixed4 texColor = tex2D(_MainTex, i.uv); 67 // 原理 68 // if ((texColor.a - _Cutoff) < 0.0) { discard; } 69 // 如果結果小於0,將片元舍棄 70 clip(texColor.a - _Cutoff); 71 // 反射率 72 fixed3 albedo = texColor.rgb * _Color.rgb; 73 // 環境光 74 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; 75 // 漫反射 76 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 77 return fixed4(ambient + diffuse, 1.0); 78 } 79 ENDCG 80 } 81 } 82 }
透明度混合
那我們看看Unity給我們提供的混合命令——Blend。給出Blend的常用語義。
- Blend Off:關閉混合
- Blend SrcFactor DstFactor:開啟混合,設置混合因子。源顏色(片元顏色)乘以SrcFactor,目標顏色(已經在顏色緩沖中的顏色)乘以DstFactor,然后把兩者相加
- Blend SrcFactor DstFactor,SrcFactorA DstFactorA:同上,不過把透明通道(a)與顏色通道(rgb)用不同的因子。
- BlendOp BlendOperation:使用BlendOperation對其進行其他操作,非簡單相加混合。
混合公式:DstColorNew = SrcAlpha * SrcColor + (1 - SrcAlpha) * DstColorOld
下面是最簡單的調整透明通道值的效果,AlphaScale = 0.6
Shader代碼為:
1 Shader "My Shader/AlphaShader" 2 { 3 Properties 4 { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Texture", 2D) = "white" {} 7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 8 } 9 SubShader 10 { 11 // 透明度混合隊列為Transparent,所以Queue=Transparent 12 // RenderType標簽讓Unity把這個Shader歸入提前定義的組中,以指明該Shader是一個使用了透明度混合的Shader 13 // IgonreProjector為True表明此Shader不受投影器(Projectors)影響 14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } 15 16 Pass 17 { 18 Tags { "LightMode"="ForwardBase" } 19 20 // 關閉深度寫入 21 ZWrite Off 22 // 開啟混合模式,並設置混合因子為SrcAlpha和OneMinusSrcAlpha 23 Blend SrcAlpha OneMinusSrcAlpha 24 25 CGPROGRAM 26 #pragma vertex vert 27 #pragma fragment frag 28 29 #include "UnityCG.cginc" 30 #include "Lighting.cginc" 31 32 struct a2v 33 { 34 float4 vertex : POSITION; 35 float3 normal : NORMAL; 36 float4 texcoord : TEXCOORD0; 37 }; 38 39 struct v2f 40 { 41 float4 pos : SV_POSITION; 42 float2 uv : TEXCOORD0; 43 float3 worldNormal : TEXCOORD1; 44 float3 worldPos : TEXCOORD2; 45 }; 46 47 sampler2D _MainTex; 48 float4 _MainTex_ST; 49 fixed4 _Color; 50 // 用於決定調用clip函數時進行的透明度測試使用的判斷條件 51 fixed _AlphaScale; 52 53 v2f vert (a2v v) 54 { 55 v2f o; 56 57 o.pos = UnityObjectToClipPos(v.vertex); 58 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 59 o.worldNormal = UnityObjectToWorldNormal(v.normal); 60 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 61 62 return o; 63 } 64 65 fixed4 frag (v2f i) : SV_Target 66 { 67 fixed3 worldNormal = normalize(i.worldNormal); 68 fixed3 worldPos = normalize(i.worldPos); 69 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 70 // 紋素值 71 fixed4 texColor = tex2D(_MainTex, i.uv); 72 // 反射率 73 fixed3 albedo = texColor.rgb * _Color.rgb; 74 // 環境光 75 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; 76 // 漫反射 77 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 78 // 返回顏色,透明度部分乘以我們設定的值 79 return fixed4(ambient + diffuse, texColor.a * _AlphaScale); 80 } 81 ENDCG 82 } 83 } 84 }
然而這種實現方式是有問題的。就像前面說的一樣,它並不能渲染正確的順序。
解決方式:用兩個Pass來渲染模型
- 第一個Pass:開啟深度寫入,但不輸出顏色,目的僅僅為了填充深度緩沖。
- 第二個Pass:正常的透明度混合,由於上一個Pass已經得到了逐像素的正確深度信息,該Pass就可以按照像素級別的深度排序進行透明渲染。
- 缺點:多了一個Pass性能有所影響。
代碼如下:
1 Shader "My Shader/AlphaShader" 2 { 3 Properties 4 { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Texture", 2D) = "white" {} 7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 8 } 9 SubShader 10 { 11 // 透明度混合隊列為Transparent,所以Queue=Transparent 12 // RenderType標簽讓Unity把這個Shader歸入提前定義的組中,以指明該Shader是一個使用了透明度混合的Shader 13 // IgonreProjector為True表明此Shader不受投影器(Projectors)影響 14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } 15 16 Pass 17 { 18 // 開啟深度寫入 19 ZWrite On 20 // 設置顏色通道的寫掩碼,0為不寫入任何顏色 21 ColorMask 0 22 } 23 24 Pass 25 { 26 Tags { "LightMode"="ForwardBase" } 27 28 // 關閉深度寫入 29 ZWrite Off 30 // 開啟混合模式,並設置混合因子為SrcAlpha和OneMinusSrcAlpha 31 Blend SrcAlpha OneMinusSrcAlpha 32 33 CGPROGRAM 34 #pragma vertex vert 35 #pragma fragment frag 36 37 #include "UnityCG.cginc" 38 #include "Lighting.cginc" 39 40 struct a2v 41 { 42 float4 vertex : POSITION; 43 float3 normal : NORMAL; 44 float4 texcoord : TEXCOORD0; 45 }; 46 47 struct v2f 48 { 49 float4 pos : SV_POSITION; 50 float2 uv : TEXCOORD0; 51 float3 worldNormal : TEXCOORD1; 52 float3 worldPos : TEXCOORD2; 53 }; 54 55 sampler2D _MainTex; 56 float4 _MainTex_ST; 57 fixed4 _Color; 58 // 用於決定調用clip函數時進行的透明度測試使用的判斷條件 59 fixed _AlphaScale; 60 61 v2f vert (a2v v) 62 { 63 v2f o; 64 65 o.pos = UnityObjectToClipPos(v.vertex); 66 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 67 o.worldNormal = UnityObjectToWorldNormal(v.normal); 68 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 69 70 return o; 71 } 72 73 fixed4 frag (v2f i) : SV_Target 74 { 75 fixed3 worldNormal = normalize(i.worldNormal); 76 fixed3 worldPos = normalize(i.worldPos); 77 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 78 // 紋素值 79 fixed4 texColor = tex2D(_MainTex, i.uv); 80 // 反射率 81 fixed3 albedo = texColor.rgb * _Color.rgb; 82 // 環境光 83 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; 84 // 漫反射 85 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 86 // 返回顏色,透明度部分乘以我們設定的值 87 return fixed4(ambient + diffuse, texColor.a * _AlphaScale); 88 } 89 ENDCG 90 } 91 } 92 }
雙面渲染的透明效果
對於剛才的立方體,雖然是透明的,但是卻看不到里面的構造,是不是感覺也不太對,如果想看到內部構造怎么辦呢?
Unity默認會剔除物體的背面(就是內部),那么我們可以用Cull指令來控制需要剔除哪個面的渲染圖元。
- Cull Back:背對着攝像機的渲染圖元不會渲染,默認情況。
- Cull Front:朝向攝像機的渲染圖元不會渲染。
- Cull Off:關閉剔除功能,所有的都會渲染。缺點:需要渲染的數目成倍增加,除非用於特殊效果,建議不開啟。
接下來我們看一下效果:
這回也是用連個Pass來完成:第一個Pass渲染背面,第二個Pass渲染前面
Shader 代碼如下:
1 Shader "My Shader/AlphaShader" 2 { 3 Properties 4 { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Texture", 2D) = "white" {} 7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 8 } 9 SubShader 10 { 11 // 透明度混合隊列為Transparent,所以Queue=Transparent 12 // RenderType標簽讓Unity把這個Shader歸入提前定義的組中,以指明該Shader是一個使用了透明度混合的Shader 13 // IgonreProjector為True表明此Shader不受投影器(Projectors)影響 14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } 15 16 Pass 17 { 18 Tags { "LightMode"="ForwardBase" } 19 20 // 只渲染背面 21 Cull Front 22 // 關閉深度寫入 23 ZWrite Off 24 // 開啟混合模式,並設置混合因子為SrcAlpha和OneMinusSrcAlpha 25 Blend SrcAlpha OneMinusSrcAlpha 26 27 CGPROGRAM 28 #pragma vertex vert 29 #pragma fragment frag 30 31 #include "UnityCG.cginc" 32 #include "Lighting.cginc" 33 34 struct a2v 35 { 36 float4 vertex : POSITION; 37 float3 normal : NORMAL; 38 float4 texcoord : TEXCOORD0; 39 }; 40 41 struct v2f 42 { 43 float4 pos : SV_POSITION; 44 float2 uv : TEXCOORD0; 45 float3 worldNormal : TEXCOORD1; 46 float3 worldPos : TEXCOORD2; 47 }; 48 49 sampler2D _MainTex; 50 float4 _MainTex_ST; 51 fixed4 _Color; 52 // 用於決定調用clip函數時進行的透明度測試使用的判斷條件 53 fixed _AlphaScale; 54 55 v2f vert (a2v v) 56 { 57 v2f o; 58 59 o.pos = UnityObjectToClipPos(v.vertex); 60 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 61 o.worldNormal = UnityObjectToWorldNormal(v.normal); 62 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 63 64 return o; 65 } 66 67 fixed4 frag (v2f i) : SV_Target 68 { 69 fixed3 worldNormal = normalize(i.worldNormal); 70 fixed3 worldPos = normalize(i.worldPos); 71 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 72 // 紋素值 73 fixed4 texColor = tex2D(_MainTex, i.uv); 74 // 反射率 75 fixed3 albedo = texColor.rgb * _Color.rgb; 76 // 環境光 77 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; 78 // 漫反射 79 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 80 // 返回顏色,透明度部分乘以我們設定的值 81 return fixed4(ambient + diffuse, texColor.a * _AlphaScale); 82 } 83 ENDCG 84 } 85 86 Pass 87 { 88 Tags { "LightMode"="ForwardBase" } 89 90 // 只渲染前面 91 Cull Back 92 // 關閉深度寫入 93 ZWrite Off 94 // 開啟混合模式,並設置混合因子為SrcAlpha和OneMinusSrcAlpha 95 Blend SrcAlpha OneMinusSrcAlpha 96 97 CGPROGRAM 98 #pragma vertex vert 99 #pragma fragment frag 100 101 #include "UnityCG.cginc" 102 #include "Lighting.cginc" 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 float2 uv : TEXCOORD0; 115 float3 worldNormal : TEXCOORD1; 116 float3 worldPos : TEXCOORD2; 117 }; 118 119 sampler2D _MainTex; 120 float4 _MainTex_ST; 121 fixed4 _Color; 122 // 用於決定調用clip函數時進行的透明度測試使用的判斷條件 123 fixed _AlphaScale; 124 125 v2f vert (a2v v) 126 { 127 v2f o; 128 129 o.pos = UnityObjectToClipPos(v.vertex); 130 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 131 o.worldNormal = UnityObjectToWorldNormal(v.normal); 132 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 133 134 return o; 135 } 136 137 fixed4 frag (v2f i) : SV_Target 138 { 139 fixed3 worldNormal = normalize(i.worldNormal); 140 fixed3 worldPos = normalize(i.worldPos); 141 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 142 // 紋素值 143 fixed4 texColor = tex2D(_MainTex, i.uv); 144 // 反射率 145 fixed3 albedo = texColor.rgb * _Color.rgb; 146 // 環境光 147 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; 148 // 漫反射 149 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 150 // 返回顏色,透明度部分乘以我們設定的值 151 return fixed4(ambient + diffuse, texColor.a * _AlphaScale); 152 } 153 ENDCG 154 } 155 } 156 }