深度緩沖(Depth Buffer)
透明度混合時應關閉深度寫入(ZWrite Off)
如果不關閉深度寫入,一個半透明表面背后的表面本就是透過它被我們看到的,但由於深度測試時判斷結果是該半透明表面)距離攝像機更近,導致后面的表面會被剔除掉,也就無法通過半透明面觀察到后面的物體。
另外注意關閉深度寫入后要考慮物體的渲染順序。如下圖,一個半透明物體A和一個不透明物體B,B在A后方,如果先渲染A再渲染B會出現A被B遮擋的情況,這是錯誤的。因此渲染順序在關閉深度寫入的情況下極為重要。

為了保證渲染順序正確,渲染引擎一般會對物體進行排序,再渲染,常用的方法是
- 1)先渲染所有不透明物體,並開啟它們的深度測試和深度寫入。
- 2)把半透明物體按他們離攝像機的遠近進行排序,然后按照從后往前的順序渲染透明物體,並開啟它們的深度測試,但關閉深度寫入。
但這種方法無法解決物體重疊的情況,因此需要額外的解決方案,比如分割網格等。但是也可以試着讓透明通道更柔和,是穿插重疊看起來不那么明顯。
Unity的渲染順序
Unity通過一組Queue標簽來決定模型歸於哪個渲染隊列,隊列由整數索引表示,索引號越小越先被渲染。
| 渲染隊列 | 渲染隊列描述 | 渲染隊列值 |
| Background | 這個隊列被最先渲染。它被用於skyboxes等。 | 1000 |
| Geometry | 這是默認的渲染隊列。它被用於絕大多數對象。不透明幾何體使用該隊列。 | 2000 |
| AlphaTest | 通道檢查的幾何體使用該隊列。它和Geometry隊列不同,對於在所有立體物體繪制后渲染的通道檢查的對象,它更有效。 | 2450 |
| Transparent | 該渲染隊列在Geometry和AlphaTest隊列后被渲染。任何通道混合的(也就是說,那些不寫入深度緩存的Shaders)對象使用該隊列,例如玻璃和粒子效果。 | 3000 |
| Overlay | 該渲染隊列是為疊加效果服務的。任何最后被渲染的對象使用該隊列,例如鏡頭光暈。 | 4000 |
透明度測試
只要有一個片元的透明度不滿足條件(通常是小於某個閾值),那么它對應的片元便會被舍棄,不做任何處理。
在Unity中是用如下函數來進行透明度測試:
clip(texColor.a - _Cutoff);
texColor.a為紋理的alpha值,_Cutoff為閾值,該函數等價於
if(texColor.a - _Cutoff < 0)
discard;
完整的代碼
Shader "Unity Shader Book/Chapter 8/AlphaTest"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "IgnoreProjector"="True" "LightMode"="ForwardBase"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal:NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv : TEXCOORD2;
};
fixed4 _Color;
fixed _Cutoff;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed4 texColor = tex2D(_MainTex,i.uv);
//Alpha Test
clip(texColor.a - _Cutoff);
//equal to
//if(texColor.a - _Cutoff < 0) discard;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
return fixed4(diffuse + ambient,1.0);
}
ENDCG
}
}
}
效果如下,可以看到只是單純的顏色剔除,並不算正常的透明效果。

為了得到正確的透明效果,可以使用透明度混合。
透明度混合
透明度混合即將透明物體的源顏色與其表面后方的目標顏色混合,其基本公式為:
DstColor-new(混合后的顏色) = SrcAlpha * SrcColor + (1- SrcAlpha) * DstColor。(SrcAlpha為混合因子)
該公式可以在Unity的ShaderLab語義中表示為
Blend SrcAlpha OneMinusSrcAlpha
更多語義可以參考官方文檔
完整代碼實現
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unity/Chapter 8/AlphaBlend"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale("Alpha Scale",Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "IgnoreProjector"="True" "Queue"="Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
ZWrite On
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float3 normal:NORMAL;
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD2;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float4 pos : SV_POSITION;
};
fixed4 _Color;
fixed _AlphaScale;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex,i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
return fixed4(ambient + diffuse,texColor.a * _AlphaScale);
}
ENDCG
}
}
Fallback "Transparent/VertexLit"
}
該代碼得到的效果如下

嗯,這才是正常的透明效果
開啟深度寫入的半透明效果
上述的透明混合並沒有開啟深度寫入(ZWrite On),因此如果物體有重疊,那么會產生錯誤的效果,如下

這時候需要在上文的透明度混合代碼中的Pass代碼塊之前,插入一個新的Pass代碼塊
Pass
{
ZWrite On
ColorMask 0 //用於設置顏色通道的寫掩碼,0表示不寫入任何通道
}
這樣就會得到正確的效果

雙面渲染的透明效果
仔細觀察上文的透明效果可以發現,我們並不能透過半透明物體觀察它們的內部情況,這是不合乎常理的,因此我們需要進行雙面渲染。
在Unity中,我們可以通過Cull指令來控制剔除哪一面的渲染圖元
Cull Back //剔除背面,只渲染前面,引擎的默認設置 Cull Front //剔除前面,僅渲染背面 Cull Off //關閉剔除
透明度測試的雙面渲染
僅需要在Tags標簽后插入一行
Cull Off
效果對比

透明度混合的雙面渲染
將Pass代碼塊復制一份,一個Pass負責渲染前面,一個Pass負責渲染后面就行,格式如下
Shader "........."{
propeties
{
/*.....*/
}
SubShader{
Pass
{
Tags{.......}
Cull Front //剔除前面
/*其余代碼保持不變
....
*/
}
Pass
{
Tags{.......}
Cull Back //剔除后面
/*其余代碼保持不變
....
*/
}
}
}
實現的效果如下

