• 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,最终绘制的效果如下:

    绘制10*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彪叔公众号
    ITman彪叔公众号
  • 相关阅读:
    cf C. Vasya and Robot
    zoj 3805 Machine
    cf B. Vasya and Public Transport
    cf D. Queue
    cf C. Find Maximum
    cf B. Two Heaps
    cf C. Jeff and Rounding
    cf B. Jeff and Periods
    cf A. Jeff and Digits
    I Think I Need a Houseboat
  • 原文地址:https://www.cnblogs.com/flyfox1982/p/10070578.html
Copyright © 2020-2023  润新知