3

实例化数组

实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染一个物体时从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定义了countcount个数据(注意每个数据有3个分量,所以数据个数不等于offsetArray数组长度),因此绘制的示例个数不能超过count * count 个,但是可以少于。

案例效果说明

如果把count 指定为10,最终绘制的效果如下:

绘制10*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彪叔
ITman彪叔公众号


netcy
204 声望120 粉丝

欢迎对canvas、webgl、图形学感兴趣的读者订阅专栏。