HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!

关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com 

欢迎来到WebGL教程的第八节课,这节课的内容基于NeHe OpenGL教程的第8节改编的。在这节课上,我们将会介绍混合,并且稍微介绍一下这个相当有用的深度缓存是如何工作的。

下面的视频就是我们这节课将会完成的最终效果。

点击这里打开一个独立的WebGL页面,如果你的浏览器不支持WebGL,请点击这里

你将会看到一个半透明并且缓慢旋转的立方体,看上去是用有色玻璃制作的。你还可以像上节课中一样调节光照。

你可以点击画布下边的复选框以开启或者关闭混合模式和透明效果。你还可以调节alpha 的参数(这个我们将在稍后解释),当然,还有光线明暗。

下面我们来看看它是怎么工作的……

惯例声明:本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而可以快速地创建自己的3D Web页面。如果你还没有阅读前一课,请先阅读前一课的内容吧。因为本课中我只会讲解那些与前一课中不同的新知识。

另外,我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。

有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;或者点击这里下载我们为您准备好的压缩包。

深度缓存(Depth Buffer)

当你命令WebGL绘制物体的时候,必须要经过某些必要的步骤。按等级高低排序:

  1. 在所有的顶点上运行顶点着色器以绘制出物体所在的位置。
  2. 线性地在顶点之间进行插值运算,这样做是告诉顶点哪些片元(这个时候,你可以把片元当做像素对待)需要上色。
  3. 对于每个片元来说,运行片元着色器以绘制出它的颜色。
  4. 把它写入帧缓冲。

最终,帧缓冲就是屏幕上显示出来的内容。但是当你需要绘制两个物体时,又会是怎样的呢?比如,当你需要绘制两个大小一样的正方形,一个中心位于(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);

这行代码是给WebGL系统一个说明,告诉系统在把一段新的片元写入帧缓冲时,系统需要做些什么,基本上的意思是“注意深度缓存”。它会和另一个WebGL设置深度函数一起协同发生作用。实际上这个函数默认拥有一个合适的值,如果我们将它设置为默认值,它会是这样的:

1
    gl.depthFunc(gl.LESS);

这个函数的意思是:如果片元的Z值小于当前值,将使用新的值,而不是原有的值。这个深度检测,和其他实现他的代码一起,会给我们一个合理的透视行为——近处的物体会掩盖远处的物体。(你还可以尝试着将深度函数中的参数设为其他不同的值,尽管我想这些值并不太常用。)

混合(Blending)

混合是上述这一过程的另一个替代方案。通过深度检测,我们使用深度函数来判断是否用新的片元替换现有片元。当我们使用混合时,我们使用一个混合函数,把现有片元和新片元的颜色组合到一起,创建一个全新的片元,接着将它写入缓冲区内。

让我们来看一下代码。大部分代码是和第七课中的代码相同的,最重要的部分几乎都在drawScene的一小段代码中。首先,我们先来看一下混合复选框是否被选取。

436
        var blending = document.getElementById("blending").checked;

如果被选取,我们将设置一个函数用来组合这两个片元。

437
438
        if (blending) {
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

这些参数定义了混合是如何完成的。这需要一定技巧,但是并不困难。首先,我们需要定义两个术语:源片元(source fragment)和目标片元(destination fragment)。其中源片元是我们正在绘制的片元,而目标片元是已经存在于帧缓冲之中的。gl.blendFunc函数中的第一个参数定义了source factor,而第二个参数,定义了,这些factor是在混合函数中使用的一些数字。在这节课的例子中,我们指定用源片元的alpha值作为source factor,而destination factor则是一个常量1。当然,也有其他可能性,如果将源色指定为SRC_COLOR,你最后得到的source factor会是单独的红、绿、蓝和alpha值,其中每个都与原始的RGBA的分量相等。

现在,让我们来假设一下如果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);

这一段很简单,和其他许多WebGL中的特性一样,混合是默认关闭的。所以,在这里我们必须启用它。

440
            gl.disable(gl.DEPTH_TEST);

这里有一个很有趣的现象,那就是在开启混合的同时,我们必须要关闭深度检测。如果我们不这么做,混合效果将会出现在某些地方,但是在另一些地方却又不会出现。例如,当我们在绘制一个立方体时,假设我们正巧先绘制立方体的背面,这样,立方体的背面就会被写入帧缓冲之中;接着,当正面完成时,正面就会出现在背面之前,并带有混合效果,这也是我们需要的效果。但是,相反如果我们先绘制正面,然后绘制背面,这时背面将会在我们运行混合函数之前,在深度检测中被丢弃,显然这不是我们希望得到的效果。

眼尖的读者应该会在上面的混合函数中注意到这一点,混合很依赖绘制物体的顺序,这一点是在我们前几节课中都没有遇到过的。我们将在后面详细介绍,让我们先来讲完最后一点代码。

441
            gl.uniform1f(shaderProgram.alphaUniform, parseFloat(document.getElementById("alpha").value));

现在,我们正从页面中的文本框里加载一个alpha值,并且将它传递给着色器。这是因为我们作为纹理使用的图像没有自带alpha通道(它只有RGB通道,所以每个像素来说alpha值都固定为1)。所以,alpha值最好是可以调节的,这样我们就能看出它是如何影响图像的。

drawScene函数中的其余部分是用于当混合被关闭时,进行一般运算的。

442
443
444
445
        } else {
            gl.disable(gl.BLEND);
            gl.enable(gl.DEPTH_TEST);
        }

在片元着色器中还有一些小的改动,那就是处理纹理时将使用到alpha值,参见第18行代码。

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的教程中最难理解的一部分。

下节课我们将改进代码结构,不再使用麻烦的全局变量,并实现多个不同物体在场景中的运动。

发表评论

邮箱地址不会被公开。 必填项已用*标注