【转载】WebGL教程 Lesson 1 三角与方块的故事
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com
欢迎来到我的第一个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(); } |
它调用了其他函数来初始化WebGL以及之前提到的着色器,并传递给我们将要绘制3D图形的canvas元素模型,然后使用initBuffers来初始化了一些数组对象。这些数组对象用于储存我们将要绘制的三角形和矩形的细节,稍后我们会详细解释这一部分。接下来,函数对WebGL进行了一些基本的设置,具体的含义是当我们清空canvas时应当把颜色设置为黑色,并且要启用深度检测(这样在后面的物体就会被前面的物体遮挡住)。这些设置都是调用gl对象的一些方法完成的,我们稍后来解释它是如何初始化的。最后,函数调用了drawScene函数,你从名字函数名就能看出来,它的用途是使用数组对象来绘制三角形和矩形。
我们稍后会详细讲解initGL和initShaders的,因为对于理解这个页面是如何工作来说它们相当重要。但是首先,我们还是来看看initBuffers和drawScene。
先来一步一步的说说initBuffers:
118 119 |
var triangleVertexPositionBuffer; var squareVertexPositionBuffer; |
我们声明了两个全局变量来存放数组对象(Array Buffer Object)。(实际上在正式的WebGL页面上你不需要为场景中的每个物体分别声明一个全局变量,我们这里这样做是因为在学习之初,解释起来更加容易。)
接下来:
121 122 |
function initBuffers() { triangleVertexPositionBuffer = gl.createBuffer(); |
我们创建了一个数组对象来储存三角形的顶点位置。顶点是在3D空间中定义我们要绘制的图形的点。对于三角形来说,我们需要定义3个点(我们一会就要这么做了)。这个数组对象位于显卡中;在初始化代码里,我们把储存有顶点位置信息的数组对象放到显卡中,当我们需要绘制场景的时候,实际上等于告诉WebGL说“按照我之前告诉你的画出这些形状”。这样可以使得我们的代码更加有效率,特别是当我们需要绘制一个动画场景的时候,会想要在1秒钟内画10次物体来模拟运动的效果。当然,目前只是3个顶点的位置信息,即使不放入显卡中也不会消耗太多的资源,但是当你要处理一个大一点的模型例如有好几万个顶点的时候,这种处理方式就会显示出它的优点。接下来:
123 |
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); |
这一行代码告诉WebGL要为下面的操作指定一个数组对象。这个“当前数组对象”(Current Array Buffer)的概念会一直存在下去,函数只能对当前数组对象进行操作而不允许你为函数指定一个数组对象。很奇怪是吧!但我想这背后一定有一个绝妙的可以提高性能的理由吧。
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 ]; |
然后,我们用Javascript的列表的形式定义了我们的顶点位置信息。你可以看出来这是一个以(0,0,0)为中心的等腰三角形。
129 |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); |
现在我们创建了一个基于Javascript列表的Float32Array对象,然后告诉WebGL用它来填充当前数组对象,当然就是我们的triangleVertexPositionBuffer啦。我们会在以后的课程中在详细讲解Float32Array,现在你所要记住的就是它的作用就是可以把Javascript列表转换成可以传递给WebGL的形式以便填充到当前数组对象中。
130 131 |
triangleVertexPositionBuffer.itemSize = 3; triangleVertexPositionBuffer.numItems = 3; |
最后我们要做的就是为数组对象设置两个新的属性,这两个属性并不会传递给WebGL但是在以后会非常有用。Javascrip有个优点(也许有人认为这是个缺点),那就是对象的属性可以在对象创建后进行动态定义。所以虽然之前数组对象并没有itemSize和numItems这两个属性,但现在,我说它有,它就有了!我们使用这两个属性来说明这个含有9个元素的数组对象实际代表3个不同的顶点位置(numItems),每个顶点由3个数字组成(itemSize)。
现在我们已经成功建立了三角形的数组对象,下面再创建矩形的数组对象。
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; } |
上面的代码应该非常清晰明了了,矩形拥有4个顶点位置信息而不是3个,所以数组会更大,numItems的值也与三角形不同。
OK,这就是我们需要向显卡推送的两个物体的顶点位置信息。现在我们来研究一下drawScene,我们将要在这个函数中使用之前创建的顶点位置信息来绘制我们最终看到的图形。请跟着我一步一步的慢慢看:
147 148 |
function drawScene() { gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); |
首先要使用viewport函数来告诉WebGL一些canvas的尺寸信息,我们再之后的课程中会讨论到为什么要这样做的重要性;现在,你只需要知道在你开始绘制之前程序需要调用canvas的尺寸。下面,我们清空canvas,开始准备绘制:
149 |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
然后:
151 |
pMatrix = okMat4Proj(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0); |
我们建立一个用于观测场景的透视图。在默认的情况下,WebGL会把近处的物体和远处的物体用同样的尺寸绘制(在3D图形学中被称为“正射投影”)。为了使远处的物体看起来要小一些,我们需要明确告诉WebGL我们使用的透视。对于这个场景,我们告诉WebGL我们的(垂直)视野是45°、canvas的宽高比以及从我们的视点看到的最近距离是0.1个单位,最远距离是100个单位。
你会发现,实际上这个透视使用了一个来自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); |
我们用okMat4Trans函数生成了一个位移矩阵,然后赋值给mvMatrix。位移的起点就是3D空间的中心,我们先向左移动1.5个单位(即X轴的负半轴),然后向场景内部移动7个单位(即Z轴的负半轴)。下面终于到了要开始绘制的时候了!
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我们当前的模型视图矩阵(以及投影矩阵,这个概念稍后我们再说)。尽管你已经可以通过修改mvMatrix变量来进行任何移动和旋转,但是这些还都只存在于Javascript的私有空间中。而setMatrixUniforms这个函数将会超越网页文件的范畴,将这些操作推送到显卡中去。
随着这一步的完成,WebGL已经知道了一个用来表示顶点位置信息的数字数组以及我们的矩阵。下一步将会告诉WebGL用它们来做什么。
157 |
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems); |
换一种通俗的说法就是“用之前我给你的顶点数组来绘制一个三角形,顶点从item 0开始一直到numItems的值”。
到此为止,三角形绘制完毕。记下来我们看看如何绘制矩形。
159 |
mvMatrix = okMat4Trans(1.5, 0.0, -7.0); |
开始我们先把我们的模型视图矩阵向右移动3个单位。记住,我们之前的位置是中心偏左1.5个单位、中心偏内7个单位,现在我们的位置应该是中心偏右1.5个单位、中心偏内7个单位。下面:
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 |
} |
如果你能坚持看到这里,一定跃跃欲试准备开始一番试验。拷贝代码到你本地或者点击这里下载打包好的代码。在本地运行一下看看是否正常,然后试着改变一下顶点位置;现在的场景还特别平坦,所以再试着改变一下矩形Z轴的位置,改成2或者-3,看看它是否变大或者变小,也就是变近或者变远。或者改变其中一个或两个值,看一下矩形在透视中的变形。是不是很神奇呢?慢慢来,别着急,我会等着你的!
……
好吧,你终于回来了。那让我们来看看之前我们忽略掉的函数吧。如同我之前说过的,如果你很乐于忽略这些细节和工作原理并且仅仅是复制粘贴这些函数的代码,例如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 :-("); } } |
这段代码很简单。你也许已经注意到了,在initBuffers和drawScene函数中频繁的提及一个叫做gl的对象,每次提及的时候通常都与一些WebGL核心的东西有关。在上面的函数中,明确了这个核心的东西的概念,那就是WebGL上下文(WebGL Context),并且从canvas那里获得了一个拥有标准名称的上下文。(也许你会问,上下文的标准名称会不会变呢?比如从“experimental-webgl”变成“webgl”。是的,也会说不准哪天就变了。但到时候我会更新这篇教程的,所以请关注我们的网站吧!)一旦我们获得了上下文,我们就可以使用之前提到的Javascript的动态定义对象属性的优点(或者有人认为是缺点),在上下文中为任何对象创建新的属性,比如canvas的宽度、长度和其他相关数据。到这里,我们的WebGL上下文已经建立起来了。
在调用initGL之后,webGLStart又调用了initShaders函数。这个函数,毫无疑问的,是用来初始化着色器的。我们等会再聊这个函数,先让我们看看之前说过的模型视图矩阵和投影矩阵吧。
108 109 |
var mvMatrix; var pMatrix; |
我们定义了一个名叫mvMatrix的变量来储存模型视图矩阵,另一个叫pMatrix的变量来储存投影矩阵。这里有必要多说说投影矩阵。也许你还记得,在drawScene函数的开始部分,我们把这个变量用Oak3D中的okMat4Proj函数建立了我们的透视。这是因为WebGL并不直接支持透视,就像它同样也不直接支持模型视图矩阵一样。然而就如同把移动物体、旋转物体等几何变换封装到模型视图矩阵中一样,表现出让远处的物体看起来小一些的这种透视效果也是矩阵的拿手好戏。现在不用猜也知道,投影矩阵就是用来做这件事情的。okMat4Proj函数根据宽高比和视野信息,将我们需要的透视效果的值填充到矩阵中。
现在我们已经了解了除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); |
如同你看见的,它使用了getShader函数来获取两样东西,一个是“片元着色器”(Fragment Shader),另一个是“顶点着色器”(Vertex Shader),然后把它们俩同时附加到了一个叫做“program”的WebGL对象里。Program是系统中原生于WebGL里的二进制码,你可以把它看作是一种在显卡中运行指定指令的方法。和你想的一样,你可以把若干着色器组合捆绑到一个program中,每个着色器都可以看做一段program中的代码。特别是每个program可以储存一个片元着色器和一个顶点着色器。
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"); } |
initShaders最后做的事情是从program中取得另外两个值,也就是两个叫做uniform的变量的地址。一会我们还会用到它们;现在你只需要记住和属性类似,为了方便,这两个值也是储存在program对象中。
现在我们来看看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; } |
这又是一个看起来复杂但实际上非常简单的函数。我们这里所作的一切就是从我们的HTML页面中寻找一个id能够对应传递进去的参数的元素,取出它的内容,建立一个片元着色器或一个顶点着色器(之后的课程中我们会讲到它们俩的区别),然后传递到WebGL端编译成可以在显卡中运行的形式。代码会再来处理发生的错误。就是这样!当然,我们也可以在Javascript中把着色器定义为字符串并且不与从HTML中提取出来的字符串混在一起,但是显然我们代码中的方式更加简单易读,因为他们在网页中被定义为脚本,一如他们本来的身份就是Javascript脚本!
我们看看着色器的代码:
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> |
首先你要记住的是,这段代码不是用Javascript写成的!尽管看起来语法很像,但事实上这段代码属于一种特殊的着色器语言——叫做GLSL——很像C的一种语言(当然也就像Javascript了)。首先在第一段代码中,片元着色器实际上没有做任何事情,我们只是按照约定俗成惯的例加上这段代码,告诉显卡我们需要精确到浮点值的数字运算,然后指定绘制物体的时候要用白色(下一课我们还会详细讲述物体的颜色的)。相对来说第二个着色器更有趣一些。这是一个顶点着色器,你也许会记得它的作用是让显卡对顶点做任何想要做的事。与它相关的两个uniform变量,叫做uMVMatrix和uPMatrix。uniform变量非常有用,因为你可以在着色器之外访问它们,甚至在它们的容器program之外!回忆一下,在initShaders函数中我们提取出了它们的地址,在下面的代码里我们把它们赋值给模型视图矩阵和投影矩阵。你或许可以把着色器的program想象成一个对象(面对对象的观念),把uniform变量想象成字段。
由于在把属性和数组对象联系到一起的时候,我们在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()); } |
使用我们在initShaders中得到的储存模型视图矩阵和投影矩阵的uniform变量,我们把变量值从Javascript风格的矩阵推送到WebGL当中去。
好了!第一课到此结束!可真长啊!希望你能理解这些基础知识,为我们以后创作更多更有趣的东西(颜色、移动、三维模型)做好准备!
这篇最长,也是最难懂,看了很久