WebGL教程1:一个三边形和一个四边形

欢迎来到我的第一个WebGL教程!这节教程是以NeHe的OpenGL教程第二课为基础的(译者注:这里有网上找到的NeHe的OpenGL中文教程),NeHe的OpenGL教程是学习3D图形游戏编程很流行的一个教程。这节课向你解释怎样在页面上绘制一个三边形和一个四边形。也许这本身不是那么令人兴奋,但这是对WebGL基础的一个很好的介绍:如果你知道它是这样工作,剩下的会是十分相似……
这儿是这节课的代码在支持WebGL的浏览器运行看起来的效果:



WebGL教程1:一个三边形和一个四边形_第1张图片


一个小的忠告:这些教程所针对的人群是具有一定的编程知识,但是没有真正的3D图形编程经验;目的是使你入门并且知道这些代码是怎样运行的,以至于你能够尽快开始制作自己的3D网页。我自己写这些代码是因为我在自学WebGL,所以可能有错误;你自己负责是否使用它。然而我一直在校准bugs和改正错误的地方当我听到相关信息的时候,所以如果你看到哪些地方有错误,请在下面的留言里面让我知道。
有两种方法你能够得到这个示例的代码:当你看现场演示的时候,查看源代码或是如果你使用GitHub,你能够复制它(还有以后的教程)从那个代码库。两者选一,一旦你有了代码,加载到你最喜欢的文本编辑器里看一看。看第一眼是非常让人畏惧的,尽管你对OpenGL有一定的熟悉。对了,在开始,我们定义了一对渲染器,渲染器一般被认为是相当高级的……但是不要绝望,它实际上比看起来简单多了。
就像大部分的编程,这个WebGL页面是通过在页面底部定义一系列被高层次代码所使用的低层次函数开始运行的。为了解释它,我将从页面底部开始讲解并且逐渐向上面讲,所以如果你想让代码跟上来,跳到页面的底部。
你将看到下面的HTML的代码:
< body οnlοad="webGLStart();">
<< Back to Lesson 1




<< Back to Lesson 1


这完全是一个网页的body部分——其余的部分都是JavaScript(尽管如果你通过“查看源代码”获得代码,你将获得一些其他的被我的服务器解析的代码,你可以忽略。)显然地我们能够放置很多标准的HTML标签放在 标签之间来将WebGL图像构建为一个标准的网页,但是对于这个简单的页面我们仅仅获得WebGL并且设置指向这个博客的链接,这个 标签是用来放置3D图形。Canvas是HTML5的一个新特性——它支持JavaScript在网页上绘制2D和(通过WebGL)3D。我们不需要指明其它信息除了在canvas标签里指明简单的布局属性,而是将WebGL设置代码留到一个简单的名为webglStart的JavaScript函数上,一旦页面被加载,这个函数就被调用。
现在让我们向上滚动到函数的地方来看一下:
  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初始化一些缓存;缓存记录了我们将要绘制的三边形和四边形的详细信息——过一会儿我们将会详细谈论它们。接下来,是一些基本的GL引擎设置,当我们清除画面的时候我们会将画面设置为黑色,100%的清除我们将展示的图形,并且我们需要进行深度测试(以至于绘制的后面物体要被前面的物体遮挡)。这些步骤是通过调用gl对象的方法来完成的——后面我们将会看到是怎么初始化的。最后,调用drawScene函数;这个函数(正如你根据它的名字推测的)使用缓存绘制一个三角形和一个四边形。
稍后,我们再来看看initGL和initShader这两个函数,因为它们对于理解页面怎样工作很重要,但是,首先,让我们来看看initBuffers和drawScene这两个函数吧。
首先看看initBuffers;一行一行分析:
    var triangleVertexPositionBuffer;
    var squareVertexPositionBuffer;
我们声明两个变量存储缓存。(在真正的WebGL页面,你不需要为屏幕中的每一个对象分配不同的变量,但是在第一课我们这样做是为了让事情变得简单点)
接下来:
    function initBuffers() {
     triangleVertexPositionBuffer = gl.createBuffer();
我们为三边形的位置创建一个缓存。顶点(你不喜欢有规律的复数吗?)在三维空间里面确定我们要绘制图形的点。对于我们的三边形,我们将会有三个点(等下我们会设置)。这个缓存实际上只占用一点点显存;在初始化代码中通过将顶点位置写入显卡,然后当再次绘制图形的时候,本质上我们只是告诉WebGL“绘制先前我告诉你的图形”,这样我们能够使得我们的代码很有效率。当然,在这种情况下我们只设置了三个顶点,将它们写入显卡不需要太多的开销——但是当你处理含有上万个顶点的大模型的时候,这样做会体现出它真正的优势的。
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
这一行告诉WebGL接下来的操作必须在我们指定的缓存中运行。一直要有一个概念就是“当前的数组缓存”,函数是在当前的数组缓存中运行而不是你想在哪个缓存中运行就能运行的。比较奇怪,但是我确定后面的代码能够很好解释原因……
    var vertices = [
         0.0, 1.0, 0.0,
        -1.0, -1.0, 0.0,
         1.0, -1.0, 0.0
    ];
下一步,我们定义我们的顶点位置作为一个JavaScript列表。你能够发现它们是以(0,0,0)为中心的等腰三角形的顶点。
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
现在我们根据我们的JavaScript列表创建一个Float32Array对象,并且告诉WebGL使用它来填充当前的缓存,也就是我们的triangleVertexPositionBuffer。我们将会谈论更多关于Float32Arrays的用法在以后的教程中,但对于现在所有你需要知道的是它们将JavaScript列表转换为一种格式,我们能够将这种格式传入WebGL填充缓存。
    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;
最后关于缓存的设置就是给它添加两个新属性。这不是WebGL的一部分,但是它在后面的代码里面会很有用。一个关于JavaScript非常好的事情(有些人也会说,坏事情)就是一个对象不一定要明确地支持一个特殊的属性,你可以在对象上随便设置属性。因此尽管buffer对象以前没有itemSize和numItems属性,一旦你设置,它们就有了。我们使用这两个属性来说明这个含九个元素的缓存实际上代表着三个分开的顶点位置(numItems),每个顶点位置又由三个数组成(itemSize)。
现在我们完全设置了三边形的缓存,该轮到四边形了:
    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;
     }
上面的代码是非常明显的——四边形有四个顶点位置而不是三个,所以数组更大,numItems属性也不相同。
OK,这些是将对象的顶点位置写入显卡所需要做的。现在让我们看看drawScene函数,这个函数完成了将这些缓存真正绘制成我们所见到的这个图像。一行一行来分析:
     function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
第一步是使用viewport函数告诉WebGL画布的大小;在比较后面的课程中我们将会返回来看看为什么这一步是非常重要的;现在你只需要知道开始绘制之前要获得canvas的尺寸。接下来,我们清除画布为在它上面绘图做准备:
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
……然后:
    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
这儿我们设置想要的透视效果。默认的,WebGL会将远处近处的物体绘制成一样大的尺寸(也就是3D的正交投影)。为了使远处的物体看起来更小,我们需要告诉WebGL我们使用了透视。对于这个场景,我们指定了(垂直)视野是45°;,我们告诉WebGL画布的宽-高比,并且我们不想要看到离0.1单元近的物体,100单元远的物体。
正如你见到的,perspective使用了mat4模块中的一个函数,并且包含了一个名字比较有趣的变量pMatrix。稍后会谈论得更多;如果顺利的话,现在我们不用知道它的具体情况但可以很清楚知道它们怎么使用。
既然我们已经设立了perspective,我们就能够继续绘制我们的物体了:
    mat4.identity(mvMatrix);
第一步是移动到3D场景的中心。在OpenGL里,当你绘制一个场景的时候,你告诉它在“当前的”位置“当前的”角度绘制图形——所以,比如,“向前移动20单元,旋转32°,然后绘制机器人”,后面一些复杂的设置“移动多少,旋转多少,绘制什么”说明了自身的一些特性。这是非常有用的,因为你能够将绘制机器人的代码封装为一个函数,然后可以很容易在某个地方绘制这个机器人通过改变移动/旋转的数据再调用那个函数。
当前的位置和旋转的角度储存在一个矩阵中,正如你在学校里可能学到的,矩阵能够代表移动(从一个地方到另一个地方),旋转和其它的几何变化。由于一些原因我将不会详细讲,你能够使用4×4矩阵(不是3×3)代表3D空间中的任一一个变化;你以单位矩阵开始——也就是说不代表任何移动——然后乘以代表第一个移动的矩阵,紧临着乘以代表第二个移动的矩阵,等等。这个复合的矩阵就代表你所有的移动了。我们使用的代表当前移动/旋转状态的矩阵被叫做模型视图矩阵,现在你可能推测出变量mvMatrix保存着我们的模型视图矩阵,并且我们刚才调用的mat.identity函数是将模型视图矩阵变为单位矩阵以至于为后面的移动和旋转变换做准备。或者,换一句话说,它将我们从开始绘制3D的世界移动到了原点。
眼睛敏锐的读者将会注意在开始讨论矩阵的时候我是说“在OpenGL里”,不是在“WebGL里”。这是因为WebGL没有将这些东西内建在图形库里。作为替代,我们使用了一个第三方的矩阵库—— Brandon Jones的优秀的glMatrix——加上一些极好的能够达到同样效果的WebGL技巧。一些更多的相关技巧会在后面谈到。
下一步是真正绘制图形!
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
你需记住为了使用其中的缓存,我们调用gl.bindBuffer指明当前的缓存,然后调用将在其上面运行的代码。这里我们选择了triangleVertexPositionBuffer,然后告诉WebGL缓存里面的值被用作顶点的位置。后面我将会解释更多关于它们运行原理,现在,你能够看到我们使用先前在缓存上设置的属性来告诉WebGL缓存中的每一项含三个数。
下一步,我们:
    setMatrixUniforms();
告诉WebGL要考虑当前模型视图矩阵(也包括投影矩阵,在后面将有介绍)。这是必要的因为WebGL没自建矩阵。你能够通过用来移动的mvTranslate函数来查看矩阵信息,但是矩阵的运算都是在JavaScript私有域中发生的。setMatrixUniforms函数将数据传递给显卡。
一旦这些设置好了之后,WebGL知道了有个数组要作为顶点位置,而且知道我们设置的矩阵。下一步告诉它用这些数组和矩阵做什么:
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
或者换一句话,“将刚才给你的含顶点的数组用三角形的方式绘制,从数组中的第零项开始到第numItems项。”
一旦完成,WebGL将绘制我们的三边形。下一步,绘制四边形:
    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
开始我们向右移动模型视图矩阵3个单位。记住,我们之前已经向左移动了1.5个单位、向屏幕里面移动了7个单位,因此我们还处于向右1.5向里7的位置。
下一步:
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
告诉WebGL将四边形缓存作为顶点位置来使用……
    setMatrixUniforms();
我们再次将模型视图矩阵和投影矩阵调出来(这与最近的mvTranslate函数有关),这意味着我们最后能够完成:
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
绘制这些点。你也许会问,什么是三角形带(triangle strip)?好的,三角形带是非常有用的,它是一串三角形,你给出的前三个点确定第一个三角形,前面三点的后面两点加上另外确定的一点确定第二个三角形,等等。在这里,用这种方法来指定一个四边形比较快。在一些更复杂的情形下,这是一种真正很有用的方法通过指定很多三角形来模拟复杂的表面。
一旦这个做了,我们就已经完成了drawScene函数。
        }
如果学到了这里,确切地说,你可以准备开始实验了。复制代码到本地文件上,可以从GitHub得到或直接从现场演示网页得到;如果你从演示网页得到代码,你需要index.html和glMatrix-0.9.4.min.js。确定在本地上能够运行,然后尝试改变上面的顶点坐标;特别地,目前的场景是非常的平;尝试将四边形所有顶点的Z值改为2或者-3,由于四边形向前或向后移动,能够看到它变大或变小。或者尝试改变其中的一个或两个,观察一下它在透视中变扭曲。是不是快疯了啊,不要介意我。我在这等着。
……
好的,既然你回来了,让我们看看使得代码运行的支持函数吧。正如我以前说的,如果你乐意忽视细节仅仅复制黏贴页面中initBuffers函数以上的支持函数,你也许能够不理它并且能制作出很有趣的WebGL页面(尽管是白色和黑色——设置颜色在下一课).但是其中的详细内容没有难以理解的,通过理解这些代码是怎样运行的,你很有可能在以后写出更好的WebGL代码。
继续下去?谢谢。让我们首先看看这个碍事的最麻烦的函数吧。第一个是被webGLStart函数调用的initGL函数,它靠近页面的顶部,这儿是代码以供参考:
    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文本的东西,通过请求文本中的canvas,然后使用一个标准的文本名,来获得它。(正如你可能猜测的,在某种程度上这个文本名会从“experimental-webgl”改为“webgl”;如果改了,我将会把这节教程更新并且在博客上说明相关信息)。一旦我们获得了文本,我们再次使用JavaScript允许我们在任何一个对象上设置任何属性的优点来存储与画布(canvas)相关的宽(width)和高(height);我们在drawScene函数开始的地方设置viewport和perspective中使用了这些属性。完成上面的内容之后,我们的GL文本就设置好了。
调用initGL函数之后,webGLStart函数又调用initShaders函数。当然,这个函数是用来初始化渲染器。我们将会在后面返回来看它,因为你需要先看看模型视图矩阵,投影矩阵我在前面提到了。这里是代码:
    var mvMatrix = mat4.create();
     var pMatrix = mat4.create();
我们声明一个mvMatrix变量来存储模型视图矩阵,声明一个pMatrix变量存储投影矩阵,然后设置它们为空(都为0)矩阵作为开始。在这里很值得更多地说明一下投影矩阵。你应该还记得,在drawScene函数的开始,我们将glMatrix函数mat4.perspective应用到这个变量来设置我们的perspective函数。这是因为WebGL没有内建perspective函数,就像WebGL不能直接支持模型视图矩阵一样。但是就像将物体的移动和旋转封装在模型视图矩阵的处理方法一样,使得远处的物体看起来比近处的物体要适当小一点的处理方法是矩阵很擅长的功能。现在你会毫无疑问地推测,投影矩阵做这些事情。这个含有宽-高比和视野参数的mat4.perspective函数用我们想要的透视效果来填充矩阵。
好的,我们已经分析了所有代码除了setMatrixUniforms函数和可怕的相关渲染器的代码,我前面提到过setMatrixUniforms函数,它的作用是将模型视图矩阵和投影矩阵从JavaScript移动到WebGL上。渲染器和这些函数是相互联系的,所以让我们来看看相关背景知识吧。
现在来看看,什么是渲染器呢?你可能会问。好的,在某种程度上在3D图形历史上渲染器跟它听起来的意思一样——一些能够告诉系统在图形绘制之前怎样来着色或是渲染的代码。然而,随着时间的流逝,它们的范围变大了,现在更好的定义它们为:能够绘制场景里任何东西的代码。这实际上是非常有用的,1、因为它们能够在显卡上运行,所以它们运行起来确实快,2、它们能够实现的变换是非常方便的甚至在这节课的简单例子中。
我们在一节简单的WebGL教程中介绍渲染器的原因(这在OpenGL中算是一个“中级”的教程了)是我们使用它来获得WebGL系统,让其在显卡上运行,这样我们场景的模型视图矩阵和投影矩阵不需要在(相对)慢的JavaScript中移动每个顶点和每个向量。这是极其有用的,并且值得提供显卡额外的开销。
这儿是建立它们的代码。你可能记得,webGLStart函数调用initShader函数,让我们一行一行查看:
    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函数得到两个东西,一个“片段渲染器”和一个“顶点渲染器”,然后给它们俩附加上“program”。“program”是一些WebGL体系里面的代码;它能指定在显卡上运行的东西。你可能会预料,我们可以将一些渲染器和“program”联系起来,在“program”里能够看到每个渲染器的片段代码;确切地说,每一个“program”能够储存一个片段渲染器和一个顶点渲染器。我们来看看:
    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
一旦函数设置了“program”,附加上了渲染器,这个函数将传递一个参数给“attribute”,这个“attribute”被存储在一个叫做“vertexPossitionAttribute”的“program”对象的新区域中。我们再一次利用了JavaScript可以给任何对象添加任何区域的特性;默认情况下“program”对象没有vertexPositionAttribute区域,但是我们将这两个值结合起来是很方便的,我们仅仅使得“attribute”变为“program”的一个新的区域。
那么vertexPositionAttribute是用来干什么的呢?你可能记得,我们在drawScene函数中使用了它;如果你返回去看设置存储三边形顶点位置的缓存的代码,你能够看到我们将缓存与这个属性联系到了一起。你等会儿会明白这个意思;现在,你只需注意我们需要使用gl.enableVertexAttribArray来告诉WebGL我们想要提供数组数据给这个属性。
    shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
       }
initShaders函数最后要做的是从“program”中得到另外两个数据——两个一致变量。我们很快会碰到它;现在,你只需要注意,为了方便,我们将像这样的变量存储在“program”对象中。
来看看getShader函数:
  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脚本本身。
看过这些代码之后,我们再来看看渲染器的代码:
 
   
第一个要注意的是这不是用JavaScript代码写的,尽管这个语言的最初版本和它非常相似。实际上,它们是用一门特殊的属于C(当然,JavaScript也是)的渲染语言写的。第一个渲染器,片段渲染器,其实没有做什么;它含有一些必要的模板代码,告诉WebGL要多么精确地使用浮点数,简单地指定所有的东西将会被绘制成白色(怎样添加颜色是第二课的内容)。第二个渲染器更有趣一点。它是顶点渲染器——你应该还记得,它是一些能够随意处理在显卡上的顶点的代码。它含有两个一致变量uMVMatrix和uPMatrix。一致变量是非常有用的,因为它们能够进入到渲染器的外部——实际上,你可能记得当在initShader函数中抽取存储单元的时候,一致变量可以从“program”里面获得数据,还可以从我们等下分析的代码中获得数据,(也许你知道是哪儿)我们将一致变量设置为模型视图矩阵和投影矩阵数据。你可能会把渲染器的“program”作为一个对象(在面向对象程序设计里),把一致变量作为一个区域。
现在,渲染器被每个顶点调用,并且顶点数据作为aVertexPosition传入到渲染器中,多亏了在drawScene函数中使用了vertexPositionAttribute属性,当我们将属性和缓存联系在一起的时候。渲染器主程序中含有一点点代码,用来将顶点和模型视图矩阵、投影矩阵相乘,输出最后顶点位置的结果。
综上,webGLSart调用initShader、initShader函数加载页面脚本中的片段渲染器和顶点渲染器,以至于相关代码能够被编译,传入到WebGL里面,在渲染3D时被使用。
最后剩下的没有解释的代码是setMatrixUniforms函数了,一旦你明白了所有上面的解释,你就能很容易理解它了:
     function setMatrixUniforms() {
        gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
        gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
    }
获得代表我们在initShaders函数中取回的模型视图矩阵和投影矩阵的一致变量的参数,将这些数据以JavaScript形式的矩阵发送给WebGL。
哎呀!第一课的内容太多了,但是可喜的是你(还有我)已经明白了关于创建更有趣的东西的基础工作,包括添加颜色、移动、一些3D WebGL模型。想要了解更多,请看第二课。

 

你可能感兴趣的:(webGL全集)