WebGL教程2:绘制三角与方块
来源:http://www.hiwebgl.com/
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
欢迎来到我的第一个WebGL教程!这一课的内容是基于NeHe的OpenGL教程的第2章的,对于学习3D图形游戏开发来说这是一个很受欢迎的教程。在这一课中将会展示如何在网页中绘制一个三角形和一个矩形。也许这看起来一点都不酷,但是却能很好的介绍如何建立WebGL;如果你明白了其中的原理,剩下的部分将会变得相当简单……
下面你看到的这张图片(是图片,不是真正的WebGL渲染哦~)就是我们最重要完成的效果:
如果你已经准备好了支持WebGL的浏览器,那请点击这里在新窗口中打开一个真正的WebGL页面。如果你的浏览器不支持WebGL,请点击这里。
下面开始说说它是如何工作的……
事先声明一下,本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而快速地创建自己的3D Web页面。我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。
有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;你也可以在这里下载到打包好的实例代码。得到代码之后,用你喜欢的文本编辑器打开。即使你对OpenGL已经了解了个大概,第一眼望去,你还是会感觉到一种摸不着头脑的沮丧。代码的开始我们先定义了一对着色器,这可是一个通常被认为相对高级的概念。但是别失去信心,实际上它比看起来要简单得多。
和许多程序类似,这个WebGL页面一开始定义了一系列低级别的函数提供给底部那些高级别的代码使用。为了解释这些函数,我会用我的方式从代码底部开始讲起,所以请跟着我从底部开始看起。
在代码底部你会看到如下的HTML代码:
186 187 188 189 190 191 192 193 194 195 196 197 198 |
<body onload="webGLStart();"> WebGL中文教程,由HiWebGL翻译整理,感谢<a href="http://www.oak3d.com">Oak3D</a>提供图形库支持!<br/> <a href="http://www.hiwebgl.com/?p=93"><< 回到Lesson 1</a><br /> <canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas> <br/> <a href="http://www.hiwebgl.com/?p=93"><< 回到Lesson 1</a><br /> </body> |
整个Demo页面中只有这是一段完整的页面body部分的代码,其余的都是Javascript代码(你也许会看到一些其他的东西,那是我用来做站点统计的,所以请忽略不计)。显然我们可以在<body>中插入其他的HTML代码,以使WebGL图形可以嵌入到普通的网页中。在这个独立页面中我们只是简单的插入了一条返回课程正文的链接,而<canvas>标签就是3D图形的部分。Canvas是HTML5中的新概念,它支持使用Javascript来绘制2D图形和通过WebGL来绘制3D图形。我们只需要简单的指定canvas标签中的布局的属性,然后把所有建立WebGL的代码都放到一个叫做webGLStart的Javascript函数中,它在页面加载后被调用使你可以看到绘制的图形。
让我们把鼠标滚轮向上滚滚来看一下这个函数:
167 168 169 170 171 172 173 174 175 176 177 |
function webGLStart() { var canvas = document.getElementById("lesson01-canvas"); initGL(canvas); initShaders(); initBuffers(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); drawScene(); } |
我们稍后会详细讲解initGL和initShaders的,因为对于理解这个页面是如何工作来说它们相当重要。但是首先,我们还是来看看initBuffers和drawScene。
先来一步一步的说说initBuffers:
118 119 |
var triangleVertexPositionBuffer; var squareVertexPositionBuffer; |
接下来:
121 122 |
function initBuffers() { triangleVertexPositionBuffer = gl.createBuffer(); |
我们创建了一个数组对象来储存三角形的顶点位置。顶点是在3D空间中定义我们要绘制的图形的点。对于三角形来说,我们需要定义3个点(我们一会就要这么做了)。这个数组对象位于显卡中;在初始化代码里,我们把储存有顶点位置信息的数组对象放到显卡中,当我们需要绘制场景的时候,实际上等于告诉WebGL说“按照我之前告诉你的画出这些形状”。这样可以使得我们的代码更加有效率,特别是当我们需要绘制一个动画场景的时候,会想要在1秒钟内画10次物体来模拟运动的效果。当然,目前只是3个顶点的位置信息,即使不放入显卡中也不会消耗太多的资源,但是当你要处理一个大一点的模型例如有好几万个顶点的时候,这种处理方式就会显示出它的优点。接下来:
123 |
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); |
124 125 126 127 128 |
var vertices = [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ]; |
129 |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); |
130 131 |
triangleVertexPositionBuffer.itemSize = 3; triangleVertexPositionBuffer.numItems = 3; |
现在我们已经成功建立了三角形的数组对象,下面再创建矩形的数组对象。
133 134 135 136 137 138 139 140 141 142 143 144 |
squareVertexPositionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); vertices = [ 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); squareVertexPositionBuffer.itemSize = 3; squareVertexPositionBuffer.numItems = 4; } |
OK,这就是我们需要向显卡推送的两个物体的顶点位置信息。现在我们来研究一下drawScene,我们将要在这个函数中使用之前创建的顶点位置信息来绘制我们最终看到的图形。请跟着我一步一步的慢慢看:
147 148 |
function drawScene() { gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); |
149 |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
151 |
pMatrix = okMat4Proj(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0); |
你会发现,实际上这个透视使用了一个来自Oak3d的模块的函数,并且包含一个名字很神秘的变量pMatrix。稍后我会详细解释一下,但是现在只希望你明白如何使用它而不必理会细节。
现在我们已经建立起了透视,我们可以继续绘制工作了。
第一步是“移动”到3D场景的中心。在OpenGL中,当你想要绘制一个场景的时候,你会告诉OpenGL在当前位置、按照当前旋转角度来绘制每个物体。比如,你说“向前移动20个单位,旋转32度,绘制一个机器人”,这基本上就是一个“位移多少多少,旋转多少多少,画什么什么”的综合过程。这个过程非常实用,因为你可以把“绘制机器人”的代码封装到一个函数中,然后就可以修改函数中位移和旋转的参数来轻松实现机器人的移动。
当前位置和当前旋转角度都存放在一个矩阵中,大概你应该在学校里学过,矩阵可以代表位移(从一个坐标移动到另一个坐标)、旋转和其他几何变换。我现在先不细说,你只需要记住一个4×4矩阵(不是3×3矩阵!)可以用来表示3D场景中的任何多种变换。首先从单位矩阵开始——这个矩阵意味着不进行任何变换——然后乘以你将要进行的第一个变换的矩阵,再乘以你将要进行的第二个变换的矩阵,然后第三个、第四个……最后乘积所得到的矩阵代表了所有你进行过的变换。用来表示当前位移/旋转状态的矩阵我们称之为“模型视图矩阵”(model-view matrix),现在你应该明白了mvMatrix就是用来储存我们的模型视图矩阵的变量。
译者注:
这里有一个一般初学者很容易混淆的地方————由于opengl默认采用的是列向量, 所以矩阵变换的累加在opengl中是从右向左累加的,就是说:rotateMat * moveMat这条语句, 解释起来实际是先做move, 再做rotate,因为最终变换顶点时是rotateMat * moveMat * v。另外一个例子,执行下列语句
glRotate;
glMove;
glScale;
实际执行起来却是先做Sacle,再做Move,最后做Rotate。
在WebGL中,由于是完全shader化的,所以不存在这个问题,但大多数图形库为了保持和OpenGL一致,也使用了左乘,Oak3D也是这样。
眼尖的读者或许已经发现了,上面讨论矩阵的过程中,我一直是说在“OpenGL”中而不是在“WebGL”中。这是因为WebGL没有这套内置的图形库。所以我们只好使用第三方图形库——由Oak3D Team编写的Oak3D。这些第三方图形库使用了一些很巧妙的技巧,使得在WebGL中也可以实现相同的效果。在后面我们还会详细解释这些技巧的。
现在,让我们看看在场景左边绘制三角形的代码吧!
153 |
mvMatrix = okMat4Trans(-1.5, 0.0, -7.0); |
154 155 |
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); |
你应该还记得,如果要使用我们的数组对象,我们必须调用gl.bindBuffer来将其指定为当前数组对象,然后再调用代码来进行操作。现在我们选择triangleVertexPositionBuffer,然后告诉告诉WebGL这个数组对象中的值是用来表示顶点位置的。我稍后会详细解释它的工作原理。现在你会看到,我们用之前赋予数组对象的itemSize这个属性来告诉WebGL,数组对象中每个顶点位置信息包括3个数字。
接下来:
156 |
setMatrixUniforms(); |
随着这一步的完成,WebGL已经知道了一个用来表示顶点位置信息的数字数组以及我们的矩阵。下一步将会告诉WebGL用它们来做什么。
157 |
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems); |
到此为止,三角形绘制完毕。记下来我们看看如何绘制矩形。
159 |
mvMatrix = okMat4Trans(1.5, 0.0, -7.0); |
160 161 |
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); |
我们告诉WebGL去使用矩形的数组对象来获得顶点位置信息。
162 |
setMatrixUniforms(); |
163 |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems); |
不管如何,到此我们结束了drawScene函数。
164 |
} |
……
好吧,你终于回来了。那让我们来看看之前我们忽略掉的函数吧。如同我之前说过的,如果你很乐于忽略这些细节和工作原理并且仅仅是复制粘贴这些函数的代码,例如initBuffers,你也许可以绘制出有趣的WebGL页面(还不仅是黑白,下节课我们就要讲到颜色了)。但是这些细节和工作原理其实并不难理解,而理解了它们会帮助你创作出更好地WebGL页面的。
你决定继续看下去?好吧,非常感谢!让我们来看看这些无聊的函数中的第一个,这个被webGLStart函数调用的叫做initGL的函数。它位于代码的最顶端,如下:
33 34 35 36 37 38 39 40 41 42 43 44 |
var gl; function initGL(canvas) { try { gl = canvas.getContext("experimental-webgl"); gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; } catch (e) { } if (!gl) { alert("Could not initialise WebGL, sorry :-("); } } |
在调用initGL之后,webGLStart又调用了initShaders函数。这个函数,毫无疑问的,是用来初始化着色器的。我们等会再聊这个函数,先让我们看看之前说过的模型视图矩阵和投影矩阵吧。
108 109 |
var mvMatrix; var pMatrix; |
现在我们已经了解了除setMatrixUniforms 函数以外的所有部分,如同我之前说过的,这个函数可以将模型视图矩阵和投影矩阵从平易近人的Javascript推送到高深莫测的WebGL中。WebGL和着色器其实是内在关联的,让我们先来了解一下一些背景知识。
你也许会问,到底什么是着色器? 好吧,从3D图形学历史的角度来讲,它的确曾经扮演过和它名字一样的角色——二进制码告诉系统如何在绘制一个场景之前进行渐变或上色。但随着时间的推移,着色器开始渐渐扩展自己的应用范围,现在应当把它定义为二进制码在绘制一个场景之前做任何想要做的事情。这的确非常实用,一是因为这些操作是在显卡中进行的,所以运行速度非常快;二是因为这些操作调用起来非常方便,即使在这种简单的实例中。
我们之所以在一个WebGL的初级实例中引入着色器的概念(要知道在OpenGL中,到中级开发才会出现着色器啊!),是因为我们要使用着色器来进入WebGL系统,将我们的场景的模型视图矩阵和投影矩阵的运算交给显卡来进行,而不是在相对较慢的Javascript中移动每一个点和每一个顶点。它是如此难以置信的实用,提高了我们的效率,所以我们值得花时间去学习和了解它。
下面,我们看看是如何设置着色器的。你也许会记得,webGLStart调用了initShaders函数,让我们一步一步来看:
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
var shaderProgram; function initShaders() { var fragmentShader = getShader(gl, "shader-fs"); var vertexShader = getShader(gl, "shader-vs"); shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("Could not initialise shaders"); } gl.useProgram(shaderProgram); |
100 101 |
shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); |
函数设置好program并捆绑好着色器之后,就会引用一个“属性”(attribute),也就是program对象的新字段vertexPositionAttribute。在这里我们再一次用到了Javascript的优点,那就是可以给任何对象增加任何字段。默认情况下program对象并没有vertexPositionAttribute这个字段,但是有了它我们就可以方面的把两个值放到一起,所以我们就给他赋予了“属性”这个新字段。
那vertexPositionAttribute又是用来做什么的呢?你也许会记得,我们在drawScene函数中用到过他。如果你回头去看那段从相应数组对象中设置三角形的顶点位置的代码,你会发现我们在处理数组对象的时候用到了这个属性。你一会就会明白它的含义,现在让我们看下gl.enableVertexAttribArray,我们使用它来告诉WebGL我们会用一个数组来为属性赋值。
103 104 105 |
shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); } |
现在我们来看看getShader函数:
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
function getShader(gl, id) { var shaderScript = document.getElementById(id); if (!shaderScript) { return null; } var str = ""; var k = shaderScript.firstChild; while (k) { if (k.nodeType == 3) { str += k.textContent; } k = k.nextSibling; } var shader; if (shaderScript.type == "x-shader/x-fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (shaderScript.type == "x-shader/x-vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else { return null; } gl.shaderSource(shader, str); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(gl.getShaderInfoLog(shader)); return null; } return shader; } |
我们看看着色器的代码:
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<script id="shader-fs" type="x-shader/x-fragment"> precision mediump float; void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script> <script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; void main(void) { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); } </script> |
由于在把属性和数组对象联系到一起的时候,我们在drawScene函数中使用了vertexPositionAttribute,所以现在可以为每个顶点调用着色器,并且把顶点作为aVertecPosition传递给着色器代码。在着色器的二进制码的主程序中,顶点位置与模型视图矩阵和投影矩阵相乘,然后把最终顶点位置作为结果输出。
总结一下,webGLStart函数调用了initShaders,后者使用了getShader来从网页文件的脚本中载入片元着色器和顶点着色器,所以他们可以被编译并传送给WebGL,然后提供给稍后进行的3D场景渲染时使用。
最后,还剩最后一个没有提到的函数,那就是setMatrixUniforms。如果上面的讲解你都明白了,那你会容易理解它的含义的。
111 112 113 114 |
function setMatrixUniforms() { gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix.toArray()); gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix.toArray()); } |
好了!第一课到此结束!可真长啊!希望你能理解这些基础知识,为我们以后创作更多更有趣的东西(颜色、移动、三维模型)做好准备!