×

Alpha Test 与 Alpha Blending

前端技术网 前端技术网 发表于2024-01-08 13:41:21 浏览3311 评论0

抢沙发发表评论

一、Unity Shader-渲染队列,ZTest,ZWrite,Early-Z(转)

简介

在渲染阶段,引擎所做的工作是把所有场景中的对象按照一定的策略(顺序)进行渲染。最早的是画家算法,顾名思义,就是像画家画画一样,先画后面的物体,如果前面还有物体,那么就用前面的物体把物体覆盖掉,不过这种方式由于排序是针对物体来排序的,而物体之间也可能有重叠,所以效果并不好。所以目前更加常用的方式是z-buffer算法,类似颜色缓冲区缓冲颜色,z-buffer中存储的是当前的深度信息,对于每个像素存储一个深度值,这样,我们屏幕上显示的每个像素点都会进行深度排序,就可以保证绘制的遮挡关系是正确的。而控制z-buffer就是通过ZTest,和ZWrite来进行。但是有时候需要更加精准的控制不同类型的对象的渲染顺序,所以就有了渲染队列。今天就来学习一下渲染队列,ZTest,ZWrite的基本使用以及分析一下Unity为了Early-Z所做的一些优化。

Alpha Test 与 Alpha Blending

Unity中的几种渲染队列

首先看一下Unity中的几种内置的渲染队列,按照渲染顺序,从先到后进行排序,队列数越小的,越先渲染,队列数越大的,越后渲染。

Unity中设置渲染队列也很简单,我们不需要手动创建,也不需要写任何脚本,只需要在shader中增加一个Tag就可以了,当然,如果不加,那么就是默认的渲染队列Geometry。比如我们需要我们的物体在Transparent这个渲染队列中进行渲染的话,就可以这样写:

我们可以直接在shader的Inspector面板上看到shader的渲染队列:

另外,我们在写shader的时候还经常有个Tag叫RenderType,不过这个没有Render Queue那么常用,这里顺便记录一下:

Opaque:用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。

Alpha Test 与 Alpha Blending

Transparent:用于半透明着色器(透明着色器、粒子着色器、字体着色器、地形额外通道的着色器)。

TransparentCutout:蒙皮透明着色器(Transparent Cutout,两个通道的植被着色器)。

Background:天空盒着色器。

Overlay: GUITexture,镜头光晕,屏幕闪光等效果使用的着色器。

TreeOpaque:地形引擎中的树皮。

TreeTransparentCutout:地形引擎中的树叶。

TreeBillboard:地形引擎中的广告牌树。

Grass:地形引擎中的草。

GrassBillboard:地形引擎何中的广告牌草。

相同渲染队列中不透明物体的渲染顺序

拿出Unity,创建三个立方体,都使用默认的bump diffuse shader(渲染队列相同),分别给三个不同的材质(相同材质的小顶点数的物体引擎会动态合批),用Unity5带的Frame Debug工具查看一下Draw Call。(Unity5真是好用得多了,如果用4的话,还得用NSight之类的抓帧)

可以看出,Unity中对于不透明的物体,是采用了从前到后的渲染顺序进行渲染的,这样,不透明物体在进行完vertex阶段,进行Z Test,然后就可以得到该物体最终是否在屏幕上可见了,如果前面渲染完的物体已经写好了深度,深度测试失败,那么后面渲染的物体就直接不会再去进行fragment阶段。(不过这里需要把三个物体之间的距离稍微拉开一些,本人在测试时发现,如果距离特别近,就会出现渲染次序比较乱的情况,因为我们不知道Unity内部具体排序时是按照什么标准来判定的哪个物体离摄像机更近,这里我也就不妄加猜测了)

相同渲染队列中半透明物体的渲染顺序

透明物体的渲染一直是图形学方面比较蛋疼的地方,对于透明物体的渲染,就不能像渲染不透明物体那样多快好省了,因为透明物体不会写深度,也就是说透明物体之间的穿插关系是没有办法判断的,所以半透明的物体在渲染的时候一般都是采用从后向前的方法进行渲染,由于透明物体多了,透明物体不写深度,那么透明物体之间就没有所谓的可以通过深度测试来剔除的优化,每个透明物体都会走像素阶段的渲染,会造成大量的Over Draw。这也就是粒子特效特别耗费性能的原因。

我们实验一下Unity中渲染半透明物体的顺序,还是上面的三个立方体,我们把材质的shader统一换成粒子最常用的Particle/Additive类型的shader,再用Frame Debug工具查看一下渲染的顺序:

半透明的物体渲染的顺序是从后到前,不过由于半透相关的内容比较复杂,就先不在这篇文章中说了,打算另起一篇。

自定义渲染队列

Unity支持我们自定义渲染队列,比如我们需要保证某种类型的对象需要在其他类型的对象渲染之后再渲染,就可以通过自定义渲染队列进行渲染。而且超级方便,我们只需要在写shader的时候修改一下渲染队列中的Tag即可。比如我们希望我们的物体要在所有默认的不透明物体渲染完之后渲染,那么我们就可以使用Tag{“Queue”=“Geometry+1”}就可以让使用了这个shader的物体在这个队列中进行渲染。

还是上面的三个立方体,这次我们分别给三个不同的shader,并且渲染队列不同,通过上面的实验我们知道,默认情况下,不透明物体都是在Geometry这个队列中进行渲染的,那么不透明的三个物体就会按照cube1,cube2,cube3进行渲染。这次我们希望将渲染的顺序反过来,那么我们就可以让cube1的渲染队列最大,cube3的渲染队列最小。贴出其中一个的shader:

其他的两个shader类似,只是渲染队列和输出颜色不同。

通过渲染队列,我们就可以自由地控制使用该shader的物体在什么时机渲染。比如某个不透明物体的像素阶段操作较费,我们就可以控制它的渲染队列,让其渲染更靠后,这样可以通过其他不透明物体写入的深度剔除该物体所占的一些像素。

PS:这里貌似发现了个问题,我们在修改shader的时候一般不需要什么其他操作就可以直接看到修改后的变化,但是本人改完渲染队列后,有时候会出现从shader的文件上能看到渲染队列的变化,但是从渲染结果以及Frame Debug工具中并没有看到渲染结果的变化,重启Unity也没有起到作用,直到我把shader重新赋给材质之后,变化才起了效果...(猜测是个bug,因为看到网上还有和我一样的倒霉蛋被这个坑了,本人的版本是5.3.2,害我差点怀疑昨天是不是喝了,刚实验完的结果就完全不对了...)

**ZTest(深度测试)和ZWrite(深度写入)**

上一个例子中,虽然渲染的顺序反了过来,但是物体之间的遮挡关系仍然是正确的,这就是z-buffer的功劳,不论我们的渲染顺序怎样,遮挡关系仍然能够保持正确。而我们对z-buffer的调用就是通过ZTest和ZWrite来实现的。

首先看一下ZTest,ZTest即深度测试,所谓测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。ZTest提供的状态较多。 ZTest Less(深度小于当前缓存则通过, ZTest Greater(深度大于当前缓存则通过),ZTest LEqual(深度小于等于当前缓存则通过),ZTest GEqual(深度大于等于当前缓存则通过),ZTest Equal(深度等于当前缓存则通过),ZTest NotEqual(深度不等于当前缓存则通过),ZTest Always(不论如何都通过)。注意,ZTest Off等同于ZTest Always,关闭深度测试等于完全通过。

下面再看一下ZWrite,ZWrite比较简单,只有两种状态, ZWrite On(开启深度写入)和ZWrite Off(关闭深度写入)。当我们开启深度写入的时候,物体被渲染时针对物体在屏幕(更准确地说是frame buffer)上每个像素的深度都写入到深度缓冲区;反之,如果是ZWrite Off,那么物体的深度就不会写入深度缓冲区。但是,物体是否会写入深度,除了ZWrite这个状态之外,更重要的是需要深度测试通过,也就是ZTest通过,如果ZTest都没通过,那么也就不会写入深度了。就好比默认的渲染状态是ZWrite On和ZTest LEqual,如果当前深度测试失败,说明这个像素对应的位置,已经有一个更靠前的东西占坑了,即使写入了,也没有原来的更靠前,那么也就没有必要再去写入深度了。所以上面的ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共就是四种情况:

1.深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;

2.深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;

3.深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区;

4.深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;

Unity中默认的状态(写shader时什么都不写的状态)是ZTest LEqual和ZWrite On,也就是说默认是开启深度写入,并且深度小于等于当前缓存中的深度就通过深度测试,深度缓存中原始为无限大,也就是说离摄像机越近的物体会更新深度缓存并且遮挡住后面的物体。如下图所示,前面的正方体会遮挡住后面的物体:

写几个简单的小例子来看一下ZTest,ZWrite以及Render Queue这几个状态对渲染结果的控制。

让绿色的对象不被前面的立方体遮挡,一种方式是关闭前面的蓝色立方体深度写入:

通过上面的实验结果,我们知道,按照从前到后的渲染顺序,首先渲染蓝色物体,蓝色物体深度测试通过,颜色写入缓存,但是关闭了深度写入,蓝色部分的深度缓存值仍然是默认的Max,后面渲染的绿色立方体,进行深度测试仍然会成功,写入颜色缓存,并且写入了深度,因此蓝色立方体没有起到遮挡的作用。

另一种方式是让绿色强制通过深度测试:

这个例子中其他立方体的shader使用默认的渲染方式,绿色的将ZTest设置为Always,也就是说不管怎样,深度测试都通过,将绿色立方体的颜色写入缓存,如果没有其他覆盖了,那么最终的输出就是绿色的了。

那么如果红色的也开了ZTest Always会怎么样?

在红色立方体也用了ZTest Always后,红色遮挡了绿色的部分显示为了红色。如果我们换一下渲染队列,让绿色在红色之前渲染,结果就又不一样了:

更换了渲染队列,让绿色的渲染队列+1,在默认队列Geometry之后渲染,最终重叠部分又变回了绿色。可见,当ZTest都通过时,上一个写入颜色缓存的会覆盖上一个,也就是说最终输出的是最后一个渲染的对象颜色。

再看一下Greater相关的部分有什么作用,这次我们其他的都使用默认的渲染状态,绿色的立方体shader中ZTest设置为Greater:

这个效果就比较好玩了,虽然我们发现在比较深度时,前面被蓝色立方体遮挡的部分,绿色的最终覆盖了蓝色,是想要的结果,不过其他部分哪里去了呢?简单分析一下,渲染顺序是从前到后,也就是说蓝色最先渲染,默认深度为Max,蓝色立方体的深度满足LEqual条件,就写入了深度缓存,然后绿色开始渲染,重叠的部分的深度缓存是蓝色立方体写入的,而绿色的深度值满足大于蓝色深度的条件,所以深度测试通过,重叠部分颜色更新为绿色;而与红色立方体重合的部分,红色立方体最后渲染,与前面的部分进行深度测试,小于前面的部分,深度测试失败,重叠部分不会更新为红色,所以重叠部分最终为绿色。而绿色立方体没有与其他部分重合的地方为什么消失了呢?其实是因为绿色立方体渲染时,除了蓝色立方体渲染的地方是有深度信息的,其他部分的深度信息都为Max,蓝色部分用Greater进行判断,肯定会失败,也就不会有颜色更新。

有一个好玩的效果其实就可以考ZTest Greater来实现,就是游戏里面经常出现的,当玩家被其他场景对象遮挡时,遮挡的部分会呈现出X-光的效果;其实是在渲染玩家时,增加了一个Pass,默认的Pass正常渲染,而增加的一个Pass就使用Greater进行深度测试,这样,当玩家被其他部分遮挡时,遮挡的部分才会显示出来,用一个描边的效果渲染,其他部分仍然使用原来的Pass即可。

Early-Z技术

传统的渲染管线中,ZTest其实是在Blending阶段,这时候进行深度测试,所有对象的像素着色器都会计算一遍,没有什么性能提升,仅仅是为了得出正确的遮挡结果,会造成大量的无用计算,因为每个像素点上肯定重叠了很多计算。因此现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化之后,fragment之前)进行一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此在性能上会有很大的提升。但是最终的ZTest仍然需要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查,如下图:

Early-Z的实现,主要是通过一个Z-pre-pass实现,简单来说,对于所有不透明的物体(透明的没有用,本身不会写深度),首先用一个超级简单的shader进行渲染,这个shader不写颜色缓冲区,只写深度缓冲区,第二个pass关闭深度写入,开启深度测试,用正常的shader进行渲染。其实这种技术,我们也可以借鉴,在渲染透明物体时,因为关闭了深度写入,有时候会有其他不透明的部分遮挡住透明的部分,而我们其实不希望他们被遮挡,仅仅希望被遮挡的物体半透,这时我们就可以用两个pass来渲染,第一个pass使用Color Mask屏蔽颜色写入,仅写入深度,第二个pass正常渲染半透,关闭深度写入。

关于Early-Z技术可以参考ATI的论文Applications of Explicit Early-Z Culling以及PPT,还有一篇Intel的文章。

Unity渲染顺序总结

如果我们先绘制后面的物体,再绘制前面的物体,就会造成over draw;而通过Early-Z技术,我们就可以先绘制较近的物体,再绘制较远的物体(仅限不透明物体),这样,通过先渲染前面的物体,让前面的物体先占坑,就可以让后面的物体深度测试失败,进而减少重复的fragment计算,达到优化的目的。Unity中默认应该就是按照最近距离的面进行绘制的,我们可以看一下Unity官方的文档中显示的:

从文档给出的流程来看,这个Depth-Test发生在Vertex阶段和Fragment阶段之间,也就是上面所说的Early-Z优化。

简单总结一下Unity中的渲染顺序:先渲染不透明物体,顺序是从前到后;再渲染透明物体,顺序是从后到前。

Alpha Test(Discard)在移动平台消耗较大的原因

从本人刚刚开始接触渲染,就开始听说移动平台Alpha Test比较费,当时比较纳闷,直接discard了为什么会费呢,应该更省才对啊?这个问题困扰了我好久,今天来刨根问底一下。还是跟我们上面讲到的Early-Z优化。正常情况下,比如我们渲染一个面片,不管是否是开启深度写入或者深度测试,这个面片的光栅化之后对应的像素的深度值都可以在Early-Z(Z-Cull)的阶段判断出来了;而如果开启了Alpha Test(Discard)的时候,discard这个操作是在fragment阶段进行的,也就是说这个面片光栅化之后对应的像素是否可见,是在fragment阶段之后才知道的,最终需要靠Z-Check进行判断这个像素点最终的颜色。其实想象一下也能够知道,如果我们开了Alpha Test并且还用Early-Z的话,一块本来应该被剃掉的地方,就仍然写进了深度缓存,这样就会造成其他部分被一个完全没东西的地方遮挡,最终的渲染效果肯定就不对了。所以,如果我们开启了Alpha Test,就不会进行Early-Z,Z Test推迟到fragment之后进行,那么这个物体对应的shader就会完全执行vertex shader和fragment shader,造成over draw。有一种方式是使用Alpha Blend代替Alpha Test,虽然也很费,但是至少Alpha Blend虽然不写深度,但是深度测试是可以提前进行的,因为不会在fragment阶段再决定是否可见,因为都是可见的,只是透明度比较低罢了。不过这样只是权宜之计,Alpha Blend并不能完全代替Alpha Test。

关于Alpha Test对于Power VR架构的GPU性能的影响,简单引用一下官方的链接以及一篇讨论帖:

最后再附上两篇参考文章

http://blog.csdn.net/candycat1992/article/details/41599167

http://blog.csdn.net/arundev/article/details/7895839

二、OpenGL中的混色问题(Alpha通道)

OpenGL混色2009-10-14 10:05今天介绍关于OpenGL混合的基本知识。混合是一种常用的技巧,通常可以用来实现半透明。但其实它也是十分灵活的,你可以通过不同的设置得到不同的混合结果,产生一些有趣或者奇怪的图象。

混合是什么呢?混合就是把两种颜色混在一起。具体一点,就是把某一像素位置原来的颜色和将要画上去的颜色,通过某种方式混在一起,从而实现特殊的效果。

假设我们需要绘制这样一个场景:透过红色的玻璃去看绿色的物体,那么可以先绘制绿色的物体,再绘制红色玻璃。在绘制红色玻璃的时候,利用“混合”功能,把将要绘制上去的红色和原来的绿色进行混合,于是得到一种新的颜色,看上去就好像玻璃是半透明的。

要使用OpenGL的混合功能,只需要调用:glEnable(GL_BLEND);即可。

要关闭OpenGL的混合功能,只需要调用:glDisable(GL_BLEND);即可。

注意:只有在RGBA模式下,才可以使用混合功能,颜色索引模式下是无法使用混合功能的。

一、源因子和目标因子

前面我们已经提到,混合需要把原来的颜色和将要画上去的颜色找出来,经过某种方式处理后得到一种新的颜色。这里把将要画上去的颜色称为“源颜色”,把原来的颜色称为“目标颜色”。

OpenGL会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加,这样就得到了新的颜色。(也可以不是相加,新版本的OpenGL可以设置运算方式,包括加、减、取两者中较大的、取两者中较小的、逻辑运算等,但我们这里为了简单起见,不讨论这个了)

下面用数学公式来表达一下这个运算方式。假设源颜色的四个分量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As),目标颜色的四个分量是(Rd, Gd, Bd, Ad),又设源因子为(Sr, Sg, Sb, Sa),目标因子为(Dr, Dg, Db, Da)。则混合产生的新颜色可以表示为:

(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)

当然了,如果颜色的某一分量超过了1.0,则它会被自动截取为1.0,不需要考虑越界的问题。

源因子和目标因子是可以通过glBlendFunc函数来进行设置的。glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。这两个参数可以是多种值,下面介绍比较常用的几种。

GL_ZERO:表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算。

GL_ONE:表示使用1.0作为因子,实际上相当于完全的使用了这种颜色参与混合运算。

GL_SRC_ALPHA:表示使用源颜色的alpha值来作为因子。

GL_DST_ALPHA:表示使用目标颜色的alpha值来作为因子。

GL_ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来作为因子。

GL_ONE_MINUS_DST_ALPHA:表示用1.0减去目标颜色的alpha值来作为因子。

除此以外,还有GL_SRC_COLOR(把源颜色的四个分量分别作为因子的四个分量)、GL_ONE_MINUS_SRC_COLOR、 GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前两个在OpenGL旧版本中只能用于设置目标因子,后两个在OpenGL旧版本中只能用于设置源因子。新版本的OpenGL则没有这个限制,并且支持新的GL_CONST_COLOR(设定一种常数颜色,将其四个分量分别作为因子的四个分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、 GL_ONE_MINUS_CONST_ALPHA。另外还有GL_SRC_ALPHA_SATURATE。新版本的OpenGL还允许颜色的alpha值和RGB值采用不同的混合因子。但这些都不是我们现在所需要了解的。毕竟这还是入门教材,不需要整得太复杂~

举例来说:

如果设置了glBlendFunc(GL_ONE, GL_ZERO);,则表示完全使用源颜色,完全不使用目标颜色,因此画面效果和不使用混合的时候一致(当然效率可能会低一点点)。如果没有设置源因子和目标因子,则默认情况就是这样的设置。

如果设置了glBlendFunc(GL_ZERO, GL_ONE);,则表示完全不使用源颜色,因此无论你想画什么,最后都不会被画上去了。(但这并不是说这样设置就没有用,有些时候可能有特殊用途)

如果设置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,则表示源颜色乘以自身的alpha值,目标颜色乘以1.0减去源颜色的alpha值,这样一来,源颜色的alpha值越大,则产生的新颜色中源颜色所占比例就越大,而目标颜色所占比例则减小。这种情况下,我们可以简单的将源颜色的alpha值理解为“不透明度”。这也是混合时最常用的方式。

如果设置了glBlendFunc(GL_ONE, GL_ONE);,则表示完全使用源颜色和目标颜色,最终的颜色实际上就是两种颜色的简单相加。例如红色(1, 0, 0)和绿色(0, 1, 0)相加得到(1, 1, 0),结果为黄色。

注意:

所谓源颜色和目标颜色,是跟绘制的顺序有关的。假如先绘制了一个红色的物体,再在其上绘制绿色的物体。则绿色是源颜色,红色是目标颜色。如果顺序反过来,则红色就是源颜色,绿色才是目标颜色。在绘制时,应该注意顺序,使得绘制的源颜色与设置的源因子对应,目标颜色与设置的目标因子对应。不要被混乱的顺序搞晕了。

二、二维图形混合举例

下面看一个简单的例子,实现将两种不同的颜色混合在一起。为了便于观察,我们绘制两个矩形:glRectf(-1,-1, 0.5, 0.5);glRectf(-0.5,-0.5, 1, 1);,这两个矩形有一个重叠的区域,便于我们观察混合的效果。

先来看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的结果与不使用混合时相同。

void myDisplay(void)

{

glClear(GL_COLOR_BUFFER_BIT);

glEnable(GL_BLEND);

glBlendFunc(GL_ONE, GL_ZERO);

glColor4f(1, 0, 0, 0.5);

glRectf(-1,-1, 0.5, 0.5);

glColor4f(0, 1, 0, 0.5);

glRectf(-0.5,-0.5, 1, 1);

glutSwapBuffers();

}

尝试把glBlendFunc的参数修改为glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);以及 glBlendFunc(GL_ONE, GL_ONE);,观察效果。第一种情况下,效果与没有使用混合时相同,后绘制的图形会覆盖先绘制的图形。第二种情况下,alpha被当作“不透明度”,由于被设置为0.5,所以两个矩形看上去都是半透明的,乃至于看到黑色背景。第三种是将颜色相加,红色和绿色相加得到黄色。

三、实现三维混合

也许你迫不及待的想要绘制一个三维的带有半透明物体的场景了。但是现在恐怕还不行,还有一点是在进行三维场景的混合时必须注意的,那就是深度缓冲。

深度缓冲是这样一段数据,它记录了每一个像素距离观察者有多近。在启用深度缓冲测试的情况下,如果将要绘制的像素比原来的像素更近,则像素将被绘制。否则,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时非常有用——不管是先绘制近的物体再绘制远的物体,还是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果总是近的物体遮住远的物体。

然而在你需要实现半透明效果时,发现一切都不是那么美好了。如果你绘制了一个近距离的半透明物体,则它在深度缓冲区内保留了一些信息,使得远处的物体将无法再被绘制出来。虽然半透明的物体仍然半透明,但透过它看到的却不是正确的内容了。

要解决以上问题,需要在绘制半透明物体时将深度缓冲区设置为只读,这样一来,虽然半透明物体被绘制上去了,深度缓冲区还保持在原来的状态。如果再有一个物体出现在半透明物体之后,在不透明物体之前,则它也可以被绘制(因为此时深度缓冲区中记录的是那个不透明物体的深度)。以后再要绘制不透明物体时,只需要再将深度缓冲区设置为可读可写的形式即可。嗯?你问我怎么绘制一个一部分半透明一部分不透明的物体?这个好办,只需要把物体分为两个部分,一部分全是半透明的,一部分全是不透明的,分别绘制就可以了。

即使使用了以上技巧,我们仍然不能随心所欲的按照混乱顺序来进行绘制。必须是先绘制不透明的物体,然后绘制透明的物体。否则,假设背景为蓝色,近处一块红色玻璃,中间一个绿色物体。如果先绘制红色半透明玻璃的话,它先和蓝色背景进行混合,则以后绘制中间的绿色物体时,想单独与红色玻璃混合已经不能实现了。

总结起来,绘制顺序就是:首先绘制所有不透明的物体。如果两个物体都是不透明的,则谁先谁后都没有关系。然后,将深度缓冲区设置为只读。接下来,绘制所有半透明的物体。如果两个物体都是半透明的,则谁先谁后只需要根据自己的意愿(注意了,先绘制的将成为“目标颜色”,后绘制的将成为“源颜色”,所以绘制的顺序将会对结果造成一些影响)。最后,将深度缓冲区设置为可读可写形式。

调用glDepthMask(GL_FALSE);可将深度缓冲区设置为只读形式。调用glDepthMask(GL_TRUE);可将深度缓冲区设置为可读可写形式。

一些网上的教程,包括大名鼎鼎的NeHe教程,都在使用三维混合时直接将深度缓冲区禁用,即调用glDisable(GL_DEPTH_TEST);。这样做并不正确。如果先绘制一个不透明的物体,再在其背后绘制半透明物体,本来后面的半透明物体将不会被显示(被不透明的物体遮住了),但如果禁用深度缓冲,则它仍然将会显示,并进行混合。NeHe提到某些显卡在使用glDepthMask函数时可能存在一些问题,但可能是由于我的阅历有限,并没有发现这样的情况。

那么,实际的演示一下吧。我们来绘制一些半透明和不透明的球体。假设有三个球体,一个红色不透明的,一个绿色半透明的,一个蓝色半透明的。红色最远,绿色在中间,蓝色最近。根据前面所讲述的内容,红色不透明球体必须首先绘制,而绿色和蓝色则可以随意修改顺序。这里为了演示不注意设置深度缓冲的危害,我们故意先绘制最近的蓝色球体,再绘制绿色球体。

为了让这些球体有一点立体感,我们使用光照。在(1, 1,-1)处设置一个白色的光源。代码如下:

void setLight(void)

{

static const GLfloat light_position[]={1.0f, 1.0f,-1.0f, 1.0f};

static const GLfloat light_ambient[]={0.2f, 0.2f, 0.2f, 1.0f};

static const GLfloat light_diffuse[]={1.0f, 1.0f, 1.0f, 1.0f};

static const GLfloat light_specular[]={1.0f, 1.0f, 1.0f, 1.0f};

glLightfv(GL_LIGHT0, GL_POSITION, light_position);

glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);

glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);

glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

glEnable(GL_LIGHT0);

glEnable(GL_LIGHTING);

glEnable(GL_DEPTH_TEST);

}

每一个球体颜色不同。所以它们的材质也都不同。这里用一个函数来设置材质。

void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)

{

static const GLfloat mat_specular[]={0.0f, 0.0f, 0.0f, 1.0f};

static const GLfloat mat_emission[]={0.0f, 0.0f, 0.0f, 1.0f};

glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);

glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);

glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);

glMaterialf(GL_FRONT, GL_SHININESS, mat_shininess);

}

有了这两个函数,我们就可以根据前面的知识写出整个程序代码了。

坐标是可以设置的。OpenGL默认坐标系的确是楼上两位说的那样,但是我本人更习惯Z轴垂直显示器平面向内,所以把它修改掉了。

glOrtho(-1, 1,-1, 1, 1,-1);//默认情形

glOrtho(-1, 1,-1, 1,-1, 1);//我设置的情形

这里只给出了绘制的部分,其它部分大家可以自行完成。

void myDisplay(void)

{

//定义一些材质颜色

const static GLfloat red_color[]={1.0f, 0.0f, 0.0f, 1.0f};

const static GLfloat green_color[]={0.0f, 1.0f, 0.0f, 0.3333f};

const static GLfloat blue_color[]={0.0f, 0.0f, 1.0f, 0.5f};

//清除屏幕

glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);

//启动混合并设置混合因子

glEnable(GL_BLEND);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

//设置光源

setLight();

//以(0, 0, 0.5)为中心,绘制一个半径为.3的不透明红色球体(离观察者最远)

setMatirial(red_color, 30.0);

glPushMatrix();

glTranslatef(0.0f, 0.0f, 0.5f);

glutSolidSphere(0.3, 30, 30);

glPopMatrix();

//下面将绘制半透明物体了,因此将深度缓冲设置为只读

glDepthMask(GL_FALSE);

//以(0.2, 0,-0.5)为中心,绘制一个半径为.2的半透明蓝色球体(离观察者最近)

setMatirial(blue_color, 30.0);

glPushMatrix();

glTranslatef(0.2f, 0.0f,-0.5f);

glutSolidSphere(0.2, 30, 30);

glPopMatrix();

//以(0.1, 0, 0)为中心,绘制一个半径为.15的半透明绿色球体(在前两个球体之间)

setMatirial(green_color, 30.0);

glPushMatrix();

glTranslatef(0.1, 0, 0);

glutSolidSphere(0.15, 30, 30);

glPopMatrix();

//完成半透明物体的绘制,将深度缓冲区恢复为可读可写的形式

glDepthMask(GL_TRUE);

glutSwapBuffers();

}

大家也可以将上面两处glDepthMask删去,结果会看到最近的蓝色球虽然是半透明的,但它的背后直接就是红色球了,中间的绿色球没有被正确绘制。

小结:

本课介绍了OpenGL混合功能的相关知识。

混合就是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色经过一定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,然后相加。

源因子和目标因子是可以设置的。源因子和目标因子设置的不同直接导致混合结果的不同。将源颜色的alpha值作为源因子,用1.0减去源颜色alpha值作为目标因子,是一种常用的方式。这时候,源颜色的alpha值相当于“不透明度”的作用。利用这一特点可以绘制出一些半透明的物体。

在进行混合时,绘制的顺序十分重要。因为在绘制时,正要绘制上去的是源颜色,原来存在的是目标颜色,因此先绘制的物体就成为目标颜色,后来绘制的则成为源颜色。绘制的顺序要考虑清楚,将目标颜色和设置的目标因子相对应,源颜色和设置的源因子相对应。

在进行三维混合时,不仅要考虑源因子和目标因子,还应该考虑深度缓冲区。必须先绘制所有不透明的物体,再绘制半透明的物体。在绘制半透明物体时前,还需要将深度缓冲区设置为只读形式,否则可能出现画面错误。

简单的透明

OpenGL中的绝大多数特效都和某些类型的(色彩)混合有关。

混色的定义为,将某个象素的颜色和已绘制在屏幕上和其对应的象素颜色相互结合。

至于怎么结合这两个颜色则依赖于颜色的alpha通道的分量值,及/或所使用的混色函数。

Alpha通常是位于颜色值末尾的第4个颜色组成分量。

前面这些课我们都是用GL_RGB来指定颜色的三个分量。

相应的GL_RGBA能指定alpha分量的值。

更进一步,我们能使用glColor4f()来代替glColor3f()。

绝大多数人都认为Alpha分量代表材料的透明度。

这就是说,alpha值为0.0时所代表的材料是完全透明的。

alpha值为1.0时所代表的材料则是完全不透明的。

混色的公式

若你对数学不感冒,而只想看看怎么实现透明,请跳过这一节。

若你想深入理解(色彩)混合的工作原理,这一节应该适合你吧。

『CKER的补充:其实并不难^-^。原文中的公式如下,CKER再唠叨一下吧。

其实混合的基本原理是就将要分色的图像各象素的颜色及背景颜色均按照RGB规则各自分离之后,

根据-图像的RGB颜色分量*alpha值+背景的RGB颜色分量*(1-alpha值)

-这样一个简单公式来混合之后,最后将混合得到的RGB分量重新合并。』

公式如下:

(Rs Sr+ Rd Dr, Gs Sg+ Gd Dg, Bs Sb+ Bd Db, As Sa+ Ad Da)

OpenGL按照上面的公式计算这两个象素的混色结果。

小写的s和r分别代表源象素和目标象素。大写的S和D则是相应的混色因子。

这些决定了你怎么对这些象素混色。

绝大多数情况下,各颜色通道的alpha混色值大小相同,

这样对源象素就有(As, As, As, As),

目标象素则有1, 1, 1, 1)-(As, As, As, As)。

上面的公式就成了下面的模样:

(Rs As+ Rd(1- As), Gs As+ Gd(1- As), Bs As+ Bs(1- As), As As+ Ad(1- As))

这个公式会生成透明/半透明的效果。

OpenGL中的混色

在OpenGL中实现混色的步骤类似于我们以前提到的OpenGL过程。

接着设置公式,并在绘制透明对象时关闭写深度缓存。

因为我们想在半透明的图像背后绘制对象。

这不是正确的混色方法,但绝大多数时候这种做法在简单的项目中都工作的非常好。

Rui Martins的补充:正确的混色过程应该是先绘制全部的场景之后再绘制透明的图像。

并且要按照和深度缓存相反的次序来绘制(先画最远的物体)。

考虑对两个多边形(1和2)进行alpha混合,不同的绘制次序会得到不同的结果。

(这里假定多边形1离观察者最近,那么正确的过程应该先画多边形2,再画多边形1。

正如你再现实中所见到的那样,

从这两个<透明的>多边形背后照射来的光线总是先穿过多边形2,

再穿过多边形1,最后才到达观察者的眼睛。)

在深度缓存启用时,你应该将透明图像按照深度进行排序,

并在全部场景绘制完毕之后再绘制这些透明物体。否则你将得到不正确的结果。

我知道某些时候这样做是非常令人痛苦的,但这是正确的方法。

我们将使用第七课的代码。

一开始先在代码开始处增加两个新的变量。出于清晰起见,我重写了整段代码。

}

Var

h_RC: HGLRC;// Rendering Context(着色描述表)。

h_DC: HDC;// Device Context(设备描述表)

h_Wnd: HWND;//窗口句柄

h_Instance: HINST;//程式Instance(实例)。

keys: Array[0..255] Of Boolean;//用于键盘例程的数组

light: Boolean;//光源的开/关

blend: Boolean;// Blending OFF/ON?(新增)

lp: Boolean;// L键按下了么?

fp: Boolean;// F键按下了么?

bp: Boolean;// B键按下了么?(新增)

xrot: GLfloat;// X旋转

yrot: GLfloat;// Y旋转

xspeed: GLfloat;// X旋转速度

yspeed: GLfloat;// Y旋转速度

z: GLfloat=-5.0 f;//深入屏幕的距离

LightAmbient: Array[0..3] Of GLfloat=(0.5, 0.5, 0.5, 1.0);//环境光参数(新增)

LightDiffuse: Array[0..3] Of GLfloat=(1.0, 1.0, 1.0, 1.0);//漫射光参数(新增)

LightPosition: Array[0..3] Of GLfloat=(0.0, 0.0, 2.0, 1.0);//光源位置(新增)

filter: GLuint;//滤波类型

texture: Array[0..2] Of GLuint;// 3种纹理的储存空间

Procedure glGenTextures(n: GLsizei; Var textures: GLuint); stdcall; external

opengl32;

Procedure glBindTexture(target: GLenum; texture: GLuint); stdcall; external

opengl32;

Function gluBuild2DMipmaps(target: GLenum; components, width, height: GLint;

format, atype: GLenum; data: Pointer): Integer; stdcall; external glu32 name

gluBuild2DMipmaps;

{

然后往下移动到 LoadGLTextures()这里。

找到 if(TextureImage[0]=LoadBMP(Data/Crate.bmp))

这一行。我们目前使用有色玻璃纹理来代替上一课中的木箱纹理。

if(TextureImage[0]=LoadBMP("Data/glass.bmp"));//载入玻璃位图(已修改)

}

Function LoadTexture: boolean;//载入位图并转换成纹理

Var

Status: boolean;// Status指示器

TextureImage: Array[0..1] Of PTAUX_RGBImageRec;//创建纹理的存储空间

Begin

Status:= false;

ZeroMemory(@TextureImage, sizeof(TextureImage));//将指针设为 NULL

TextureImage[0]:= LoadBMP(Walls.bmp);

If TextureImage[0]<> Nil Then

Begin

Status:= TRUE;//将 Status设为 TRUE

glGenTextures(1, texture[0]);//创建纹理

//创建 Nearest滤波贴图

glBindTexture(GL_TEXTURE_2D, texture[0]);

//生成纹理

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);//(新增)

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//(新增)

glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0].sizeX,

TextureImage[0].sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE,

TextureImage[0].data);

glBindTexture(GL_TEXTURE_2D, texture[1]);//使用来自位图数据生成的典型纹理

//生成纹理

glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0].sizeX,

TextureImage[0].sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE,

TextureImage[0].data);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//线形滤波

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//线形滤波

//创建 MipMapped纹理

glBindTexture(GL_TEXTURE_2D, texture[2]);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,

GL_LINEAR_MIPMAP_NEAREST);//(新增)

gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0].sizeX,

TextureImage[0].sizey, GL_RGB, GL_UNSIGNED_BYTE,

TextureImage[0].data);//(新增)}

End;

If assigned(TextureImage[0]) Then//纹理是否存在

If assigned(TextureImage[0].data) Then//纹理图像是否存在

TextureImage[0].data:= Nil;//释放纹理图像占用的内存

TextureImage[0]:= Nil;//释放图像结构

result:= Status;//返回 Status

End;

{

在glInit()代码段加入以下两行。

第一行以全亮度绘制此物体,并对其进行50%的alpha混合(半透明)。

当混合选项打开时,此物体将会产生50%的透明效果。

第二行设置所采用的混合类型。

Rui Martins的补充:

alpha通道的值为 0.0意味着物体材质是完全透明的。

1.0则意味着完全不透明。

}

End;

不过怎样才能在使用纹理贴图的时候指定混合时的颜色呢?非常简单,

在调整贴图模式时,文理贴图的每个象素点的颜色都是由alpha通道参数

和当前地象素颜色相乘所得到的。

比如,绘制的颜色是(0.5, 0.6, 0.4),

我们会把颜色相乘得到(0.5, 0.6, 0.4, 0.2)

(alpha参数在没有指定时,缺省为零)。

就是如此!OpenGL实现Alpha混合的确非常简单!

}

{

原文注(11/13/99)

我(NeHe)混色代码进行了修改,以使显示的物体看起来更逼真。

同时对源象素和目的象素使用alpha参数来混合,会导致物体的人造痕迹看起来非常明显。

会使得物体的背面沿着侧面的地方显得更暗。

基本上物体会看起来非常怪异。

我所用的混色方法也许不是最佳的,但的确能够工作。

启用光源之后,物体看起来非常逼真。

感谢Tom提供的原始代码,他采用的混色方法是正确的,

但物体看起来并不象所期望的那样吸引人:)

代码所作的再次修改是因为在某些显卡上glDepthMask()函数存在寻址问题。

这条命令在某些卡上启用或关闭深度缓冲测试时似乎不是非常有效,

所以我已将启用或关闭深度缓冲测试的代码转成老式的glEnable和glDisable。

纹理贴图的Alpha混合

用于纹理贴图的alpha参数能象颜色相同从问题贴图中读取。

方法如下,你需要在载入所需的材质同时取得其的alpha参数。

然后在调用glTexImage2D()时使用GL_RGBA的颜色格式。

}

文章分享到这里,希望我们关于Alpha Test 与 Alpha Blending和OpenGL中的混色问题(Alpha通道)的内容能够给您带来一些新的认识和思考。如果您还有其他问题,欢迎继续探索我们的网站或者与我们交流,我们将尽力为您提供满意的答案。