Shader 学习笔记
前言
这里是前言介绍。
正文
一口气解决 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 |
|
** 而这个工作流程,就是所谓的 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 | Stencil |
** 注意墙的深度缓冲要关掉,否则茶壶在做 Ztest 的时候会因为遮挡关系而被弃掉像素。**
** 接着是茶壶的 shader:**
1 | Stencil |


茶壶被透明墙遮挡住的部分,因为其 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 的图层混合里找到相同的效果。**


to be continued…