1. 程式人生 > >WebGL2系列之例項陣列(Instanced Arrays)

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彪叔公眾號