深度緩沖(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 //剔除后面 /*其余代碼保持不變 .... */ } } }
實現的效果如下