初識 WebGL
什麼是 WebGL
webgl 在支援 canvas 的瀏覽器中進行 2d 或 3d 渲染。
webgl 程式除了有 Html、javascript,還需要加入著色器語言(GLSL ES)。
WebGL 使得網頁在支援 HTML <canvas>
標籤的瀏覽器中,不需要使用任何外掛,便可以使用基於 OpenGL ES 2.0 的 API 在 canvas 中進行 3D 渲染 —— MDN WebGL 教程
透過 caniuse 得知 webgl
(98.15%) 和 webgl 2.0
(94.12%) 的支援情況。請看下圖:
Tip:個人計算機上,繪製三維最廣泛使用的技術有 Direct3D 和 OpenGL,前者是微軟的,後者是開源免費的。OpenGL 有個特殊版本 OpenGL ES 專門用於嵌入式計算機、手機,而 WebGL 就是從 OpenGL ES 派生出來的。下圖是 OpenGL、OpenGL ES、WebGL 三者之間的關係。其中 webgl 2.0 基於 OpenGL ES 3.0 未畫出來:
canvas
Canvas_API 提供了一個透過JavaScript 和 HTML的 <canvas>
元素來繪製圖形的方式。它可以用於動畫、遊戲畫面、資料視覺化、圖片編輯以及實時影片處理等方面。
Canvas API 主要聚焦於 2D 圖形。而同樣使用<canvas>
元素的 WebGL API 則用於繪製硬體加速的 2D 和 3D 圖形。
示例:
// canvas.html
<body>
<canvas id="canvas" width="300" height="300">
抱歉,您的瀏覽器不支援 canvas 元素
(這些內容將會在不支援<canvas>元素的瀏覽器或是禁用了 JavaScript 的瀏覽器內渲染並展現)
</canvas>
<script>
var canvas = document.getElementById('canvas');
// getContext - 方法返回canvas 的上下文,如果上下文沒有定義則返回 null
var ctx = canvas.getContext('2d');
// 設定填充顏色
ctx.fillStyle = 'green';
// 繪製矩形
ctx.fillRect(10, 10, 100, 100);
</script>
</body>
效果如下:
Tip:不管繪製二維還是三維都是這三步:
- 獲取 canvas
- 請求繪圖上下文
- 呼叫繪圖上下文中的繪圖函式
第一個webgl示例
需求
:清空繪圖區。也就是使用背景色清空 canvas 的繪圖區
實現如下:
// webgl01.html
<body>
<canvas id="canvas" width="300" height="300">
抱歉,您的瀏覽器不支援 canvas 元素
(這些內容將會在不支援<canvas>元素的瀏覽器或是禁用了 JavaScript 的瀏覽器內渲染並展現)
</canvas>
<script>
var canvas = document.getElementById('canvas');
const gl = canvas.getContext("webgl");
// 使用完全不透明的藍色清除所有影像
gl.clearColor(0.0, 0.0, 1.0, 1.0);
// 用上面指定的顏色清除緩衝區
gl.clear(gl.COLOR_BUFFER_BIT);
</script>
</body>
效果如下:
仍舊是3步:
- 獲取 canvas
- 請求繪圖上下文
- 呼叫繪圖上下文中的繪圖函式
在 canvas 繪製矩形之前需要指定顏色(ctx.fillStyle = 'green';
),在 webgl 中類似,清空繪圖區之前也得指定背景色,一旦指定背景色,背景色就會在 webgl 系統中存留,將來還需要使用同樣的顏色清空繪圖區,,就不需要再次指定背景色。
clearColor 和 clear語法如下:
// WebGLRenderingContext.clearColor() 方法用於設定清空顏色緩衝時的顏色值。指定呼叫 clear() 方法時使用的顏色值
void gl.clearColor(red, green, blue, alpha)
// WebGLRenderingContext.clear() 方法使用預設值來清空緩衝。
void gl.clear(mask);
mask
gl.COLOR_BUFFER_BIT // 顏色緩衝區
gl.DEPTH_BUFFER_BIT // 深度緩衝區 - 三維世界中使用
gl.STENCIL_BUFFER_BIT // 模板緩衝區 - 很少使用
如果沒有指定背景色,預設值如下:
- 顏色緩衝區 -
(0.0, 0.0, 0.0, 0.0)
- 深度緩衝區 - 1.0
繪製一個點
需求
需求:在 canvas 中心畫一個 10px 紅色的點。
效果如下:
思路
用 canvas 繪製一個矩形很簡單,先指定顏色,在繪製矩形。就像這樣:
// canvas繪製矩形
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);
但 webgl 需要使用著色器,著色器提供了靈活且強大的繪製二維或三維的方法,也更加複雜。
我們先看程式碼,有一個具體的感受後,在分析其中細節。
程式碼
共3個檔案。重點關注 point01.js
即可。
- 新建入口檔案 point01.html:
<!-- point01.html -->
<script src="./cuon-utils.js"></script>
<script src="./point01.js"></script>
<body onload="main()">
<canvas id="webgl" width="300" height="300"> 抱歉,您的瀏覽器不支援 canvas 元素</canvas>
</body>
Tip:以上這段程式碼在 chrome 中執行透過,瀏覽器會自動補全格式,例如把 script 標籤放入 head 中。
- 新建 point01.js:
// point01.js
// 頂點著色器
const VSHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`
// 片元著色器
const FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
function main() {
const canvas = document.getElementById('webgl');
const gl = canvas.getContext("webgl");
// 初始化著色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
}
文件載入後執行 main() 方法,相對第一個 webgl 示例,這裡增加了初始化著色器
。
Tip:現在只需要把初始化著色器的方法(initShaders() - 請看本篇 cuon-utils.js
章節)作為一個庫中的輔助方法看待,後續文章將介紹其中原理。
- 新建 cuon-utils.js(內容見本篇
擴充套件
),主要提供初始化著色器的方法
程式碼解析
總體流程
文件載入後執行 main()
方法,有如下5個階段:
- 獲取canvas
- 取得 webgl 上下文
初始化著色器
- 清除繪圖區
- 呼叫
drawArrays
繪圖
下面我們主要講一下第三步和最後一步。
齊次座標
齊次座標就是將一個原本是 n 維的向量用一個 n+1 維向量來表示。齊次座標能提高處理三維資料的有效率,所以在三維繫統中大量使用。齊次座標(x, y, z, w)
等價於三維座標 (x/w, y/w, z/w)
頂點著色器
頂點著色器
(Vertex Shader) - 用來描述頂點特徵的程式。例如這裡的位置和大小。頂點指二維(x, y)或三維(x, y, z)空間中的一個點,例如端點或交點。
內建變數:
gl_Position
- 用於描述頂點位置,必傳,型別是vec4
(即4個float)gl_PointSize
- 使用者描述頂點的尺寸(畫素),如果不傳,預設 1.0,型別是float
關於位置,我們只有 (x, y, z) 三個變數,但 vec4 是 4 個,所以需要使用內建函式 vec4() 幫忙建立 vec4 型別的變數。
程式碼中 vec4(0.0, 0.0, 0.0, 1.0)
,這裡第四個分量是 1.0,使用的是齊次座標。
Tip:先記著 (0.0, 0.0, 0.0) 就是繪圖區的中心,本篇 座標系統
中會詳細講解。
片元著色器
片元著色器
(Fragment Shader) - 進行逐片元處理過程如光照的程式。片元
是 webgl 的一個術語,暫時可以將其理解成畫素
。
內建變數:
gl_FragColor
- 指定片元顏色(RGBA格式),型別是 vec4
初始化著色器
webgl 需要兩種著色器:頂點著色器
(Vertex Shader)、片元著色器
(Fragment Shader)。
在三維場景中,僅僅用線條和顏色把圖畫出來不夠,還需要考慮光照上去或者觀察者的視角發生變化,對場景有什麼影響。著色器可以靈活的完成這些工作。
初始化著色器之前,頂點著色器和片元著色器都是空白,把著色器程式作為字串形式傳給 initShaders(initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)
)之後,webgl 系統中的著色器就建立好。
下圖是執行 initShaders() 前後的情形:
Tip: 先執行頂點著色器,然後把 gl_Position
和 gl_PointSize
傳給片元著色器。實際上片元著色器接收到的是經過柵格化處理後的片元(柵格化
在畫三角形時在講解)。
繪圖
建立著色器之後,首先清空繪圖區域,然後使用 gl.drawArrays() 進行繪製。
gl.drawArrays(mode, first, count) 執行頂點著色器,按照 mode 指定的引數繪製圖形。first 指定從哪個點開始繪製,count 指繪製需要幾個點。
Tip:mode 型別有:
- gl.POINTS: 繪製一系列點。
- gl.LINE_STRIP: 繪製一個線條。即,繪製一系列線段,上一點連線下一點。
- gl.LINE_LOOP: 繪製一個線圈。即,繪製一系列線段,上一點連線下一點,並且最後一點與第一個點相連。
- gl.LINES: 繪製一系列單獨線段。每兩個點作為端點,線段之間不連線。
- gl.TRIANGLE_STRIP:繪製一個三角帶。
- gl.TRIANGLE_FAN:繪製一個三角扇。
- gl.TRIANGLES: 繪製一系列三角形。每三個點作為頂點。
例如我們這裡是:gl.drawArrays(gl.POINTS, 0, 1)
,繪製圖形(點),需要一個點,從第一個點開始繪製。後續畫多個點時會對 first 和 count 有更清晰的理解。
程式碼註釋
// point01.js
// 頂點著色器
const VSHADER_SOURCE = `
// 和 C 語言一樣,必須包含一個 main() 函式,void 表示沒有返回值
// 注:不能給 main() 指定引數
void main() {
// 頂點著色器內建變數: gl_Position 頂點位置、gl_PointSize 頂點尺寸
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`
// 片元著色器
const FSHADER_SOURCE = `
void main() {
// 片元著色器內建變數: gl_FragColor 指定片元顏色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
function main() {
const canvas = document.getElementById('webgl');
const gl = canvas.getContext("webgl");
// 初始化著色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化著色器失敗');
return;
}
// 清空繪圖區
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪製圖形(點),需要一個點,從第一個點開始繪製
gl.drawArrays(gl.POINTS, 0, 1);
}
擴充套件
座標系統
webgl 的座標系(x, y, z)和 canvas 的座標系(x, y)不同。
canvas 的原點(0, 0)在左上角。webgl 處理的是三維,所以使用三維座標系統(笛卡爾座標系),可用(x, y, z) 表示。也可認為是右手座標系。請看下圖:
webgl 座標和 canvas 座標對應關係如下(可對照上面中間那張圖):
cuon-utils.js
// cuon-utils.js (c) 2012 kanda and matsuda
/**
* Create a program object and make current
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return true, if the program object was created and successfully made current
*/
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
/**
* Create the linked program object
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return created program object, or null if the creation has failed
*/
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}
// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// Link the program object
gl.linkProgram(program);
// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
/**
* Create a shader object
* @param gl GL context
* @param type the type of the shader object to be created
* @param source shader program (string)
* @return created shader object, or null if the creation has failed.
*/
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
// Set the shader program
gl.shaderSource(shader, source);
// Compile the shader
gl.compileShader(shader);
// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}
return shader;
}