Web编程技术营地
研究、演示、创新

升级至WebGL 2.0

撰写时间:2023-07-25

修订时间:2023-12-31

在这一章中,我们准备升级到WebGL 2.0WebGL 2.0有不少新的特性,其中一个运用最多的新特性是VAO,本章会详细讨论它。

要想了解所使用的浏览器是否支持WebGL 2.0,以及支持哪些扩展,可访问WebGL Report网站,它将列出所访问的浏览器的所有支持特性。

应用程序框架

新建一个名为color-triangle-webgl2.html的文件。其框架如下:

<!DOCTYPE html> <html> <head> <title>Color Triangle (WebGL V2.0)</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="/css/FullScreenCanvas.css" /> <script id="vShader" type="x-shader/x-vertex"> </script> <script id="fShader" type="x-shader/x-fragment"> </script> <script type="module"> import WebGLUtils from './js/esm/WebGLUtils-v3.js'; let glu, gl, program; let verticesVBO, colorsVBO; let vao; init(); function init() { initContext(); initVAOs(); render(); } function initContext() { ... } function initVAOs() { ... } function render() { ... } </script> </head> <body> <canvas id="webgl-canvas">Your browser does not support <code>canvas</code>!</canvas> </body> </html>

6个全局变量glu, gl, program, verticesVBO, colorsVBO, 及vao

init函数中,原来的initVBOs改为initVAOs

修改WebGLUtils类

WebGLUtils-v2.js复制为WebGLUtils-v3.js

修改getContext方法如下:

getContext(canvasId) { if (!window.WebGL2RenderingContext) { alert("Error! Window does not support WebGL2RenderingContext!"); } let canvas = document.getElementById(canvasId); if (!canvas) { alert(`Error! Can not get canvas for ${canvasId}!`); } let context = canvas.getContext("webgl2"); if (!context) { alert("Error! Can not get context for webgl!"); } this.canvas = canvas; this.gl = context; this.updateViewport(); }

首先,WebGL 2.0的渲染环境,其类名为WebGL2RenderingContext。其次,canvasgetContext方法的参数改为webgl2

修改着色器

设置顶点着色器

<script id="vShader" type="x-shader/x-vertex"> #version 300 es precision mediump float; in vec4 aPosition; in vec4 aColor; out vec4 vColor; void main() { gl_Position = aPosition; vColor = aColor; } </script>

WebGL 2.0着色器要求版本为300 es以上,因此第一行须设置为#version 300 es。注意,此语句须放在着色器代码的第一行。

第二行语句precision mediump float;,这是顶点着色器的默认设置,不加也可以,但片断着色器必须添加。从前后一致的角度,这里也显式地添加此行。

顶点属性,WebGL 1.0的语法为:

attribute vec4 aPosition; attribute vec4 aColor; varying vec4 vColor;

WebGL 2.0的语法为:

in vec4 aPosition; in vec4 aColor; out vec4 vColor;

WebGL 2.0中,in表示该顶点属性用于接收VBO的数据,out表示该变量用于向其他着色器或帧缓冲区输出数据。因此,代码vColor = aColor表示,顶点属性aColor将传入进来的VBO的数据赋值于vColor变量,后者准备向片断着色器传输数据。

设置片断着色器

<script id="fShader" type="x-shader/x-fragment"> #version 300 es precision mediump float; in vec4 vColor; out vec4 fragColor; void main() { fragColor = vColor; } </script>

在片断着色器中重新声明vColor,但修饰符改为in

in vec4 vColor;

便完成了将顶点着色器vColor的数据传输至片断着色器的过程。

WebGL 1.0向帧缓冲区输出顶点颜色数据,必须使用特殊变量gl_FragColor,但在WebGL 2.0中,允许用户自行设定此变量。

out vec4 fragColor; void main() { fragColor = vColor; }

因此,上面的代码,我们将fragColor自定义为向帧缓冲区输出顶点颜色信息的变量,并将从顶点着色器传进的颜色值赋值于它。在渲染时,WebGL将自动读取该变量的值传输至帧缓冲区中以设置像素的颜色。

这就完成了着色器的配置工作。

修改JavaScript代码

initContext函数

function initContext() { glu = new WebGLUtils({ vertexAttribs: ['aPosition', 'aColor'], renderFunc: render }); gl = glu.gl; program = glu.program; gl.clearColor(0, 0, 0, 1); }

glu有一个program属性,我们将其赋值于全局变量program,以便在下面的initVAOs函数中访问。

initVAOs函数

function initVAOs() { verticesVBO = glu.createVBO([ 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 ], 2, 'aPosition'); colorsVBO = glu.createVBO([ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ], 3, 'aColor'); vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, verticesVBO); gl.vertexAttribPointer(program['aPosition'], 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(program['aPosition']); gl.bindBuffer(gl.ARRAY_BUFFER, colorsVBO); gl.vertexAttribPointer(program['aColor'], 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(program['aColor']); gl.bindVertexArray(null); }

创建VBO时,根据上面对glucreateVBO所作的修改,需要传入顶点数组、成分数量,以及所要绑定的顶点属性的名称。

之后,升级至WebGL 2.0后最富有价值的代码出现了:createVertexArray,创建顶点数组。实际上,最准确的说法应是创建顶点属性数组。该方法将创建并返回一个vertex array object顶点数组对象, VAO),将其作为参数调用bindVertexArray方法后,将记住后续的相关操作。

上面代码,将记住vao的状态为:verticesVBOaPosition数组连接,colorsVBOaColor数组连接。如下图所示:

createVertexArray

从细节上来说,它将记住3个方法bindBuffer, vertexAttribPointerenableVertexAttribArray所设置的状态。这3个方法缺一不可。

根据上一章的重构,glubindVboToAttribs方法正好封装了这3个方法,因此,上面的代码也可以改为:

vao = gl.createVertexArray(); gl.bindVertexArray(vao); glu.bindVboToAttribs(verticesVBO); glu.bindVboToAttribs(colorsVBO); gl.bindVertexArray(null);

最后一行语句gl.bindVertexArray(null)可要可不要。这里编写此行代码,一是标记为状态的结束,且中间的代码往右缩进,以明示所记状态的内容。二是作为防御性编程的一种手段,可避免当我们忘记重新绑定特定VAO时出现一些不易觉察的bug。

render函数

VAO记住相关状态后,我们在发布渲染指令时,只需重新绑定该VAO即可。

function render() { gl.clear(gl.COLOR_BUFFER_BIT); gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, 3); }

其效果等同于当不使用VAO时我们调用WebGLUtils类的bindVboToAttribs方法时的效果:

function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.bindVboToAttribs(verticesVBO); glu.bindVboToAttribs(colorsVBO); gl.drawArrays(gl.TRIANGLES, 0, 3); }

两相比较,尽管使用重构的WebGLUtils类也能使我们的代码足够简洁,但VAO提供了一个更为抽象的包装特性,我们可将其视为一个可渲染对象的内部细节的封装。例如,只要我们绑定特定VAO,我们就可以用一条语句一下子渲染该对象,而不用考虑它在内部到底激活了哪些顶点属性数组,哪些VBO具体与哪些顶点属性数组连接。随着可渲染对象的增多,以及各个可渲染对象在内部可能使用了较多的顶点属性时,这种机制的便捷性将愈发体现其价值。

运行应用,虽然这段代码同为渲染了一个多彩三角形,但其代码已经换上了更加强健的心脏。

将VAO特性加入WebGLUtils类

WebGLUtils-v3.js复制为WebGLUtils-v4.js。加入createVAO方法:

createVAO(...vbos) { const { gl } = this; let vao = gl.createVertexArray(); gl.bindVertexArray(vao); for (let vbo of vbos) { this.bindVboToAttribs(vbo); } gl.bindVertexArray(null); vao.verticesNum = vbos[0].verticesNum; return vao; }

代码...vbos表示vbos是一个变长的数组,它能够将参数列表中任意长度的参数打包为一个数组。

在方法内部,先调用bindVertexArray绑定刚创建的vao,然后再调用自身方法bindVboToAttribs依序绑定各个VBO。由于只有一个VAO,却可能有多个VBOs传进来,且每个VBOverticesNum属性值都一样,因此我们将第一个VBOverticesNum属性值保存进vaoverticesNum属性中,最后返回此vao

由于VAO能同时记住多个VBOs的状态而对外仅表现为一个对象,因此,我们甚至可以将渲染一个VAO的细节封装进一个方法中。

renderVAO(vao) { const { gl } = this; gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, vao.verticesNum); }

当然,这个方法比较僵化,例如只能渲染为三角形等。但没关系,当后面有更具体的需求时,我们随时可以再次重构。

新建一个名为color-triangle-webgl2-refactored.html的文件,相应代码如下:

let glu, gl; let vao; ... function initVAOs() { let verticesVBO = glu.createVBO([ 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 ], 2, 'aPosition'); let colorsVBO = glu.createVBO([ 0.0, 0.3, 0.6, 0.0, 0.3, 0.6, 0.0, 0.3, 0.6 ], 3, 'aColor'); vao = glu.createVAO(verticesVBO, colorsVBO); } function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.renderVAO(vao); }

创建一个VAO,然后再渲染此VAO,就这么简单。现在,只有vao是全局变量,而verticesVBOcolorsVBO均可以安全地转变为局域变量了。

运行应用

参考资源

  1. WebGL 1.0 Specification
  2. WebGL 2.0 Specification