在Unity中,通常使用两种方法来实现透明效果,一种是透明度测试(Alpha Test),但这种方法无法得到真正的半透明效果; 另一种是透明度混合(Alpha Blend)。
对于不透明的物体,不考虑它们的渲染顺序也能得到正确的排序效果,因为有着强大的深度缓冲(z-buffer)的存在。但如果要实现透明度混合时,事情会变得复杂,因为透明度混合会关闭深度写入(ZWrite).
基本原理:
1.透明度测试:只要一个片元的透明度不满足条件(比如小于某个阈值),那么对应的片元就被舍弃掉,被舍弃了的片元不会再进行任何处理,也不会对颜色缓冲产生影响;否则,就按照普通的不透明物体的处理方式来处理它,也就是进行深度测试,深度写入等。 透明度测试不需要关闭深度写入。因此,透明度测试的效果比较极端: 要么完全透明,要么完全不透明。
2.透明度混合:透明度混合可以得到真正的半透明效果。 透明度混合会使用当前片元的透明度作为混合因子,与已经存在颜色缓冲中的颜色值进行混合,来得到新的颜色。透明度混合需要关闭深度写入,但不会关闭深度测试,也就是说,如果深度测试不通过,就不会再进行混合操作。对于透明度混合来说,深度缓冲是只读的。
Unity提供了渲染队列(Render Queue)来解决渲染顺序。可以使用SubShader的Queue标签来决定模型归于哪一个渲染队列。 Unity内部使用一些整数索引来表示每个渲染队列,索引号越小越早被渲染。
通常我们在片元着色器中使用clip函数来进行透明度测试。
它的定义为:void clip(float4 x);void clip(float3 x);void clip(float2 x);void clip(float x);
参数中任何一个分量是负数,就会舍弃当前像素的输出颜色。
它的实现大概如下:
void clip(float4 x)
{
if (any(x < 0))
{
discard;
}
}
透明度测试的shader代码如下:
Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white"{}
_CutOff ("Alpha CutOff", Range(0, 1)) = 0.5
}
SubShader
{
Tags { "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
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 : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
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);
// alpha test
clip(texColor.a - _CutOff);
if ((texColor.a - _CutOff) < 0.0f)
{
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"
}
我们在SubShader中使用了AlphaTest的队列,而RenderType标签可以让Unity把这个shader归入到提前定义的组(TransparentCutout组),它通常被用于着色器替换功能。 把IgnoreProjector设置为True,表示这个shader不会受到投射器的影响。
我们使用clip来对texColor.a-_Cutoff做判断,如果不通过,就舍弃该片元的输出。
透明度混合,会使用Unity提供的混合命令:Blend.
ShaderLab的Blend命令:
我们在设置混合因子的时候,也同时开启了混合模式。
对于Blend SrcFactor DstFactor混合命令来说,经过混合后的颜色是:
DstColornew = SrcAlpha * SrcColor + (1 - ScrcAlpha) * DstColorold
透明度混合的shader例子如下:
Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white"{}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
Pass
{
Tags {"LightMode" = "ForwardBase"}
// 关闭深度写入,设置混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
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 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
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"
}
我们使用了Transparent的渲染队列来进行透明度混合。然后在Pass中,我们需要关闭深度写入ZWrite Off,然后开始混合模式并设置混合因子Blend SrcAlpha OneMinusSrcAlpha。
由于关闭了深度写入,我们无法对模型进行像素级别的深度排序。因此会导致一些错误的情况。一种解决方法是,使用两个Pass来渲染模型,第一个Pass开启深度写入,但不输出颜色,仅仅为了把模型的深度值写入到深度缓冲中;第二个Pass进行正常的透明度混合,因为第一个Pass已经得到了逐像素的正确深度信息,第二个Pass就可以按照像素的深度排序而进行透明渲染。因为这种做法多使用了一个Pass,因此会对性能造成一定的影响。
Shader "Unity Shaders Book/Chapter 8/Alpha Blend ZWrite"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white"{}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
Pass
{
ZWrite On
ColorMask 0
}
Pass
{
Tags {"LightMode" = "ForwardBase"}
// 关闭深度写入,设置混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
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 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
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"
}
第一个Pass里,我们开启了深度写入,因此可以把模型的深度信息写入到深度缓冲当中,从而剔除模型中被自身遮挡的片元;然后我们使用了ColorMask来设置颜色通道的写掩码(write mask),语法如下:
ColorMask RGB | A | 0 | 其他R,G,B,A的组合
ColorMask R,输出颜色中只有R通道会被写入
ColorMask 0,不会输出任何颜色
默认值为RGBA,即四个通道都写入
混合的实现就是,当片元产生一个颜色的时候,可以选择与颜色缓冲中的颜色进行混合。 因此,混合就和两个操作数有关:源颜色和目标颜色,源颜色S表示片元产生的颜色值,目标颜色D表示颜色缓冲中读到的颜色值。将它们混合后的输出颜色O再重新写入颜色缓冲当中。S,D和O都包含了RGBA四个通道。
使用混合的前提是开启混合,Untiy中我们使用了Blend命令(除了Blend Off)时,会设置混合状态并开启混合。
混合的时候需要一个混合等式来计算输出颜色。当进行混合时,我们需要两个混合等式,一个计算RGB值,一个计算A的值。我们使用Blend命令时,实际上就是设置混合等式里的操作和因子。默认情况混合等式使用加操作,我们只需要再设置一下混合因子就可以。
图里的两个混合命令,可以看到,第一个命令提供了两个混合因子,因此Unity会使用同样的混合因子来混合RGB通道和A通道,相对于第二个命令,第一个命令此时SrcFactorA等于SrcFactor,以此类推。
混合公式就比如:
Orgb = SrcFactor * Srgb + DstFactor * Dfgb
Oa = SrcFactorA * Sa + DstFactorA * Da
介绍了命令后,我们可以看看具体的混合因子有哪些:
举个例子来说,使用Blend SrcFactor DstFactor, SrcFactorA DstFactorA命令,带入上述的因子:
Blend SrcAlpha OneMinusSrcAlpha, One Zero
这时候,输出颜色的透明度值就是源颜色的透明度值。
刚才说Blend命令默认使用加运算来计算输出颜色,我们可以使用ShaderLab的BlendOp BlendOperation命令来使用不同的运混合操作:
需要注意的是,当使用Min或者Max混合操作时,混合因子实际上是不起任何作用的,它们仅会判断源颜色和目标颜色之间的比较结果。
通过混合操作和混合因子的组合,可以得到一些类似PhotoShop混合模式中的混合效果:
Blend SrcAlpha OneMinusSrcAlpha
// 柔和相加
Blend OneMinusDstColor One
// 正片叠底,相乘
Blend DstColor Zero
// 两倍相乘
Blend DstColor SrcColor
// 变暗
BlendOp Min
Blend One One
// 变亮
BlendOp Max
Blend One One
// 滤色
Blend OneMinusDstColor One
Blend One OneMinusSrcColor
// 线性减淡
Blend One One
由于默认情况下渲染引擎剔除了物体背面,只渲染了物体正面,但如果一个物体是透明的,我们不仅仅可以透过它看到其他物体,我们也应该可以看到它自己的内部结构。
在Unity中,使用Cull指令来控制需要剔除哪个面的渲染图元:
Cull Back | Front | Off
设置了Back,就会剔除背对着相机的图元,设置了Front,会剔除正面,设置了Off,就会关闭剔除功能。
Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side"
{
Properties
{
_Color ("Color", 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"}
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Front
// 关闭深度写入,设置混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
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 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
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
}
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Back
// 关闭深度写入,设置混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
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 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
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"
}
我们使用了两个Pass,第一个Pass只渲染背面,第二个Pass只渲染正面。这样做可以保证背面总是在正面渲染之前渲染,从而可以得到正确的深度渲染关系。