使用 WebGL 進行 3D 開發,第 1 部分: WebGL 簡介
使用 WebGL 進行 3D 開發,第 2 部分: 使用 WebGL 庫以更少的編碼做更多的事情
使用 WebGL 進行 3D 開發,第 3 部分: 添加用戶交互
WebGL API 讓 JavaScript 開發人員能夠直接利用如今的 PC 及移動設備硬件中強大的內置 3D 圖形加速功能。現代瀏覽器透明地支持 WebGL,它使人們可以為主流 Web 用戶創建高性能的 3D 游戲、應用程序以及 3D 增強的用戶界面。本文是由三部分組成的系列文章中的第 1 部分,該系列面向剛剛接觸 WebGL 的 JavaScript 開發人員。在這一部分中,我們將通過一個基本的示例來介紹 WebGL 的基礎知識和相關的 3D 圖形概念。
我們生活在一個 3D 世界中,但我們與計算機及計算機化設備的幾乎所有交互都發生在 2D 用戶界面上。直到最近,高速、流暢、逼真的 3D 應用程序(曾經是計算機動畫師、科研用戶和游戲愛好者的專屬領域)對於主流 PC 用戶還是遙不可及的。(見邊欄:3D 硬件進化:簡史。)
如今,所有主流 PC 的 CPU 都內置了 3D 圖形加速,並且游戲 PC 有額外的專用高性能圖形處理單元(GPU)來處理 3D 渲染。手機和平板電腦中基於精簡指令集計算(RISC)的 CPU 反映了這一趨勢。 目前所有的移動 CPU 都包括支持 3D 的強大圖形加速 GPU。配套的軟件驅動程序也日漸成熟,並且現在也更加穩定高效。
現代瀏覽器技術的進步為它們帶來了硬件加速的 WebGL,這是一個與具有豐富特性的 HTML5 一起運行的 3D JavaScript API。JavaScript 開發人員現在可以創建交互式 3D 游戲、應用程序和 3D 增強的用戶界面。由於 WebGL 被集成到主流瀏覽器中,只配備了瀏覽器和文本編輯器的大量開發人員終於可以進行 3D 應用程序開發了。
本文是由三部分組成的系列文章中的第 1 部分,主要介紹 WebGL。首先簡要概述 3D 軟件棧的演變。然后,您將有機會通過涵蓋 WebGL 編程關鍵方面的動手示例來體驗 WebGL API。(參見 下載 部分的示例代碼。)該示例既全面又易於理解,並且伴隨着解釋了一些基本的 3D 圖形概念。(假定您熟悉 HTML5 的 canvas 元素。)第 2 部分介紹高級 WebGL 庫。在第 3 部分中,您可以將一切都融合在一起,開始創建引人注目的 3D 用戶界面和應用程序。
3D 應用程序軟件棧
在 PC 早期歷史的大部分時間中,3D 硬件驅動程序都與應用程序捆綁,或被編譯到應用程序中。該配置可以優化對硬件的硬件加速特性的訪問,從而實現最佳性能。基本上,您可以直接編碼實現硬件的功能。精心設計的游戲或計算機輔助設計(CAD)應用程序可以充分利用底層硬件。圖 1 顯示了該軟件配置。
但捆綁的成本很昂貴。應用程序一經發布和安裝,硬件驅動程序就如同被凍結一般(包括錯誤在內的一切)。如果圖形卡供應商修復了一個錯誤或者推出了性能增強的驅動程序,應用程序用戶必須安裝或升級應用程序才可以利用它。此外,由於圖形硬件在迅速發展,使用編譯的 3D 驅動程序的應用程序或游戲很容易很快就過時。當新的硬件推出時(包含新的或更新后的驅動程序),軟件供應商必須開發並發布新版本。在高速寬帶網絡普遍可訪問之前,這是一個主要的分發問題。
作為驅動程序更新問題的解決方案,操作系統承擔了托管 3D 圖形驅動程序的角色。應用程序或游戲調用操作系統提供的一個 API,操作系統隨后將調用轉換為原生 3D 硬件驅動程序可接受的原語。圖 2 展示了這一安排。
通過這種方式(至少在理論上),可以針對操作系統的 3D API 對應用程序進行編程。對 3D 硬件驅動程序的修改,甚至 3D 硬件本身的進化對於應用程序都是屏蔽的。對於許多應用程序而言,包括所有主流瀏覽器,這樣的配置足以在很長一段時間內滿足需求。操作系統充當中間人,試圖大膽地迎合各種類型或樣式的應用程序,以及迎合來自競爭廠商的圖形硬件。但是,這種一刀切的方法會影響 3D 渲染性能。要求最佳硬件加速性能的應用程序仍然必須發現實際安裝的圖形硬件,實施調整,為每一組硬件優化代碼,並往往根據廠商對操作系統的 API 的特定擴展進行編程,再次使應用程序受制於底層驅動程序或物理硬件。
WebGL 時代
進入現代社會,高性能的 3D 硬件已內置到每個桌面和移動設備中。人們越來越多地利用 JavaScript 開發應用程序,以利用瀏覽器功能,Web 設計師和 Web 應用程序開發人員強烈要求獲得更快、更好的 2D/3D 瀏覽器支持。其結果是:主流瀏覽器廠商廣泛支持 WebGL。
WebGL 以 OpenGL Embedded System (ES) 為基礎,這是用於訪問 3D 硬件的低級過程 API。OpenGL(由 SGI 在 20 世紀 90 年代初創建)現在被視為是一個易於理解且成熟的 API。WebGL 讓 JavaScript 開發人員有史以來第一次能夠以接近原生的速度訪問設備上的 3D 硬件。WebGL 和 OpenGL ES 都在非營利組織 Khronos Group 的贊助下不斷發展。
通過瀏覽器支持庫和操作系統的 3D API 庫,WebGL API 幾乎可以直接訪問底層的 OpenGL 硬件驅動程序,而無需首先轉換代碼。圖 3 展示了這種新的模型。
硬件加速的 WebGL 支持瀏覽器上的 3D 游戲、實時 3D 數據可視化應用程序,以及未來的交互式 3D 用戶界面(僅舉幾例)。OpenGL ES 的標准化確保可以安裝新的廠商驅動程序,而不影響現有的基於 WebGL 的應用程序,並兌現理想化的 “在任何平台上支持任何 3D 硬件” 這一承諾。
動手體驗 WebGL
現在是時候動手體驗 WebGL 了。啟動最新版本的 Firefox、Chrome 或 Safari,並從代碼中打開 triangles.html(參見 下載 部分)。理想場景是通過一個 Web 服務器訪問 HTML 頁面,但在本例中,您也可以從您的文件系統打開它。(如果您直接打開 HTML 文件,本系列中后面的示例可能無法正常工作,因為可能需要通過 Web 服務器加載額外的圖形數據。)頁面看起來應該類似於圖 4 中的屏幕截圖,該頁面是從運行於 OS X 之上的 Safari 打開的。
兩個看似完全相同的藍色三角形出現在頁面上。然而,並非所有三角形的創建方式都一樣。兩個三角形都是使用 HTML5 canvas 繪制的。但是,左邊的那個是 2D 圖,並且用於繪制它的 JavaScript 代碼不到 10 行。右邊的那個是一個四面的 3D 金字塔對象,需要超過 100 行 JavaScript WebGL 代碼來渲染。
如果查看網頁的源代碼,就可以確認有大量的 WebGL 代碼繪制右邊的三角形。然而,該三角形看起來肯定不是 3D 圖形。(不是 3D 圖形,戴上紅藍色 3D 眼鏡也無濟於事。)
WebGL 繪制 2D 視圖
您在 triangles.html 的右側看到一個三角形,這是因為金字塔的方向。您看到的是一個多色金字塔的藍色的一面,類似於直視建築物的一面,只看到一個 2D 矩形。(快速看一下 圖 5,可以看到 3D 的金字塔。)此實現強調了在瀏覽器中使用 3D 圖形的本質:最終的輸出始終是一個 3D 場景的 2D 視圖。 因此,通過 WebGL 進行的 3D 場景的任何靜態渲染都是一個 2D 圖像。
接下來,在您的瀏覽器中加載 pyramid.html。在此頁面上,繪制金字塔所需的代碼幾乎與 triangles.html 中的代碼完全一樣。一個區別是,添加了一些代碼,用於沿 y 軸連續旋轉金字塔。換言之,以一定的時間延遲(使用 WebGL)相繼繪制了相同 3D 場景的多個 2D 視圖。隨着金字塔旋轉,可以清楚地看到,之前位於 triangles.html 右側的藍色三角形其實是一個多色 3D 金字塔的一面。圖 5 顯示了運行於 OS X 之上的 Safari 中的 pyramid.html 快照。
編寫 WebGL 代碼
清單 1 顯示了 triangles.html 中的兩個 canvas 元素的 HTML 代碼。
清單 1. 包含兩個 canvas 元素的 HTML 代碼
1
2
3
4
5
6
7
8
9
10
11
12
|
<
html
>
<
head
>
...
</
head
>
<
body
onload
=
"draw2D();draw3D();"
>
<
canvas
id
=
"shapecanvas"
class
=
"front"
width
=
"500"
height
=
"500"
>
</
canvas
>
<
canvas
id
=
"shapecanvas2"
style
=
"border: none;"
width
=
"500"
height
=
"500"
>
</
canvas
>
<
br
/>
</
body
>
</
html
>
|
onload
處理程序調用了兩個函數:draw2D()
和 draw3D()
。draw2D()
函數在左側畫布上 (shapecanvas
) 繪制 2D 圖形。draw3D()
函數在右側畫布上 (shapecanvas2
) 繪制 3D 圖形。
在左側畫布繪制 2D 三角形的代碼如清單 2 所示。
清單 2. 在 HTML5 畫布上繪制 2D 三角形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function
draw2D() {
var
canvas = document.getElementById(
"shapecanvas"
);
var
c2dCtx =
null
;
var
exmsg =
"Cannot get 2D context from canvas"
;
try
{
c2dCtx = canvas.getContext(
'2d'
);
}
catch
(e)
{
exmsg =
"Exception thrown:"
+ e.toString();
}
if
(!c2dCtx) {
alert(exmsg);
throw
new
Error(exmsg);
}
c2dCtx.fillStyle =
"#0000ff"
;
c2dCtx.beginPath();
c2dCtx.moveTo(250, 40);
// Top Corner
c2dCtx.lineTo(450, 250);
// Bottom Right
c2dCtx.lineTo(50, 250);
// Bottom Left
c2dCtx.closePath();
c2dCtx.fill();
}
|
在 清單 2 中簡單直觀的 2D 繪圖代碼中,繪圖上下文 c2dCtx
從 canvas 元素獲取而來。然后調用上下文的繪圖方法,創建一組跟蹤三角形的路徑。最后,使用 RGB 顏色 #0000ff
(藍色)填充封閉路徑。
獲取 3D WebGL 繪圖上下文
清單 3 顯示,用於從 canvas 元素獲取 3D 繪圖上下文的代碼與 2D 情況下的代碼幾乎一樣。其區別是,要請求的上下文名稱是 experimental-webgl
,而不是 2d
。
清單 3. 從 canvas 元素獲取 WebGL 3D 上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function
draw3D() {
var
canvas = document.getElementById(
"shapecanvas2"
);
var
glCtx =
null
;
var
exmsg =
"WebGL not supported"
;
try
{
glCtx = canvas.getContext(
"experimental-webgl"
);
}
catch
(e)
{
exmsg =
"Exception thrown:"
+ e.toString();
}
if
(!glCtx)
{
alert(exmsg);
throw
new
Error(exmsg);
}
...
|
在清單 3 中,如果瀏覽器不支持 WebGL,draw3D()
函數會顯示一個警報,並引發一個錯誤。在生產應用程序中,您可能會想使用更加特定於應用程序的代碼來處理這種情況。
設置視口(viewport)
為了告訴 WebGL 渲染輸出應該去哪里,您必須設置視口,方法是在 WebGL 可以繪圖的畫布內以像素為單位指定區域。在 triangles.html 中,整個畫布區域都用於渲染輸出:
1
2
|
// set viewport
glCtx.viewport(0, 0, canvas.width, canvas.height);
|
在接下來的步驟中,您必須開始創建數據,饋送到 WebGL 渲染管道。該數據必須描述構成場景的 3D 對象。在本例中,場景僅僅是一個四面的多色金字塔。
描述 3D 對象
要為 WebGL 渲染描述 3D 對象,您必須使用三角形來表示對象。WebGL 采用的描述可以是一組離散的三角形的形式,或者是有共享頂點的三角形的一個條帶。在金字塔的示例中,四面的金字塔用一組四個不同的三角形來描述。每個三角形由它的三個頂點指定。圖 6 顯示了金字塔其中一面的頂點。
在圖 6 中,這一面的三個頂點是 y 軸上的 (0,1,0)
、z 軸上的 (0,0,1)
和 x 軸上的 (1,0,0)
。在金字塔本身,這一面是黃色的,在可以看見的藍色一面的右側。擴展同樣的模式,您可以遵循此規則勾畫出金字塔的其他三面。清單 4 中的代碼在名為 verts
的數組中定義了金字塔的四個面。
清單 4. 描述組成金字塔的一組三角形的頂點數組
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// Vertex Data
vertBuffer = glCtx.createBuffer();
glCtx.bindBuffer(glCtx.ARRAY_BUFFER, vertBuffer);
var
verts = [
0.0, 1.0, 0.0,
-1.0, 0.0, 0.0,
0.0, 0.0, 1.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
1.0, 0.0, 0.0,
0.0, 0.0, -1.0,
0.0, 1.0, 0.0,
0.0, 0.0, -1.0,
-1.0, 0.0, 0.0
];
glCtx.bufferData(glCtx.ARRAY_BUFFER,
new
Float32Array(verts),
glCtx.STATIC_DRAW);
|
注意,金字塔的底部(它實際上是 x-z 平面上的一個正方形)沒有包含在 verts
數組中。因為金字塔繞 y 軸旋轉,觀看者永遠看不到底部。在 3D 作品中不渲染觀看者永遠看不到的對象表面,這是慣例。保持不渲染它們,可以顯著加快復雜對象的渲染。
在 清單 4 中,verts
數組中的數據被打包到一個二進制格式的緩沖中,3D 硬件可以高效地訪問該緩沖。這都通過 JavaScript WebGL 調用完成:首先,使用 WebGLglCtx.createBuffer()
調用創建一個零大小的新緩沖區,並通過 glCtx.bindBuffer()
調用將它綁定到 OpenGL 級別的 ARRAY_BUFFER
目標。接下來,在 JavaScript 中定義要加載的數據值數組,glCtx.bufferData()
調用設置當前綁定的緩沖區的大小,並將 JavaScript 數據(首先將 JavaScript 數組轉換成 Float32Array
二進制格式)打包到設備驅動程序緩沖區中。
其結果是一個 vertBuffer
變量,它引用包含所需頂點信息的硬件緩沖區。該緩沖區中的數據可以由 WebGL 渲染管道中的其他處理器直接高效地訪問。
指定金字塔各個面的顏色
必須設置的下一個低級緩沖由 colorBuffer
引用。此緩沖中包含金字塔的每一面的顏色信息。在該示例中,顏色是藍、黃、綠和紅。清單 5 顯示了如何設置 colorBuffer
。
清單 5. colorBuffer
設置指定金字塔的每一面的顏色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
colorBuffer = glCtx.createBuffer();
glCtx.bindBuffer(glCtx.ARRAY_BUFFER, colorBuffer);
var
faceColors = [
[0.0, 0.0, 1.0, 1.0],
// front (blue)
[1.0, 1.0, 0.0, 1.0],
// right (yellow)
[0.0, 1.0, 0.0, 1.0],
// back (green)
[1.0, 0.0, 0.0, 1.0],
// left (red)
];
var
vertexColors = [];
faceColors.forEach(
function
(color) {
[0,1,2].forEach(
function
() {
vertColors = vertColors.concat(color);
});
}); glCtx.bufferData(glCtx.ARRAY_BUFFER,
new
Float32Array(vertexColors), glCtx.STATIC_DRAW);
|
在清單 5 中,通過 createBuffer()
、bindBuffer()
和 bufferData()
調用來設置低級 colorBuffer
緩沖,這些函數與為 vertBuffer
所使用的那些函數完全一樣。
但 WebGL 沒有金字塔 “面” 這一概念。相反,它僅使用三角形和頂點。顏色數據必須與頂點相關聯。在 清單 5 中 ,名為 faceColors
的一個中間 JavaScript 數組初始化 vertColors
數組。 vertColors
是在加載低級 colorBuffer
時所使用的 JavaScript 數組。faceColors
數組包含 4 種顏色(藍、黃、綠和紅),分別對應於四個面。 這些顏色以紅、綠、藍、Alpha (RGBA) 格式指定。
vertColors
數組包含每一個三角形的每個頂點的顏色,其順序對應於它們在 vertBuffer
中出現的順序。因為四個三角形中每一個都有三個頂點,最終的 vertColors
數組包含總共 12 個顏色條目(其中每一個條目都是由 4 個 float
數字構成的數組)。使用一個嵌套的 forEach
循環將相同的顏色分配給代表金字塔一面的每個三角形的三個頂點。
了解 OpenGL 着色器(shaders)
可能會自然地浮現在腦海中的一個問題是,指定一個三角形的三個頂點的顏色如何能夠用該顏色渲染整個三角形。要回答這個問題,您必須了解 WebGL 渲染管道中兩個可編程組件的操作:頂點着色器 和片段(像素)着色器。可以將這些着色器編譯成能夠在 3D 加速硬件 GPU 上執行的代碼。一些現代的 3D 硬件可以並行執行數百個着色器操作,實現高性能的渲染。
頂點着色器處理每個指定的頂點。着色器接受的輸入包括顏色、位置、紋理以及與頂點相關聯的其他信息。然后,着色器計算和轉換數據,以確定在應渲染該頂點的視口上的 2D 位置,以及頂點的顏色和其他屬性。片段着色器確定在頂點之間組成三角形的每個像素的顏色和其他屬性。使用 OpenGL Shading Language (GLSL) 通過 WebGL 對頂點着色器和片段着色器進行編程。
GLSL
GLSL 是一種編程語言,其語法類似於 ANSI C(有一些 C++ 的概念)。它是特定於域的,支持從可用的對象形狀、位置、角度、顏色、照明、紋理,以及其他相關信息映射到將要渲染 3D 對象的每個 2D 畫布像素所顯示的實際顏色。
關於使用 GLSL 編寫自己的着色器程序的細節已超出本文的范圍。但您需要對 GLSL 代碼有最基本的認識才可以理解該示例程序的其余部分。我將指導您完成本例中使用的這兩個普通 GLSL 着色器的操作,以幫助您理解有關它們的所有代碼。
在本系列的下一篇文章中,您將學習如何使用更高級別的 3D 庫和框架來與 WebGL 配合。這些庫和框架透明地融入了 GLSL 代碼,所以您可能永遠都不需要自己編寫一個着色器。
在 WebGL 中處理着色器程序
着色器程序是相關着色器(在 WebGL 中通常是頂點着色器和片段着色器)的一個鏈接的二進制文件,隨時可供硬件 GPU 執行。每個着色器可以有幾乎微不足道的一行代碼,也可以有數百行高度復雜且多特性的並行代碼。
在通過 WebGL 執行着色器之前,必須將程序的 GLSL 源代碼編譯成二進制代碼,然后鏈接在一起。廠商提供的 3D 驅動程序嵌入了編譯器和鏈接器。 您必須通過 JavaScript 提交 GLSL 代碼,檢查編譯錯誤,然后鏈接准備作為參數的矩陣。WebGL 有一個 API 可用於所有這些操作。圖 7 展示了通過 WebGL 提交 GLSL 代碼的序列。
用於獲取、編譯和鏈接該示例的 GLSL 着色器的代碼如清單 6 所示。
清單 6. 在 WebGL 中編譯和鏈接 GLSL 着色器代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
var
vertShaderCode = document.getElementById(
"vertshader"
).textContent;
var
fragShaderCode = document.getElementById(
"fragshader"
).textContent;
var
fragShader = glCtx.createShader(glCtx.FRAGMENT_SHADER);
glCtx.shaderSource(fragShader, fragShaderCode);
glCtx.compileShader(fragShader);
if
(!glCtx.getShaderParameter(fragShader, glCtx.COMPILE_STATUS)) {
var
errmsg =
"fragment shader compile failed:"
+ glCtx.getShaderInfoLog(fragShader);
alert(errmsg);
throw
new
Error()
}
var
vertShader = glCtx.createShader(glCtx.VERTEX_SHADER);
glCtx.shaderSource(vertShader, vertShaderCode);
glCtx.compileShader(vertShader);
if
(!glCtx.getShaderParameter(vertShader, glCtx.COMPILE_STATUS)) {
var
errmsg =
"vertex shader compile failed :"
+ glCtx.getShaderInfoLog(vertShader);
alert(errmsg);
throw
new
Error(errmsg)
}
// link the compiled vertex and fragment shaders
shaderProg = glCtx.createProgram();
glCtx.attachShader(shaderProg, vertShader);
glCtx.attachShader(shaderProg, fragShader);
glCtx.linkProgram(shaderProg);
|
在清單 6 中,頂點着色器的源代碼被存儲為 vertexShaderCode
中的一個字符串,而片段着色器的源代碼存儲在 fragmentShaderCode
中。借助 document.getElementById().textContent
屬性,從 DOM 中的 <script>
元素提取這兩組源代碼。
通過 glCtx.createShader(glCtx.VERTEX_SHADER)
創建頂點着色器。通過 glCtx.createShader(glCtx.FRAGMENT_SHADER)
創建片段着色器。
使用 glCtx.shaderSource()
將源代碼加載到着色器中,然后通過 glCtx.compileShader()
編譯源代碼。
編譯后,調用 glCtx.getShaderParameter()
來確保編譯成功。可以通過 glCtx.getShaderInfoLog()
從編譯器日志獲取編譯錯誤。
頂點着色器和片段着色器均編譯成功后,它們被鏈接在一起,形成一個可執行的着色器程序。首先,調用 glCtx.createProgram()
來創建低級程序對象。然后,使用 glCtx.attachShader()
調用將編譯好的二進制文件與該程序關聯起來。 最后,通過調用 glCtx.linkProgram()
將二進制文件鏈接在一起。
頂點着色器和片段着色器的 GLSL 代碼
頂點着色器在先前在 JavaScript vertBuffer
和 colorBuffer
變量中編寫的輸入數據緩沖區上運行。清單 7 顯示了頂點着色器的 GLSL 源代碼。
清單 7. 頂點着色器的 GLSL 源代碼
1
2
3
4
5
6
7
8
9
|
attribute vec3 vertPos;
attribute vec4 vertColor;
uniform mat4 mvMatrix;
uniform mat4 pjMatrix;
varying lowp vec4 vColor;
void main(void) {
gl_Position = pjMatrix * mvMatrix * vec4(vertPos, 1.0);
vColor = vertColor;
}
|
在清單 7 中,attribute
關鍵字是存儲限定符,用於指定 WebGL 與每個頂點數據的頂點着色器之間的聯系。在本例中,vertPos
包含每次執行着色器時來自 vertBuffer
的一個頂點位置。vertColor
包含在您之前設置的 colorBuffer
中所指定的頂點顏色。
uniform
存儲限定符指定在 JavaScript 中設置並且在着色器代碼中用作只讀參數的值。JavaScript 代碼可以修改這些緩沖區中的值(特別是在動畫過程中),但着色器代碼永遠都不能修改它們。換言之:它們只能由 CPU 修改,永遠不能被渲染 GPU 修改。設置好它們之后,由頂點着色器代碼處理的每個頂點的 uniform
值都是相同的。在本例中,mvMatrix
包含通過 JavaScript 設置的模型視圖矩陣,pjMatrix
包含投影矩陣。(我將在 下一節 介紹模型視圖矩陣和投影矩陣。)
lowp
關鍵字是一個精度限定符。它指定,vColor
變量是一個低精度 float
數字,這在 WebGL 的顏色空間中已足以描述一個顏色。 gl_position
是着色器的轉換輸出值,由 3D 渲染管道在內部使用,作進一步的處理。
vcolor
變量有一個 varying
存儲限定符。 varying
表示該變量用作頂點着色器與片段着色器之間的接口。雖然每個頂點都有惟一的 vcolor
值,但在片段着色器中,要在頂點之間插入該值。(記得我講過,片段着色器是為頂點之間的那些像素執行的。)在本文的 GLSL 示例中,vColor
變量被設置為 colorBuffer
中為頂點着色器中的每個頂點所指定的顏色。清單 8 顯示了片段着色器的代碼。
清單 8. GLSL 片段着色器的源代碼
1
2
3
4
|
varying lowp vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
|
片段着色器很平凡。它接受來自頂點着色器的 vColor
內插值,並使用它作為輸出。因為金字塔的每一面的三個頂點設置了相同的 vColor
值,該片段所使用的內插顏色仍然是相同的顏色。
讓每個三角形至少有一個頂點的顏色不同,就可以看見插值的效果。嘗試用清單 9 中以粗體顯示的代碼修改 pyramid.html。
清單 9. 修改 pyramid.html 以顯示片段着色器插值
1
2
3
4
5
6
7
8
9
|
var
vertexColors = [];
faceColors.forEach(
function
(color) {
[0,1].forEach(
function
() {
vertColors = vertColors.concat(color);
});
vertColors = vertColors.concat(faceColors[0]);
});
glCtx.bufferData(glCtx.ARRAY_BUFFER,
new
Float32Array(vertexColors), glCtx.STATIC_DRAW);
|
此修改確保每個三角形都有一個頂點是藍色的。在瀏覽器中加載修改后的 pyramid.html。現在,可以看到金字塔的各個面都有顏色漸變(插值顏色),但藍色的一面除外(因為其頂點仍然全部是藍色的)。圖 8 顯示了運行於 OS X 之上的 Chrome 中有內插顏色漸變的金字塔面。
模型視圖和投影矩陣
為了控制在畫布上渲染的 3D 場景的轉換,您要指定兩個矩陣:模型視圖和投影矩陣。此前,您看到過頂點着色器使用它們來確定如何轉換每個 3D 頂點。
模型視圖矩陣結合了模型(在本例中是金字塔)和視圖(賴以查看場景的 “攝像頭”)的轉換。基本上,模型視圖矩陣控制在場景中的哪里放置對象,以及在哪里放置觀察用的攝像頭。此代碼設置本例中的模型視圖矩陣,將金字塔放在距離攝像頭三個單位的位置:
1
2
|
modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -3]);
|
根據 vertBuffer
設置您就會知道,金字塔的寬度是兩個單位,所以前面的代碼使金字塔可以 “填滿視口的框架”。
投影矩陣通過攝像頭的視圖控制 3D 場景到 2D 視口的轉換。本例中的投影矩陣設置代碼為:
1
2
|
projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 1, 100);
|
將攝像頭設置為 Math.PI / 4
(pi 弧度除以 4,或 180 度/4 = 45 度)的視場。攝像頭能看到的距離范圍是,最近 1 個單位,最遠 100 個單位,同時保持一個透視(無失真)圖。
在視口中渲染 3D 場景
所有的設置完成后,清單 10 中的代碼在視口中將 3D 場景渲染為 2D 視圖。此代碼包含在由 update()
包裝程序調用的 draw()
函數中。update()
包裝程序使得稍后在 pyramid.html 中轉換金字塔旋轉代碼更容易。
清單 10. 用於渲染場景的 draw()
函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function
draw(ctx) {
ctx.clearColor(1.0, 1.0, 1.0, 1.0);
ctx.enable(ctx.DEPTH_TEST);
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
ctx.useProgram(shaderProg);
ctx.bindBuffer(ctx.ARRAY_BUFFER, vertBuffer);
ctx.vertexAttribPointer(shaderVertexPositionAttribute, 3 ,
ctx.FLOAT,
false
, 0, 0);
ctx.bindBuffer(ctx.ARRAY_BUFFER, colorBuffer);
ctx.vertexAttribPointer(shaderVertexColorAttribute, 4 ,
ctx.FLOAT,
false
, 0, 0);
ctx.uniformMatrix4fv(shaderProjectionMatrixUniform,
false
, projectionMatrix);
mat4.rotate(modelViewMatrix, modelViewMatrix,
Math.PI/4, rotationAxis);
ctx.uniformMatrix4fv(shaderModelViewMatrixUniform,
false
,
modelViewMatrix);
ctx.drawArrays(ctx.TRIANGLES, 0, 12
/* num of vertex */
);
}
|
在 清單 10 中,ctx.clearColor()
調用將視口清空為白色,而ctx.enable(ctx.DEPTH_TEST)
確保啟用了深度緩沖 (z-buffer)。調用ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT)
可清除顏色和深度緩沖。
shaderProg
(由您之前編譯和鏈接的 vertShader
和 fragShader
組成)被加載,以供ctx.useProgram()
調用執行 GPU。接下來,先前在 JavaScript 中設置的低級數據緩沖區(vertBuffer
和 colorBuffer
)現在通過一系列 ctx.bindBuffer()
和ctx.vertexAttribPointer()
調用被綁定到 GLSL 着色器程序的屬性。(此工作流在概念上類似於 SQL 編程中的存儲過程,其中的參數在運行時綁定,並且准備好的語句可以重新執行。)ctx.uniformMatrix4fv()
調用設置模型視圖和投影矩陣,以供頂點着色器進行只讀訪問。
最后但同樣重要的是,ctx.drawArrays()
調用將四個為一組的三角形(一共有 12 個頂點)渲染到視口。
您可能注意到在用於頂點着色器訪問的模型視圖矩陣之前出現的 mat4.rotate(modelViewMatrix, modelViewMatrix, Math.PI/4, rotationAxis)
調用已建立。該調用在渲染之前將金字塔繞 y 軸旋轉 Math.PI/4
或 45 度。如果回頭看看 圖 6,原因顯而易見。請注意,該金字塔各面的設置將金字塔的一條邊緣放在 z 軸上。想象在 z 軸上的攝像頭指向屏幕;在不旋轉金字塔的情況下,它將看到的是藍色面的一半和黃色面的一半。將金字塔旋轉 45 度可以確保只看見藍色的一面。您可以注釋掉 mat4.rotate()
調用,並再次加載頁面,就能方便地查看這種效果。 圖 9 顯示了運行於 OS X 之上的 Firefox 中的結果(注意,在這個版本中已撤銷頂點顏色插值代碼更改)。
創建金字塔旋轉動畫
作為一個低級別的 API,WebGL 不包含對動畫的內在支持。
為了顯示旋轉動畫,本例依賴於瀏覽器通過 requestAnimationFrame()
函數 (rAF) 實現的動畫支持。requestAnimationFrame()
接受一個回調函數作為參數。瀏覽器在下一次屏幕更新之前回調該函數,通常頻率高達每秒 60 次。在回調中,必須再次調用requestAnimationFrame()
,以便在下一次屏幕更新之前調用它。
在 pyramid.html 中調用 requestAnimationFrame()
的代碼如清單 11 所示。
清單 11. 調用 requestAnimationFrame
進行屏幕更新
1
2
3
4
|
function
update(gl) {
requestAnimationFrame(
function
() { update(gl); });
draw(gl);
}
|
在清單 11 中,update()
函數被作為回調加以提供,update()
也必須為下一幀調用 requestAnimationFrame()
。 每次調用 update()
時,draw()
函數也會被調用。
清單 12 顯示了如何修改 triangles.html 中的 draw()
函數,以繞 y 軸增量旋轉金字塔。補充或修改的代碼以粗體顯示。
清單 12. 金字塔旋轉動畫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
var
onerev = 10000;
// ms
var
exTime = Date.now();
function
draw(ctx) {
ctx.clearColor(1.0, 1.0, 1.0, 1.0);
ctx.enable(ctx.DEPTH_TEST);
...
ctx.uniformMatrix4fv(shaderProjectionMatrixUniform,
false
, projectionMatrix);
var
now = Date.now();
var
elapsed = now - exTime;
exTime = now;
var
angle = Math.PI * 2 * elapsed/onerev;
mat4.rotate(modelViewMatrix, modelViewMatrix, angle, rotationAxis);
ctx.uniformMatrix4fv(shaderModelViewMatrixUniform,
false
, modelViewMatrix);
ctx.drawArrays(ctx.TRIANGLES, 0, 12
/* num of vertex */
);
}
|
在清單 12 中,每幀的旋轉角度的計算是用經過的時間 elapsed
除以旋轉完整的一圈所需的時間(在本例中,onerev
= 10 秒)。
您會在本系列的后續文章中看到 rAF 的更多用法。
突破 WebGL 的極限
金字塔示例探討了 WebGL 編程中的重要基本概念,這只是觸及皮毛而已。
如果想看到一些嘆為觀止的范例(將目前的 WebGL 發揮至極限的應用程序),將在一台比較現代化的機器上運行的最新 Firefox 瀏覽器指向 Epic Games 的 Unreal Engine 3 WebGL 演示 – Epic Citadel。圖 10 顯示了運行中的 Epic Citadel(在 Windows 上的 Firefox 中運行)。
Epic Citadel 是將著名的游戲引擎產品(最初用 C/C++ 編寫)編譯到 JavaScript 和 WebGL 的產物。(所使用的編譯器技術是 emscripten,輸出 JavaScript 子集稱為 asm.js。)您可以與這個 WebGL 渲染的中世紀小鎮互動,並在里面逛一下,其中有鵝卵石街道、城堡和動畫的瀑布。
另一個有趣的例子是來自 Google 的、由 Greggman and Human Engines 開發的 WebGL 水族館。圖 11 顯示了在 Windows 上的 Chrome 中運行的水族館。
使用這個水族館應用程序中,您可以選擇在球形全玻璃水族箱中游來游去的魚的數量。您也可以使用一個選項來嘗試一些渲染效果,比如反射、霧和光線。
結束語
WebGL 開放了原始 3D 硬件供 JavaScript API 訪問,但該 API 仍然是低級別的:
- WebGL 不了解 3D 場景內顯示的是什么,也不關心這一點。WebGL API 層不知道本文示例中那個簡單的 3D 金字塔對象。您必須千辛萬苦地跟蹤構成該對象的頂點數據。
- 為繪制 WebGL 場景而進行的每次調用僅渲染 2D 畫面。動畫本身不是 WebGL 的一部分,必須通過額外的代碼來實現。
- 對象與環境之間的運動和交互(比如光反射和對象的物理效果)必須用額外的代碼來實現。
- 事件(比如用戶輸入、對象選擇或對象的碰撞)的處理必須通過更高級別的代碼來實現。
寫 100 多行代碼只是為了旋轉一個金字塔,這令人望而生畏。應該顯而易見的是,創建任何復雜的 WebGL 應用程序都需要使用較高級別的庫或框架。值得慶幸的是,我們不缺 WebGL 庫/框架,其中許多都是在開源代碼社區中免費提供的。第 2 部分將探討 WebGL 庫的使用。
來源: IBM 作者:Sing Li, 顧問, Makawave