Chapter 5 – Shading Basics 着色基础

5.5 透明度,Alpha与融合

我们有多种方法能够使光线穿过半透明的物体。对于渲染方面的算法,其大致能够被分为基于光线的效果和基于视野的效果。基于光线的效果,物体会使光线被减弱或者折射,也造成场景中的其他物体被照亮或者外观发生变化。基于视野的效果指的是半透明的物体自身被绘制。

本小节,我们主要学习最简单的基于视野的透明度,也就是说,位于该物体后方的物体,半透明的物体对它们来说是一个“颜色减弱剂”。更为复杂的视野或者光照效果,例如,毛玻璃,光线的折射,由于半透明物体得到厚度导致的光线减弱,以及由于视角变化所导致的反射等,将在之后的章节继续学习。

一种模拟透明度“假象”的方法被称为screen-door transparency(纱门透明)。其原理是使用与像素对齐的棋盘填充模式来绘制透明的三角形。也就是说,就像黑白相间的国际象棋棋盘那样,我们只绘制一般的三角形像素,因此位于三角形背后的物体就是部分可见的。一般来说,屏幕上的像素点非常接近,所以用户无法发现棋盘模式。这个方法的主要瓶颈是,其通常只能屏幕上的一个区域内绘制一个透明物体。例如,如果一个透明的红色物体和一个透明的绿色物体在在一个蓝色物体上方,那么只有两种颜色会显示在棋盘上。此外,50%的显示率也是棋盘模式的限制。虽然我们可以使用更大的像素遮罩来改变棋盘模式的显示率,但是这也会导致较差的显示效果。

上述技术的一个优势是其便于实现。透明物体可以在任何时刻被渲染,我们不需要考虑其渲染顺序,对硬件也不会有要求。只要使半透明物体所覆盖区域内的其它物体都是不透明的,也能够解决透明度的问题。

Enderton提出了stochastic transparency(随机透明度),其使用了子像素的纱门遮罩以及随机采样。通过使用随机的点刻模式来表示fragment的alpha覆盖率。为了时改善透明度的效果,每个像素都需要大量的采样点,与大量的内存空间。其最大的优势是,我们不在需要进行blend融合,而且抗锯齿,透明度和其他需要子像素采样的操作,都能包含在其中。

大多数透明度算法都会将透明物体的颜色与其后方的物体进行blend融合。这就需要alpha blend(alpha融合)。当一个物体被绘制到屏幕上时,每个像素都拥有RGB颜色和一个z-buffer的深度值。我们还可以为透明物体所覆盖的每一个像素定义另一个成员,其被称为alpha(α)。alpha这个值表示物体的不透明度和一个物体的fragment对于像素的覆盖率。alpha值为1.0表示物体是完全不透明的并且完全覆盖像素区域;0.0意味着像素完全不被遮蔽,例如,fragment是完全透明的。

一个像素的alpha可以表示不透明度,覆盖率。例如,一个泡沫的边界可能覆盖了四分之三个像素,0.75,且泡沫的fragment几乎是透明的,十分之九的光线会穿过泡沫进入眼睛,所以其不透明度为0.1。那么alpha值为0.75×0.1=0.075。但是,如果我们使用MSAA或者类似的抗锯齿技术,那么采样点会计算覆盖率。也就是说,四分之三的采样点会被泡沫的fragment影响。而每个采样点使用0.1作为不透明度。

5.5.1 融合顺序

为了使一个物体看上去是透明的,其需要绘制在场景的上方,且alpha值小于1.0。每一个被该物体覆盖的像素会从像素着色器中接收到RGBα。将该fragment的颜色值与原始像素的颜色值进行融合,其通常由运算符over表示:

其中cs表示透明物体的颜色(我们也称其为起点source),αs是该物体的alpha值,cd是融合之前的像素颜色(我们也称其为终点destination),co表示将透明物体与场景进行融合之后的颜色。也就是说在渲染管线会传输cs和αs,而像素原始的颜色cd将被co所替代。如果传输的RGBα是不透明的(αs=1.0),那么物体的颜色会完全覆盖像素的颜色。

例子:融合BLENDING。一个红色的半透明物体绘制到一个蓝色的背景上。假设半透明物体的RGB颜色为(0.9,0.2,0.1),背景为(0.1,0.1,0.9),而该物体的不透明度为0.6。那么两个颜色的融合等式为:0.6(0.9,0.2,0.1)+(1-0.6)(0.1,0.1,0.9),所以最后的颜色为(0.58,0.16,0.42)。

所以over运算符让我们绘制的物体看上去是半透明的。但这种算法的基础是,我们总能够透过带有透明度的物体看到位于其背后的物体。使用运算符over来模拟真实世界中薄纱的效果。在薄纱后方的物体是部分被遮蔽的——但是薄纱的线是不透明的。在实践中,较为松散的纺织物其alpha表示覆盖率,且该值随着角度的变化而变化。而我们需要知道在这种情况下,alpha模拟了材质对于像素的覆盖率。

运算符over不擅长模拟其他的透明效果,特别是带有颜色的玻璃以及塑料。在现实世界中,如果一个红色的物体位于一个蓝色物体的前方,那么会使蓝色物体看上去更暗,这是因为红色物体会反射部分原本可以穿过红色物体的光线。如下图所示,左边为红色的薄纱而右边是红色的塑料。

当我们使用over运算符进行融合时,最后的颜色将会是部分红色与部分蓝色相加。但更好的选择是将两种颜色相乘并且加上透明物体自身的反射。我们将在第十四章再来讨论这方面的问题。

另一个常用的运算符为相加融合(additive blending),如下所示:

这个融合模式适合那些发光的效果,例如光照或者闪光,期不会减弱透明物体背后的像素,只会使其更亮。但那时,这一模式并不适用于透明度模拟。对于那些多层及的半透明表面,例如烟雾或者火焰,相加融合可以映射出当前环境的颜色。

为了合理地绘制透明物体,我们需要将其绘制顺序放在不透明物体的后面。也就是说,我们需要先禁用融合,并绘制完所有的不透明物体,之后再使用融合来绘制所有的透明物体。理论上来说,我们可以一直启用融合,只要alpha值为1.0即可,但是这么做只会增加开销。

z-buffer的限制是每个像素只存储了一个物体的深度值。如果多个透明物体重叠在同一个像素,单单一个z-buffer不能够存储所有的可见物体再进行处理。当我们使用融合来表示透明的像素时,我们需要以从后至前的顺序绘制场景。不这么做的话会造成错误的结果。一种方法是对每个物体按照其中心点在视野方向上的距离进行排序。这一粗略的排序方法能够较好地解决问题,但是在不同的情况下也会造成一系列问题。第一,这个顺序只是一个估计,一个距离较远的物体,其一部分可能离camera非常近。对于互相穿插的物体,我们不可能通过其中心位置来判断哪个像素在前哪个像素在后。如下图所示。

上图中左侧的直升飞机使用z-buffer进行绘制,以随机顺序绘制该网格模型造成了严重的显示错误。右侧的直升飞机则使用了depth peeling(深度值剥离)技术,显示效果是正常的,但是需要额外的渲染通道。

但是,因为上述排序其实现较为简单且速度较快,同时不需要额外的内存空间或者GPU支持,对透明物体进行粗略的排序仍然在实时渲染中被广泛运用。如果以这种方法绘制透明物体,通常我们会关闭z-depth替代。也就是说,z-buffer的test仍然存在,但是通过深度测试的像素并不会改变z-buffer中的深度值;距离camera最近的不透明物体,其对应像素的深度值保持不变。因此,所有的透明物体都会以某种形式出现,不会随着camea的转动,改变了透明物体的距离排序使其突然消失或者出现。其他技术也能改善透明物体的表现,例如绘制透明的网格模型两次,第一次绘制网格模型中每个三角形的背面,之后绘制其正面。

我们修正了之前提到的运算符over,这样以从前至后的顺序进行融合也不会改变结果。这一融合模式被称为under运算符:

由上述等式可知,under运算符需要终点像素保持alpha值不变,而over运算符则保持起点像素的alpha值不变。换句话说,终点像素不再是不透明的,因此其需要一个alpha值,同时终点像素表示距离更近的透明表面上的像素点。under运算符类似于over运算符,其交换了终点像素与起点像素在等式中的位置。还需要注意的是,在计算融合后像素的alpha值时,我们无需考虑计算顺序,即使我们交换了终点与起点的alpha值,计算结果仍是相同的。

而上述计算alpha的等式的原理是将fragment的alpha值作为覆盖率。由于我们不知道每个fragment所覆盖像素的区域的形状,因此假设每个fragment的覆盖率都是alpha值。例如,如果αs=0.7,那么像素就被分为两个部分,0.7被起点fragment所覆盖,而0.3不被覆盖。假设终点fragment的覆盖率,αs=0.6。那么上述公式的几何意义如下图所示。

5.5.2 无关顺序的透明度

我们之前提到的under运算符能够将所有的透明物体绘制到一个独立的buffer中,之后再使用这个buffer与场景中不透明的物体进行融合,而此时我们将使用运算符overunder运算符的另一个用处是进行order-independent transparency(OIT,无关顺序的透明度)算法,其也被成为depth peeling(深度值剥离)。无关顺序,这意味着我们不需要对物体进行排序。深度值剥离的原理是使用两个z-buffer以及多个渲染通道。首先,我们使用一个渲染通道将所有表面的z-depth存储在z-buffer中,其中也包括透明物体的z-depth。在第二个渲染通道中,我们绘制所有的透明物体。如果一个物体的z-depth值与第一个z-buffer中的值相匹配,那么该物体就是距离camera最近的透明物体,而我们将其RGBα存入独立的color buffer中。如果透明物体对应的z-depth超过上一个z-buffer中的z-depth而且离camera最近,那么我们将这些像素记录到另一个color buffer中。不断持续这个过程并且使用under运算符来添加透明层。几个渲染通道之后,我们再将透明物体的color buffer与不透明物体的color buffer进行融合。

而这个算法也有一定的改进。例如,Thibieroz提出了一种算法,从后至前进行运算,其能够让alpha值立刻进行融合,这意味着我们不在需要单独的alpha通道。深度值剥离的一个问题是,我们并不知道到底需要多少渲染通道才能捕捉到所有的透明物体。硬件端的一个解决办法是,提供一个像素绘制计数器,其会告诉我们在渲染时写入了多少个像素;当没有像素会渲染时,我们的整个绘制过程就结束了。使用under运算符的优势是,最重要的透明物体层级——也就是距离camera最近的透明物体——永远会最先绘制。一般来说,每一个透明表面都会增加该像素的alpha值。如果像素的alpha值几乎为1,那么该像素几乎是不透明的,所以距离较远的物体对像素几乎不会有影响。这也是为什么这一算法由于从后至前的渲染顺序。

由于每一个透明物体层级的“剥离”都需要将所有的透明物体绘制一遍,也就是一个独立的渲染通道,整个过程较慢。Bavoil和Myers提出了双重深度值剥离(dual depth peeling),在每个渲染通道中玻璃最近以及最远的透明层级,这样就能将渲染通道的数量减半。

在我们对透明物体进行融合时最大的问题是如何将一系列算法高效地运用于GPU。1984年,Carpenter提出了A-buffer,其为另一种形式的多重采样。在A-buffer中每个绘制的三角形都会为每个屏幕单元格创建一个coverage mask(覆盖遮罩)。每一个像素都存储了一组相关的fragment列表。不透明的fragment可以剔除位于其背后的fragment,这一原理类似于z-buffer。我们存储所有透明物体的fragment。当所有的fragment列表形成之后,我们在遍历所有的fragment并且进行处理。

在GPU上创建相关联的fragment列表随着DX11暴露的新功能而变得可以实现。这需要使用DX中的UAV(unordered access view)与atomic操作。M这一算法的原理是光栅化透明表面上的每一个点并且将生成的fragment插入一个长长的数组。一个独立的指针结构体将被生成,其链接了每个fragment与其之前的fragment。之后我们进行一次独立的渲染通道,因为我们是绘制一张图片,所以每一个像素点都会由像素着色器处理。着色器将在处理每一个像素时读取所有的相对应的透明fragment。这个排序的列表之后以从后至前的顺序进行融合以得到最终的颜色。由于blend操作由像素着色器负责,所以我们可以采用不同的融合模式。

A-buffer的优势是只会存储像素所需要的fragment,就像我们在GPU端实现的链表那样。但这也是缺点,因为在渲染开始之前,我们并不知道需要多少存储空间。场景中的毛发,烟雾或者其他透明物体都会需要大量的fragment。

GPU通常拥有内存资源,例如buffer,数组等等,它们都会预先进行分配,而链表也不例外。程序员需要决定分配的内存大小,而一旦超过其容量就会造成各种显示问题。Salvi与Vaidyanathan提出了一种解决方案,multi-layer alpha blending,其使用了Intel提出的pixel synchronization(像素同步)技术。这一技术使得融合操作是可编程的,且开销小于atomics。在DX11.3中引入的rasterizer order view,这一类型的buffer允许这一类的透明度融合算法可以在任何GPU上实现。移动平也有类似的技术,其被成为tile local storage

这一方法建立在k-buffer之一理论之上,其表明最初的能被看见的一些层级应被保存并且排序,而较深的层级应该被舍弃。Maule使用了一个k-buffer并且对较深且距离camera较远的层级使用加权平均。加权总和以及加权平均的透明度技术无需考虑绘制顺序,只需要一个渲染通道,几乎所有的GPU都支持这一算法。例如,我们使用alpha值表示覆盖率,一个红色的薄纱位于蓝色的薄纱上面,我们应该看见一个红色的薄纱中穿插着一些蓝色薄纱。但是几乎不透明的物体的显示效果较差,这一类的算法只可以用于高透明度的表面与例子。如下图所示,随着不透明度的增加,物体绘制的顺序变得越来越重要。

加权总和的透明度公式如下:

其中n表示透明表面的数量,ci和αi表示一组透明度值,cd表示场景中不透明部分的颜色值。在透明度物体的渲染通道中,这个等式会运用于每一个像素。但这一方法的问题是,第一个总和可能会越界,例如,我们所生成的颜色值超过(1,1,1),而第二个综合可能是一个负值,例如,α的总和大于1。

一般来说我们更青睐加权平均的方法,因为其避免了上述两个问题:

上述公式的第一行表示绘制透明物体时两个独立buffer中的结果。每一个会影响csum的表面都是以其alpha值作为权重;几乎不透明的表面能更多地影响物体表面的颜色,而几乎透明的表面则不能影响物体表面的颜色。将csum除以αsum,我们能够得到一个加权平均的颜色值。而αavg是所有alpha值的平均。u表示终点像素(不透明的场景)经过n个透明表面后的可见度。而等式的最后一行则表示over运算符,我们可以把(1-u)作为起点像素的alpha值。

加权平均算法的限制是,如果alpha的值相同,那么它会平均地融合所有的颜色,也不会考虑其顺序。McGuire和Bavoil提出了无关顺序的透明度权重融合,其稍许改善了显示效果。在他们的公式中,到表面的距离也会影响权重,也就说距离越近对表面的影响越大。

McGuire和Mara延伸了这一算法并加入了颜色过渡的效果。正如我们之前提到的,本小节中所有的透明度算法都是对不同的颜色进行融合而不是筛选。为了呈现一个颜色筛选的效果,通过像素着色器读取不透明的物体的颜色值,之后再用透明物体的颜色值乘以其覆盖的不透明物体的颜色值,将结果存储在第三个buffer中。也就是说,这个buffer中所有的不透明物体都被透明物体“染色”了,之后在我们处理透明buffer时再使用该buffer。这个方法不同于之前的像素遮盖,颜色的投射是无关顺序的。

5.5.3 预乘Alphas和Compositing

运算符over也用于照片之间的融合或者两个渲染物体的合成。而这一过程则被称为compositing。这种情况下,每个像素的alpha值与物体的RGB颜色值一同被存储。由alpha通道所形成的图像有时候被称为matte。其展示了物体的轮廓,或者说形状。这个RGBα图像能用于与其他图像或者背景进行融合。

使用合成RGBα数据的方法时,我们需要预乘alpha(premultiplied alpha)。也就是说,在使用RGB值之前,其已经乘以了alpha值。这会使运算符over的效率更高:

其中cs‘就是预乘的起点像素通道,其替代了之前的αscs。预乘alpha也使得我们在处于blend时,不需要改变融合状态就能直接使用over运算符进行相加融合。需要注意的是,如果使用了预乘alpha,那么RGB的值一般是小于alpha的值(一般来说RGB都小于1,而且alpha也小于1)。

渲染合成图像时,一般我们会选用预乘alpha。一个抗锯齿的不透明物体,如果绘制在黑色背景上,那么其默认状态下就提供了预乘的值。假设一个白色(1,1,1)三角形的边界上某个fragment覆盖了一个像素的40%。由于抗锯齿的作用,像素值可能会被设置为0.4,也就是灰色,那么这个像素的颜色就是(0.4,0.4,0.4)。如果有alpha值,那么也是0.4,表示三角形fragment对于该像素的覆盖率。RGBα值就是(0.4,0.4,0.4,0.4),其是一个预乘的值。

另一种存储图像的方法是使用不预乘alpha(unmultiplied alpha,unassociated alpha,nonpremultiplied alpha)。也就是说RGB值不会乘以alpha值。对于上述例子中的白色三角形,不预乘的颜色将会是(1,1,1,0.4)。其优势是存储了三角形初始的颜色,但是在显示这个颜色之前,都需要将颜色值乘以存储的alpha值。在进行filtering操作或者融合操作时,我们应该使用预乘的数据,因为如果我们使用不预乘的数据,线性插值的结果会错误。

对于图像编辑软件来说,一个不预乘的alpha值可以作为照片的遮罩,且不会影响图像的原始数据。同时,一个不预乘的alpha也意味着我们可以使用颜色通道的完整精度范围。也就是说,我们在转换不预乘的RGBα时需要格外小心。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据