Unity3D學習(八):《Unity Shader入門精要》——透明效果


前言
在實時渲染中要實現透明效果,通常會在渲染模型時控制它的透明通道。
Unity中通常使用兩種方法來實現透明 :(1)透明度測試(AlphaTest)(2)透明度混合(AlphaBlend)。前者往往無法實現真正的半透明效果。

深度緩沖(Depth Buffer)

深度緩沖是用於解決可見性問題的,它可以決定物體的哪些部分渲染在前面,哪些部分被其他物體遮擋。其基本思想是:根據深度緩存中的值來判斷該片元距離攝像機的距離,當渲染一個片元時,需要把它的深度值和已經存在深度緩存中的值進行比較(前提是開啟了深度測試),如果它的值距離攝像機更遠,那么說明它不應該被渲染到屏幕上(被擋住了);否則,這個片元應該覆蓋掉此時顏色緩沖中的像素值,並把它的深度更新到深度緩沖中(前提是開啟了深度寫入,Unity中為ZWrite On)。

透明度混合時應關閉深度寫入(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  //剔除后面
         /*其余代碼保持不變
           ....
         */
     }

   }  
}

實現的效果如下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM