升级至WebGL 2.0
撰写时间:2023-07-25
修订时间:2023-12-31
在这一章中,我们准备升级到WebGL 2.0。WebGL 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。其次,canvas的getContext方法的参数改为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时,根据上面对glu的createVBO所作的修改,需要传入顶点数组、成分数量,以及所要绑定的顶点属性的名称。
之后,升级至WebGL 2.0后最富有价值的代码出现了:createVertexArray,创建顶点数组。实际上,最准确的说法应是创建顶点属性数组
。该方法将创建并返回一个vertex array object(顶点数组对象, VAO),将其作为参数调用bindVertexArray方法后,将记住后续的相关操作。
上面代码,将记住vao的状态为:verticesVBO与aPosition数组连接,colorsVBO与aColor数组连接。如下图所示:

从细节上来说,它将记住3个方法bindBuffer, vertexAttribPointer及enableVertexAttribArray所设置的状态。这3个方法缺一不可。
根据上一章的重构,glu的bindVboToAttribs方法正好封装了这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传进来,且每个VBO的verticesNum属性值都一样,因此我们将第一个VBO的verticesNum属性值保存进vao的verticesNum属性中,最后返回此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是全局变量,而verticesVBO及colorsVBO均可以安全地转变为局域变量了。
运行应用。