第七章 透明效果(2)

@

1. 透明度测试

我们来看一下如何在Unity中实现透明度测试的效果。在上面我们已经知道了透明度测试的原理。
透明度测试:只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则就会按照普通的不透明物体的处理方式来处理它。
通常,我们会在片元着色器中使用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)为了在材质面板中控制透明度测试时使用的阈值,我们在Properties语义块中声明一个范围在[0,1]之间的属性_Cutoff:

Properties{
_Color("Main Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="white"{}
_Cutoff("Alpha Cutoff",Range(0,1))=0.5
}

_Cutoff参数用于决定我们调用clip进行透明度测试时使用的判断条件。它的范围是[0,1],这是因为纹理像素的透明度就是在此范围内。
(2)然后,我们在SubShader语义块中定义了一个Pass语义块:

SubShader{
Tags{"Queue"="AlphaTest""IgnoreProjector"="True" "RenderType"="TransparentCutout"}
Pass{
Tags{"LightMode"="ForwardBase"}
}
}

我们在前面已经知道了渲染的重要性,并且知道在Unity中透明度使用的渲染队列是名为AlphaTest的队列,因此我们需要把Queue标签设置为AlphaTest。而RenderType标签可以让Unity把这个shader归入到提前定义的组(这里就是TransparentCutout组)中,以指明该shader是一个使用了透明度测试的shader。RenderType标签通常被用于着色器替换功能。我们还把IgnoreProjector设置为True,这意味着这个Shader不会受到投影器(Projectors)的影响。通常,使用了透明度测试的Shader都应该在SubShader中设置这三个标签。最后,LightMode的标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。
(3)为了和Properties语义块中声明的属性建立联系,我们需要定义和各个属性类型相匹配的变量:

fixed4 _Color;
sampler2D _Maintex;
float4 _MainTex_ST;
fixed _Cutoff;

由于_Cutoff的范围在[0,1],因此我们可以使用fixed精度来存储它。
(4)然后我们定义顶点着色器输入和输出结构体,接着定义顶点着色器:

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(_Object2World,v.vertex).xyz;
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

上面的代码我们已经见过很多次了,我们在顶点着色器计算出世界空间的法线方向和顶点位置以及变换后的纹理坐标,再把他们传递给片元着色器。
(5)最重要的透明度测试的代码写在片元着色器中:

fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor=tex2D(_MainTex,i.uv);
//Alpha text
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 fixed(ambient+diffuse,1.0);
}

(6)最后,我们需要为这个UnityShader设置合适的Fallback:

Fallback"Transparent/Cutout/VertexLit"

和之前使用的Diffuse和Specular不同,这次我们使用内置的Transparent/Cutout/VertexLit来作为回调Shader。这不仅能够保证我们编写的SubShader无法在当前显卡上工作时可以有合适的替代Shader,还可以保证使用透明度测试的物体可以正确的向其他物体投射阴影,具体原理我们后面讲到。
材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阈值,当纹理像素的透明度小于该值时,对应的片元就会被舍弃。当我们逐渐调大该值时,立方体上的网格就会消失,如下图所示:

在这里插入图片描述
从上图可以看出,透明度测试得到的透明效果很极端——要么完全透明,要么完全不透明,它的效果往往像在一个不透明的物体上挖了一个洞。而且得到的透明效果在边缘处往往参差不齐,有锯齿,这是因为在边界处纹理的透明度的变化精度问题。为了得到更加柔滑的透明效果,就可以使用透明度混合。

2.透明度混合

透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明的物体几乎是一样的,只是在片元着色器中增加了对透明度的判断并裁剪片元的代码。而想要实现透明度混合就没有这么简单了。我们回顾之前提到的透明度混合的原理:
透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲区中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,设使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity提供的混合命令——Blend。Blend是Unity提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。下表给出了Blend命令的语义:

在本节里,我们会使用第二种语义,即Blend SrcFactor DstFactor来进行混合。需要注意的是,这个命令在设置混合因子的同时也开启了混合模式。这是因为只有开启了混合之后,设置片元的透明通道才有意义,而Unity在我们使用Blend命令的时候就自动帮我们打开了。很多初学者总是抱怨为什么自己的模型没有任何透明的效果,这往往是因为他们没有在pass中使用Blend命令,一方面是没有设置混合因子,但更重要的是,根本没有打开混合模式。我们会把源颜色的混合因子SrcFactor设为SrcAlpha,而目标颜色的混合因子DstFactor设置为OneMinusSrcAlpha。这意味着混合后新的颜色是:

在这里插入图片描述

通常透明度的混合使用的就是这样的混合命令,在后面,我们会看到更多混合语义用法。
使用和上小结同样的纹理,我们可以得到下面的效果:
在这里插入图片描述
步骤:
(1)修改Properties语义块:

Properties{
_Color("Main Tint",Color)={1,1,1,1}
_MainTex("Main Tex", 2D)="white"{}
_AlphaScale("Alpha Scale",Range(0,1))=1
}

我们使用一个新的属性_AlphaScale来代替原先的_Cutoff属性。_AlphaScale用于在透明纹理的基础上控制整体的透明度。相应的,我们也需要在Pass中修改和属性对应的变量:

fixed4  _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _AlphaScale;

(2)修改SubShader使用的标签:

SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
}

(3)与透明度测试不同的是,我们还需要在Pass中为透明度混合进行合适的混合状态设置:

Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
}

Pass的标签仍和之前的一样,这是为了让Unity能够按向前渲染路径的方式为我们正确提供各个光照变量。除此之外,我们还该把该Pass的深度写入(ZWrite)设置为关闭状态(Off),我们在之前已经讲过为什么要这样做了。这是非常重要的。然后我们开启并设置了该Pass的混合模式。如在本节开头所讲的,我们将源颜色(该片元着色器产生的颜色)的混合因子设为SrcAlpha,把目标颜色(已经存在于颜色缓冲区中的颜色)的混合因子设为OneMinusSrcAlpha,以得到合适的半透明效果。
(4)修改片元着色器:

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 fixed(ambient+diffuse,texColor.a*_AlphaScale);
}

上述代码和以前的几乎一样,只是移除了透明度测试的代码,并设置了该片元着色器返回值中的透明通道,它是纹理像素的透明通道和材质参数_AlphaScale的乘积。正如本节一开始所说,只有使用Blend命令打开混合后,我们在这里设置的透明通道才有意义,否则这些透明度并不会对片元的透明效果有任何影响。
(5)最后,修改UnityShader的Fallback:

Fallback"Transparent/VertexLit"

我们可以调节材质面板上的AlphaScale参数,以控制整体透明度。下图给出了不同AlphaScale参数下的半透明效果。
在这里插入图片描述
我们在以前解释了由于关闭深度写入带来的各种问题。当模型本身有复杂的遮挡关系或是包含了复杂的非凸格网格时候,就会有各种各样因为排序错误而产生的错误的透明效果。下图给出了使用UnityShader渲染Knot模型时得到的效果。
在这里插入图片描述
这都是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。在上面我们提出了一种解决方法就是分割网格,从而可以得到一个“质量优等”的网格。但是很多情况下这往往时不切实际的。这是我们可以想办法重新利用深度写入,让模型可以像半透明物体一样进行淡入淡出。这就是我们的下一节内容。

posted @ 2019-05-16 07:49  御坂御坂001  阅读(869)  评论(0编辑  收藏  举报