1. 概述
在上一篇教程《WebGL簡易教程(三):繪制一個三角形(緩沖區對象)》中,通過使用緩沖區對象(buffer object)來向頂點着色器傳送數據。那么,如果這些數據(與頂點相關的數據,如法向量、顏色等)需要繼續傳送到片元着色器該怎么辦呢?
例如這里給三角形的每個頂點賦予不同的顏色,繪制一個彩色的三角形。這個時候就需要用到之前(《WebGL簡易教程(二):向着色器傳輸數據》)介紹過的varying變量了。
2. 示例:繪制三角形
改進上一篇中繪制三角形(HelloTriangle.js)的代碼:
// 頂點着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // attribute variable
'attribute vec4 a_Color;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' + // Set the vertex coordinates of the point
' v_Color = a_Color;\n' +
'}\n';
// 片元着色器程序
var FSHADER_SOURCE =
'precision mediump float;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';
function main() {
// 獲取 <canvas> 元素
var canvas = document.getElementById('webgl');
// 獲取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 intialize 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.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
// 頂點坐標和顏色
var verticesColors = new Float32Array([
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0,
]);
//
var n = 3; // 點的個數
var FSIZE = verticesColors.BYTES_PER_ELEMENT; //數組中每個元素的字節數
// 創建緩沖區對象
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}
// 將緩沖區對象綁定到目標
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩沖區對象寫入數據
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
//獲取着色器中attribute變量a_Position的地址
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_Position變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);
// 連接a_Position變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(a_Position);
//獲取着色器中attribute變量a_Color的地址
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
// 將緩沖區對象分配給a_Color變量
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
// 連接a_Color變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(a_Color);
// 解除綁定
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return n;
}
1) 數據的組織
與之前的例子相似,數據仍然通過緩沖區傳遞到頂點着色器。在頂點着色器中,定義了兩個attribute變量,分別代表位置和顏色信息:
// 頂點着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // attribute variable
'attribute vec4 a_Color;\n' +
…
'}\n';
這意味着需要向頂點着色器傳遞兩次數據。這里采取的做法仍然是一次性向緩沖區寫入位置和顏色等所有的數據,然后分批次傳入頂點着色器:
// 創建緩沖區對象
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}
// 將緩沖區對象綁定到目標
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩沖區對象寫入數據
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
//獲取着色器中attribute變量a_Position的地址
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_Position變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);
// 連接a_Position變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(a_Position);
//獲取着色器中attribute變量a_Color的地址
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
// 將緩沖區對象分配給a_Color變量
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
// 連接a_Color變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(a_Color);
可以看到創建緩沖區對象、將緩沖區對象綁定到目標、向緩沖區對象寫入數據這三個步驟都是一致的。但分配attribute變量和連接attribute變量這兩個步驟分別進行了兩次。其中的關鍵點就在於gl.vertexAttribPointer()這個函數。之前使用這個函數都是使用的默認值,這里通過設置步進和偏移值,分別訪問了緩沖區中不同的數據。
通過gl.vertexAttribPointer()函數定義可以知道,傳送到緩沖區的數據是2(size)的位置數據和3(size)的顏色數據,所以步進參數stride都是5(size)。第一次傳送位置數據的時候是從初始位置開始的,所以offset是0;而第二次傳送顏色數據的時候需要偏移第一個位置數據,所以offfset是2(size)。
2) varying變量
在之前的教程(《WebGL簡易教程(二):向着色器傳輸數據》)中提到,可以傳送數據給片元着色器,來給繪制場景賦予顏色。但是這里卻通過緩沖區把數據傳遞給了頂點着色器。因此,在這里給頂點着色器和片元着色器,分別定義了一個相同的varying變量:
// 頂點着色器程序
var VSHADER_SOURCE =
…
'varying vec4 v_Color;\n' +
'void main() {\n' +
…
' v_Color = a_Color;\n' +
'}\n';
// 片元着色器程序
var FSHADER_SOURCE =
…
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';
varying變量表達的正是一種可變的變量,它的作用就是從頂點着色器向片元着色器傳輸數據。在頂點着色器的main函數中,將從緩沖區對象中獲取的attribute變量a_Color賦值給預先定義的varying變量v_Color;同時在片元着色器中又定義了一個同類型同名的varying變量v_Color,那么頂點着色器中該變量的值就會自動傳入到片元着色器中。最后在片元着色器的main函數中將該值傳入到gl_FragColor中,就得到最終的結果了。其示意圖如下:
3. 結果
最后的運行結果如下,最后會發現得到了一個顏色平滑過渡的,三個角各是紅、綠、藍顏色的三角形:
4. 理解
1) 圖形裝配和光柵化
更進一步思考下,這里雖然給每個頂點賦予的顏色值,但是為什么三角形的表面都賦予了顏色,並且是平滑過渡的效果呢?其實這里省略了頂點着色器與片元着色器之間數據傳輸細節——圖形裝配和光柵化。
點組成線,線組成面,將孤立的點組成基本圖形(圖元)的過程就是圖形裝配。圖形裝配的輸入數據就是頂點着色器中gl_Position得到的值,由gl.drawArrays()中第一個參數值來確定裝配成什么樣的圖元。在這個例子中,頂點着色器告訴WebGL系統,准備了三個點,WebGL通過圖像裝配,將其裝配成三角形。
知道裝配的圖形還是不夠的,理論上的三角形是連續不斷的圖形,而一般的圖形顯示設備都是離散的片元(像素)。圖像轉換成片元,就是光柵化的過程。
圖形裝配和光柵化的示意圖如下:
2) 內插過程
在第二節詳解示例中的代碼時,提到了頂點着色器和片元着色都定義了相同的varying變量v_Color,數據就會從頂點着色器傳入到片元着色器。但其實兩者雖然同名,但並不是一回事。在頂點着色器中,這個varying變量是與頂點相關的值,而經過圖形裝配和光柵化后,片元着色器的varying變量就是與片元相關的值了。並且,這個值是經過內插過程得到的。
在這個例子中,給三個頂點賦予了三個不同的顏色值。WebGL就根據三個頂點的顏色值內插了三角形中每個片元(像素)的顏色值,並傳遞給片元着色器。所謂內插過程,可以想象成一條漸變色帶,知道確定了起止顏色,就能獲取中間任意位置的顏色。
5. 參考
本來部分代碼和插圖來自《WebGL編程指南》。