Chapter 6 – Texturing 贴图

6.6 Alpha 映射

alpha值能够被用于实现alpha融合或者alpha test,例如绘制植被,爆炸,距离较远的物体,等等。本小节我们将学习alpha贴图的应用,并了解其中不同的限制与解决办法。

其中一个与贴图相关的效果为decaling(贴花)。例如,你想要将一幅鲜花的图画“放在”茶壶上。但是,你只想要图画中鲜花的部分。将纹理的alpha值设为0,那么该纹理就是透明的,所以这个纹理就产生任何影响。因此,只要合理地设置decal贴图的alpha值,你就能将物体表面与decal进行融合或者直接将decal的颜色值设置为物体表面的颜色值。下图展示了如何实现decal。我们在第二十章将学习更多关于decal的知识。

上图展示了实现decal的一种方法。场景中的物体首先被绘制到frame buffer中,之后我们将绘制一个包围盒,而decal贴图将被投影到物体表面上所有位于包围盒之内的点所形成的区域。decal贴图中,最左侧的纹理是透明的,所以它不会影响frame buffer。而最右侧的黄色纹理是不可见的,这是因为该纹理所投影的点对应的frame buffer区域正好被背面剔除算法剔除了。

另一个相似的运用alpha值的例子是制作cutout。假设,你制作了一张灌木丛的decal图像,并将其运用在一个矩形上。其原理与之前的decal贴图相同,除了我们不再需要将贴图与物体表面原本的颜色进行融合,灌木丛将直接被绘制在位于其后方的物体之上。通过这种方法,我们可以使用一个矩形来绘制一个拥有复杂轮廓的物体。

在上述例子中,如果我们控制游戏中的角色绕道灌木丛的后方,那么“诡计”就被识破了,因为我们绘制的灌木丛是没有厚度的。其中一个解决办法是,拷贝一份灌木丛对应的矩形,并且沿着“树干”将其旋转90°。这样,我们就用了两个矩形来表示一个三维的灌木丛,其开销非常的小,有时我们将其成为“cross tree”。下图中,Pelzer提出了一种类似的方法,使用三张cutout来表示草丛。最左侧的两张贴图分别为图像贴图和1位的alpha通道贴图。而右侧则是两张贴图的结合。

在第十三章中,我们将学习一种被称为billboarding的技术,其只使用一个矩形进行类似的绘制。如果我们控制游戏中的player,移动到一定的高度,那么“诡计”将再一次被识破如下图所示。为了解决这个问题,我们可以通过不同的方式添加更多的cutouts。在之后的章节中我们将学习更多这方面的知识。

将alpha贴图与贴图动画相结合能够实现各种特殊效果,例如,火炬中火焰的闪烁,植物的生长,爆炸和其他的一些效果。

在使用alpha贴图绘制物体时,我们拥有多个选择。alpha融合支持透明度为浮点数,这一技术可以用于物体边缘的抗锯齿也可以绘制那些半透明的物体。但是,alpha融合需要我们先绘制不透明的物体,再以从后至前的顺序绘制那些需要融合的物体。对于上述例子中的“cross tree”,两个矩形面片个相交形成十字形,因此我们并不能简单的说某一个矩形位于另一个矩形的前方。即使我们计算出了某一个面片位于另一个面片的前方,这一类操作也是低效的。假设我们的树木由数千个类似的面片组成,那么每一帧都对其进行排序显然是不可能的。

而上述问题可以通过多种不同的方法来解决。其中的一个就是使用alpha test,如果某个fragment的alpha值低于我们设置的标准,那么在像素着色器中我们将舍弃该fragment。

if (texture.a < alphaThreshold)    discard;

上述等式中,texture.a就是该fragment在贴图中所对应的纹理的alpha值,而参数alphaThreshold是我们设置的alpha标准,其决定了哪一些fragment会被舍弃。有了该二元(可以理解为非黑即白)测试之后,绘制场景中各个物体时无需考虑其顺序,因为透明的fragment将会被舍弃。通常,我们想要舍弃alpha值为0.0的fragment。舍弃完全透明的fragment也能节省着色器的处理能力并且免去了融合的开销,同时也能避免我们错误地将z-buffer中的像素设为可见的。对于cutout来说,一般会将alpha的标准设置为一个大于0.0的小树,例如0.5或者更高。这么做能够避免绘制顺序错误所产生的一系列问题。但是,画面质量也会较低,因为起只支持两种透明度,要么完全不透明,要么完全透明。另一个解决办法是为每一个模型配置两个渲染通道——一个渲染通道针对一种cutout,其会被写入z-buffer,而另一种则针对那些半透明的采样点,该通道中的fragment不会写入z-buffer。

alpha test在magnification和minification时也会从造成问题。当alpha test用于mipmapping时,如果我们不对alpha值进行处理,那么alpha test的结果将会产生问题。下面两张图片,上部分图片中的树叶明显过于“透明”。

举个例子,假设,我们的贴图是一维的,且拥有四个alpha值,(0.0,1.0,1.0,0.0)。而经过mipmapping的平均之后,贴图将变成(0.5,0.5),而最上层的mipmap则是(0.5)。现在,假设我们使用at=0.75。当我们读取等级为0的mipmap时,原先四个纹理中有两个可以通过alpha test。但是当我们读取等级为1或者等级为2的mipmap时,所有的像素都会被舍弃,因为0.5<0.75。下图为另一个例子。

上图中,上半部分的贴图表示原始贴图的mipmap chain。而下半部分则是使用了alpha标准值为0.5的alpha test的显示效果,我们可以明显地看到,随着mipmap等级的提升,显示的内容在不断的减少。

Castano提出了一种简单的解决办法,在我们生成mipmap时,对于等级为k的mipmap,其覆盖率ck为:

其中nk表示,当mipmap的等级为k时,mipmap中纹理的数量,a(k,i)则是mipmap等级为k时,像素i的alpha值,at则是我们定义的alpha标准。这里我们假设,当a(k,i)>at为true时,其表示1,反之则为0。需要注意,k=0则表示最低的mipmap等级,也就是原始贴图。对于每一个mipmap等级,我们将知道一个新的alpha标准值ak与其对应,而不是使用at。这样的话ck等于c0。这一些列操作可以通过二分搜索来实现。最后,等级为k的mipmap中所有纹理的alpha值都将通过at/ak进行缩放。Nvidia的贴图工具对这一算法也有支持。

Wyman和McGuire提出了一种不同的解决办法,其原理为:

if (texture.a < random()) discard;

函数random返回一个范围[0,1]的值。例如,贴图中某个纹理的alpha值为0.3,那么该纹理对应的fragment被舍弃的概率就是30%。这是一种给予每单一像素的一种随机透明度形式。在实践中,函数random将被hash函数替代,以避免部分高频率的噪点:

float hash2D(x,y) { return fract(1.0e4*sin(17.0*x+0.1*y) * (0.1+abs(sin(13.0*y+x)))); }

三维的hash为:float hash3D(x,y,z) { return hash2D(hash2D(x,y),z); },其返回一个范围为[0,1)的值。hash函数的输入参数为物体本地空间的坐标并除以物体本地坐标在屏幕空间内最大的导数,之后再进行clamp。该技术中的alpha值会随着距离进行衰减,也就是说,如果我们距离物体足够近,就不会有任何随机效果。该技术的好处是,以平均的角度来看,每一个fragment都是正确的,而Castano的方法则为每一个等级的mipmap创建了一个ak。但是,这个值很可能会随着mipmap等级的改变而改变,这会影响显示的质量可能还会需要美术进行调整。

在贴图处于magnification时,Alpha test可能会造成波纹的效果,此时我们需要以距离来与计算alpha贴图(我们将在第十五章中详细地进行学习)。

Alpha to coverage(alpha转换为覆盖率),其类似于transparency adaptive antialiasing(透明度适应抗锯齿),将fragment的alpha值当作是覆盖率,意味着一个像素内有多少个采样点被覆盖。这一理念类似于我们在5.5小节学习的纱门透明。假设,每一个像素拥有四个采样坐标,同时一个fragment覆盖一个像素,但是其拥有25%的透明度(75%的不透明度)。而AlphaToCoverage将使fragment完全不透明,但是其只覆盖四个采样点中的三个。这一技术可以用于重叠的树叶或者草丛cutout贴图。由于每一个采样点都会被绘制为完全不透明的,离camera最近的树叶将遮住其后面的物体。而我们也不需要对半透明的物体进行排序,因为我们并不会进行alpha融合。

AlphaToCoverage较为适合抗锯齿alpha test,但是也会造成显示错误。例如,如果两个alpha融合的fragment拥有相同的alpha覆盖率,那么距离camera更近的fragment永远会覆盖其后方的fragment,这样就无法体现alpha融合的效果。Golus提出了使用着色器的内置函数fwidth()来使贴图的边缘更为清晰。

如果我们需要使用alpha贴图,那必须理解双线性插值将如何影响颜色值。假设两个纹理相邻:rgbα=(255,0,0,255)是一个不透明的红色纹理,rgbα=(0,0,0,2)是一个与其相邻的几乎为透明的纹理。那么两个纹理中间处的rgbα是多少呢?如果只进行简单的插值,那么其为(127,0,0,128),这会让rgb表现为一个暗红色。但是,这个实际的结果并不应该是暗红色,其表示红色被alpha值预乘(premultiplied,也就是说插值后的纹理如果不进行预乘,其应该是(255,0,0,128),也就是一个半透明的红色)。如果我们对alpha值进行插值,需要保证在插值前,将要被插值的颜色已经预乘alpha值了。例如,我们将之前相邻的纹理设为(0,255,0,2),一个几乎透明的绿色。这个颜色还未被alpha值预乘,在插值后,就会变为(127,127,0,128)——淡绿色突然转变为黄色。如果我们将这个纹理进行预乘,那么其将变为(0,2,0,2),那么插值后的颜色将变为(127,1,0,128)。显然,后面的计算结果更有现实意义。

如果我们不将插值后的rgbα值作为一个预乘的值,那么会导致decal或者cutout的边缘(透明处与不透明处的交界)会变得较暗。上述例子中,如果并未将“暗红色”当作一个预乘值,那么贴图的边缘就会变黑。而最好的处理方法就是在双线性插值前进行预乘,WebGL支持这一特性。但是,双线性插值一般由GPU负责处理,因此在着色器中我们只有等函数返回纹理值之后才能对其进行处理。有些文件格式,例如PNG,并不会对数据进行预乘的处理,因为这将降低颜色的精度。而这两个因素将造成alpha贴图边缘较暗的错误。一个常用的解决办法是对cutout图像进行预处理,将所有透明纹理的颜色设置为周边不透明纹理的颜色,而不是黑色,这样我们在生成mipmap时也不会造成这一错误。需要注意的是,在生成带有alpha值的mipmap时,我们需要使用预乘的值。

留下评论

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