WebGL教程9:深度缓存、透明度和混合
来源:http://www.hiwebgl.com
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com
欢迎来到WebGL教程的第八节课,这节课的内容基于NeHe OpenGL教程的第8节改编的。在这节课上,我们将会介绍混合,并且稍微介绍一下这个相当有用的深度缓存是如何工作的。
下面的视频就是我们这节课将会完成的最终效果。
你将会看到一个半透明并且缓慢旋转的立方体,看上去是用有色玻璃制作的。你还可以像上节课中一样调节光照。
你可以点击画布下边的复选框以开启或者关闭混合模式和透明效果。你还可以调节alpha 的参数(这个我们将在稍后解释),当然,还有光线明暗。
下面我们来看看它是怎么工作的……
惯例声明:本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而可以快速地创建自己的3D Web页面。如果你还没有阅读前一课,请先阅读前一课的内容吧。因为本课中我只会讲解那些与前一课中不同的新知识。
另外,我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。
有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;或者点击这里下载我们为您准备好的压缩包。
深度缓存(Depth Buffer)
当你命令WebGL绘制物体的时候,必须要经过某些必要的步骤。按等级高低排序:
• 在所有的顶点上运行顶点着色器以绘制出物体所在的位置。
• 线性地在顶点之间进行插值运算,这样做是告诉顶点哪些片元(这个时候,你可以把片元当做像素对待)需要上色。
• 对于每个片元来说,运行片元着色器以绘制出它的颜色。
• 把它写入帧缓冲。
最终,帧缓冲就是屏幕上显示出来的内容。但是当你需要绘制两个物体时,又会是怎样的呢?比如,当你需要绘制两个大小一样的正方形,一个中心位于(0,0,-5),另一个中心位于(0,0,-10),又会是怎样的呢?你肯定不希望第二个正方形覆盖在第一个正方形上,因为它距离更远,应当被挡住的。
WebGL正是运用深度缓存来处理这样的情况的。当片元着色器处理完片元以及RGBA颜色值,并把它们写入帧缓冲时,也会将与Z值相关的深度值储存在里面,但是这个值并不完全与Z值相同。(这不奇怪,因为深度缓存经常也被叫做Z缓存。)
为什么我说“相关的”呢?是这样的,WebGL总是将所有的Z值按从0到1顺序排列,0为最近,1为最远。其实这些事情在drawScene函数一开始,当我们调用透视并创建投影矩阵时,就背着我们发生了。现在,你所需要知道的就是,Z-buffer的值越大,物体距离就越远,这一点与我们通常的坐标系统是相反的。
好了,这些就是深度缓存。你应该还记得我们在第一课中初始化WebGL的代码,有这样一行。
1 |
gl.enable(gl.DEPTH_TEST); |
1 |
gl.depthFunc(gl.LESS); |
混合(Blending)
混合是上述这一过程的另一个替代方案。通过深度检测,我们使用深度函数来判断是否用新的片元替换现有片元。当我们使用混合时,我们使用一个混合函数,把现有片元和新片元的颜色组合到一起,创建一个全新的片元,接着将它写入缓冲区内。
让我们来看一下代码。大部分代码是和第七课中的代码相同的,最重要的部分几乎都在drawScene的一小段代码中。首先,我们先来看一下混合复选框是否被选取。
436 |
var blending = document.getElementById("blending").checked; |
437 438 |
if (blending) { gl.blendFunc(gl.SRC_ALPHA, gl.ONE); |
现在,让我们来假设一下如果WebGL正在试图用计算出一个片元的颜色,这个片元有一个既存的目标片元和一个即将被加入的源片元,目标片元的RGBA值为 (Rd, Gd, Bd, Ad),源片元的值为(Rs, Gs, Bs, As)。
另外,假设RGBA的source factor是(Sr, Sg, Sb, Sa),而destination factor是(Dr, Dg, Db, Da)。
对于每个颜色分量,WebGL将会进行以下运算。
Rresult = Rs * Sr + Rd * Dr
Gresult = Gs * Sg + Gd * Dg
Bresult = Bs * Sb + Bd * Db
Aresult = As * Sa + Ad * Da
为了简单起见,这里我们仅会对红色分量进行运算。
Rresult = Rs * As + Rd
通常来说,这并不是创建透明物体的最理想方法。但当开启光照时,它确是解决问题的一个好方法。还有一件需要强调的事情是,混合并不是透明!相对于其他技术,它仅仅是一种能够实现透明效果的技巧。我在学习Nehe时,我花了好大功夫才弄懂这些,所以,请原谅我在这里过于强调这一点。
好啦,继续我们的课程。
439 |
gl.enable(gl.BLEND); |
440 |
gl.disable(gl.DEPTH_TEST); |
眼尖的读者应该会在上面的混合函数中注意到这一点,混合很依赖绘制物体的顺序,这一点是在我们前几节课中都没有遇到过的。我们将在后面详细介绍,让我们先来讲完最后一点代码。
441 |
gl.uniform1f(shaderProgram.alphaUniform, parseFloat(document.getElementById("alpha").value)); |
drawScene函数中的其余部分是用于当混合被关闭时,进行一般运算的。
442 443 444 445 |
} else { gl.disable(gl.BLEND); gl.enable(gl.DEPTH_TEST); } |
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
precision mediump float; varying vec2 vTextureCoord; varying vec3 vLightWeighting; uniform float uAlpha; uniform sampler2D uSampler; void main(void) { vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a * uAlpha); } |
好了,我们现在来讲一讲绘制顺序。示例中的透明效果十分出色——看上去很像彩色玻璃吧。 但现在我们再来看看它,这次我们改变一下光线的方向,让光线从Z轴正方向射入,也就是把文本框中的“-”号都去掉。恩,看上去很棒,但是,那种可以乱真的彩色玻璃效果却消失了。
为什么呢?这是因为在原来的光照处理中,立方体朝后的那一面,也就是背对光源的那一面一般都是昏暗的。也就是说那一面的R、G、B的值都十分小。所以,在进行运算时:
Rresult = Rs * Ra + Rd
那一面看上去就不会那么明显。换句话说,我们的光照处理让后侧可见度较低。如果我们改变光照效果,让前侧可见度较低的话,那我们的透明效果看起来就不那么好了。
但是,我们要如何才能得到一个“合适”的透明效果呢?OpenGL FAQ告诉我们,需要使用SRC_ALPHA 作为source factor,用ONE_MINUS_SRC_ALPHA作为destination factor。但是还有一个问题,源片元和目标片元的运算方式不同,所以,我们将会十分依赖绘制物体的顺序。这也引出了我认为是OpenGL或WebGL中的一个不光彩的地方;好再让我们来看一下OpenGL的FAQ:
当使用深度缓存时,你必须十分注意呈现图元的顺序。按照从后到前的顺序,完全不透明的图元应当首先被呈现,接着是部分透明的图元;如果你没有按照此顺序呈现图元,深度检测会屏蔽那些原本借助于透明图元才可以显示的图元。
深度缓存只能保证最近的面。而透明物体,实际上需要的是保存多层面数据,才能重现正确的效果。所以需要先画不透明的物体,然后再画透明的物体;同时,透明物体需要先按由远到近排好顺序才行。 如果你先画了透明的,后画不透明的,显然是不对的。因为即使不透明的更近,也不能通过深度检测屏蔽掉不透明的,所以必须先画不透明的。透明的物体,也需要由远到近来画。这样一层层blend上去,才是正确的结果。如果先blend了近的,那再blend远的时候,实际结果是错的。
如果在开启depth test的情况下,先画一个透明物体,那如果它后面有一个不透明物体,而且又是后画的话,depth test会使得后面的物体画不上去。
大概就是这样了。使用混合来做出透明效果是十分有技巧、并且十分繁琐的,但是只要你能像控制光照一样,控制场景中的其他大部分对象,我们就能很容易地实现我们需要的正确效果。仅仅能够正确地绘制物体还不够,为了让物体看上去更好看,我们还必须用特定的顺序去绘制它们。
值得一提的是,混合是一项非常有用的技术,还可以用来实现许多其他的效果,下一节课你将会看到。那么,这一节课所讲的内容就到此为止了。这节课上我们讲了深度缓存,还有如何通过混合来做出透明效果。
如果你有任何问题、评论或者修正,都请告诉我!特别是这一课,当我第一次学习时,我感觉这是NeHe的教程中最难理解的一部分。
下节课我们将改进代码结构,不再使用麻烦的全局变量,并实现多个不同物体在场景中的运动。