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

drawElements

撰写时间:2023-08-02

修订时间:2026-06-03

drawArrays方法只能从数组的特定起始位置取出连续的特定数量的元素来构建基本图元,我们不能从数组中随意挑选元素来构建图像,因此很不方便。

渲染方法除了drawArrays后,还有一个drawElements方法,该方法允许我们通过指定数组元素索引值的方式,引用任意次序、任意数量的数组元素来构建图元。因此在构建较为复杂的图元时,drawElements方法更为方便。

全局变量与应用入口

import WebGLUtils from './js/esm/WebGLUtils-v4.js'; import defaultExport from '/js/esm/ArrayExtensions.js'; import { GLColors } from '/js/esm/GLColors.js'; let glu, gl; let vao; let ibo; init(); function init() { initContext(); initVAOs(); initIBOs(); render(); }

与前面章节的例子相比,多了一个全局变量ibo,以及一个initIBOs函数。

initVAOs函数

function initVAOs() { let verticesVBO = glu.createPosVBO([ 0.0, 0.0, 0.0, // V0 -0.5, 0.5, 0.0, // V1 -0.5, -0.5, 0.0, // V2 0.5, -0.5, 0.0, // V3 0.5, 0.5, 0.0 // V4 ]); let colorsVBO = glu.createColorVBO(GLColors.GetRandomSoftRGB().repeat(verticesVBO.verticesNum)); vao = glu.createVAO(verticesVBO, colorsVBO); }

verticesVBO的客户端数据是一个有5个顶点的数组,顶点VO位于NDC坐标系的原点位置,顶点V1V4分别对应于NDC坐标系的左上角、左下角、右下角、右上角的位置。

在本例中,我们准备取VO, V1, V23顶点渲染为一个三角形,取VO, V3, V43顶点渲染为另一个三角形。现在,如果将这两组顶点索引值组成一个数组,则该数组各元素值如下:

下一节,我们将根据此数组来创建一个IBO (index buffer object, 索引缓冲区对象),并最终调用gl.drawElements方法来渲染IBO中所指定的各个顶点。

而不管是否使用IBO,创建VAO的方式与之前完全一样。

可见,VAO不会记住IBO的状态,这反倒允许我们对一个VAO自由指定各种IBOs

initIBOs函数

function initIBOs() { let indices = [ 0, 1, 2, 0, 3, 4 ]; ibo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); }

根据上节中所需要构建的indices数组,创建了一个IBO并赋值于全局变量ibo

创建IBO的代码与创建VBO的代码很相似。但有两个地方不一样。一是绑定的target不一样,创建IBO需要绑定ELEMENT_ARRAY_BUFFER。二是由于索引值的数据类型是整数的类型化数组,因此这里选用了无符号16位的类型化数组Uint16Array。意味着我们可在indices变量中存储216 = 65536个数组元素。

render函数

function render() { gl.clear(gl.COLOR_BUFFER_BIT); gl.bindVertexArray(vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); let indicesElementsToBeUsed = 6; let indicesElementOffset = 0; gl.drawElements(gl.TRIANGLES, indicesElementsToBeUsed, gl.UNSIGNED_SHORT, Uint16Array.BYTES_PER_ELEMENT * indicesElementOffset); }

在渲染时,先调用bindVertexArray绑定VAO,接着调用bindBufferIBO绑定至gl.ELEMENT_ARRAY_BUFFER,最后调用drawElements方法进行渲染。

需要注意的是,drawElements方法的参数数量与位置与drawArrays方法不一样。drawElements方法的原型如下:

voiddrawElelements
  • GLenummode
  • GLsizeicount
  • GLenumtype
  • GLintptroffset

参数mode指定要构建的基本图元类型。

参数count指定要使用索引数组中的元素数量。

参数type指定索引数组元素的类型。可选值为UNSIGNED_BYTEUNSIGNED_SHORT,及UNSIGNED_INT。前面我们使用Uint16Array来创建IBO,故这里应使用对应的常量UNSIGNED_SHORT

参数offset指定开始的偏移位置,以字节数表示。

一般来讲,以字节数指定偏移值不够直观,较为直观的是从第几个元素开始。设变量elementOffset代表第几个元素,则该元素的字节偏移值可通过下面代码计算出来:

对于数组:

代码:

表示,从indices索引数组中,共取用6个数组元素,从第0的元素的偏移处开始,用以构建多个独立的三角形。每个三角形由3个顶点构成,由此最终所渲染的三角形的数量为:6 ÷ 3 = 2个三角形。

若修改代码为:

则将只渲染右边的三角形。

注意,在将indicesElementOffset的值改为3后,则indices数组中只剩下3个元素,因此indicesElementsToBeUsed的值也应随之而改小,不能再为6了。否则将抛出:

的异常。以后当看到此异常,排查线索为:若从第indicesElementOffset个元素开始,此时indices数组中所剩下的数组元素数量是否仍大于等于所指定的indicesElementsToBeUsed?

运行应用VO, V1V2构建了左边的三角形,VO, V3V4构建了右边的三角形。

在这里,drawElements方法的便利之处体现在两方面。一是在一条渲染语句中,顶点VO使用了两次。二是VOV3在原来的顶点数组中是不相连的,但我们却可以随意提取出来自由组合。这些便利之处,是drawArrays方法所不具备的。

这类似于我们先在电路板上整齐在排列好各个电子元件,再通过烧制电路板及焊接技术,将原来不相连的电子元件连起来,从而制作出功能不一的电子产品。或者说,我们可以将各个顶点自由地穿针引线了。

drawElementsInstanced

WebGL 2.0drawElementsInstanced方法可让客户端只需调用一次渲染命令,却可一下子渲染出多个实例效果。

基本用法

首先,将上节中render函数代码修改为:

其次,将顶点着色器代码修改为:

运行应用

instanceCount作为参数传给drawElementsInstanced方法,则该方法只需被调用一次,却能最终渲染出3个正方形。

WebGL内部,代码:

等效于:

顶点着色器中,gl_InstanceIDGLSL的预定义变量,专用于接收drawElementsOneInstance方法中实参i的值。因此,对于3次渲染中每次渲染,gl_InstanceID的值将分别取自于[0, 1, 2]

由此,每渲染一个实例时,offsetX的值变化如下:

然后,将此值转换为vec4后,与aPosition相加后再传递给gl_Position,则最终渲染出水平位置各不相同的3个实例。

将IBO封装进VAO

一般来讲,IBO独立存在没有太大意义,它应从属于VAO,因此IBO可以作为VAO的一个属性而存在。如果这样,原来WebGLUtilsrenderVAO方法可以将此情况也涵盖进来。

WebGLUtils-v4.js复制为WebGLUtils-v5.js。在文件第一行之后,添加一行:

const FB_SIZE = Float32Array.BYTES_PER_ELEMENT; const U16B_SIZE = Uint16Array.BYTES_PER_ELEMENT;

添加createIBO方法:

createIBO(vao, indices) { const { gl } = this; let ibo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); ibo.indicesNum = indices.length; vao.ibo = ibo; }

创建一个ibo后,将其indicesNum属性值设为索引数组的长度,以方便drawElements方法调用。并将传进来的参数vaoibo属性值设置为此ibo

修改renderVAO方法如下:

renderVAO(vao) { const { gl } = this; gl.bindVertexArray(vao); if (vao.ibo) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vao.ibo); gl.drawElements(gl.TRIANGLES, vao.ibo.indicesNum, gl.UNSIGNED_SHORT, U16B_SIZE * 0); } else { gl.drawArrays(gl.TRIANGLES, 0, vao.verticesNum); } }

如果参数vaoibo属性,则调用drawElements来渲染,否则,调用drawArrays方法来渲染。

drawElements.html复制为vao-refactored.js。修改相应代码如下:

import WebGLUtils from './js/esm/WebGLUtils-v5.js'; ... init(); function init() { initContext(); initVAOs(); render(); } ... function initVAOs() { let verticesVBO = glu.createPosVBO([ 0.0, 0.0, 0.0, // V0 -0.5, 0.5, 0.0, // V1 -0.5, -0.5, 0.0, // V2 0.5, -0.5, 0.0, // V3 0.5, 0.5, 0.0 // V4 ]); let colorsVBO = glu.createColorVBO(GLColors.GetRandomSoftRGB().repeat(verticesVBO.verticesNum)); vao = glu.createVAO(verticesVBO, colorsVBO); glu.createIBO(vao, [ 0, 1, 2, 0, 3, 4 ]); } function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.renderVAO(vao); }

在像之前一样创建vao后,glucreateIBO方法根据索引数组创建了一个IBO,并设置为vao的属性值。渲染时,只需以vao作为参数调用glurenderVAO方法就行了。

运行应用

修改上面glu.createIBO()方法的索引数组的值,运行应用,观察效果。重构后,我们将上面各节所有的技术细节都浓缩为一条glu.createIBO()语句调用,由于用户只需在这仅有的一个地方输入索引数组值,从用户的角度,他会感到很放心,不会担心代码相互污染。再将这段代码屏蔽掉,应用程序照样正常运行。应用程序的功能扩展了,同时保持了很好的向下兼容性。

drawElements方法在WebGL编程中的作用举足轻重,结合WebGL 2.0VAO的新功能,我们得以最大限度地挖掘其潜力与功效。

参考资源

  1. WebGL 2.0 Specification