實例化數組
實例化是一種只調用一次渲染函數卻能繪製出很多物體的技術,它節省渲染一個物體時從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
然後定義每個對象的偏移量數據的數組:
``` var offsetArray = [];
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個示例](http://upload-images.jianshu.io/upload_images/6271001-70e41e9924b21c8d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看出,一次繪製調用,繪製出了100個對象;
如果通過WebGL1的方式需要遍歷100次繪製。因此可以看出減少了繪製的遍歷。
當然如果只是繪製100個四邊形,遍歷方法也沒什麼不好,實例化的威力主要體現在,當數據量變到很大的時候,比如在筆者電腦上,把count值改爲4000,那麼會繪製4000 * 4000 = 一千六百萬個四邊形,如下:
![九百萬個四邊形](http://upload-images.jianshu.io/upload_images/6271001-fc5ee472f58590c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看出,還是可以很好的繪製出來(雖然由於對象太多,已經看不清楚界限)
而採用WebGL1 循環遍歷的方式,估計最多也就能夠達到萬級別的繪製循環數量,千萬級別的數量簡直不可想象。
當然這個數量 也是有限制的,比如在筆者的機器上,把count改成5000,也就是5000 * 5000 = 兩千五百萬的時候,機器就奔潰了。
![奔潰了](http://upload-images.jianshu.io/upload_images/6271001-f60f0a01f4055d99.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
## 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彪叔公衆號](https://upload-images.jianshu.io/upload_images/6271001-71b1902df91ba0cb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/256)