WebGL2系列之实例数组(Instanced Arrays)
实例化数组
实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染一个物体时从cpu到gpu的通信时间。
实例数组是这样的一个对象,使用它,可以把原来的的uniform变量转换成attribute变量,而且这个attribute变量对应的缓冲区可以被多个对象使用;这样在绘制的时候,可以减少webgl的调用次数。
背景
假设这样的一个场景:你需要绘制很多个形状相同的物体,但是每个物体的颜色、位置却不一样,通常的做法是这样的:
for(var i = 0; i < amount_of_models_to_draw; i++) { dosomepreparations(); // bind vao, bind textures, set uniforms etc. gl.drawarrays(gl.triangles, 0, amount_of_vertices); }
但是这种做法的一个缺点是:当绘制的对象的数量巨大之后,执行的效率就会变的很慢了;这是因为每一次绘制的时候,都需要调用很多webgl 的很多方法,比如绑定vao对象,绑定贴图,设置uniform变量,告诉gpu从哪个缓冲区区读取顶点数据,以及从哪里找到顶点属性,所有这些都会是cpu和gpu的资源消耗过多。
实例化
如果能够讲数据一次性发送给gpu,然后告诉webgl使用一个绘制函数,绘制多个物体,就会更方便。这种技术,便是实例化技术。这种技术的实现思路,就是把原本的uniform变量,比如变换矩阵,变成attribute变量,然后把多个对象的矩阵数据,写在一起,然后创建所有矩阵的vbo对象(顶点缓存区); 创建好缓冲区后,把所有对象的矩阵数据通过bufferdata 上传到缓冲区中,这和普通的attribute变量的缓冲区没什么差别。
接下来,就是和普通的vbo差异的部分:该缓冲区可以在多个对象之间共享。每个对象 取该缓冲区的一部分数据,作为attribute变量的值,方法如下:
gl.vertexattribdivisor(index, divisor)
通过gl.vertexattribdivisor方法指定缓冲区中的每一个值,用于多少个对象,比如divisor = 1,表示每一个值用于一个对象;如果divisor=2,表示一个值用于两个对象。 index表示的attribute变量的地址。
然后,通过调用如下方法进行绘制:
gl.drawarraysinstanced(mode, first, count, instancecount); gl.drawelementsinstanced(mode, count, type, offset, instancecount);
这两个方法和 gl.drawarrays与gl.drawelements类似,不同的是多了第四个参数 instancecount,表示一次绘制多少个对象。
通过这个方法,便能实现一次调用绘制多个对象的目标。
案例说明
代码展示
本案例 将一次绘制多个四边形,代码如下:
var count = 3000; var positions = new float32array([ -1/count, 1/count, 0.0, -1/count, -1/count, 0.0, 1/count, 1/count, 0.0, 1/count, -1/count, 0.0, ]); var positionbuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, positionbuffer); gl.bufferdata(gl.array_buffer, positions, gl.static_draw); gl.vertexattribpointer(0, 3, gl.float, false, 0, 0); gl.enablevertexattribarray(0); var colors = new float32array([ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, ]); var colorbuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, colorbuffer); gl.bufferdata(gl.array_buffer, colors, gl.static_draw); gl.vertexattribpointer(1, 3, gl.float, false, 0, 0); gl.enablevertexattribarray(1); var indices = new uint8array([ 0,1,2, 2,1,3 ]); var indexbuffer = gl.createbuffer(); gl.bindbuffer(gl.element_array_buffer,indexbuffer); gl.bufferdata(gl.element_array_buffer,indices,gl.static_draw); //给缓冲区填充数据 var offsetarray = []; for(var i = 0;i < count;i ++){ for(var j = 0; j < count; j ++){ var x = ((i + 1) - count/2) / count * 4; var y = ((j + 1) - count/2) / count * 4; var z = 0; offsetarray.push(x,y,z); } } var offsets = new float32array(offsetarray) var offsetbuffer = gl.createbuffer(); var aoffsetlocation = 2; 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); gl.vertexattribdivisor(aoffsetlocation, 1); // //////////////// // // draw // //////////////// gl.clear(gl.color_buffer_bit);// 清空颜色缓冲区 // // 绘制第一个三角形 gl.bindvertexarray(trianglearray); gl.drawelementsinstanced(gl.triangles,indices.length,gl.unsigned_byte,0,count * count);
定义四边形vbo、ibo数据
首先定义一个变量count,绘制四边形的个数为 count * count,也就是count 列 count行个四边形。 然后一下代码定义四边形的顶点坐标、颜色和索引相关数据,这在webgl1中多次使用,不在赘述:
var positions = new float32array([ -1/count, 1/count, 0.0, -1/count, -1/count, 0.0, 1/count, 1/count, 0.0, 1/count, -1/count, 0.0, ]); var positionbuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, positionbuffer); gl.bufferdata(gl.array_buffer, positions, gl.static_draw); gl.vertexattribpointer(0, 3, gl.float, false, 0, 0); gl.enablevertexattribarray(0); var colors = new float32array([ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, ]); var colorbuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, colorbuffer); gl.bufferdata(gl.array_buffer, colors, gl.static_draw); gl.vertexattribpointer(1, 3, gl.float, false, 0, 0); gl.enablevertexattribarray(1); var indices = new uint8array([ 0,1,2, 2,1,3 ]); var indexbuffer = gl.createbuffer(); gl.bindbuffer(gl.element_array_buffer,indexbuffer); gl.bufferdata(gl.element_array_buffer,indices,gl.static_draw); //给缓冲区填充数据
uniform变量改成attribute变量
接下来,为了把每个四边形分开,我们给每个四边形定义一个偏移量(此处的偏移量可以相当于变换矩阵),在webgl1中,这个偏移量会以uniform变量的方式定义,但是在实例化的技术下,该偏移量定义为attribute变量, layout(location=2) in vec4 offset:
var vssource = `#version 300 es ...... layout(location=2) in vec4 offset; ...... void main() { vcolor = color; gl_position = position + offset; } `;
定义偏移量的数据及vbo
然后定义每个对象的偏移量数据的数组:
for(var i = 0;i < count;i ++){ for(var j = 0; j < count; j ++){ var x = ((i + 1) - count/2) / count * 4 - 2/count; var y = ((j + 1) - count/2) / count * 4 - 2/count; var z = 0; offsetarray.push(x,y,z); } }
这个偏移量,将会使所有的四边形,按照count 行 count 列排列。
定义了偏移量数组之后,创建相应的缓冲区和开启attribute变量:
var offsetbuffer = gl.createbuffer(); var aoffsetlocation = 2; // 偏移量attribute变量地址 gl.bindbuffer(gl.array_buffer, offsetbuffer); gl.bufferdata(gl.array_buffer, offsets, gl.static_draw); gl.enablevertexattribarray(aoffsetlocation); // 启用偏移量attribute变量从缓冲区取数据 gl.vertexattribpointer(aoffsetlocation, 3, gl.float, false, 12, 0); // 定义每个数据的长度为3个分量,长度为12 = 3 * 4(浮点数长度)。 gl.vertexattribdivisor(aoffsetlocation, 1);
gl.vertexattribdivisor
注意 gl.vertexattribdivisor(aoffsetlocation, 1); 这一行,1表示指定每个数据(定义每个数据的长度为3个分量,长度为12 = 3 * 4(浮点数长度)) 被一个四边形所用,而每一个四边形的绘制期间,attribute变量offset保持不变,这个uniform变量类似。
gl.drawelementsinstanced 绘制多个实例
接下来,调用方法绘制多个实例,
// //////////////// // // draw // //////////////// gl.clear(gl.color_buffer_bit);// 清空颜色缓冲区 // // 绘制第一个三角形 gl.bindvertexarray(trianglearray); gl.drawelementsinstanced(gl.triangles,indices.length,gl.unsigned_byte,0,count * count);
gl.drawelementsinstanced 将会绘制count * count个四边形的实例,需要注意的是,绘制实例的个数,不能多于attribute变量offset变量的对应的缓冲区的数据个数,前面代码offsetarray定义了count*count个数据(注意每个数据有3个分量,所以数据个数不等于offsetarray数组长度),因此绘制的示例个数不能超过count * count 个,但是可以少于。
案例效果说明
如果把count 指定为10,最终绘制的效果如下:
可以看出,一次绘制调用,绘制出了100个对象;
如果通过webgl1的方式需要遍历100次绘制。因此可以看出减少了绘制的遍历。
当然如果只是绘制100个四边形,遍历方法也没什么不好,实例化的威力主要体现在,当数据量变到很大的时候,比如在笔者电脑上,把count值改为4000,那么会绘制4000 * 4000 = 一千六百万个四边形,如下:
可以看出,还是可以很好的绘制出来(虽然由于对象太多,已经看不清楚界限)
而采用webgl1 循环遍历的方式,估计最多也就能够达到万级别的绘制循环数量,千万级别的数量简直不可想象。
当然这个数量 也是有限制的,比如在笔者的机器上,把count改成5000,也就是5000 * 5000 = 两千五百万的时候,机器就奔溃了。
webgl1 扩展
在webgl1中,可以通过扩展来angle_instanced_arrays来实现,相关函数如下:
var ext = gl.getextension('angle_instanced_arrays'); ext.vertexattribdivisorangle(index, divisor); ext.drawarraysinstancedangle(mode, first, count, primcount); ext.drawelementsinstancedangle(mode, count, type, offset, primcount);
更多精彩内容,请关注公众号:itman彪叔