前言

这里是前言介绍。

正文

一口气解决RenderQueue、Ztest、Zwrite、AlphaTest、AlphaBlend和Stencil

知道吗,如果只是想要实现Xray效果的话,其实并不难。

实现上图的效果,原理就是对角色画两次。第一次是被遮挡住的效果(半透明、单色),第二次是正常的效果(为了简化这里使用unlight只显示贴图)

这两个pass最大的区别,在于使用不同的Ztest(深度测试)。但是这一次我决定不仅仅只写关于Ztest的问题。反正我已经决定对抗懒癌晚期,那就干脆一口气把RenderQueue、Ztest、Zwrite、AlphaTest、AlphaBlend、StencilTest这些烂七八糟的东西都拎出来写一遍,因为这些东西有很多地方都是相通的,一起说明白反而省些力气。

不过说实话,这些东西确实是有点麻烦。我尽自己最大的努力去把这些东西说明白。但是鉴于个人能力实在有限,如果有哪里说得不对或者不清楚,还请见谅。

如上图,现在有三个多边形分别是红色盒子绿色盒子和蓝色盒子,在镜头里红色的盒子在最前面(距离摄像机最近),所以盖住了其他两个颜色的盒子。

按照我们的生活常识,显示最前面的红盒子这样的结果是再正常不过了。可是计算机并不存在所谓的“人类的常识”,它只依靠数学的方法去处理问题。而如何判断谁在前谁在后,这个问题却并非那么简单,并且很容易让人陷入混乱。因为这牵扯到Ztest(深度测试)ZWrite(深度写入或者叫深度缓存)和RenderQueue(渲染序列)。

如果是2D的话,只需要一个Zindex就可以确定Sprite之间的前后(覆盖)关系。RenderQueue(渲染序列)和这个Zindex的概念很像,都是直截了当指定了一个渲染的顺序。 关于RenderQueue可用的标签,有:

Background:1000

Geometry:2000

AlphaTest:2450

Transparent:3000

Overlay:4000

(写起来的样子是这样的:”Queue” = “TransParent”)

数字越大的物体,其渲染顺序就越靠后,就会遮住数字小的物体。从名字里也能看得出来,BackGround自然是那种最先渲染然后被所有东西覆盖掉的东西(比如天空盒)。而像Overlay这样的东西在绝大部分物体之后渲染,适合用来制作UI。

值得注意的是半透明物体(Transparent Objects)的渲染顺序十分靠后。一般情况下是在所有非半透明物体渲染之后,再渲染半透明物体。至于其原因等稍后再说明。

除了使用默认的标签之外,还可以更详细指定渲染序列,写起来大概是这样的: “Queue” = “Geometry+1” 。这样这个物体会在所有Geometry渲染之后再渲染,顺序增加了一个“身位”。如果是”Queue” = “Geometry+5000” ,那可就是比Overlay还靠后,绝对是最最后渲染的东西,理论上覆盖在一切东西之上。

听起来似乎很简单,好像我们已经拿到了一把万能钥匙,可以随意控制那个小小3D世界里的所有一切。然而进度条告诉你事情并没这么简单(雾)。

因为显卡既不允许你用这么简单粗暴的方式控制渲染结果,实际上你也没法用简单的Queue值来确定物体渲染的前后关系。

试想一个大场景里动辄成千上万的物体,你如何去一个一个指定他们的RenderQueue?即便你真的这么做了,一旦镜头转个180°是不是就全错了?更不要提每一帧都在变换位置的角色。就是神仙也不可能预知他们所处的位置到底应该是渲染序列的哪一个位置。

这一点和2D游戏有着本质上的区别。在2D游戏里指定ZIndex的做法在3D游戏里肯定是走不通的。

所以在大多数情况下(除了制作UI和天空盒之外),这个RenderQueue并没有什么卵用。我用几张图来具体说明。

如上图,正常情况下这三个盒子都是”Queue” = “Geometry”。因为是“正常情况”,所以显示的效果肯定是正确的(红色的盒子挡住其他两个,同时绿色盒子挡住蓝色盒子)。但是打开FrameDebugger你会发现,渲染的顺序是很混乱的。也许是因为做测试的时候改动过RenderQueue。现在莫名其妙的是先中间后两边。

关于非透明物体渲染的排序问题,我在这里多说两句。3D实时渲染性能消耗的两个重要部分是CPU和GPU。如果想节省GPU的时间,就要在渲染之前计算一次渲染顺序,这样在Ztest之后就,被遮挡的部分就不会进入fragment shader;反之想要解放CPU的负担,就不要对渲染物体进行排序(排序这个东西大家都懂的)。当然这样会多次渲染被遮挡的像素。

在Unity3d文档里,我找到了关于控制非透明物体渲染顺序的API,其描述如下:

在我的印象当中以前是没有Camera.opaqueSortMode这个东西的,估计是新版本后加入的(我的5.4.0版本已经比较老了)。大家可以根据自己游戏性能的考虑去做优化。

如上图,当我们强行让绿色盒子的RenderQueue发生改变(“Queue”=”Geometry+1”),这样绿色盒子的渲染序列变为最后渲染,然而实际的效果依然没有改变,红色盒子一如既往地盖住了绿色盒子(哪怕红色盒子是在绿色盒子之前就渲染出来的)。

RenderQueue之所以只决定了物体的渲染顺序,却没能决定物体的渲染结果,是因为显卡在渲染的时候,更多的是依靠深度测试(Ztest)来进行判断。

Ztest的工作原理是这样的(假设这3个盒子是屏幕上的3个像素点):

Step1:显卡按照渲染顺序先画出了蓝色盒子的像素(渲染的每一个步骤都可以在FrameDebugger里看到,真是方便)

在画蓝色盒子的像素的时候,除了RGB三个颜色的值以外,显卡还会把这个像素与当前镜头的距离记录下来(这里记录为z1)。与背景相比,蓝盒子显然距离镜头更近,即z1<∞。按照“默认”的做法(注意在这个例子里我一直强调是在“默认”的情况,或者“默认”的做法),画出蓝色的盒子,并且将摄像机在这个像素上的深度值替换为z1。

Step2:接下来按照渲染顺序,开始渲染红色的盒子。

当然红色盒子也有一个深度值(记录为z2)。这个时候显卡会用z2和摄像机在当前像素的深度值z1进行比较,发现z2<z1(因为红色盒子距离镜头比较近)。于是按照“默认”的做法画出红色的盒子,并且将摄像机当前像素值更新为z2。

Step3:接下来按照渲染顺序,开始渲染绿色的盒子。

虽然这张图和上一张很像,但是注意这个时候渲染的是被“神隐”的绿色盒子

当渲染绿色盒子的时候,情况就发生了变化。我们知道绿色盒子之所以最后渲染,是因为我们强行改变了绿色盒子的渲染顺序(“Queue” = “Geometry+1”)。但是绿色盒子距离摄像机的距离是大于红色盒子的。

所以当渲染绿色盒子的时候,其深度值(记录为z3)必然会比当前像素的深度值z2大(z3>z2,和上一步完全相反的情况)。于是显卡按照“默认”的做法,扔掉了绿色盒子的像素,并且保持当前像素值为z2。其结果就是看起来绿色盒子完全被红色盒子遮挡住了(哪怕它是最后渲染出来的物体)。

这一套流程走下来我们不难看出,所谓“默认”的工作原理(注意我再次强调是“默认”),就是当一个物体像素的z值小于当前镜头在该位置像素的深度值时,画出该物体的这个像素,并且将这个较小的z值更新为当前镜头在这个像素上的深度值。

反之,当一个物体的像素的z值大于当前镜头在该位置像素的深度值时,不画出该物体的这个像素,并且保留摄像机在这个像素上的深度值。

说起来实在是拗口,也不知道各位是否能看明白。反正我是尽力了。如果非要打个比方来说,我想和当初学C语言的时候进行数字排序的做法差不多。不知道各位同学是不是看起来很怀念呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h> 
void main()
{
int a[15],i,j,temp;
printf("请输入十五个整数:");
for(i=0;i<15;i++)
{
scanf("%d",a[i]);
}

for(i=0;i<15;i++)
{
for(j=i+1;j<15;j++)
{
if(a[i]>a[j])
{
temp=a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
for(i=0;i<15;i++)
printf("排序后的数是:%d",a[i]);
}

而这个工作流程,就是所谓的Ztest+Zwrite。

比较新旧z值的大小,就是Ztest;之后更新摄像机每一个像素的z值,就是Zwrite。Ztest影响的是当前物体的显示;Zwrite影响的是之后渲染物体的显示。

可以看出来如果不进行Zwrite更新镜头的z值,那么Ztest的时候就会出现不正常的结果(完全不知道前面渲染出来的物体的深度,只能完全依赖RenderQueue);而Zwrite是否更新摄像机在当前像素上的z值,根据两个条件:

一是要看是否允许进行Zwrite(默认是Zwrite On。当然很多时候我们会手动关掉Zwrite,即Zwrite Off);二是要看Ztest是否通过,只有通过了ZWrite才会更新新的z值。

请务必注意这里:z值是否更新并不在于物体在该像素上的z值比摄像机在该像素上的z值小。而在于是否通过Ztest。只不过在默认的情况下,通过Ztest的条件是小于等于。如果Ztest的条件改变,那么Zwrite写入的新值就未必比原来的值小(关于Ztest的条件马上就会提到)。

Zwrite的概念相对简单,无非就是根据条件,对一个变量进行反复地赋值。比较有意思的Ztest。在三个盒子的例子里,我一直都在强调“默认”两个字。那么默认是什么呢,就是Zwrite On + Ztest On。Zwrite就两种情况(On或者Off)。而对于Ztest来说,条件就要丰富得多得多。Ztest的条件总共有如下几种:

Less (当物体的这个像素的Z值小于当前摄像机在这个像素上的Z值,则通过Ztest)

LEqual(条件变为小于等于)

Greater(条件变为大于)

GEqual(条件变为大于等于)

Equal(条件变为相等)

NotEqual(条件变为不相等)

Always(Ztest永远通过)

Never(Ztest永远不通过)

Off(等同于 ZTest Always)

On(等同于ZTest LEqual)

ZTest LEqual也就是上面一直提到的“Ztest默认工作的原理”。当不写明Ztest的处理方式的时候,ZTest的通过条件LEqual。因此我们就总能看到距离摄像机近的物体(Z值小)盖住了距离摄像机远(Z值大)的物体,这样“理所当然”的效果。

有意思的是当我们相要搞些事情的时候,就可以利用ZTest那些非默认的选项。当物体被遮挡住的时候(即Ztest Greater),原本是看不见的。但是Xray的效果不就正是要看见原本看不见的东西么?

所以Xray效果的第一个pass。我使用以下的“黑科技”:

Blend SrcAlpha OneMinusSrcAlpha说明我们要用alpha blend的方式进行渲染(关于Alpha Blend后面会提到)。Ztest Greater意味着我就是要处理z值大于摄像机z值的情况(只有在别的物体后面z值才会比较大,也就是说只有实际上被别的物体挡住的时候,才会用这种方式渲染)。同时关掉Zwrite。

关闭Zwrite是比较重要的一步,开着Zwrite会把错误的z值(比较大的z值)更新上去。正如前面特别强调的,Zwrite的条件之一是通过Ztest。这一次Ztest的条件是Greater,所以通过Ztest以后z值是比原来大的,更新上去以后会对其他物体的深度判断造成影响,关于这一点我们马上举例说明。

第一个pass效果如下:

我们看到,Z值比较小的像素(即未被遮挡住的像素),反而因为没有处理Ztest Lequal的Pass而无法显示出来。

接下来就是第二个Pass。我们使用新的Ztest条件:

其实这就是刚才我们一直所说的“默认情况”。换句话是其实Zwrite On 和 ZTest LEqual完全可以不用写。效果如下:

那么问题来了,如果我们在第一个pass中打开Zwrite会出现什么结果呢?

第一个Pass打开Zwrite的效果如下:

无论是否被遮挡,人物都会显示成Pass2的效果(而且还有明显得错误)。

我们利用刚才获知的原理来分析一下。在Pass1通过Ztest之后,因为打开了Zwrite,所以将角色在Pass1阶段渲染出来的像素的深度值写入到屏幕当前的深度值。注意这个深度值是大于墙的像素的深度值的,但是依然被写进镜头的深度当中。

当来到Pass2时,Ztest的条件是LEqual(小于等于)。因为当前摄像机中该像素的深度值就是角色身上像素的深度(因为上一步通过Zwrite已经写入)。所以完全符Equal(相等)的条件。于是Pass2的像素成功通过ZTest并被画出来,Pass1画出的像素自然就被Pass2覆盖掉了。

有兴趣的朋友也可以在Pass2中试一试,当Ztest的条件是Less的时候会出现什么效果。这里就不一一举例了。

以上是关于ZTest、Zwrite和RenderQueue三个容易产生混乱的概念。下面又是一个类似的概念:Stencil(模板)。

Stencil和深度一样,是写进buffer里的一个数值(Z buffer和Stencil Buffer这两个词你应该听过很多次了)。

Stencil的工作原理和Ztest+Zwrite很相似,但是灵活性更高一些。关于Stencil的一些具体例子和讲解,网上有很多。我这里的重点就不放在实际例子上,而是关于模板和深度这两个东西在用法和原理上的异同。

关于Ztest+Zwrite我已经提到过很多次了,最简单的理解就是“比较”+“写入”。如果你真的对其原理理解得非常好,那么搞定Stencil就没有任何问题。

在Unity3D里面并不存在“Stencil Test”和“Stencil Write”这两个字眼儿。Stencil就是一个过程,同时包含了“比较”和“写入”两个步骤。

Stencil的完整语法:

stencil{

Ref referenceValue

ReadMask readMask

WriteMask writeMask

Comp comparisonFunction

Pass stencilOperation

Fail stencilOperation

ZFail stencilOperation

}

具体详尽的用法写起来太麻烦(我实在是怕麻烦怕得要死),我就稍微总结一下:总的来说你只要关注Ref\Comp\Pass三个关键词。再稍微复杂一点儿的情况,你可能需要用到Fail\Zfail。最后在需要更复杂的判断的时候,你也许会需要用到那两个Mask。

我们再回顾一下Ztest+Zwrite的原理。获取Z值->测试(比较)Z值->写入新的Z值(如果通过测试)。

我们假定Stencil也有一个值叫Ref值。那么Stencil的用法也实在是看着眼熟:获取Ref值->测试(比较)Ref值->写入新的Ref值(如果通过测试)。

说到底这俩玩意儿的区别,就是在第一步,获取当前物体在这个像素上的这个变量。

Z值是根据像素到摄像机的距离算出来的,不会因为你的个人意愿而改变;S值是你可以随便填的(是的随便填,想写几就写几,范围0-255)。

这样一来Stencil可以帮助你突破Ztest所带来的限制,用更灵(jian)活(dan)便(cu)捷(bao)的方式来控制渲染效果。

Ref就是写入这个像素的Ref值,正如我之前提到的想写几就写几完全看心情(所以我一直都认为叫Stencil Buffer模板缓冲实在是有点唬人的感觉。改成“看哪个数字顺眼就用哪个数字比大小”更贴切一些)。

Com是进行Test的条件,当你看到一大堆Less\LEqual\Greater\GEqual\Equal\NotEqual\Always\Never这样的字眼儿,是不是感到非常的眼熟?这一步比较的过程和Ztest完全一样。

Pass和Zwrite简直就是一个妈生出的俩个孩儿。区别就是这个小哥比他兄弟花样儿多点。Zwrite无非就是写入或者不写入(On or Off)。Pass甚至还可以控制如何写入(虽然大多数情况下可能用不到)。

Pass支持的条件一览,其中Keep类似于Zwrite里的Off;Replace类似于ZWrite里的On(此图来自互联网)。

举个栗子,如下图所示,现在有一面墙和一个茶壶,按照与镜头的位置关系,墙体遮挡住茶壶的下半部分。

如果我们想要做一个如Flash里的Mask Layer效果。就可以使用Stencil来做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Stencil 
{
//摄像机在当前像素的默认stencil值是0
//设置当前物体当前像素的参考值为100
Ref 100
//永远通过stencil测试
//这个shader的唯一目的就是在这个物体所占的像素上写入stencil值100
Comp Always
//通过后(因为Comp Always所以必然会通过),将当前stencil值更新为ref的值(100)
Pass replace
//这样墙所占有的像素的Stencil值就被确定下来了
//如果有多个墙,也可以用Comp GEqual或者Comp LEqual
//来找一个最大/最小的stencil值作为当前像素的stencil值
}
Zwrite Off

注意墙的深度缓冲要关掉,否则茶壶在做Ztest的时候会因为遮挡关系而被弃掉像素。

接着是茶壶的shader:

1
2
3
4
5
6
7
8
9
10
11
12
Stencil 
{
//像素默认的stencil值应该是0
//设置茶壶在当前像素的参考值为90
Ref 90
//因为之前墙的ref值100已经写入到摄像机里,所以当前像素的ref值已经是100
//因为茶壶的Ref值(90)小于当前摄像机在该位置的像素的ref值100,测试通过
Comp LEqual
//通过后,不更新ref值
Pass keep
//这样墙在该像素上的Stencil值(100)依然是摄像机在当前像素上的人ref值
}

茶壶被透明墙遮挡住的部分,因为其Stencil值通过测试,所以被显示了出来。

当然这里存在一个潜在的问题。试想如果这两个非透明物体在渲染的时候,顺序并不是先画墙再画茶壶。其结果就会因为墙的ref值没有提前更新好,而造成了茶壶在比较的ref值的时候出现我们不期望的结果。所以说,虽然我们并没有太多注意过非透明物体的渲染顺序。但是这东西确实会在各种意想不到的地方,造成莫名其妙的显示错误。

最后就是Alpah Test 和 Alpha Blend。看到XXXTest是不是第一反应又是Test + Write这种东西。然后又是一堆Lequal、Gequal这些乱七八糟的条件。

好消息是这个世界上并不存在“Alpha Write”这种东西,并且Alpha Test也远没有之前那两个Test那么复杂;坏消息是你需要多了解一个新的概念——Alpha Blend,一个既麻烦又特别容易出问题的玩意儿。

首先一句话解决Alpha Test。与其他的Test概念相通的是:Alpha Test的运作原理也是当条件成立时,画出该像素,否则抛弃该像素。但是它的特点是无需(也无法)同镜头中同一个位置的其他像素值进行比较(自然更加无法进行写入)。

相对而言,其他的Test还需要跟别的东西比较一下,Alpha Test并不存在这个过程,它只和自己本身存在的变量进行比较,是一个非常自闭的过程。

因为AlpahTest有以上的特性,所以在Unity的shader里并没有Alpha Test On\Off这样的关键字。Alpha Test可用的函数只有两个,一个是clip一个是discard。clip(x)函数的变量x必须小于0才会通过测试。比如说简单粗暴的clip(-1)就把所有像素都干掉了;而用if(){discard;}可以使用任意条件触发。相对而言discard比较灵活,但是要用到if让我很不爽。这两个函数的具体用法大家可自行百度(好吧是我懒得贴)。

一般做渐变消失的时候,会用到clip\discard。比如下图

把不断变化的时间值传入shader,来不断减小clip()函数的变量,就能做出如上的效果。当然这个效果还可以进一步改进,因为和本文无关所以就不展开了。需要注意的是在移动平台上,Alpha Test的消耗较大,属于能不用就不用的东西(就像if、for这些东西能不用尽量别用)。

如果你非要搞明白为什么简单粗暴的alpha test反而消耗大,就自己去查关于PowerVR GPUs、Deferred Tile-Based-Rendering、Early-Z等等这些知识点,对于我一个懒人来说搬运这些东西简直跟要了我的命没什么区别。

Alpha Test是一个非黑即白的过程。通过或者不通过,画出或者抛弃,简单粗暴一目了然。当然我们大多数时候并不喜欢如此粗暴的处理,毕竟人不是机器,凡事还需要温柔一点。所以我们更多的时候用的是Alpha Blend而非Alpha Test。

Alpha Blend即透明混合。我们之前提到的所有Test方式,不是你盖住我就是我盖住你,总之没有任何“和谐共处”的可能性。而Alpha Blend提供了这种可能性。根据Blend的方式不同,该物体在这个像素的rgb值会和其他物体在这个像素上的rgb进行混合。

Alpha Blend的效果,在一般意义上这就是我们理解的“半透明”。

我们之前曾经提到过,半透明的物体(也就是需要用Alpha Blend方式渲染的物体)一般来说渲染序列比较靠后(通常我们用”Queue” = “Transparent”)。道理很简单,你要和别的像素混合,那么必须要有其他像素已经画出来才行。如果透明物体被提前渲染出来,而当时还不存在后面要跟它混合的像素,自然就会出现错误。

所以难怪只有Overlay这种做UI的物体,渲染顺序会排在Transparent之后——毕竟UI是不需要和场景中的半透物体进行混合。

如图所示,当半透明物体(红色方形粒子)没有被指定渲染顺序为Transparent的时候,在混合天空盒的时候发生了明显错误。红圈是渲染粒子的部分,黑圈是渲染天空盒的部分。很明显在渲染粒子的时候,并没有渲染出来天空盒,所以也就没有混合(Blend)操作时可以用来混合的颜色。

当半透明的渲染顺序被正确指定为Transparent的时候,渲染天空盒发生在渲染粒子之前,也就是在画粒子的时候天空盒的像素就已经存在了。这样粒子就有了可以进行混合操作的颜色,因此半透明粒子与天空盒的混合效果正确。(说实话我很奇怪为什么Unity默认的天空盒渲染顺序居然不是BackGround,也许他们有他们自己的考虑吧。)

注意在谈关于Alpha Blend的时候,几乎每一个细节都和RenderQueue息息相关。这和之前的Ztest完全不同。其区别在于Ztest只关心谁盖住了谁,一旦被盖住就不再在意被盖住的像素到底是个什么样;然而Alpha Blend却需要关注任何一个画在当前位置的像素颜色,只有获得这些颜色的全部信息,才可能进行正确的混合。这也是为什么Alpha Blend的消耗很大(因为所有在该像素上的物体都要进入fragment shader进行绘制),而且常常会引发各种非常棘手的问题。

在写Unity Shader的时候,Alpha Blend有两个非常重要的语句:Zwrite Off和Blend的方式。

一般情况下我们渲染半透明物体的时候,都是Zwrite Off。

为什么一定是Zwrite Off?我们最开始说,只有打开Zwrite,才有可能进行“正确有效”的Ztest,否则所有关闭Zwrite的物体,其渲染将完全依赖于RenderQueue。

但是对于透明物体之间来说(注意是透明物体之间,而不是透明和非透明物体之间),我们需要的恰恰是不要进行有效的Ztest——因为我们的初衷就是不能让“正确”的遮挡关系产生作用。试想如果透明物体之间因为Ztest判定了“正确”的遮挡关系,而造成部分像素被显卡丢弃不画,又怎么可能产生之后混合的过程呢?

而一旦放弃Zwrite。透明物体之间的Ztest其实都是统统通过的,换言之任何一个半透明物体的像素在与其他半透明物体的像素进行Ztest的时候,将不会被认为是需要弃掉不画的像素(我再次强调,因为RenderQueue的关系,所有谈到的东西都仅限于半透明物体之间)。

来看这张图,注意粒子后面的角色和墙不一样,这个角色与粒子相同也是个半透明的物体。当Zwrite On的时候,整个渲染过程是先画了方块形的粒子(Draw Dynamic),再画的绿色的角色(那三个Draw Mesh)。当开始绘制角色的时候显卡做了Ztest,其判定这个角色被粒子遮挡住,所以像素并没有画出来。

当Zwrite Off以后,注意这个时候依然是先画出粒子再画出角色,在角色做Ztest的时候,被判定并没有被粒子遮挡(因为粒子的深度信息并没有写入,角色像素的Z值小于等于当前摄像机在当前像素上的Z值),所以角色的像素被绘制出来,并且与粒子的颜色进行了正常的混合。

你可能会问为什么墙不会被挡住,因为墙是”Queue” = “Geometry”,作为一个渲染序列靠前的物体,在画粒子的时候其像素就已经存在了。

根据上面的实例,我总结一下关于显卡的工作机制。显卡只能确定当下的像素是否可以绘制以及如何绘制。其结果可能是1、弃掉这个像素不画。2、这个像素会覆盖掉之前的像素。3、如果是Alpha Blend就和之前的像素进行混合。但是注意无论如何渲染的过程都不可能影响之前的已经被画出来的像素——显卡也许会抛弃当前的像素不画,但是绝不可能让之前画出来的像素消失掉。这个规则非常重要,请务必理解。

所以说对于Alpha Blend来说,RenderQueue非常的重要。已经画出来的像素只能被混合却不能被消除。所以基本上出问题的一定是透明物体和透明物体之间,因为他们的RenderQueue是相同的。先渲染的永远存在,而后渲染的却有可能被抛弃。

当然ZTest Off也许会解决这种因为遮挡而造成的不画像素的问题,但是相信我你绝对不会这么去做,因为会引发更多的麻烦(因为没了Ztest,就是非透明物体也不能正确覆盖住透明物体了)。

因为存在着如此“危险”的规则(之后的渲染不能改变之前的渲染),渲染的先后顺序就绝对不可能是完全随机的。和非透明物体的渲染顺序控制类似,Unity也提供了控制透明物体排序的机制。

因为透明物体之间的排序比较重要,所以我稍微多说两句。按照Unity3D的默认做法,在对透明物体在渲染之前的排序,是根据多边形中心点与摄像机的远近来比较的。比较之后显卡会从后向前对透明物体依次进行渲染。所以绝大多数情况下你看到的粒子特效,其前后遮挡关系还是没什么大问题的。

但是这么做又会引出一个新的问题——当半透明物体交叉在一起的时候,这种判断方式几乎没有任何帮助。所以当一个复杂的多边形(例如有很多部件的角色)在使用Alpha Blend的时候,经常会出现显示效果错误,也是因为这种原因。

所以从优化的角度来讲,我们一直希望尽量少用或者不用Alpha Blend,但是现在的游戏几乎到处都充斥着Alpha Blend的物体。好在现在的处理器性能比之过去实在是强了太多,这些问题似乎也渐渐地不再成为游戏开发的限制。

那么之所以我还要特意写出来,是希望大家能知道关于Alpha Blend消耗的来龙去脉。毕竟无论处理器的性能如何发展,我们做游戏还是要以能省一点儿是一点儿的态度去抠这些细节。

单以上面的例子而言,如果你对之前的讲解理解深入的话,应该知道除了关闭Zwrite这一个办法之外,也可以用指定RenderQueue的方式强行让角色先绘制出来(或者让粒子后绘制)。这种强行改变(指定)RenderQueue也能解决两个半透明物体遮挡的问题。但是正如我之前所说的,强行指定RenderQueue是一种极其不被推荐的做法。还是那句话,如果这个时候镜头转动了180°(即物体和物体之间的前后关系完全反转),强行指定RenderQueue就会造成更严重的渲染错误。

如上图,在没有关闭Zwrite的前提下, 改变粒子的渲染顺序(“Queue” = “Transparent+1” )。这样绿色的半透明角色就在粒子之前被渲染出来,红色的粒子也就有了可以进行混合的对象。

Zwrite Off虽然已经成为Alpha Blend的“标配”,但是不能进行Zwrite其实是很麻烦的。如果你认为上一个效果没毛病就万事大吉,那可就大错特错了。来看下图:

大多数时候我们当然希望第一张图的效果(打开Zwrite,遮挡住原本应该被遮挡的壶把)而非第二张图的效果(关闭Zwirte,这样该物体的任何一个像素都不会改变摄像机在该像素上的深度,就会出现无法遮挡住问题)。

很显然,在不打开Zwrite的前提下,是不可能做出第一张图的效果的。但是正如我们之前所提到的,透明物体如果不是Zwrite Off,又会引发半透明物体之间因为遮挡而无法混合的问题。这真是一个让人头疼的麻烦。

以下是官方一个例子的原理(实在搜不到了只好自己动手,惨),是目前解决半透明问题比较常规的做法。首先做一个pass进行Zwrite,然后在第二个pass里关闭Zwrite,其他不变。可以做出一个完全是剪影的半透明效果。如下图右面的茶壶。

ColorMask是指定输出通道,这里让第一个pass完全不输出任何东西,仅仅只是写入深度。这样一来茶壶就像是个非透明物体一样在屏幕上改变了当前像素的深度值。第二个pass正常绘制,在其Ztest的时候比较的就是刚刚自己留在屏幕上的Z值。这样一个完美的剪影就做出来了。

这里说点题外话。一直以来我都以为把Tag放到Pass里是可行的,直到写本文的时候我才发现只有将Tag放在Pass外面才会真正起作用。那就意味着多pass之间来回切换Tag是不可能的(或许是我理解上有问题,毕竟我刚刚才发现)。

最后要说的是混合方式。如果你用PhotoShop的话,应该对图层混合的模式并不陌生,而Blend方式其实也是一样的概念。所以关于Blend的方式,我就不过多展开了,相关资料网上很多有兴趣可以自行百度。

一般来说正常的Blend方式是:

Blend SrcAlpha OneMinusSrcAlpha

这个语法翻译成中文,大意是这个像素的颜色乘以这个像素的alpha值(SrcAlpha) + 这个像素背后的颜色 * (1 - 这个像素的alpha值)(OneMinusSrcAlpha)。

比如一个红色的像素(1,0,0,0.7),期身后的颜色是蓝色(0,0,1,1)。那么在摄像机里,这个像素最终的颜色就应该是(0.7,0,0.3,0.7)((1,0,0) * 0.7 + (0,0,1)* (1 - 0.7))。如果再出现一个半透明的物体,那就继续用这个步骤计算。

这是“正常”的方式,得到的效果是我们习惯的“默认”的效果。那么非“正常”的效果呢?半透的混合方式还有如下几种。作为比较特殊的混合方式,所有这些方式你都可以在PS的图层混合里找到相同的效果。


参考来源: https://zhuanlan.zhihu.com/p/28557283


to be continued…