在实时渲染中要实现透明效果,通常会在渲染模型时控制他的透明通道。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值之外,它还有另一个属性——透明度。当透明度为1时表示该像素是完全不透明的,当为0时,则表示像素完全不会显示。
在unity中,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试,这种方法其实无法得到真正的半透明效果;另一种就是透明度混合。
对于不透明物体,不考虑他们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲的存在。在实时渲染中,深度缓冲是用于解决可见性问题的,它可以决定那个物体的那些部分会被渲染在前面,那些部分会被其他物体遮挡。他的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较,如果他的之距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上;否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中。
使用深度缓冲,可以让我们不用关心不透明物体的渲染顺序,即使我们先渲染A再渲染B也不用担心B会遮盖掉A,因为再进行深度测试时会判断出B距离摄像机更远,也就不会写入到颜色缓冲中。但如果想要实现透明效果,我们就会关闭深度写入。
透明度测试:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件,那么他对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则就会按照普通的不透明物体的处理方式来处理它,即进行深度测试,深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是他会根据透明度来舍弃一些片元,虽然简单,但是它产生的效果也很极端,要么完全透明,要么完全不透明。
透明度混合:这种方法可以得到真正的半透明效果。他会使用当前片元的透明度作为混合因子,与已经存储再颜色缓冲中的颜色进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合之关闭了深度写入,但没有关闭深度测试,意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果他的深度值距离摄像机更远,那么就不会再次进行深度混合操作。这一点决定了,当一个不透明物体出现在一个透明物体前,我们先渲染了不透明物体,它依然可以正常地遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。
为什么渲染顺序很重要
对于透明混合技术,需要关闭深度写入,此时我们就要小心处理透明物体的渲染。
为什么要关闭深度写入呢?如果不关闭深度写入,一个半透明表明背后的表面本来是可以透过它被我们看到的,但是由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们也就无法透过半透明表面看到后面的物体了。但是,我们由此破环了深度缓冲的工作机制。
不同的渲染顺序会有什么结果呢。
我们先渲染B再渲染A,那么由于不透明物体开启了深度测试和深度检验,此时深度缓冲中没有任何有效数据,因此B会首先写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们会发现和B相比A距离摄像机更近,我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。
如果我们先渲染A再渲染B。渲染A时,深度缓冲中没有任何有效数据,因此A直接写入颜色缓冲中,但由于对半透明物体关闭了深度写入,此时A不会修改深度缓冲。等到渲染B时,B会进行深度测试,发现,深度测试中还没有数据那B就写入到颜色缓冲中了,结果B就会覆盖A的颜色。从视觉上B就出现在了A的前面就错误了。
我们还是考虑不同的渲染顺序有什么不同的结果。
第一种,先渲染B再渲染A。那么B会正常写入颜色缓冲,然后会和颜色缓冲中的B颜色进行混合,得到半透明的效果。
第二种情况,我们先渲染A再渲染B。那么A会先写入颜色缓冲,随后B会和颜色缓冲中的A进行混合,这样混合后的结果会反过来,看起来B就在A的前面,得到错误的半透明结果。
基于这两点,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是。
- 先渲染所有不透明的物体,并开启他们的深度测试和深度写入。
- 把半透明物体按照他们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启他们的深度测试,但关闭深度写入。
Unity自定义的五个渲染队列
透明度测试
从图中可以看到,透明测试得到的透明效果很极端,要么完全透明,要么完全不透明,他的效果往往像是在不透明物体上挖一个空洞。而且得到的透明效果在边缘处往往参差不起有锯齿,因为在边界纹理的透明度的变化精度问题。为了得到更加柔滑的透明效果,就可以使用透明度混合。
代码
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
Properties {
_Color ("Color Tint", 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
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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 = 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);
// Alpha test
clip (texColor.a - _Cutoff);
// Equal to
// 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。Blend是unity提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身色颜色和已经存在于颜色缓冲中的颜色进行混合,混合时使用的函数就是由该命令决定的。
代码
Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
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"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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 = 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));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
开启深度写入的半透明效果
使用两个Pass来渲染模型:第一个pass开启深度写入,但不输出颜色,他的目的仅仅是为了把模型深度值写入深度缓冲中;第二个pass进行正常的透明度混合,由于上一个pass已经按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点在于,多使用一个pass会对性能造成一定的影响。
这个新添加的Pass的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元。因此,在pass的第一行开启了深度写入。在第二行,我们使用一个新的渲染命令——ColorMask。在ShaderLab中,ColorMask用于设置颜色通道的写掩码。
当ColorMask为0时,意味着该Pass不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的——该pass只需要写入深度缓存即可。
代码
Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
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"}
// Extra pass that renders to depth buffer only
Pass {
ZWrite On
ColorMask 0
}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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 = 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));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
ShaderLab的混合命令
混合还有很多其他的用处,不仅仅是用于透明度的混合。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来混合就和两个操作数有关:源颜色和目标颜色。源颜色我们可用S表示,指的是由片元着色器产生的颜色值:目标颜色我们用D表示,指的是从颜色缓存中读取到的颜色值。对对他们进行混合后得到的输出颜色,我们用O来表示,他会重新写入到颜色缓存中,需要注意的是,当我们谈及混合中的源颜色目标颜色和输出颜色时,他们都包含了RGBA四个通道的值,而并非仅仅是RGB通道。
想要使用混合,我们必须开启它。在Unity中,当我们使用Blend命令时,除了设置混合状态以外也开启了混合。但是,在其他图形api中我们是需要手动开启的。
混合操作
常见的混合类型
通过混合操作和混合因子命令的组合,我们可以得到一些类似Photoshop混合模式中的混合效果:
//正常,即透明度混合
Blend SrcAlpha OneMinusSrcAlpha
//柔和相加
Blend OneMinnusDstColor 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
双面渲染的透明效果
虽然上面使用Min和Max混合操作时仍然设置了混合因子,但实际上他们并不会对结果有任何影响,因为Min和Max混合操作回忽略混合因子。另一点是,虽然上面有混合模式并没有设置混合操作的种类,但是他们默认就是使用加法操作,相当于设置了BlendOpAdd。
在显示生活中,如果一个物体是透明的,意味着我们不但能透过她看到其他物体还能看到她的内部结构。在前面无论是透明度测试还是透明度混合,我们都没有办法观察到正方形内部以及背面的形状,导致物体看起来就像是只有半个一样。这是因为默认情况下渲染引擎剔除了物体背面的渲染图元,只渲染物体的正面。如果我们想要得到双面的渲染效果,我们可以使用Cull命令来控制需要剔除那个面的渲染图元。
Cull Back Front Off
如果设置为Back,那么那些背对摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态:如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染:如果设置为Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是特殊效果,例如这里的双面渲染的透明效果,通常情况是不会关闭剔除功能的。
代码
Shader "Unity Shaders Book/Chapter 8/Alpha Test With Both Side" {
Properties {
_Color ("Color Tint", 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" }
// Turn off culling
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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 = 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);
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"
}