透明效果(二)
本文最后更新于:2025年4月17日 下午
开启深度写入的半透明效果
在上一篇中,我们给出了一种由于关闭深度写入而造成排序错误的情况。一种解决办法是使用两个 Pass 来渲染模型:第一个 Pass 开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入到深度缓冲中;第二个 Pass 进行正常的透明度混合,由于上一个 Pass 已经得到了逐像素的正确的深度信息,该 Pass 就可以按照像素级别的深度排序结果进行透明渲染。但这个方法的缺点在于,多使用一个 Pass 会对性能造成一定的影响。
1 |
|
这个新添加的 Pass 的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元。因此,Pass 的第一行开启了深度写入。在第二行,我们使用了一个新的渲染命令——ColorMask。在 ShaderLab 中,ColorMask 用于设置颜色通道的写掩码(write mask)。它的语义如下:
1 |
|
当 ColorMask 设为 0 时,意味着该 Pass 不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的——该 Pass 只需写入深度缓存即可。
效果如下图所示:
可以看出,使用这种方法,我们仍然可以实现模型与它后面的背景混合的效果,但模型内部之间不会有任何真正的半透明效果。
ShaderLab 的混合命令
混合除了用于透明度混合,还有很多其他用处。下面我们将更加详细地了解混合中的细节问题。
我们首先来看一下混合是如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色(destination color)。源颜色,我们用 S 表示,指的是由片元着色器产生的颜色值;目标颜色,我们用 D 表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用 O 表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了 RGBA 四个通道的值,而并非仅仅是 RGB 通道。
想要使用混合,我们必须首先开启它。在 Unity 中,当我们使用 Blend(Blend Off 命令除外) 命令时,除了设置混合状态外也开启了混合。但是,在其他图形 API 中我们是需要手动开启的。例如在 OpenGL 中,我们需要使用 glEnable(GL_BLEND) 来开启混合。但在 Unity 中,它已经在背后为我们做了这些工作。
混合等式和参数
混合是一个逐片元的操作,而且它不是可编程的,但却是高度可配置的。也就是说,我们可以设置混合时使用的运算操作、混合因子等来影响混合。那么,这些配置又是如何实现的呢?
现在,我们已知两个操作数:源颜色 S 和目标颜色 D,想要得到输出颜色 O 就必须使用一个等式来计算。我们把这个等式称为混合等式(blend equation)。当进行混合时,我们需要使用两个混合等式:一个用于混合 RGB 通道,一个用于混合 A 通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子。在默认情况下,混合等式使用的操作都是加操作(我们也可以使用其他操作),我们只需要再设置一下混合因子即可。由于需要两个等式(分别用于混合 RGB 通道和 A 通道),每个等式有两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),因此一共需要 4 个因子。下表给出了 ShaderLab 中设置混合因子的命令。
命令 | 描述 |
---|---|
Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色(该片元产生的颜色)会乘以 SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以 DstFactor,然后把两者相加后在存入颜色缓冲中 |
Blend SrcFactor DstFactor, SrcFactorA DstFactorA | 和上面几乎一样,只是使用不同的因子来混合透明通道 |
可以发现,第一个命令只提供了两个因子,这意味着将使用同样的混合因子来混合 RGB 通道和 A 通道,即此时 SrcFactorA 将等于 SrcFactor,DstFactorA 将等于 DstFactor。下面就是使用这些因子进行加法混合时使用的混合公式:
$$
O_{rgb} = SrcFactor \times S_{rgb} + DstFactor \times D_{rgb}
$$
$$
O_a = SrcFactorA \times S_a + DstFactorA \times D_a
$$
那么,这些混合因子可以有哪些值呢?下表给出了 ShaderLab 支持的几种混合因子。
参数 | 描述 |
---|---|
One | 因子为 1 |
Zero | 因子为 0 |
SrcColor | 因子为源颜色值,当用于混合 RGB 的混合等式时,使用 SrcColor 的 RGB 分量作为混合因子;当用于混合 A 的混合等式时,使用 SrcColor 的 A 分量作为混合因子 |
SrcAlpha | 因子为源颜色的透明度值(A 通道) |
DstColor | 因子为目标颜色值。当用于混合 RGB 通道的混合等式时,使用 DstColor 的 RGB 分量作为混合因子;当用于混合 A 通道的混合等式时,使用 DstColor 的 A 分量作为混合因子 |
DstAlpha | 因子为目标颜色的透明度值(A 通道) |
OneMinusSrcColor | 因子为(1-源颜色)。当用于混合 RGB 的混合等式时,使用结果的 RGB 分量作为混合因子;当用于混合 A 的混合等式时,使用结果的 A 分量作为混合因子 |
OneMinusSrcAlpha | 因子为(1-源颜色的透明度值) |
OneMinusDstColor | 因子为(1-目标颜色)。当用于混合 RGB 的混合等式时,使用结果的 RGB 分量作为混合因子;当用于混合 A 的混合等式时,使用结果的 A 分量作为混合因子 |
OneMinusDstAlpha | 因子为(1-目标颜色的透明度值) |
使用上面的指令进行设置时,RGB 通道的混合因子和 A 通道的混合因子都是一样的,有时我们希望可以使用不同的参数混合 A 通道,这时就可以利用 Blend SrcFactor DstFactor, SrcFactorA DstFactorA 指令。例如,如果我们想要在混合后,输出颜色的透明度值就是源颜色的透明度,可以使用下面的命令:
1 |
|
混合操作
在上面涉及的混合等式中,当把源颜色和目标颜色与它们对应的混合因子相乘后,我们都是把它们的结果加起来作为输出颜色的。那么可不可以选择不用加法,而使用减法呢?答案是肯定的,我们可以使用 ShaderLab 的 BlendOp BlendOperation 命令,即混合操作命令。下表给出了 ShaderLab 中支持的混合操作。
操作 | 描述 |
---|---|
Add | 将混合后的源颜色和目的颜色相加。默认的混合操作使用的混合等式是: $O_{rgb} = SrcFactor \times S_{rgb} + DstFactor \times D_{rgb}$ $O_a = SrcFactorA \times S_a + DstFactorA \times D_a$ |
Sub | 用混合后的源颜色减去混合后的目的颜色。使用的混合等式是: $O_{rgb} = SrcFactor \times S_{rgb} - DstFactor \times D_{rgb}$ $O_a = SrcFactorA \times S_a - DstFactorA \times D_a$ |
RevSub | 用混合后的目的颜色减去混合后的源颜色。使用的混合等式是: $O_{rgb} = DstFactor \times D_{rgb} - SrcFactor \times S_{rgb}$ $O_a = DstFactorA \times D_a - SrcFactorA \times S_a$ |
Min | 使用源颜色和目的颜色中较小的值,是逐分量比较的。使用的混合等式是: $O_{rgba} = (min(S_r, D_r), min(S_g, D_g), min(S_b, D_b), min(S_a, D_a))$ |
Max | 使用源颜色和目的颜色中较大的值,是逐分量比较的。使用的混合等式是: $O_{rgba} = (max(S_r, D_r), max(S_g, D_g), max(S_b, D_b), max(S_a, D_a))$ |
其他逻辑操作 | 仅在 DirectX 11.1 中支持 |
混合操作命令通常是与混合因子命令一起工作的。但需要注意的是,当使用 Min 或 Max 混合操作时,混合因子实际上是不起任何作用的,它们仅会判断原始的源颜色和目的颜色之间的比较结果。
常见的混合类型
通过混合操作和混合因子命令的组合,我们可以得到一些类似 Photoshop 混合模式中的混合效果:
1 |
|
效果如下:
需要注意的是,虽然上面使用 Min 和 Max 混合操作时仍然设置了混合因子,但实际上它们并不会对结果有任何影响,因为 Min 和 Max 混合操作会忽略混合因子。另一点是,虽然上面有些混合模式并没有设置混合操作的种类,但是他们默认就是使用加法操作,相当于设置了 BlendOp Add。
双面渲染的透明效果
在现实生活中,如果一个物体是透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。但在前面实现的透明效果中,无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个一样。这是因为,默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的渲染图元。在 Unity 中,Cull 指令的语法如下:
1 |
|
如果设置为 Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为 Front,那么那些朝向摄像机的渲染图元就不会被渲染:如果设置为 Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,例如这里的双面渲染的透明效果,通常情况下是不会关闭剔除功能的。
透明度测试的双面渲染
实现透明度测试物体的双面渲染效果非常简单,只需要在 Pass 的渲染设置中使用 Cull 指令来关闭剔除即可。
1 |
|
如上所示,这行代码的作用是关闭剔除功能,使得该物体的所有的渲染图元都会被渲染,此时,我们可以透过正方体的镂空区域看到内部的渲染结果。效果如下图所示:
透明度混合的双面渲染
和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,而这是“一切混乱的开端”。我们知道,想要得到正确的透明效果,渲染顺序是非常重要的——我们想要保证图元是从后往前渲染的。对于透明度测试来说,由于我们没有关闭深度写入,因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然而一旦关闭了深度写入,我们就需要小心地控制渲染顺序来得到正确的深度关系。如果我们仍然只是直接关闭剔除功能,那么我们就无法保证同一个物体的正面和背面图元的渲染顺序,就有可能得到错误的半透明效果。
为此,我们选择把双面渲染的工作分成两个 Pass——第一个 Pass 只渲染背面,第二个 Pass 只渲染正面,由于 Unity 会顺序执行 SubShader 中的各个 Pass,因此我们可以保证背面总是在正面渲染之前渲染,从而可以保证正确的深度渲染关系。
1 |
|
通过上面的代码,我们可以得到下图中的效果:
参考
《Unity Shader入门精要》