在Unity中一般使用两种方式来实现透明效果,即透明度测试和透明度混合。
透明度测试:使用透明度测试只会产生2种结果,要么完全透明,要么完全不透明。原因是只要一个片元的透明度不满足条件(一般是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试,深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是他会根据透明度来舍弃一些片元。不能实现半透明的效果。
透明度混合:该方式可以实现半透明效果。它会使用当前片元的透明度作为混合因子,与已经存在在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这就需要非常注意物体的渲染顺序。但并没有关闭深度测试,也就是说,当使用透明度混合渲染一个片元时,还是会比较它的深度值和当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么就不进行混合操作。
那么,为什么在透明度混合的时候需要关闭深度写入呢?关闭深度写入后又应该如何注意其渲染顺序呢?
如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面被剔除,我们也就无法透过半透明表面看到后面的物体了。但是,这也就破坏了深度缓冲的工作机制。对于如何注意渲染顺序,我们首先来看不同的渲染顺序会造成什么样的结果如下:
分2种情况:
对于第一种情况:
- 假如先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验,而此时深度缓冲中没有任何有效数据,因此B首先会写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B相比A距离摄像机更近,因此,我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。
- 假如先渲染A,再渲染B。渲染A时,深度缓冲区中没有任何有效数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,此时由于没有A的深度信息,B就会直接覆盖A的颜色。视觉上的效果就是B在A的前面了。
对于第二种情况:
- 假如先渲染B,再渲染A。那么B会正常写入颜色缓冲,然后A会和颜色缓冲中的B颜色进行混合,得到正确的半透明效果。
- 假如先渲染A,再渲染B。那么A会先写入颜色缓冲,随后B会和颜色缓冲中的A进行混合,这样的结果刚好与上一条相反,视觉上呈现为B在A的前面,即得到错误的半透明结构。
那么,在unity shader代码中如何控制其渲染顺序呢?
图中1代表渲染队列,图中2代表关闭深度写入,3代表开启混合模式。具体单词意思参考该文章:
渲染队列的几种模式如下:
开启的混合模式一般有以下几种:
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:线性减淡
这些混合效果对应如下图:
最后,透明度测试和透明度混合的代码分别如下:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/AphlaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader
{
//"Queue" = "AlphaTest"表示开启透明度测试,"IgnoreProjector" = "True表示该shader不会受投影器(Projector)影响
//"RenderType" = "TransparentCutout"表示让unity把这个shader归入到提前定义的组(这里指TransparentCutout)
//一般开启透明度测试需要设置这三个标签
Tags { "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _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 = 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);
//内置函数,表示进行透明度测试,texColor.a - _Cutoff为负时则透明,为正时则不透明
clip(texColor.a - _Cutoff);
//纹理采样的结果和颜色变量的混合
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
}
}
//保证编写的SubShader无法在当前显卡上工作时可以有合适的代替shader,还可以保证使用透明度测试的物体可以正确地向其他物体投射阴影
FallBack "Transparent/Cutout/VertexLit"
}
透明度混合shader代码如下:
Shader "Custom/AlphaBlend"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale("Alpha Scale", Range(0, 1)) = 1
}
SubShader
{
//"Queue" = "Transparent"表示开启了透明度混合的都要使用此模式,"IgnoreProjector" = "True表示该shader不会受投影器(Projector)影响
//"RenderType" = "TransparentCutout"表示让unity把这个shader归入到提前定义的组(这里指TransparentCutout)
//一般开启透明度测试需要设置这三个标签
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
//关闭深度写入
ZWrite Off
//该博客中有讲
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _AlphaScale;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float3 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 = 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);
//纹理采样的结果和颜色变量的混合
fixed3 albedo = texColor.rgb * _Color.rgb;
//环境光计算
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//漫反射光计算
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
//texColor.a * _AlphaScale,表示纹理的透明通道与_AlphaScale变量混合
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}