目標:繪製多個點組成的圖形。本節的例子將會在屏幕上繪製三個紅色小點。
結果:
前一章有一個示例程序 ClickedPoints,它在鼠標點擊的位置會致點。ClickedPoints 將所以後的點的座標數據存儲在一個JS數組 g_points[]中,然後使用了一個循環遍歷該數組,每次遍歷就向着着色器傳入一個點,並調用gl.drawArrays()將這個點繪製出來。
for(var i = 0; i < len; i+=2){
//將點的位置傳遞到變量中
gl.vertexAttrib3f(a_Position, g_points[i], g_points[i+1], 0.0);
//繪製點
gl.drawArrays(gl.POINTS, 0, 1);
}
顯然,這種方法只能繪製一個點。對那些由多個頂點組成的圖形,比如三角形、矩形和立方體來說,你需要一次性地將圖形的頂點全部傳入頂點着色器,然後才能把圖形畫出來。
WebGL 提供了一種很方便的機制,即緩衝區對象,它可以一次性地向着色器傳入多個頂點的數據。緩衝區對象是 WebGL 系統中的一塊內存區域,我們可以一次性地向緩衝區對象中填充大量的頂點數據,然後將這些數據保存在其中,供頂點着色器使用。
示例程序
本例子的流程與第2章中的 ClickPoints.js 與 ColoredPoints.js 的流程基本一致,唯一的不同就是加入了一個新的步驟:設置點的座標信息。
MultiPoints.js
//頂點着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'void main(){'+
'gl_Position=a_Position;'+
'gl_PointSize=10.0;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
function main() {
//獲取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the <canvas> element");
return;
}
//獲取WebGL繪圖上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//設置頂點位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空<canvas>顏色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
//繪製三個點
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //點的個數
//創建緩衝區對象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//將緩衝區對象保存到目標上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//向緩存對象寫入數據
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0){
console.log("Failed to get the storage location of a_Position");
return -1;
}
//將緩衝區對象分配給a_Postion變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//連接a_Postion變量與分配給它的緩衝區對象
gl.enableVertexAttribArray(a_Position);
return n;
}
新加入的函數 initVertexBuffers()的任務是創建定點緩衝區對象,並將多個頂點的數據保存在緩衝區中,然後將緩衝區傳給頂點着色器。
函數的返回值是待繪製頂點的數量,保存在變量n中。注意,如果函數內發生錯誤,返回的負值。
示例程序僅調用了一次 gl.drawArrays()函數就完成了繪圖操作,這與之前的不同。調用這個函數時,本例傳入的第3個參數是 n,而不是1。
//繪製三個點
gl.drawArrays(gl.POINTS, 0, n);
因爲我們在 initVertexBuffers()函數中利用緩衝區對象向頂點着色器傳輸了多個頂點的數據,所以還需要通過第3個參數告訴 gl.drawArrays()函數需要繪製多少個頂點。
使用緩衝區對象
先創建一個緩衝區,然後向其寫入頂點數據,你就能一次性地向頂點着色器中傳入多個頂點的 attribute 變量的數據。
在示例程序中,向緩衝區對象寫入數據是一種特殊的JS數組(Float32Array):
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
使用緩衝區對象向點點着色器傳入多個頂點的數據,需要遵循以下五個步驟:
- 創建緩衝區對象(gl.creatBuffer())。
- 綁定緩衝區對象(gl.bindBuffer())。
- 將數據寫入緩衝區對象(gl.bufferData())。
- 將緩衝區對象分配給一個 attribute 變量(gl.vertexAttribPointer())。
- 開啓attribute 變量(gl.enableVertexAttribArray)。
創建緩衝區對象
在使用緩衝區對象之前,你必須創建它。
//創建緩衝區對象
var vertexBuffer = gl.createBuffer();
下面的圖示意了該方法執行前後 WebGL 系統的中間狀態,上面一張圖是執行前的狀態,下面一張圖是執行後的狀態。執行該方法的結果就是,WebGL 系統中多了一個新創建出來的緩衝區對象。
下面是 gl.creatBuffer()的函數規範
綁定緩衝區
創建緩衝區之後的第2個步驟就是將緩衝區對象綁定到 WebGL 系統中已經存在的“目標”上。這個“目標”表示緩衝區對象的用途(在這裏,就是向頂點着色器提供傳給 attribute 變量的數據),這樣 WebGL 才能正確處理其中的內容:
//將緩衝區對象保存到目標上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
下面是gl.bindBuffer( )的函數規範
在示例程序中,我們將緩衝區對象綁定到了 gl.ARRAY_BUFFER 目標上,緩衝區對象中存儲着的關於頂點的數據。在這個函數執行完畢後,WebGL 系統內部狀態發生了改變:
接下來,我們就可以向緩衝區對象中寫入數據了。
向緩衝區對象寫入數據
第3步是,開闢空間並向緩衝區中寫入數據。
//向緩存對象寫入數據
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
該方法的效果是,將第2個參數 vertices 中的數據寫入了綁定到第1個參數 gl.ARRAY_BUFFER 上的緩衝區對象。我們不能直接向緩衝區寫入數據,而只能向“目標”寫入數據,所以要向緩衝區寫數據,必須先綁定。該方法執行後,WebGL系統的內部狀態如圖:
從上圖你可以看到,定義在JS程序中的數據被寫入了綁定在 gl.ARRAY_BUFFER 上的緩衝區對象。下面是對 gl.bufferData()的範圍。
現在我們來看看 gl,bufferData()方法向緩衝區中傳入了什麼數據。該方法使用了一個特殊的數組 vertices 將數據傳給頂點着色器。我們是用 new 運算符,並以<第一個頂點的x座標和y座標><第二個頂點的x座標和y座標>,等等的形式創建這個數組:
如你所見,我們使用了 Float32Array 對象,而不是JS中更常見的 Array 對象。這是因爲JS中通用的數組 Array 是一種通用的類型,即可以在裏面存儲數字也可以存儲字符串,而並沒有對“大量元素都是同一種類型”這種情況進行優化。爲了解決這個問題,WebGL 引入了類型化數組,Float32Array 就是其中之一。
類型化數組
爲了繪製三維圖形,WebGL 通常需要同時處理大量相同類型的數據,例如頂點的座標和顏色數據,爲了優化性能,WebGL 爲每種基本數據類型引入了一種特殊的數組(類型化數組)。瀏覽器事先知道數組中的數據類型,所以處理起來也更加有效率。
例子中的Float32Array 就是一種類型化數組,通常用來存儲頂點的座標或顏色數據。應當牢記,WebGL 中的很多操作都要用到類型化數組,比如 gl.bufferData()中的第2個參數 data。
下表列舉了各種可用的類型化數組。
與JS中的 Array 數組相似,類型化數組也有一系列方法和屬性,如下表所示。注意,與普通的 Array 數組不同,類型化數組不支持 push()和 pop()方法。
和普通的數組一樣,類型化數組可以通過 new 運算符調用構造函數並傳入數據而被創造出來。注意,創建類型化數組的唯一方法就是使用 new 運算符,不能使用 []運算符。
此外,你也可以通過指定數組元素的個數來創建一個空的類型化數組,例如:
var vertices = new Float32Array(4);
到目前爲止,你就完成了建立和使用穿衝去的前三個步驟(創建緩衝區,綁定緩衝區對象到目標,向緩衝區對象中寫入數據)。我們們來看看在接下倆的兩步中,WebGL 是如何真正使用緩衝區來進行繪圖的。
向緩衝區對象分配給 attribute 變量
如第2章所述,你可以使用 gl.vertexAttrib[1234]f系列函數爲 attribute 變量分配值。但是,這些方法一次只能向 attribute 變量分配一個值。而現在,你需要將整個數組中的所有值一次性地分配給一個 attribute 變量。
gl.vertexAttribPointer()方法解決了這個問題,它可以將整個緩衝區對象(實際上是緩衝區對象的引用或指針)分配給 attribute 變量。示例程序將緩衝區對象分配給 attribute 變量 a_Position。
//將緩衝區對象分配給a_Postion變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer()的規範如下所示:
執行完第4步後,我們就將整個緩衝區對象分配給了 attribute 變量,爲 WebGl 繪圖進行的準備工作(即向 location 處的 attribute 變量傳入緩衝區)就差最後一步了:進行最後的“開啓”,使這次分配真正生效,如下圖所示:
開啓 attribute 變量
爲了使頂點着色器能夠訪問緩衝區內的數據,我們需要使用 gl.enableVertexAttribArray()方法來開啓 attribute 變量。
//連接a_Postion變量與分配給它的緩衝區對象
gl.enableVertexAttribArray(a_Position);
注意,雖然函數的名稱似乎表示該函數是用來處理“頂點函數”的,但實際上它處理的對象是緩衝區。
當你執行 gl.enableVertexAttribArray()並傳入一個已經分配好緩衝區的 attribute 變量後,我們就開啓了該變量,也就是說,緩衝區對象和 attribute 變量之間的連線就真正建立起來了。
現在你只需要orange頂點着色器運行起來,它會自動將緩衝區中的頂點畫出來。如第2章,你使用 gl.drawArrays()方法繪製了一個點,現在你要畫多個點,所用的方法仍是 gl.drawArrays()方法,但使用的是方法中的第2個和第3個參數。
注意,開啓 atrribute 變量後,你就不能再用gl.vertexAtrrib[1234]f 向它傳數據了,除非你顯式地關閉該 attribute 變量。實際上,你無法同時使用這兩個函數。
gl.drawArrays()的第2個和第3個參數
在對 gl.drawArray()作進一步詳細說明之前,看一下他的規範:
這個示例程序如下調用這個方法:
gl.drawArrays(gl.POINTS, 0, n);
當程序運行到這個方法時,實際上頂點着色器執行了count次,我們通過存儲在緩衝區中的頂點座標數據被依次傳給 attribute 變量,如下圖所示
注意,每次執行頂點着色器,a_Postion 的 z 和 w 分量值都會被自動設爲0.0或1.0,因爲 a_Position 需要4個分量,而你只提供了兩個。
運行出結果: