Lesson 4 真正的3D物体

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

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

 

欢迎大家来到WebGL教程的第四课。我将会向大家展示一些真正的3D的物体。这节课的内容是基于NeHe的OpenGL教程的第五节改写的。

以下这个视频就是本节课最终完成的效果。

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

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

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

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

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

本课和上一课的不同集中在animate, initBuffers和drawScene这三个函数中. 如果你正在看代码, 请跳到animate函数的函数体部分, 你会看到第一处细小的差别: 用来记录两个物体当前旋转状态的变量名改变了, 上一课中的rTri, rSquare变成了rPyramid与rCube, 同时, Cube的旋转方向也与上一课相反(这样显得更漂亮一些). 所以, 本课中的旋转代码如下:

338
339
            rPyramid += (90 * elapsed) / 1000.0;
            rCube -= (75 * elapsed) / 1000.0;

好,介绍完这个函数了,接着我们来看一下drawScene函数,在定义函数之前,我么将定义两个新的变量。

284
285
    var rPyramid = 0;
    var rCube = 0;

接着是函数头,绘制工作的初始化及确认椎体绘制位置,之后我们让它沿着Y轴转动,就像我们在前一节课上对那个三角形所做的操作一样,角度大小由rPyramid指定。

296
        mvMatrix.rotY(OAK.SPACE_LOCAL, rPyramid, true);

然后开始绘制。新代码中,我们画这个彩色的锥体的代码与上一节教程里我们画那个彩色的三角形所用的代码的唯一不同就是它拥有更多的顶点和颜色,而这些工作, 都是由initBuffers来完成的.(这也是我们马上要讲的)。这就意味着,除了我们所使用的buffer名字不同外,代码是完全相同的。

298
299
300
301
302
303
304
305
        gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);

看吧,很简单,接着我们再来看一下立方体的代码。第一步,就是旋转它。这一次,我们不仅仅需要让他沿X轴旋转,还需要让他沿着一个从右至上的轴,向你的方向(观看者的方向)旋转。

313
        mvMatrix.rot(OAK.SPACE_LOCAL, rCube, 1.0, 1.0, 1.0, true);

接着,我们开始画这个立方体,这可能会有点复杂。有三种方法可以画出一个立方体。

  1. 使用三角形带(Triangle Strip), 如果整个立方体是单色的, 那Triangle Strip方式可以很好的绘制我们想要的东西——首先,我们使用最开始的三个顶点来绘制一个三角面,之后,增加一个顶点(译者注:原文中为增加两个点,但似乎strip方式绘制三角形每次都只需增加一个点,也许原文作者想要表达的是webgl目前并不支持的quad strip),并和刚才绘制的三角形中最后两个顶点组合为一个新的三角面进行绘制,以此类推,直到完成整个绘制过程, 这非常简单高效,但可惜在这里我们不能使用这种技术,因为我们想要立方体在不同的面可以有不同的颜色,立方体中,每一个顶点都被三个不同的面所共享,因此我们需要每个顶点被独立使用三次。这样做十分的麻烦,没必要花时间解释这个……
  2. 我们可以采用一些投机取巧的方法来绘制这个我们需要的立方体,让我们先来绘制六个独立正方形,每一个正方形都是立方体的一个面,我们分别设置这些正方形的顶点和颜色。这个课程的第一个版本(2009年10月30日之前的版本)就是这样做的。很好用,但是却并不是实用的好方法,因为每次独立的绘制调用都会带来额外的性能开销,我们应当将调用drawArrays的次数最小化来提高绘制的性能.
  3. 最后一种选择就是将立方体指定为六个正方形,每个正方形有两个三角形构成,但是我们将让WebGL一次性完成这些图形的绘制。这个方法有点类似我们用三角形带制作立方体。但由于我们每次都是定义一个完整的三角形,而不是通过添加点来定义三角形,我们很容易来定义每一个面的颜色。这个方法的另一个好处就是它的代码是最简洁的,也方便我来介绍一个新函数 – drawElements, 我们就用这个方法来实现它。

首先,我们将包含有这个立方体的顶点的数组对象和我们将会在initbuffers中用到的颜色通过适当的属性关联起来,就像我们处理锥体的过程一样。

316
317
318
319
320
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

下一个步骤就是绘制那些三角形,这里有一些小问题,我们需要考虑一下哪个面在前,我们一共有4个顶点,每一个点都有一个与之相关的颜色。每一个面需要有两个三角形构成。由于我们使用的是简单三角形,我们需要为每个三角形分别指定顶点,而不是像三角形带一样共享顶点,这样的话,我们就需要为它制定6个顶点。但是我们在数组对象里面只提供了四个。

我们将要做的是,先绘制一个由前三个储存在数组对象中的顶点组成的三角形,接着用第一个、第三个和第四个顶点绘制另一个三角形,这样,我们就有了立方体的正面。绘制余下三角形的方法也和这个类似,这就是我们所需要做的。

在这里,我们将使用的是元素数组对象(Element Array Buffer)和一个叫做 drawElements的函数。和我们一直使用的数组对象(Array Buffer) 一样,元素数组对象将在 initBuffers里面被赋予适当的值,并且,它会通过基于零点索引的方式,为顶点位置和顶点颜色保留一份顶点数据列表。

为了使用它,我们需要指定我们的立方体的元素数组对象成为我们当前使用的数组对象(WebGL 保留不同的当前数组对象和当前元素数组对象,因此我们必须调用gl.bindBuffer来指定哪一个数组对象是我们绑定的),之后我们就像往常一样,将模型视图矩阵和投影矩阵传送到显卡端,最后调用drawElements 来绘制我们需要的三角形。

322
323
324
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
        setMatrixUniforms();
        gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

这就是drawScene代码。代码的剩余部分在initBuffers 里面,而且十分浅显易懂。我们将会使用新的名字来定义新的buffers,以反映我们将要处理的新的物体。我们在立方体的顶点索引数组对象里面将加入新的项。

142
143
144
145
146
    var pyramidVertexPositionBuffer;
    var pyramidVertexColorBuffer;
    var cubeVertexPositionBuffer;
    var cubeVertexColorBuffer;
    var cubeVertexIndexBuffer;

我们在锥体的顶点位置数组内为每个面填入相应的值,另外对numItems参数也作出相应的修改。

149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
        pyramidVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
        var vertices = [
            // Front face
             0.0,  1.0,  0.0,
            -1.0, -1.0,  1.0,
             1.0, -1.0,  1.0,
 
            // Right face
             0.0,  1.0,  0.0,
             1.0, -1.0,  1.0,
             1.0, -1.0, -1.0,
 
            // Back face
             0.0,  1.0,  0.0,
             1.0, -1.0, -1.0,
            -1.0, -1.0, -1.0,
 
            // Left face
             0.0,  1.0,  0.0,
            -1.0, -1.0, -1.0,
            -1.0, -1.0,  1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        pyramidVertexPositionBuffer.itemSize = 3;
        pyramidVertexPositionBuffer.numItems = 12;

对于锥体的顶点颜色数组也一样:

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
        pyramidVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
        var colors = [
            // Front face
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
 
            // Right face
            1.0, 0.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
 
            // Back face
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
 
            // Left face
            1.0, 0.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            0.0, 1.0, 0.0, 1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        pyramidVertexColorBuffer.itemSize = 4;
        pyramidVertexColorBuffer.numItems = 12;

对于立方体的顶点位置数组:

204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
        cubeVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
        vertices = [
            // Front face
            -1.0, -1.0,  1.0,
             1.0, -1.0,  1.0,
             1.0,  1.0,  1.0,
            -1.0,  1.0,  1.0,
 
            // Back face
            -1.0, -1.0, -1.0,
            -1.0,  1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0, -1.0, -1.0,
 
            // Top face
            -1.0,  1.0, -1.0,
            -1.0,  1.0,  1.0,
             1.0,  1.0,  1.0,
             1.0,  1.0, -1.0,
 
            // Bottom face
            -1.0, -1.0, -1.0,
             1.0, -1.0, -1.0,
             1.0, -1.0,  1.0,
            -1.0, -1.0,  1.0,
 
            // Right face
             1.0, -1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0,  1.0,  1.0,
             1.0, -1.0,  1.0,
 
            // Left face
            -1.0, -1.0, -1.0,
            -1.0, -1.0,  1.0,
            -1.0,  1.0,  1.0,
            -1.0,  1.0, -1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        cubeVertexPositionBuffer.itemSize = 3;
        cubeVertexPositionBuffer.numItems = 24;

颜色数组会稍微复杂一些,因为我们使用一个循环来生成一个顶点颜色的列表,这样我们就不必定义4次颜色,每个顶点一次:

247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
        cubeVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
        colors = [
            [1.0, 0.0, 0.0, 1.0], // Front face
            [1.0, 1.0, 0.0, 1.0], // Back face
            [0.0, 1.0, 0.0, 1.0], // Top face
            [1.0, 0.5, 0.5, 1.0], // Bottom face
            [1.0, 0.0, 1.0, 1.0], // Right face
            [0.0, 0.0, 1.0, 1.0]  // Left face
        ];
        var unpackedColors = [];
        for (var i in colors) {
            var color = colors[i];
            for (var j=0; j < 4; j++) {
                unpackedColors = unpackedColors.concat(color);
            }
        }
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
        cubeVertexColorBuffer.itemSize = 4;
        cubeVertexColorBuffer.numItems = 24;

最后,我们要定义元素数组对象。请注意gl.bindBuffer和gl.bufferData这两个函数的第一个参数与之前的不同,之前都是gl.ARRAY_BUFFER,用于指定这是一个用于存放顶点属性数据的缓存对象;现在则是gl.ELEMENT_ARRAY_BUFFER,用于指定这是一个用于存放索引数据的缓存对象。
)。

268
269
270
271
272
273
274
275
276
277
278
279
280
        cubeVertexIndexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
        var cubeVertexIndices = [
            0, 1, 2,      0, 2, 3,    // Front face
            4, 5, 6,      4, 6, 7,    // Back face
            8, 9, 10,     8, 10, 11,  // Top face
            12, 13, 14,   12, 14, 15, // Bottom face
            16, 17, 18,   16, 18, 19, // Right face
            20, 21, 22,   20, 22, 23  // Left face
        ];
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
        cubeVertexIndexBuffer.itemSize = 1;
        cubeVertexIndexBuffer.numItems = 36;

记住,每个在buffer里面的数字都是对于一个顶点位置和颜色buffer的索引。所以,在第一行内,结合在drawScene里面绘制三角形的说明,这个代码意味着我们利用顶点0,1,2绘制一个三角形,接着用0,2,3绘制了另一个三角形,这两个三角形的颜色都是一样的。因为两个三角形颜色一样,彼此邻近,这样就绘制出了一个以0,1,2,3为顶点的正方形。在绘制下一个正方形的时候再次循环这个过程。

这样,你就知道了如何使用3D物体制作WebGL场景。并且你们还知道了该如何通过element array buffers 和 drawElements 来循环使用你在array buffers 指定的顶点。如果有什么问题,请留下评论。

在下节课中,我们将会引入纹理。

分享到: 更多
Posted in WebGL教程 and tagged , , . Bookmark the permalink.

2 trackbacks

WebGL教程 | WebGL中文教程翻译工程启动!HiWebGL | HiWebGL | 最好的HTML5 WebGL中文资讯站
2011 年 9 月 3 日 下午 11:43
WebGL新闻 | WebGL 教程第三、四课中文学习笔记HiWebGL | HiWebGL | 最好的HTML5 WebGL中文资讯站
2011 年 12 月 9 日 上午 8:34

15 comments

  1. 这个需要怎么的opengl的基础?

    回复

  2. WEBGL接口继承自OpenGLES2.0, 严格来说, 学习WEBGL并不需要有OPENGL的基础, 你可以直接将WEBGL作为你进入3D领域的切入点, 因为它很简单, 不像opengl需要考虑很多系统相关的事情.
    不过当然, 如果你已经有了opengl的基础, 那么对你学习webgl是非常有帮助的

    回复

  3. 这个立方体,实际上只有8个顶点,那么我们在定义array_buffrt的时候,通过索引使用8个顶点,是否也可以实现这样的效果。。

    回复

    • 索引8个顶点是不行的,因为每次画正方形的时候,都是先通过画三角形来实现的; 所以一个面的正方形必须画两次完整的三角形,即一个面会需要6个顶点索引。

      回复

      • 在这个例子里,需要24个顶点是每个面的颜色决定的。
        如果不考虑面的颜色,按照顶点绘制颜色,8个顶点就是足够的。

        回复

        • 我觉得8个还是不行的。因为并没有画正方形的。

          回复

  4. 不太清楚元素数组对象和数组对象的区别,楼主可否再详细帮忙详细说明一下?谢谢啦O(∩_∩)O~

    回复

    • 按我的理解,元素数组对象跟数组对象是因为 所定义的实例对象的不同,相应的对象类型就不同;索引是element元素类型,所以定义缓存块的时候,要赋予相应的类型。

      回复

  5. 不太清楚元素数组对象和数组对象的区别,楼主可否再详细帮忙详细说明一下?谢谢啦O(∩_∩)O~

    回复

  6. 我把立方体的六个面的顶点顺序混以顺时针和逆时针共存,也能够画出同样的效果,但是我看NEHE的教程中说了下面这句话:“注意所有的面-三角形都是逆时针次序绘制的。这点十分重要,在以后的课程中我会作出解释。现在,您只需明白要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非您有足够的理由必须这么做。” 这句话的意思是不是混用会带来麻烦?
    另外还有一句话:”接下来开始画立方体。他由六个四边形组成。所有的四边形都以逆时针次序绘制。就是说先画右上角,然后左上角、左下角、最后右下角。您也许认为画立方体的背面的时候这个次序看起来好像顺时针,但别忘了我们从立方体的背后看背面的时候,与您现在所想的正好相反。“ 在这一课中我们的顶点并非从右上角,然后左上角、左下角、最后右下角这样一个顺序。我试着打乱原先代码的中的一些顶点顺序,似乎没有对结果产生影响。
    那么问题是:在webgl中定义顶点时是否都要按照逆时针顺序?定义是否需要按照右上角,然后左上角、左下角、最后右下角这样一个顺序? 请赐教。

    回复

    • 以我的Opengl的知识来看,这里之所以要保持顺时针或者逆时针的一致是为了确保你画出的物体每个面的正面都冲外或者冲里,因为之后可能会做像“取消显示背面”这样的操作(貌似这样会对渲染的速度有所优化)或者”正背面贴上不同的纹理“,如果不一致会使物体看起来奇怪

      回复

    • 可以在顺时针上绘制逆时针图行达到裁切效果。

      回复

  7. 感觉这个例子还可以做优化,本例子画立方体用了24个顶点,其实重复的有很多,画立方体的时候用8个顶点就够了,然后通过索引来使用数组中不同的顶点来画出多个三角形

    回复

    • 它的目的就是每个顶点要有3种颜色,所以自然需要24个点了。

      回复

  8. 我之前试过用gl.quads画正方形,但是为什么画出来的只有四个点呢?难道一定要用triangle_strip画?

    回复

Post a Comment

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

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>