【译】Lesson 1: 一个三角形和一个方块
【声明】:本系列文章译自:http://learningwebgl.com/blog/?page_id=1217, 感谢Giles Thomas;限于我的英文水平,本文翻译并不一定严格遵从原文,但也不会严重背离原文(如果有,请务必知会我一下,多谢);如果处于非商业目的,你可以自由转载并修改完善之;一切目的都是促进交流。如果能注明出处就最好不过了~~
-----------------------------------------
欢迎来到我的第一篇WebGL教程。这篇教程基于著名的Nehe Opengl教程的第二课。将向你展示怎样在网页中画一个三角形和一个方块。这个可能引不起你的多大兴趣,但是确实是WebGL的很好的基础介绍;如果你理解了它怎样工作,剩下的就非常简单了。。。
下面是本课运行在支持WebGL的浏览器上的效果:
猛击这里可以看到真实运行的WebGL版本(前提是你有一个支持其运行的浏览器);如果没有,猛击这里。
更多的关于其如何工作的在下面。。。
一个小知会:这些教程的目标人群是有适当的编程经验,但是没有多少3d图形编程的经验的人;目标是让你知其然并能知其所以然的理解代码里将的是什么,然后你就可以尽快的开发自己的3D网页程序。我自己边学习WebGL边写这个的,所以可能会有一些错误;使用这些代码你需要自己承担风险。但是我正在修改并修正我所知道的bug和误解,如果你发现了任何不对的请留言让我知道。
有两种途径获取这个例子的代码:当你观看实时运行的版本时,通过浏览器的”查看源代码“即可;或者如果你使用GitHub,你就可以从仓库中克隆一份出来(包括以后的所有课程)。无论哪种方式,当你获取了代码之后,用你喜欢的文本编辑器中加载阅读。初一见的时候难免会觉得代码不堪入目,即使你以前已经有了一些初步的经验,比如:OpenGL。刚一开始我们就定义了两个Shader,这通常被看做是比较先进的技术。。。但是不要气馁,它只是纸老虎。
像很多程序一样,这个WebGL页面一开头就定义了一堆会被后面高层逻辑调用的底层函数。为了解释他们,我将从底下的代码讲起,所以如果你正在顺序的跟读代码,请跳转到最底下。
你将看到如下的HTML代码:
<body onload="webGLStart();">
<a href="http://learningwebgl.com/blog/?p=28"><< Back to Lesson 1</a><br />
<canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>
<br/>
<a href="http://learningwebgl.com/blog/?p=28"><< Back to Lesson 1</a><br />
</body>
这个就是整个页面的body部分 -- 所有其他的东西都用JavaScript实现(但是如果你通过”查看源代码“来取得代码,你将看到很多额外的代码段用来解析我的网站,这些你可以跳过)。显然我们可以放更多的HTML代码到<body>标签中来让我们的WebGL图像来构建一个常见的网页,但对于这个简单的demo我们只需要一个回到本blog文章的链接和一个容纳3d实时图形的<canvas>标签。Canvas 是HTML5的新特性,就是他们在网页中支持了JavaScript的各种新的绘制元素,包括2D和3D(webGL)。在这个标签中,除了一些简单的层级属性我们不再涉及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和我之前提到过的shaders,并将我们想绘制3d图形的canvas元素作为参数传递到前者中,然后用initBuffers初始化一些缓存;所谓缓存就是保存我们将要绘制的三角形和方块的地方 -- 后面我们将详述之。然后,执行了一些基本的WebGL初始化函数,比如:将画布清除为黑色并开启深度测试(即Z-Test),这些步骤都是通过调用gl对象上的方法实现的 -- 后面我们将看到它是怎样初始化的。最后,调用函数drawScene;就像你看到的名字一样这个函数使用buffers绘制出那个三角形和方块。
因为其对于理解页面怎样工作具有很重要的作用,后面我们将讲到initGL和initShaders;但是首先,我们先看一下initBuffers和drawScene两个函数。
首先initBuffers:
var triangleVertexPositionBuffer;
var squareVertexPositionBuffer;
我们定义两个全局变量来操作缓存。(真实情况下不可能为每个对象都定义一个全局变量,但是这里因为我们才刚开始,所以一切从简)
下一步:
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
我们为三角形的顶点位置创建一个缓冲区,顶点就是3d空间中定义我们需要绘制的形体的点。对于我们的三角形,我们将有三个顶点。这些缓冲区实际上就是显卡上的一些显存;通过在初始化时将顶点位置传递到显卡上,然后当我们绘制场景时,本质上讲就是告诉WebGL去绘制之前告诉它要绘制的那些东西,这样我们就可以使得我们的代码有很高的执行效率,特别是当我们开始让场景动起来时,想通过每秒绘制几十次来使得物体动起来时。当然,在现在的例子里因为只有三个顶点,所以将他们压到显卡里是不会有太多消耗的 -- 但是当我们处理拥有成千上万顶点的大模型的时候,这样的处理方式将会显示出其优势的。接着:
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
这一行告诉WebGL接下来所有buffer上的操作都使用我们指定的这个buffer。这里有一个概念“当前数组缓冲区”,并且所有函数操作都是基于这个“当前”缓冲区而非让你自己指定哪个你想操作的buffer。奇怪,但是我相信在这个的背后肯定是有性能方面的原因的。
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
接着,我们用javascript的list来定义顶点位置。可以看到这是一个中心点在(0,0,0)的等腰三角形。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
现在,我们基于前边的Javascript的list创建一个Float32Array的对象,并且告诉WebGL使用它来填充“当前缓冲”,当然它就是我们的triangleVertexPositionBuffer。我们将在未来的课程里详细讨论Float32Arrays,但是现在你只需要知道我们是有方法可以将Javascript的list转化成WebGL可以用来填充缓冲区的东西的。
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
最后关于buffer的操作时为其设置两个新的属性。这不是WebGL内置的什么东西,但是后面将会非常有用。Javascript一个很好的特性是其不需要显示的声明一个你想设置的属性(对此,褒贬不一)。所以即使buffer之前并不存在itemSize和numItems两个属性,现在它也有了。我们称作这样的buffer为9元素buffer,实际上表示其右三个独立的顶点位置(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,这个就是我们使用那些buffer进行绘制的地方。一步一步来:
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
第一步是用viewport函数告诉WebGL画布的大小;很后面的课程里我们将讲到为什么这个非常重要;现在,你只需要知道在你绘制之前需要调用这个函数就好了。接着,我们清除画布:
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
…然后:
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
这里我们设置好我们要观察场景的透视角度。默认,WebGL使用正交投影绘制物体(作者单词拼错了。。。)。为了使得远处的物体看起来小一些,我们需要告知WebGL我们使用的透视角度。对于当前场景,我们说我们的FOV是45°,并告诉其我们画布的长宽比,并且说远近裁剪面的距离分别是0.1个单位和100个单位。
正如你所看到的,这个透视的东东用的是一个叫mat4的模块中的函数设置的。并且涉及到一个叫pMatrix的变量。(好多废话)
现在我们设置好了透视角度,然后我们继续到绘制的部分:
mat4.identity(mvMatrix);
【一大段关于gl矩阵运算的废话,不翻了。。。】上面这句就是初始化一个矩阵变量留作后面用,identity就是单位阵。
OK,接着讲代码,将我们的三角形绘制到画布的左手边:
mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
通过设置矩阵为单位阵将物体移动到3d空间的中心,然后我们将三角形向左移1.5个单位(即沿X轴负向移动),并且向里移动7个单位(即,远离观察者,沿Z轴负向)。
接下来的就开始了实际的绘制工作:
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
你应该记得为了使用我们的buffer,必须首先调用gl.bindBuffer来指定一个当前缓冲区,然后调用其上的操作代码。这里我们选择我们的triangleVertexPositionBuffer,然后告诉WebGL缓冲区里的内容用来做顶点位置坐标。这里可以看到我们告诉WebGL缓冲区里的每个元素都是3个数字的长途(itemSize)。
接下来,就是:
setMatrixUniforms();
这里让WebGL考虑到我们的WVP矩阵,这个是需要的,因为所有关于矩阵的东东都不是WebGL内置的。可以看出这个的方法是:你可以通过改变mvMatrix的值来移动物体,但是所有这些都是在Javascript私有空间内执行的,setMatrixUniforms,将计算结果传递到显卡,此函数将在文件的后面定义。
一旦这些都搞定,WebGL就拥有了一个被当做顶点坐标的数组,并且知道要设置的矩阵。下一步就是告诉它怎么处理这些东东:
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
或者,换句话讲:从之前我给你的被当做三角形的顶点数组中的0号元素起至第numItems个元素止,依次画数组中的顶点
一旦这个操作完成,WebGL就已经画好了我们的三角形。下一步,绘制方块:
mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
我们首先将我们的MV矩阵向右移动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使用我们的方块buffer当做其顶点坐标。。。
setMatrixUniforms();
。。。我们将mvp矩阵重新压入显卡(这样就可以使得mvTranslate生效),这样最后我们就可以:
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
画点。【这里作者自问自答的解释了一堆为什么使用TRANGLE_STRIP,咱就不废话了】
不管怎样,一旦执行到这里,我们就完成了我们的drawScene函数。
}
如果你已经走到这里了,那你完全可以准备开始实验了。复制代码到一个本地文件,既可以从GitHub或者直接从实时版本获取;如果是后者,你需要index.html和glMatrix-0.9.4.min.js两个文件。先在本地运行确保能工作,然后尝试修改一些顶点坐标;特别是,现在的场景还很平;尝试改变方块的Z坐标为2或者-3,然后看其移动到后边和前边变小和变大。或者尝试改变其中的一个或者2个,在透视角度下观察期变形。。。。
...
OK,你应该回来了,现在让我们来看那些支持函数。【后面又有一对废话。。。】
首先来看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 context,它是通过使用标准的context名称请求canvas赋予的(你可能猜测,在某个时候context的
名字将从“
experimental-webgl”变成“
webgl”,那时我将更新我的课程和博客)
。一旦得到上下文,我们将使用
viewport
函数告诉WebGL画布的大小;在后面的课程中我们会回过来讲讲这为什么会很重要;现在,你只需知道该函数调用时需要画布的大小。一旦函数执行完,GL的context就设置好了。
调用InitGL之后,webGLStart函数继续调用initShaders。这个函数用来初始化着色器xu。我们稍后再回来看这个函数,因为我们首先要来看看处理模型视图矩阵的应用函数。下面是其代码:
var mvMatrix = mat4.create(); var pMatrix = mat4.create();
我们定义mvMatrix
变量来保存模型视图矩阵,接着定义使用该变量的loadIdentity
和mvTranslate
函数以及
multMatrix
应用函数。如果你知道
JavaScript
,你就会明白我们在此使用的矩阵代数函数并不是一般的
JavaScript的API
;事实上它们由先前提到的两个文件所支持,这两个文件位于
HTML
网页的顶部:
第一个文件, Sylvester,是一个处理矩阵和矢量代数的开源JavaScript库,第二个文件是由Vladimir Vukićević开发的Sylvester库的一系列扩展。
总之,在这些简单函数和有用的库文件的帮助下,我们可以维护模型视图矩阵。这里需要说明另一个矩阵,就是我前面提到的投影矩阵。你也许记得,WebGL不内置perspective
函数。但是模型视图矩阵封装了像移动和旋转物体这样的过程,这正是矩阵擅长的事情。你现在肯定已经猜到,投影矩阵正是这么一个矩阵。下面是其代码:
var pMatrix;
function perspective(fovy, aspect, znear, zfar) {
pMatrix = makePerspective(fovy, aspect, znear, zfar);
}
makePerspective
函数是另一个定义在glUtils.js
文件中的函数;它返回一个我们需要应用在指定的全景透视图中的特定矩阵。
现在,我们除了setMatrixUniforms
函数外已经浏览了所有函数,正如我先前所说,这个函数将模型视图矩阵和投影矩阵从JavaScript中转移到WebGL中,并与着色器相关。由于它们相互关联,所以我们将从一些背景知识开始。
你也许会问:什么是着色器?在三维图形的历史上,着色器曾经“名副其实”过:在绘制一个场景之前,它指示系统如何渲染或着色。然而,随着时间的推移,它的功能日益增强,以至于如今要这样定义它才更为合适:着色器是这样一些代码,在一个场景开始绘制之前,它能对场景的任何部分做任何处理。这的确十分有用,由于它运行在图形卡上,所以它能很快运行且能很便利地做各种变换,哪怕是在这个简单的例子中。
我们引入着色器是为了一个简单的WebGL例子(在OpenGL教程中这至少算是“中级
”),该例子能运行在图形卡上且使用着色器获得WebGL系统。它把模型视图矩阵和投影矩阵应用到场景中,而不需要使用相对较慢的JavaScript来移动场景中的每一个点和每一个三角形顶点。这相当有用并且值得额外的开销。
下面是如何设置它们。正如你所记得的,webGLStart
函数
调用initShaders
函数,那么让我们一步一步地来看一看:
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
函数来获得两个着色器:一个片段着色器和一个顶点着色器,接着将两者绑定在一个WebGL“程序”上。一个程序是一段放置在系统WebGL上的代码;你可以把它视作一种运行在图形卡上的特定方式。正如你所期望的,你可以将它和一些着色器联系在一起,每个着色器都可以视为程序中的一个代码片段;确切地说,每个程序可以拥有一个片段着色器和一个顶点着色器。让我们简单地看一下:
shaderProgram.vertexPositionAttribute =
gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
一旦设置好程序并绑定了着色器,函数将得到一个“属性”的引用,该属性存储在vertexPositionAttribute
对象中。我们再次利用
JavaScript
把任一字段设置在任一对象上;默认情况下对象没有
vertexPositionAttribute
字段,但是对于我们来说将两个值保留在一起是很方便的,因此我们仅设置程序中新字段的属性。
那么,vertexPositionAttribute
是做什么的呢?也许你还记得,我们在drawScene
函数中使用过它;如果你回过去看一看从适当的缓冲区设置三角形顶点位置的那段代码,你将看到我们所做的就是将缓冲区与该属性关联在一起。稍后你将明白这是什么意思。现在,只需注意到我们也使用
gl.enableVertexAttribArray
函数来指示
WebGL
使用一个数组来为该属性提供数值。
shaderProgram.pMatrixUniform =
gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform =
gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
initShaders
函数所做的最后一件事就是从程序中获取两个多的值,这两个变量称作uniform变量,我们很快会再遇见它们,但现在你只需注意到,正如属性一样,我们为了方便而将其存储在对象中。
现在,我们来看看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也是如此)所写。第一个着色器——即片段着色器——什么也不做;它简单地规定了被绘制的物体将被绘制成白色(怎么给物体着色是下一节课程的话题)。第二个着色器有点意思,它是一个顶点着色器——还记得吧,它是一段图形卡上的代码,能用一个顶点完成它想做的任何事。与之相关联的是,它有两个uniform变量:uMVMatrix和uPMatrix。uniform变量十分有用,因为它们能在着色器之外获得——实际上是在包含它们的程序之外,你可能还记得,当时我们从initShaders函数获得了它们,它们也可以从将其设置为模型视图矩阵和投影矩阵的代码(我敢肯定你已经实现了它)中获得。你可能认为着色器程序是一个对象(在面向对象的场景中),而统一变量是对象中的字段。现在,着色器在每个顶点上调用,顶点作为aVertexPosition参数传入到着色器的代码中,由于在drawScene函数中使用vertexPositionAttribute,此时,我们将其属性与缓冲区关联在一起。着色器主程序中的这部分代码只是将顶点与模型视图矩阵和投影矩阵相乘,然后作为顶点最终位置的结果传出。webGLStart函数调用initShaders函数,在网页的脚本中使用getShader函数装载了片段着色器和顶点着色器,以便它们能被编译后传入到WebGL,最终使用WebGL渲染出三维场景。
剩下还没有说明的代码是setMatrixUniforms函数,一旦你理解了上面所讲的,就很容易理解它。
function setMatrixUniforms() {
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false,
new WebGLFloatArray(pMatrix.flatten()));
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false,
new WebGLFloatArray(mvMatrix.flatten()));
}
通过引用uniform来表示initShaders中的投影矩阵和模型视图矩阵,我们将值从JavaScript类型矩阵传递给了WebGL。
第一课的内容真的很多,但是希望你(也包括我)能够理解所有这些基本知识,我们将以此为基础创建更加有趣的模型:五彩缤纷的、可移动的、真正的三维WebGL模型。为了了解更多关于WebGL的知识,请阅读第2课。
【发现翻译果然是一件很累的事情,尤其是作者写的废话较多的时候。。。,翻译最后一段的时候google了一下发现译言网上已经有这个系列的译文了。。。,很汗,然后后面的几个lesson就不想继续了,但是善始善终本篇还是想完成,所以就继续参考了一些译言网的内容,纠正了其一些说法的问题,把shader翻译成渲染器总是不大对劲,我改成”着色器“了】