在上篇文章有讲到实现透明度混合时需要关闭深度写入。而为了不造成错误的排序效果,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法如下:(对于上篇文章shader代码中使用的渲染队列Transparent)
- 先渲染所有不透明物体,并开启他们的深度测试和深度写入;
- 把半透明物体按他们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启他们的深度测试,但关闭深度写入;
其实这样做并没有解决所有情况下的错误的排序问题。在一些情况下,根据“距离摄像机远近”并不能解决问题。这是因为深度缓冲中的值其实是像素级别的,即每个像素有一个深度值,但是现在我们对单个物体级别进行排序,这意味着排序结果是,要么物体A全部在B前面渲染,要么A全部在B后面渲染。但如果存在循环重叠的情况,那么使用这种方法就永远无法得到正确的结果。如下展示了无法使用上述方法解决的情况:
图8.3中由于3个物体互相重叠,我们不可能得到一个正确的排序顺序。这种时候,我们可以选择把物体拆分成2个部分,然后再进行正确的排序。但是如果遇到图8.4的情况,如何判断他们距离摄像机远近呢?我们知道,一个物体的网格结构往往占据了空间中的莫一块区域,也就是说网格上的每一个点的深度值都是不一样的,那我们选择图中哪个点来判断距离摄像机远近呢?答案是不管选择哪个点都不合理。
所以,为了解决这个问题,一种方法是使用2个Pass来渲染模型:第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点是,多使用一个Pass会对性能造成一定影响,并且对于模型内部不会有半透明效果。
开始深度写入的半透明混合的shader代码如下:
Shader "Custom/AlphaBlendZWrite"
{
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
{
//开启深度写入
ZWrite On
//ColorMask语义有以下几种:ColorMask RGB|A|0|其他任何RGB组合
//为0时代表该Pass不写入任何颜色通道,即不会输出任何颜色
ColorMask 0
}
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 "Diffuse"
}
该Shader代码的结果的如下图:
由此可见该透明效果并不能看到正方体的内部。而现实生活中,如果一个物体是透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它的内部结构。而造成这种结果的原因是,默认情况下渲染引擎剔除了物体背面的渲染图元,而只渲染了物体的正面。因此我们需要得到双面的渲染效果。Unity中可以用Cull指令来控制需要剔除哪个面的渲染图元。指令如下:
Cull Back | Front | Off
如果设置为Back,那么那些背对着相机的图元就不会得到渲染,也是默认情况下的剔除状态;如果设置为Front,那么那些朝向摄像机的渲染图元就不会得到渲染;如果设置为Off,就会关闭剔除功能,所有图元都将得到渲染,但此时需要渲染的图元成倍增加,影响性能。
因此,我们还需要实现透明度混合的双面渲染效果,使效果更接近现实情况。而实现此功能,只需要将正面和反面都渲染一遍,并保证反面的渲染在正面的渲染之前,以此来保证正确的深度渲染关系(原因(八)文章中有讲)。
unity shader中SubShader下的Pass会按照从上往下的顺序依次渲染,因此,我们第一个Pass只渲染背面,第二个Pass只渲染正面,从而保证背面总在正面之前被渲染。具体shader代码如下(两个Pass代码一致,唯一的区别只是一个关闭了正面的渲染,一个只关闭了背面的渲染):
Shader "Custom/AlphaBlendBothSided"
{
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"}
//关闭正面的渲染
Cull Front
//关闭深度写入
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
}
Pass
{
Tags{"LightMode" = "ForwardBase"}
//关闭背面渲染
Cull Back
//关闭深度写入
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"
}