通過繪制彩色三角形的示例,介紹了varying變量,頂點着色器與片元着色器之間資料傳輸的過程:頂點裝配與光栅化。
目錄
- 1. 概述
- 2. 示例:繪制三角形
- 1) 資料的組織
- 2) varying變量
- 3. 結果
- 4. 了解
- 1) 圖形裝配和光栅化
- 2) 内插過程
- 5. 參考
在上一篇教程《WebGL簡易教程(三):繪制一個三角形(緩沖區對象)》中,通過使用緩沖區對象(buffer object)來向頂點着色器傳送資料。那麼,如果這些資料(與頂點相關的資料,如法向量、顔色等)需要繼續傳送到片元着色器該怎麼辦呢?
例如這裡給三角形的每個頂點賦予不同的顔色,繪制一個彩色的三角形。這個時候就需要用到之前(《WebGL簡易教程(二):向着色器傳輸資料》)介紹過的varying變量了。
改進上一篇中繪制三角形(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;
}
與之前的例子相似,資料仍然通過緩沖區傳遞到頂點着色器。在頂點着色器中,定義了兩個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)。
在之前的教程(《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中,就得到最終的結果了。其示意圖如下:
最後的運作結果如下,最後會發現得到了一個顔色平滑過渡的,三個角各是紅、綠、藍顔色的三角形:
更進一步思考下,這裡雖然給每個頂點賦予的顔色值,但是為什麼三角形的表面都賦予了顔色,并且是平滑過渡的效果呢?其實這裡省略了頂點着色器與片元着色器之間資料傳輸細節——圖形裝配和光栅化。
點組成線,線組成面,将孤立的點組成基本圖形(圖元)的過程就是圖形裝配。圖形裝配的輸入資料就是頂點着色器中gl_Position得到的值,由gl.drawArrays()中第一個參數值來确定裝配成什麼樣的圖元。在這個例子中,頂點着色器告訴WebGL系統,準備了三個點,WebGL通過圖像裝配,将其裝配成三角形。
知道裝配的圖形還是不夠的,理論上的三角形是連續不斷的圖形,而一般的圖形顯示裝置都是離散的片元(像素)。圖像轉換成片元,就是光栅化的過程。
圖形裝配和光栅化的示意圖如下:
在第二節詳解示例中的代碼時,提到了頂點着色器和片元着色都定義了相同的varying變量v_Color,資料就會從頂點着色器傳入到片元着色器。但其實兩者雖然同名,但并不是一回事。在頂點着色器中,這個varying變量是與頂點相關的值,而經過圖形裝配和光栅化後,片元着色器的varying變量就是與片元相關的值了。并且,這個值是經過内插過程得到的。
在這個例子中,給三個頂點賦予了三個不同的顔色值。WebGL就根據三個頂點的顔色值内插了三角形中每個片元(像素)的顔色值,并傳遞給片元着色器。所謂内插過程,可以想象成一條漸變色帶,知道确定了起止顔色,就能擷取中間任意位置的顔色。
本來部分代碼和插圖來自《WebGL程式設計指南》。
代碼和資料位址
上一篇
下一篇