在Unity中,我們通常使用兩種方式來實作透明效果:1.使用透明度測試 2.透明度混合.
透明度測試:隻要一個片元的透明度不滿足條件,它對應的片元就會被舍棄。被舍棄的片元将不會再進行任何處理,也不會對顔色緩沖産生任何英雄,不需要關閉深度寫入.
透明度混合: 使用目前片元的透明度作為混合因子,與以及存儲在顔色緩沖中的顔色值進行混合,得到新的顔色。需要關閉深度寫入,要非常小心物體的渲染順序.
問題1:在透明度混合中為什麼需要關閉深度寫入呢?
如果不關閉深度寫入,一個透明物體背後的物體本來是可以被我們看到的,但由于深度測試時判斷結果是該半透明物體表面距離錄影機更近,導緻後面的物體被剔除,也就無法透過透明物體看到後面的物體了。
問題2:在透明度混合中為什麼渲染順序很重要呢?
假設場景裡面有物體A和B,A是半透明物體,B是不透明物體,A在B的前面
考慮兩種情況:
1.先渲染B,在渲染A:
由于不透明物體開啟了深度測試和深度寫入,一開始深度緩沖中沒有任何資料,B首先會寫入顔色緩沖和深度緩沖,然後渲染A,透明物體仍然會進行深度測試,但是A比B距離錄影機更近,是以我們會使用A的透明度和顔色緩沖中的B的顔色進行混合,得到正确的半透明效果.
2.先渲染A,在渲染B:
首先先渲染A,此時深度緩沖中沒有任何有效資料,但是由于對半透明物體關閉了深度寫入,A不會修改深度緩沖,然後渲染B,B進行深度測試,由于深度緩沖沒有發生變化,B就直接寫入顔色緩沖和深度緩沖,造成的結果就是B出現在了A的前面.
渲染順序總結如下:
1.先繪制所有不透明的物體,并開啟它們的深度測試和深度寫入.
2.對所有透明的物體排序。
3.按從後往前的順序繪制所有透明的物體。
透明度測試
通常,我們會在片元着色器中使用
clip
函數來進行透明度測試,如果給定參數的任何一個分量是負數,就會舍棄目前像素的輸出顔色.它等同于下面的代碼:
void clip(float4 x)
{
if(any(x < 0))
discard;
}
第一步建立一個Shader,在屬性裡面添加一個變量_Cutoff用來控制透明度測試時使用的門檻值:
第二步在SubShader語義塊中定義一個Pass語義塊:
Tags{"Queue " = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
//使用渲染隊列名為AlphaTest的隊列,RenderType 讓Unity把這個shader歸入到提前定義的組,以指明該shader是一個使用了
//透明度測試的shader. ignoreProjector 意味着shader不會受到投影器的影響
pass
{
Tags{"LightMode" = "ForwardBase"}
第三步在CG代碼塊中聲明對應屬性的變量:
然後定義頂點着色器的輸入和輸出結構體:
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 wordlNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
接着定義頂點着色器:
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
接下來是片元着色器:
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
clip (texColor.a - _Cutoff);
//if((texColor.a - _Cutoff) < 0.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(ambient + diffuse, 1.0);
}
效果如下:
完整代碼如下:
Shader "AlphaTest"
{
Properties
{
_Color("Color Tint",Color) = (1, 1, 1, 1)
_MainTex("Main Tex",2D) = "white"{}
_Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5//用于決定調用clip進行透明度測試時使用的判斷條件,0-1像素透明度的範圍
}
SubShader
{
Tags{"Queue " = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
//使用渲染隊列名為AlphaTest的隊列,RenderType 讓Unity把這個shader歸入到提前定義的組,以指明該shader是一個使用了透明度測試的Shader
//透明度測試的shader. ignoreProjector 意味着shader不會受到投影器的影響
pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用Unity内置變量 如_LightColor0
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
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);
clip (texColor.a - _Cutoff);
//if((texColor.a - _Cutoff) < 0.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(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
透明度混合
為了進行混合,需要使用Unity提供的混合指令-Blend
第一步聲明屬性:
用來控制整體的透明度.
第二步修改标簽:
第三步,關閉深度寫入,設定混合模式:
Write Off
Blend SrcAlpha OneMinusSrcAlpha
第四步,修改片元着色器:
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
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);
//設定片元着色器傳回值中的透明通道
}
效果如下:
完整代碼如下:
Shader "Unlit/AlphaTest"
{
Properties
{
_Color("Color Tint",Color) = (1, 1, 1, 1)
_MainTex("Main Tex",2D) = "white"{}
_AlphaScale("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
Tags{"Queue " = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
//使用透明度混合的物體使用Transparent隊列
pass
{
Tags{"LightMode" = "ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用Unity内置變量 如_LightColor0
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 wordlNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
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"
}
開啟深度寫入的半透明效果
關閉深度寫入時,會帶來一些問題,當模型本身有複雜的遮擋關系時,就會有各種各樣因為排序而産生的錯誤的透明效果,比如下面的:
一種解決辦法是使用兩個Pass來渲染模型:第一個Pass開啟深度寫入,但不輸出顔色,它的目的僅僅是為了把模型的深度值寫入深度緩沖中。第二個Pass進行正常的透明度混合.該Pass可以按照像素級别的深度排序結果進行透明渲染
完整代碼如下
Shader "Unlit/AlphaTest"
{
Properties
{
_Color("Color Tint",Color) = (1, 1, 1, 1)
_MainTex("Main Tex",2D) = "white"{}
_AlphaScale("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
//使用透明度混合的物體使用Transparent隊列
pass//目的是把模型的深度資訊寫入深度緩沖中
{
ZWrite On//開啟深度寫入
ColorMask 0 //ColorMask用于設定顔色通道的寫掩碼 語義 ColorMask RGB|A|0
//當ColorMask設為0時,意味着Pass不寫入任何顔色通道,不會輸出任何顔色。
}
pass
{
Tags{"LightMode" = "ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用Unity内置變量 如_LightColor0
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 wordlNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
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"
}
這個代碼幾乎和前面的透明度混合幾乎完全一樣,不同的地方就是多出了一個Pass通道,僅僅是為了進行把模型的深度資訊寫入深度緩沖中.
雙面渲染的透明效果
在現實生活中,如果一個物體是透明的,意味着我們不僅可以透過它看到其他物體的樣子,也可以看到它内部的結構,然後在前面展示的例子中,效果如下:
我們并沒有能看到物體的内部結構,仿佛它就是空的。這是因為預設情況下渲染引擎剔除了物體背面的渲染圖元。
透明度測試的雙面渲染
如果我們想要使用透明度測試的物體實作雙面渲染的效果,隻需要把剔除指令關閉就行了.在Pass通道裡面添加下面的代碼就行了:
Cull Off
透明度混合的雙面渲染
相比于透明度測試,透明度混合會更複雜,因為透明度混合需要關閉深度寫入。想要得到正确的透明效果,渲染順序是非常重要的,要保證物體是從後往前渲染的.
為此,我們把雙面渲染分成兩個Pass,第一個Pass渲染背面,第二個Pass渲染正面,這樣可以保證背面總是在正面被渲染之前渲染,進而可以保證正确的深度渲染關系.
完整代碼如下:
Shader "Unlit/AlphaTest"
{
Properties
{
_Color("Color Tint",Color) = (1, 1, 1, 1)
_MainTex("Main Tex",2D) = "white"{}
_AlphaScale("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
Tags{"Queue " = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
//使用透明度混合的物體使用Transparent隊列
pass
{
Tags{"LightMode" = "ForwardBase"}
Cull Front
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用Unity内置變量 如_LightColor0
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 wordlNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
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
}
pass
{
Tags{"LightMode" = "ForwardBase"}
Cull Back
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //使用Unity内置變量 如_LightColor0
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 wordlNormal : TEXCOORD;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.wordlNormal);
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"
}
代碼和之前的混合度測試的沒有什麼差別,僅僅是使用了兩個Pass,并且每個Pass分别使用Cull指令剔除不同朝向的渲染圖元
效果如下: