假设有这样一个场景:你需要绘制若干形状相同的物体,但是每个物体的颜色、位置却不一样。通常的做法会是这样(完成示例看这里):
// 设置顶点buffer setupBuffers();
绘制代码如下:
for (var i = 0; i < 10; i++) { // 设置offset gl.uniform3fv(glProgram.offset, [(i - 5) / 5 + 0.1, 0, 0]); // 设置color gl.uniform3fv(glProgram.color, [Math.random(), Math.random(), Math.random()]); // 绘制一个正方形 gl.drawArrays(gl.TRIANGLES, 0, 6); }
首先绑定顶点数据,然后通过循环设置物体的颜色和位置的uniform,最后调用drawArrays绘制出来。这个做法很简单,但是WebGL调用次数较多,在这个例子中总调用次数为:
\[ {Count}_{calls}= 1 + {Count}_{objects}\times 3\]
考虑一般的情况下,调用总次数为:
\[ {Count}_{calls}= 1 + {Count}_{objects}\times ({Count}_{uniform}+1)\]
很明显,如果不同属性的物体数量较多且uniform个数较多,那么绘制次数就会大大增加。
我们可以使用交替组织数据(Interleaved Array Data)的方法来解决,这篇文章介绍了它的使用方法。此外还有一种方法可以解决,并且不会像交替组织数据那样占用过多GPU缓存,这就是本文要说的:ANGLE_instanced_arrays 扩展。
使用这个扩展进行绘制,其核心思想是把原来的uniform转为attributes,并且可以通过某种映射方式让每一个attribute对应一个或多个物体。
下面这张图显示了使用前和使用后的差别。
左边的流程是每次设置两次uniform并完成一次绘制,而右边instance方式只需要调用一次,WebGL内部会自动帮你根据属性个数来绘制多个不同的物体。
如果还没有明白,我们来看看具体的代码,这是setupBuffer中的代码:
// 提前准备数据 var colorsArray = []; var offsetsArray = []; for (var i = 0; i < 10; i++) { colorsArray.push(Math.random(), Math.random(), Math.random()); offsetsArray.push((i - 5) / 5 + 0.1, 0, 0); } // 获取扩展 ext = gl.getExtension("ANGLE_instanced_arrays"); // 设置offset属性 var offsets = new Float32Array(offsetsArray); var aOffsetLocation = gl.getAttribLocation(glProgram, 'aOffset'); var offsetBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer); gl.bufferData(gl.ARRAY_BUFFER, offsets, gl.STATIC_DRAW); gl.enableVertexAttribArray(aOffsetLocation); gl.vertexAttribPointer(aOffsetLocation, 3, gl.FLOAT, false, 12, 0); ext.vertexAttribDivisorANGLE(aOffsetLocation, 1); // 设置color属性 var colors = new Float32Array(colorsArray); var colorBuffer = gl.createBuffer(); var aColorLocation = gl.getAttribLocation(glProgram, 'aColor'); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); gl.enableVertexAttribArray(aColorLocation); gl.vertexAttribPointer(aColorLocation, 4, gl.FLOAT, false, 12, 0); ext.vertexAttribDivisorANGLE(aColorLocation, 1);
在setupBuffer中,我们首先准备好10个不同的位置和颜色数组。接着我们获取扩展:
ext = gl.getExtension("ANGLE_instanced_arrays");
严格来讲,在使用扩展前我们要判断浏览器是否支持,这里简化了这个步骤。
接下来的代码就是创建buffer,设置attribute,注意设置offset和color属性的最后一行:
ext.vertexAttribDivisorANGLE(aOffsetLocation, 1); ... ext.vertexAttribDivisorANGLE(aColorLocation, 1);
这两句是关键,它指定了后面绘制时要如何使用offset和color这两个属性,第二个参数1表示每个属性值绘制一个物体。如果给2则表示每个属性值绘制两个物体。
最后绘制代码如下:
var instanceCount = 10; ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, instanceCount);
如果你的绘制使用了索引,那么请用 drawElementsInstancedANGLE 方法。
完整示例请看这里。效果如下:
现在我们修改color属性的这一行:
ext.vertexAttribDivisorANGLE(aColorLocation, 2);
这样,每一个颜色会应用到两个正方形上,结果如下:
小结
使用 ANGLE_instanced_arrays 扩展可以降低WebGL绘制次数,同时避免了使用 Interleaved Array Data 方式导致的额外内存开销。
事实上在 webgl2 当中,可以直接使用 gl.vertexAttribDivisor 来直接完成实例化数组了,不过还是得为文章点个赞!
对,但是WebGL2现在还不是很普及,这一系列文章还是以WebGL1.0为准。感谢提出意见。
你好,
看了你的文章很受启发,有一个疑问想向你请教一下。WebGL中的canvas数据是无法共享的,因此如果我想将同一个图形显示两次,有两种方法,一个是用两个canvas,渲染两次,另一个途径是利用两个viewport,在一个canvas中显示,我想使用后一种方法实现在同一个canvas中显示两个一模一样,但是透视位置不同的图形,也就是在同一个canvas中设置两个摄像机,在画布中显示两个稍有错位的图像,这样的图像可以在3D电视中被自动的分割,通过立体眼镜达到立体显示的效果。在具体的实现上,我遇到了一些困难,比如如何设置两个perspective,以及如果让同一组数据显示在两个viewport里,希望能通过和你的交流,得到一些启发。
谢谢
南
你提的这个问题我之前实现过,一个Canvas画布上绘制多个viewport。简单来讲,你需要设置两次gl.viewport,每个viewport的Model View Matrix会稍有不同。稍后我会把示例地址发你参考。
请看这篇文章:http://www.jiazhengblog.com/blog/2017/05/05/3142/
发现代码实例有一个bug,http://www.jiazhengblog.com/webgl-learning/advanced/draw-rectangle-instances-2.html的130行,vertexAttribPointer第二个参数应该是3吧?
多谢反馈我看一下。