webgl 系列 —— 初識 WebGL

2023-02-27 12:02:14

初識 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:不管繪製二維還是三維都是這三步:

  1. 獲取 canvas
  2. 請求繪圖上下文
  3. 呼叫繪圖上下文中的繪圖函數

第一個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步:

  1. 獲取 canvas
  2. 請求繪圖上下文
  3. 呼叫繪圖上下文中的繪圖函數

在 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_Positiongl_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;
}