在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值外,它还有另一个属性--透明度。
Unity中常用两种方式来实现透明效果:第一种是透明度测试,这种其实无法得到真正的半透明效果;另一种是透明度混合。
从渲染顺序看,对于不透明(opaque)物体,我们不需要考虑它的渲染顺序,因为深度缓冲区(depth buffer)已经帮我们处理好了,它会根据片元深度值和深度缓冲中的值作比较(开启深度测试情况下),当其值距摄像机更远时,则不会渲染,否则会覆盖掉颜色缓冲中的像素,并把深度值更新到深度缓冲中(开启深度写入情况下)。
但要实现透明效果就不能这么做了,因为当使用透明度混合时,我们关闭了深度写入(ZWrite).
透明度测试和透明度混合的基本原理如下:
透明度测试:只有一个片元的透明度不满足条件(通常是小于某个阙值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响。否则会被当做不透明处理,进行深度测试,深度写入等。所以透明度测试是不需要关掉深度写入的。除了会舍弃片元,其实和不透明物体渲染差不多。
透明度混合:这种方法可以得到真正的半透效果。它会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色进行混合,得到新的颜色。这里需要注意,透明度混合需要关闭深度写入。但没有关闭深度测试,所以当混合渲染一个片元时,它还是会比较它的深度和深度缓冲中的深度,如果它的深度值离摄像机更远,则不再进行混合操作。所以当不透明物体在透明物体前面时,先渲染了不透明物体,它仍可以遮挡主透明物体。所以对于透明度混合来说,深度缓冲只是可读的。
渲染顺序很重要
为什么透明度混合要关闭深度写入呢?如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被看到的,但由于深度测试时该半透明表面离摄像机更近,导致后面的表面会被剔除,那就无法透过半透明表面看到后面物体了。但是这样也同时破坏了深度缓冲的工作机制,所以渲染顺序就非常重要了。
根据上图我们看看不同的渲染顺序会有什么结果:
1、先渲染B,再渲染A,由于不透明物体开启了深度测试和写入,所以B会先写入颜色缓冲和深度缓冲。然后渲染A,深度测试发现A离摄像机更近,则用A的透明度来和颜色缓冲中的B的颜色混合,得到正确结果。
2、先渲染A,再渲染B,A是半透,则先写入颜色缓冲,但不会写入深度缓冲。再渲染B,由于深度缓冲没有值,则B直接写入深度缓冲,进而直接写入颜色缓冲,则覆盖了A,这个效果不是我们想要的。
所以我们应该在不透明物体渲染完之后再渲染半透物体。
那么如果都是半透物体,渲染顺序还重要吗?答案是肯定的。
1、先渲染B,再渲染A,则B先写入颜色缓冲,然后A会和颜色缓冲中的B进行混合,得到正确结果。
2、先渲染A,再渲染B,则A先写入颜色缓冲,随后B会和A进行混合,这样混合结果就反过来了,得到错误结果。
基于以上说明,渲染引擎一般会先对物体进行排序,再渲染。常用方法是:
1、先渲染所有不透明物体,并开启它们的深度测试和写入。
2、把半透明物体按它们离摄像机远近排序,然后按照从后往前的顺序渲染,开启深度测试,关闭深度写入。
下面我们看看Unity的渲染顺序:
Unity提供了自己的渲染队列(render queue),可以用SubShader的Queue标签来决定模型属于哪个渲染队列。内部用一系列整数索引来表示每个渲染队列,索引号越小越早被渲染。如下表:
因此,如果用透明度测试实现透明效果,可以设置如下标签:
SubShader{
Tags { "Queue" = "AlphaTest" }
Pass {
...
}
}
同样,透明度混合如下:
SubShader{
Tags { "Queue" = "Transparent" }
Pass {
ZWrite off
...
}
}
透明度测试
通常,我们会在片元着色器中用clip函数来进行透明度测试。
clip是Cg中的一个函数,它的定义如下:
函数:void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float1 x); void clip(float x);
参数:裁剪时使用的标量或矢量条件。
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。它等同于如下代码:
void clip(float4 x)
{
if (any x < 0)
discard;
}
下面用一张透明纹理,实现透明度测试效果,先看透明纹理:
透明度测试图如下:
准备工作:
(1)新建场景,命名为Scene_8_3,默认情况下场景中包含一个摄像机和一个平行光,并使用了内置的天空盒。在Window->Rendering->Lighting Settings->Lighting->Skybox中去掉场景中的天空盒子。
(2)新建一个材质,命名AlphaTestMat。
(3)新建一个UnityShader,命名为AlphaTest。把这个shader赋值给上面的材质。
(4)创建一个立方体cube,并把上面的材质赋值给该模型。创建一个平面,使平面位于立方体下面。
(5)保存场景。下面直接上代码。
Shader "Unlit/AlphaTest"
{
Properties
{
_Color("Main 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"
//为了和Properties语义块中声明的属性建立联系,
//需要定义和各个属性类型相匹配的变量
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff; //由于_Cutoff的范围在[0,1],因此可以使用fixed精度来存储它。
//定义顶点着色器的输入和输出结构体,接着定义顶点着色器:
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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);//Alpha test
//if((texColor.a - _Cutoff) < 0.0){
// discard;
// } //Equal to
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"
}
1)上面的代码,为了在材质面板中控制透明度测试时使用的阙值,在Properties语义块中声明一个范围在[0,1]之间的属性_Cutoff:
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
_Cutoff 参数用于决定我们调用clip进行透明度测试使用的判断条件。它的范围是[0,1],因为纹理像素的透明度就在此范围。
2)SubShader语义块中定义了一个Pass语义块:
我们知道渲染顺序的重要性,在Unity中透明度测试使用的渲染队列是名为AlphaTest的队列,因此我们要把Queue标签设置为AlphaTest。它的顺序位于不透明物体和半透的值之间。
RenderType标签可以让Unity把这个Shader归入提前定义的组(TranparentCutout)中,以指明该Shader是一个使用了透明度测试的Shader。RanderType标签通常被用于着色器替换功能。IgnoreProjector设置为True,该Shader不会受投影器(Projectors)的影响。
通常使用了透明度测试的shader都应该在Subshader中设置这三个标签。
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
LightMode标签是Pass标签中的一种,用于定义该Pass在Unity中的光照流水线中的角色。只有定义了正确的LightMode,才能正确得到一些Unity的内置光照变量,例如_LightColor()。
3)使用CGPROGRAM和ENDCG来包围住Cg代码片,来定义重要的顶点着色器和片元着色器代码。
为了使用Unity内置的一些变量,如_LightColor0,需要包含进Unity的内置文件Lighting.cginc
#include "Lighting.cginc"
4)有关透明度测试的代码:
clip(texColor.a - _Cutoff);
clip函数的定义,它会判断它的参数,即texColor.a - _Cutoff是否为负数,如果是就会舍弃该片元的输出。即texColor.a 小于材质参数_Cutoff时,该片元就会产生完全透明的效果。
被注掉的代码,替换clip函数效果是一样的。
if (texColor.a - _Cutoff < 0.0) {
discard;
}
5)为Unity Shader设置合适的Fallback,使用内置的Transparent/Cutout/VertexLit来作为回调Shader。不仅能保证在我们编写的Subshader无法在当前显卡上工作时可以有合适的代替Shader,还可以保证使用透明度测试的物体可以正确地向其他物体投射阴影。
6)材质面板中的Alpha cutoff参数用于调整透明度测试,
我们控制_Cutoff的值,可以得到不同的效果:
测试效果图如下: