本文梳理了WebGL中從零到渲染出一個簡單幾何圖形的主要流程。幫助一些剛接觸WebGL的玩家簡單整理下在WebGL中渲染幾何圖形的過程。
目錄
- 初始化WebGL環(huán)境
- 頂點著色器(Vertex Shader)與片元著色器(Fragment Shader)
- 頂點數(shù)組對象(VBO)、索引數(shù)值對象(IBO)
- 繪制流程
- 總結(jié)
初始化WebGL環(huán)境
關(guān)于HTML5、<canvas>標(biāo)簽、WebGL的一些相關(guān)知識可以去MDN中查看,里面還有一些相關(guān)的學(xué)習(xí)干貨,初始化WebGL環(huán)境可以參考初識WebGL,我們這里按下不表。
頂點著色器與片元著色器
WebGL圖形渲染管線
介紹著色器之前,我們先過一下WebGL的圖形管線:

我們可以把WebGL的渲染管線當(dāng)做一條車間里的流水線,這個車間的原材料是一些圖形相關(guān)的數(shù)據(jù)(包括頂點坐標(biāo),頂點顏色等),這個車間生產(chǎn)的產(chǎn)品是我們屏幕上看到的各種圖形。
圖中綠色的兩個方塊就是我們要說的頂點著色器(Vertex Shader)和片元著色器(Fragment Shader)。
我們來一步一步簡單介紹下這個條流水線:
圖中最上面的藍(lán)色方塊Attribute可以看做是一條水管,一端連接頂點數(shù)據(jù)(Vertex Buffer Objects),另一端連接頂點著色器,這條水管的作用是把頂點數(shù)據(jù)輸送給頂點著色器處理。緊接著,頂點著色器把處理過的頂點數(shù)據(jù)交給片元著色器處理,最后經(jīng)過片元著色器處理過的數(shù)據(jù)將被輸送到Framebuffer中去,為了便于理解,我們暫且、姑且可以把這個Framebuffer當(dāng)做屏幕,而這最后的步驟,也就是把這些處理過的數(shù)據(jù)以圖形的方式顯示到屏幕上,至此,渲染管線也就完成了他的使命。
在這條流水線上,寫代碼的我們,扮演著的是流水線上的工人,所以我們做的事情是拿來數(shù)據(jù),然后確保數(shù)據(jù)在這條流水線上,按照既定的流程,最終可以變成我們想要的圖形。
頂點著色器與片元著色器
由上所述,WebGL編程中,我們需要為渲染流水線構(gòu)建好頂點著色器和片元著色器。
頂點著色器的功能是對傳進(jìn)來的頂點數(shù)據(jù)通過矩陣進(jìn)行換換位置、計算照明方程式以生成逐頂點的顏色以及生成或者變換紋理坐標(biāo)。
片元著色器則是對即將送到屏幕上的像素內(nèi)容進(jìn)行更進(jìn)一步的處理,包括一些特殊效果的定制等。
這兩者的內(nèi)容會在之后的學(xué)習(xí)過程中加以說明,本文只需對它們的作用有個大致的了解即可,更多的內(nèi)容可以參考《OpenGL編程指南》或者是《OpenGL ES 3.0編程指南》,我們這里重點只在于簡單整理下WebGL的渲染流程。
創(chuàng)建及使用著色器(Shader)
簡要梳理下shader的創(chuàng)建過程。shader的作用前文已經(jīng)介紹過了,我們可以把shader當(dāng)做一個程序,頂點數(shù)據(jù)輸入shader,輸出經(jīng)過shader處理過的數(shù)據(jù),用于之后的渲染流程。
創(chuàng)建和使用shader的過程分為以下步驟:
- 首先創(chuàng)建并編譯好著色器對象
- 將這些著色器對象鏈接為一個著色器程序。
對于著色器對象:
- 創(chuàng)建一個著色器對象
- 將著色器源代碼編譯為對象
- 驗證著色器的編譯是否成功
創(chuàng)建著色器代碼如下:
// 創(chuàng)建一個著色器對象
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 指定著色器源代碼
gl.shaderSource(vertexShader , vertexshaderSourceCode);
// 將著色器源代碼編譯為對象
gl.compileShader(vertexShader );
// 驗證著色器的編譯是否成功
if (!gl.getShaderParameter(vertexShader , gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader ));
return;
}
// 以上的代碼創(chuàng)建了一個頂點著色器,對于片元著色器
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 以下代碼類似
// code gose here ...
創(chuàng)建完著色器對象后,我們得到了兩個著色器 vertexShader 和 fragmentShader
對于著色器程序:
- 創(chuàng)建一個著色器程序
- 將著色器對象關(guān)聯(lián)到著色器程序
- 連接著色器程序
- 判斷著色器的鏈接過程是否成功完成
- 使用著色器來處理頂點和片元
創(chuàng)建著色器程序代碼如下:
// 創(chuàng)建一個著色器程序
var program = gl.createProgram();
// 將著色器對象關(guān)聯(lián)到著色器程序
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 連接著色器程序
gl.linkProgram(program);
// 判斷著色器的鏈接過程是否成功完成
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}
// 使用著色器來處理頂點和片元
gl.useProgram(program);
在大致了解過這條渲染流水線后,我們接著將要介紹下頂點數(shù)組對象和索引數(shù)組對象。
頂點數(shù)組對象(VBO)、索引數(shù)值對象(IBO)
頂點數(shù)組對象(VBO)
頂點數(shù)組對象(VBO)對應(yīng)著圖中最上方的紅色的方塊。頂點數(shù)組對象包含著WebGL要渲染的圖形的數(shù)據(jù)??梢钥闯墒橇魉€上的原材料。在WebGL的渲染過程中,這些圖形的數(shù)據(jù)往往通過頂點進(jìn)行儲存和輸送,頂點數(shù)組對象包含的數(shù)據(jù)一般包括頂點位置信息、頂點位置上的法線向量、紋理坐標(biāo)等。
索引數(shù)組對象(IBO)
在圖形的繪制過程中,我們把每3個頂點繪制成一個三角形,即圖形學(xué)中“面”(surface)的含義,而后通過成千上萬的面組成了我們空間中的三維模型。索引數(shù)組對象(IBO)的作用是告訴WebGL要通過什么樣的順序來將我們傳入的頂點數(shù)據(jù)連接成面。為了便于理解,我們舉個栗子:

當(dāng)我們按照如圖所示的方式繪制一個梯形時,我們弄來了5個頂點(0, 0)、(10, 10)、(20, 0)、(30, 10)、(40, 0)分別對應(yīng)從0-4的五個索引。而當(dāng)我們繪制圖形時,定義的索引數(shù)組[0, 2, 1, 1, 2, 3, 2, 4, 3]的意思就是告訴WebGL,把索引為0、2、1的三個頂點組成一個三角形,索引為1、2、3的三個頂點組成另一個三角形,索引為2、4、3的頂點也組成一個三角形(可參照圖示)。
索引數(shù)組對象的作用就是保存這些索引數(shù)組的數(shù)據(jù),用以傳輸給WebGL渲染管線。
創(chuàng)建頂點數(shù)組對象&索引數(shù)組對象
WebGL狀態(tài)機(jī)
在介紹如何創(chuàng)建這兩個對象前,我們要先知道,WebGL是個狀態(tài)機(jī)。我們可以這么理解,假設(shè)WebGL中的屬性P的值為1,你在某一次操作中,把P的值設(shè)置成了2,那么在你下一次設(shè)置P的值之前,P的值永遠(yuǎn)是2。
更直觀一點的表述,你可以想象WebGL像KFC套餐,有個套餐A,里面包含一個香辣雞腿堡,一杯百事可樂。你每次點A套餐,都會得到一個香辣雞腿堡,一杯百事可樂,而你在每次用餐的過程中,吃的漢堡都是香辣雞腿堡,喝的可樂都是百事可樂。直到某一天,KFC把A套餐的內(nèi)容改成了一個奧爾良烤雞腿堡,一杯可口可樂。在那天之后,同樣是A套餐,但是你吃的漢堡就變成了奧爾良烤雞腿堡,喝的可樂變成了可口可樂,除非KFC對A套餐的內(nèi)容再進(jìn)行調(diào)整,不然點A套餐的你只能喝上一輩子的可口可樂(本人比較喜歡百事可樂)。
所以你可以把WebGL上下文想象成是A套餐,WebGL中的一些屬性選項的對應(yīng)的是漢堡、可樂,而香辣雞腿堡、奧爾良烤雞腿堡、百事可樂、可口可樂對應(yīng)的是屬性當(dāng)前的值。類比一下。
頂點數(shù)組對象的創(chuàng)建
接下來插播一段代碼:
var vertices = [
-50.0, 50.0, 0.0,
-50.0,-50.0, 0.0,
50.0,-50.0, 0.0,
50.0, 50.0, 0.0
];
// 調(diào)用gl.createBuffer()創(chuàng)建了一個塊內(nèi)存空間,并讓 vertexBufferObject 變量指向這塊空間
var vertexBufferObject = gl.createBuffer();
// 把WebGL中用于繪制的數(shù)組數(shù)據(jù)的屬性(ARRAY_BUFFER)的值的地址指向上文我們創(chuàng)建的 vertexBuffer 內(nèi)存空間。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObject );
// bufferData 函數(shù)對 ARRAY_BUFFER 屬性對應(yīng)的空間填充值
// WebGL 狀態(tài)機(jī)概念出場!
// 由于我們在上一個語句中,修改了 ARRAY_BUFFER 的值,由于狀態(tài)機(jī)的性質(zhì),所以我們調(diào)用該函數(shù)進(jìn)行傳值時,
// 傳入的值是會給此時gl中的 ARRAY_BUFFER 屬性指向的空間,也就是
vertexBufferObject 的內(nèi)存空間。
// 此處調(diào)用把 vertices 中的頂點數(shù)據(jù)傳入了 vertexBufferObject 對象
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
// 傳值完畢,恢復(fù) ARRAY_BUFFER 的值為空
gl.bindBuffer(gl.ARRAY_BUFFER, null);
所以這段代碼就創(chuàng)建了一個名為 vertexBufferObject 的頂點數(shù)組對象。代碼中具體的API可以查閱相關(guān)的資料。
索引數(shù)組對象的創(chuàng)建
索引數(shù)組對象的創(chuàng)建與頂點數(shù)組對象的創(chuàng)建大同小異
var indices = [
0, 2, 1,
1, 2, 3
];
var indicesBufferObject = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBufferObject );
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices),
gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
這里需要注意的是,索引數(shù)組對象對應(yīng)的屬性為ELEMENT_ARRAY_BUFFER。而傳入的數(shù)組類型是Uint16Array。
繪制流程
最后,我們要把前面提到的內(nèi)容按照渲染管線的圖示結(jié)合起來。再看一眼我們的地圖:

好,上代碼:
// 按照圖示來看,要繪制一個圖形,首先我們需要原材料:
// 1. 一個頂點數(shù)組對象(VBO),用于存儲相關(guān)的頂點數(shù)據(jù),
// 同時需要一個表示頂點組合順序的索引數(shù)組對象(IBO)。
// 2. 需要兩個著色器(vertex shader 及 fragment shader)和一個著色器程序,用來保證管線的順利進(jìn)行。
// 好了,開工!
// 先來VBO和IBO
var vertices = [
-0.5,0.5,0.0,
-0.5,-0.5,0.0,
0.5,-0.5,0.0,
0.5,0.5,0.0
];
var indices = [3,2,1,3,1,0];
// 創(chuàng)建VBO
var vertexBufferObj = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 創(chuàng)建IBO
var indexBufferObj = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObj );
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// 創(chuàng)建頂點著色器
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader , vertexshaderSourceCode);
gl.compileShader(vertexShader );
// 驗證著色器的編譯是否成功
if (!gl.getShaderParameter(vertexShader , gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader ));
}
// 同理創(chuàng)建片元著色器
var fragmentShader= gl.createShader(gl.FRAGMENT_SHADER);
/* so many balabalabala... */
// 創(chuàng)建著色器程序
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 判斷著色器的鏈接過程是否成功完成
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}
目前為止,我們已經(jīng)準(zhǔn)備好了繪制圖形的所有原料了,接下里就是如何通過WebGL繪制出圖形了。
// 先獲取頂點數(shù)據(jù)進(jìn)入著色器的入口(后文會講解)
program.vertexPosition = gl.getAttribLocation(program, "aVertexPosition");
// 繪制場景的函數(shù)
function drawScene(){
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, c_width, c_height);
// 指定繪制時使用的頂點數(shù)據(jù)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
// 一下這兩行代碼對應(yīng)的作用是將頂點數(shù)據(jù)讀入著色器中,后文會加以解釋
gl.vertexAttribPointer(program.aVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.vertexPosition);
// 指定繪制時使用的索引數(shù)組
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObj);
// 以給定的形式繪制圖形
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT,0);
}
這樣我們就實現(xiàn)了圖形的繪制。
由于本文重點在于梳理繪制流程,并沒有深入介紹著色器相關(guān)的內(nèi)容,所以對于gl.vertexAttribPointer函數(shù)和gl.enableVertexAttribArray會有疑惑。
為了解釋一下這個問題,我們先看一段頂點著色器的代碼
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
void main(void) {
gl_Position = vec4(aVertexPosition,1.0);
}
</script>
其中代碼中的aVertexPosition表示的是頂點數(shù)據(jù)從這個變量進(jìn)入著色器。
就像我們前文比喻的那樣,這個aVertexPosition就像一端連接頂點數(shù)據(jù)(Vertex Buffer Objects),另一端連接頂點著色器的水管的開關(guān)。
我們執(zhí)行program.vertexPosition = gl.getAttribLocation(program, "aVertexPosition");時,相當(dāng)于把這個開關(guān)的位置記錄在program.vertexPosition上面。
以下代碼:
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
gl.vertexAttribPointer(prg.vertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.vertexPosition);
bindBuffer指定當(dāng)前WebGL繪制的數(shù)據(jù)為vertexBufferObj中的頂點數(shù)據(jù)。
vertexAttribPointer函數(shù)則把頂點數(shù)據(jù)通過水管接到著色器"aVertexPosition"的位置上,也就是我們記錄下來的program.vertexPosition的位置。
gl.enableVertexAttribArray(program.vertexPosition)則是最后一步把水管的閥門打開,讓頂點數(shù)據(jù)流入著色器。
總結(jié)
本文主要目的是簡單梳理了下WebGL繪制圖形的大致編程流程,并沒有做很深入的講解。WebGL基于OpenGL ES 2.0,而早先的OpenGL用的都是固定的渲染管線,之前學(xué)OpenGL一開始都是glBegin,glEnd,glVertex2d啥的一堆東西,精簡版的OpenGL ES一上來就拋棄了之前固定渲染管線的東西,使用了可編程的渲染管線,一上來就要求要編寫Shader,所以繪制一個圖形就變得比較繁瑣一點。于是有了這篇筆記,以供剛開始學(xué)習(xí)WebGL的玩家們參考。
對于文中的一些API并沒有進(jìn)入詳細(xì)的講解,需要的同學(xué)可以查閱相關(guān)的資料書籍。