一旦创建WEBGL上下文成功,你就可以在这个上下文里渲染画图了。而对我们而言最简单的事,莫过于绘制一个没有纹理的2D图形了。那就让我们从画出一个正方形开始吧。
渲染场景
在开始前,我们首先需要明确最重要的一点,就是虽然我们的例子只是画一个二维物体,但我们仍然是在把它画在一个三维空间里。所以,我们依然需要创建着色器,通过它来渲染我们的简单场景并画出我们的物体。往下,我们将展示正方形是怎样一步步被画出来的。
初始化着色器
WebGL着色器使用 OpenGL ES Shading Language. 为了更方便地处理和更新内容,事实上我们可以将着色器代码写在HTML文档上,而不是把所有代码都写在 JavaScript里。下面我们看一下 initShaders() 函数是怎么实现的:
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("Unable to initialize the shader program."); } gl.useProgram(shaderProgram); vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); gl.enableVertexAttribArray(vertexPositionAttribute); }
这段程序中加载了两个着色器。首先,片段着色器是从 ID 为 "shader-fs" 的 script 元素中加载的。然后,顶点着色器是从ID为"shader-vs"的 script 元素中加载的。我们将在下一节看到 getShader() 函数的细节,实现将着色器程序从DOM元素加载到着色器。
接着我们调用 WebGL 对象的 createProgram()
函数来创建着色器,并将WebGL 对象与两个着色器关联起来,然后链接着色器程序。完成以上步骤后,gl 对象的 LINK_STATUS 参数被检查以确定着色器程序是否成功连接。如果成功的话,我们激活新着色器程序。
从DOM中加载着色器
如下,getShader()
函数从DOM元素里抓取着色器程序,并返回一个编译好的着色器程序,但是,如果它加载失败或编译失败的话,将返回 null 。
function getShader(gl, id) { var shaderScript, theSource, currentChild, shader; shaderScript = document.getElementById(id); if (!shaderScript) { return null; } theSource = ""; currentChild = shaderScript.firstChild; while(currentChild) { if (currentChild.nodeType == currentChild.TEXT_NODE) { theSource += currentChild.textContent; } currentChild = currentChild.nextSibling; }
一旦找到指定ID的元素,其文本内容将被读取保存到变量 theSource 。
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 { // Unknown shader type return null; }
一旦读取到着色器源码,我们就根据着色器对象的 MIME 属性来判断它是顶点着色器(MIME type "x-shader/x-vertex"),还是片段着色(MIME type "x-shader-x-fragment"),从而创建相应的着色器。
gl.shaderSource(shader, theSource); // Compile the shader program gl.compileShader(shader); // See if it compiled successfully if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader)); return null; } return shader; }
最后,源码将传到着色器上并编译。如果在编译途中发生错误,我们会显示一个警告并返回 null,否则,就返回一个新的编译好的着色器。
着色器
接着,我们需要将着色器程序代码加入到 HTML 文档。但是,着色器的语法和具体怎样工作已经超出本篇文章讨论的范围。
片段着色器
在 WebGL 多边形中的每一个像素都叫一个片段。这个片段着色器的工作就是建立每个像素的色彩。在这个案例中,我们会简单地给每个像素填上白色。
gl_FragColor 是一个 GL 内置的特殊变量,用于片段的色彩填充。如下所示,设定它的值就是设定每个像素的颜色。
<script id="shader-fs" type="x-shader/x-fragment"> 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>
创建对象
在画正方形前,我们需要创建一个缓冲器来存储它的顶点。我们会用到名字为 initBuffers() 的函数。当我们了解到更多WebGL 的高级概念时,这段代码会将有更多参数,变得更加复杂,并且用来创建更多的三维物体。
var horizAspect = 480.0/640.0; function initBuffers() { squareVerticesBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer); var 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); }
这段代码简单给出了绘画场景的本质。首先,它调用 gl 的成员函数 createBuffer()
得到了缓冲对象并存储在顶点缓冲器。然后调用 bindBuffer()
函数绑定上下文。
当上一步完成,我们创建一个Javascript 数组去记录每一个正方体的每一个顶点。然后将其转化为 WebGL 浮点型类型的数组,并将其传到 gl 对象的 bufferData()
方法来建立对象的顶点。
绘制场景
当着色器和物体都创建好后,我们可以开始渲染这个场景了。因为我们这个例子不会产生动画,所以 drawScene() 方法非常简单。它还使用了几个工具函数,稍后我们会介绍。
function drawScene() { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0); loadIdentity(); mvTranslate([-0.0, 0.0, -6.0]); gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0); setMatrixUniforms(); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
第一步,用背景色擦除上下文,接着建立摄像机透视矩阵。设置45度的视图角度,并且宽高比设为 640/480(画布尺寸)。 指定在摄像机距离0.1到100单位长度的范围内,物体可见。
接着加载特定位置,并把正方形放在距离摄像机6个单位的的位置。然后,我们绑定正方形的顶点缓冲到上下文,并配置好,再通过调用 drawArrays()
方法来画出对象。
如果你的浏览器支持WebGL的话,可以点击这里看看DEMO。
矩阵通用计算
矩阵计算是一个很复杂的运算。 没人会想去自己写完所有代码来处理这些运算。幸运的是,这里有 Sylvester, 一个使用方便的 JavaScript处理向量和矩阵运算的库。
在这个例子中使用的 glUtils.js 文件同时也被使用到很多的其它WebGL例子中去了。似乎没有人完全清楚它的来源,但是它简化了对Sylvester 的使用,甚至添加了一些生成特殊矩阵和输出显示它们的方法函数。
另外,这个例子给出了几个有用的代码,它结合这些库来解决特定的任务。准确来说它们已经超出了这个例子讨论的范围,但是这里有一些非常好的有关于矩阵运算的在线文档。可以参考看See also 栏目找到相关资料。
function loadIdentity() { mvMatrix = Matrix.I(4); } function multMatrix(m) { mvMatrix = mvMatrix.x(m); } function mvTranslate(v) { multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4()); } function setMatrixUniforms() { var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten())); var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten())); }