上次我們介紹了如何在<canvas>中使用WebGL,以及幾個基礎的WebGL函數;實現了背景色的重置;為了擴展方便,我們把上次的代碼做了些改動,將繪制圖形的js獨立成文件,這樣我們只關注與這個js文件的編寫;以后除非HTML文件發生變化,我們就跳過它,直接討論JavaScript代碼。

1 <!doctype html>
2 <html>
3 <head>
4 <meta charset="UTF-8">
5 <meta name="Generator" content="EditPlus®">
6 <meta name="Author" content="Mirror">
7 <meta name="Keywords" content="">
8 <meta name="Description" content="">
9 <title>Hello Point</title>
10 <!--《WebGL編程指南》的作者為讀者編寫的WebGL輔助函數-->
11 <script src="lib/webgl-utils.js"></script>
12 <script src="lib/webgl-debug.js"></script>
13 <script src="lib/cuon-utils.js"></script>
14 <!--JavaScript文件,在<canvas>中繪制圖形-->
15 <script src="lib/hello-point.js"></script>
16 </head>
17 <body onload="main()">
18 <!--定義<canvas>標簽,通過width屬性和height屬性規定它是一片400×400的繪制區域-->
19 <canvas id="myCanvas" width="400" height="400">
20 <!--當瀏覽器不支持時,會直接忽略<canvas>標簽,而直接顯示下面這一行提示-->
21 Please use the browser supporting "canvas". 22 </canvas>
23 </body>
24 </html>
接下來,我們在此基礎上,繪制一個位於原點(0.0,0.0,0.0)處的10個像素大的紅色的點。因為使用的是三維圖形上下文,所以指定這個點時需要使用三維坐標。坐標系統后面介紹,這里只需要理解為原點位於<canvas>中心位置。效果如如下:
實際上,我們使用矩形而不是圓來繪制一個點,因為繪制矩形比繪制圓更快;就像在前一次中我們以RGBA的形式指定了背景色一樣,這里也需要同樣的處理;在前面,我們使用2d上下文來繪制了一個矩形;先指定了繪圖顏色,然后進行繪制。你可能認為WebGL也差不多,不幸的是,沒那么簡單。WebGL依賴於一種新的稱為着色器的繪圖機制。着色器提供了靈活且強大的繪制二維或三維圖形的方法,所有WebGL必須使用它。正因為強大,所以更復雜。
要使用WebGL繪圖,必須使用着色器,哪怕是一個點(矩形);着色器程序是以字符串的形式“嵌入”在JavaScript文件中,並且在程序開始運行前就已經設置好了。WebGL需要使用兩種着色器:頂點着色器、片源着色器;下面分別介紹:
頂點着色器:頂點着色器是用來描述頂點特性(如位置、顏色等)的程序。頂點是指二維或三維空間中的一個點,比如二維或三維圖形的端點或交點。
片元着色器:進行逐片元處理過程(如光源)的程序。片元是一個WebGL術語,你可以將其理解為像素(圖像的單元)。
在后續,我們會詳細的學習着色器。簡單的說,在三維場景中,僅僅用線條和顏色把圖形畫出來是遠遠不夠的。你必須考慮如光線照上去后,或者觀察者的視角發生變化時,對場景會有什么影響。着色器可以高度靈活的完成這些工作;提供各種渲染效果。這也就是現在制作的三維場景如此逼真的原因。
上圖顯示了WebGL系統的執行流程;左側為瀏覽器,首先執行JavaScript程序,調用了WebGL的相關方法,然后頂點着色器和片元着色器就會執行,頂點着色器指定繪制圖形的位置和尺寸;片元着色器則指定繪制圖形的顏色;然后在顏色緩沖區內進行繪制,這時就清空了繪圖區,最后,顏色緩沖區中的內容就自動在瀏覽器的<canvas>標簽上顯示出來。
回到我們今天的目標來,下面顯示了hello-point.js的代碼。

1 //頂點着色器程序 2 var VSHADER_SOURCE = 3 "void main() { \n" + 4 //設置坐標 5 "gl_Position = vec4(0.0, 0.0, 0.0, 1.0); \n" + 6 //設置尺寸 7 "gl_PointSize = 10.0; \n" + 8 "} \n"; 9 10 //片元着色器 11 var FSHADER_SOURCE = 12 "void main() {\n" + 13 //設置顏色 14 "gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" + 15 "}\n"; 16 17 function main() { 18 //獲取<canvas>標簽。 19 var canvas = document.getElementById("myCanvas"); 20 //獲取WebGL繪圖上下文。 21 var gl = getWebGLContext(canvas); 22 //如果瀏覽器不支持WebGL則提示錯誤。 23 if (!gl) { 24 console.log("Failed to get the rendering context for WebGL."); 25 return; 26 } 27 28 //初始化着色器 29 if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) { 30 console.log("Faile to initialize shaders."); 31 return; 32 } 33 34 //設置<canvas>的背景色 35 gl.clearColor(0.0, 0.0, 0.0, 1.0); 36 37 //清空<canvas> 38 gl.clear(gl.COLOR_BUFFER_BIT); 39 40 //繪制一個點 41 gl.drawArrays(gl.POINTS, 0, 1); 42 43 }
這個文件包含三個部分,頂點着色器程序(GLSL ES 語言),片元着色器程序(GLSL ES 語言)和主程序(JavaScript語言)。着色器程序代碼必須預先處理成單個字符串的形式,所以我們用+號將多行字符串連成一個長字符串。每一行以\n結束,這是由於當着色器內部出錯時,就能獲取出錯的行號,這對於檢查源代碼中的錯誤很有幫助;但是,\n並不是必須的。為了更容易維護,也可以把着色器代碼寫到單獨的文件中(就像javaScript文件一樣),然后通過javaScript程序從文件中讀取出來加載。
根據程序流程,加載頁面----->執行main()函數----->獲取<canvas>標簽----->獲取繪圖上下文;到這里,都跟之前的流程一樣;接下來,會執行名為initShaders()的函數。這個函數是《WebGL編程指南》的作者專門寫的一個輔助函數;該函數被定義在cuon.util.js中。這個函數的作用是對字符串形式的着色器進行初始化。我們來看下這個函數的具體參數定義:
Web系統由兩部分組成,即頂點着色器和片元着色器。在初始化着色器之前,頂點着色器和片元着色器都是空白的,我們需要將字符串形式的着色器代碼從JavaScript傳給WebGL系統,並建立着色器,這就是initShaders()函數要做的事情。着色器運行在WebGL系統中,而不是JavaScript程序中。
initShaders()函數執行成功后,着色器被創建好了並隨時可以使用,頂點着色器將被首先執行,它對gl_Position變量和gl_PointSize變量進行賦值,並將它們傳入片元着色器,然后片元着色器再執行。實際上,片元着色器接收到的是經過光柵化(將幾何圖形變為二維圖像的過程)處理后的片元值;現在可以簡單認為這兩個變量從頂點着色器傳入了片源着色器。
下面,我們來看看着色器如何畫出一個點(矩形),前面提到,我們需要三個信息來畫出這個點:位置,尺寸和顏色。位置和尺寸在頂點着色器中指定,和C語言一樣,着色器程序必須包含一個main()函數。main()前面的關鍵字void表示這個函數不會有返回值;而且也不能為main()函數指定參數。着色器程序使用 = 操作符為變量賦值。gl_Position和gl_pointSize這兩個變量內置在頂點着色器中,而且有着特殊的含義:前者表示頂點的位置,后者表示點的尺寸。gl_Position必須被賦值,否則着色器就無法正常工作;gl_PointSize並不是必須的,如果不賦值,着色器會為其取默認值1.0。
和JavaScript不同,GLSL ES是一種強類型語言,也就是說,開發者需要明確指出某個變量是某種類型;類似於Java和C、C#等。在這個程序中,出現了兩種數據類型,float:表示浮點數;vec4:表示由4個浮點數組成的矢量(也稱作向量)。而且着色器語言沒有類似Java、C#語言的類型隱式轉換的功能,所以這樣寫:gl_PointSize = 10;就會導致錯誤;因為gl_PointSize 需要一個float類型的值,而10是整型。另一個內置變量gl_Position的類型為vec4,但是三維坐標只有3個數,即X,Y和Z軸的坐標值,這就需要某種方法將其轉化為vec4類型的變量。着色器提供了內置函數vec4()可以完成這個事情。就如我們代碼中那樣使用:gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 前3個分量對應X,Y,Z軸坐標值,那第4個分量1.0是怎么來的呢。由4個分量表示的坐標被稱為齊次坐標,因為它能夠提高處理三維數據的效率,所以在三維圖形系統中被大量使用。當第4個分量為1.0時,這個齊次坐標就可以表示“前三個分量為坐標值”的點。
如前所述,片元可以看成是顯示在屏幕上的像素。片元着色器和頂點着色器一樣,也有一個main()函數。片元着色器將點的顏色賦值給gl_FragColor,該變量是片元着色器唯一的內置變量,它控制着像素在屏幕上的最終顏色。對這個內置變量賦值后,相應的像素就會以這個顏色值顯示。和頂點着色器中頂點位置一樣。顏色值也是vec4類型的,分別表示RGBA的4個分量。
建立了着色器后,我們開始進行繪制操作,我們使用gl.DrawArray()函數進行繪制。這個函數的功能非常強大,可以用來繪制各種圖形,具體參數說明如下:
因為我們繪制的是單獨的點,所以設置第1個參數為gl.POINTS;設置第2個參數為0,表示從第1個頂點(雖然只有一個頂點)開始畫起的,第3個參數為1,表示這個程序僅僅只畫了1個點。當程序調用gl.DrawArray()函數時,頂點着色器將被執行count次,每次處理一個頂點,這時候頂點着色器開始執行內部的main()函數,然后設置位置和尺寸;執行完成后,片元着色器開始執行其main()函數,設置顏色;最后,一個紅色的10個像素大的點就被繪制在了(0.0,0.0,0.0,1.0)處,也就是繪制區域的中心位置。
現在,我們對頂點着色器和片元着色器的工作服方式有了大致的了解,只是有個問題,為何(0.0,0.0,0.0,1.0)會繪制到了<canvas>的中心位置?下次我們介紹坐標系統會解開這個謎題。