一次搞懂WebGL API-如何從零上手

蘇桓晨
14 min readJun 1, 2021

--

WebGL is a library that provides methods to create our 3D effect in our website. It’s beneficial to draw our 3D in GPU.

本篇介紹使用WebGL的API繪製畫面的流程以及實作,透過WebGL,我們能實現很多3D特效,而多數網頁3D套件(例如P5.js, Three.js)也是使用WebGL實現的。

本篇將先透過JS刻出一個Shader,讓躍躍欲試、手很癢的玩家可以先試一遍,接著介紹GLSL以及其應用,有效的推大家進Shader的火坑(誤)

WebGL有以下特色:

  • WebGL所做的事情就是柵格化(rasterization)。
  • WebGL使用GPU運算。
  • 我們要準備Shader提供WebGL運算,其中Shader是用GLSL寫的,但我們使用Javascript完成這件事情。
  • 我們要準備兩個Shader — Vertext Shader(儲存點的位置)、Fragment Shader(計算各個點並將顏色呈現在螢幕上),並且將這兩個Shader組成一個Program給WebGL使用

我們以畫出一個三角形為例子,將文章分成三部分討論:

  1. 怎麼運作的?
  2. 程式碼

一、怎麼運作的?

像前面說,我們要準備兩個Shader,Vertext Shader(儲存點的位置)、Fragment Shader(計算各個點並將顏色呈現在螢幕上),並構成一個Program。

基本上我們要做四件事情完成畫圖。

  1. 初始化 — 製作Program(包含兩個Shader)並準備三角形座標資料
  2. 設定存取位置資料的方式
  3. shader內容

我先用文字簡介如何透過大概的流程:

A. 初始化的工作

  1. 初始化我們的webGL
  2. 製作Shader並餵給WebGL(Shader是WebGL用來處理顯示卡運算的程式碼,語言是GLSL)
  3. 為了讓Javascript的資料能透過WebGL傳送到GPU,我們建立一個Buffer,讓WebGL知道這個buffer,也讓資料放進Buffer

B. 每次更新畫布時的作業

  1. 由於WebGL透過<canvas>這個HTML標籤實現,我們要告訴WebGL我們的Canvas大小
  2. 讓canvas為透明的畫布
  3. 告訴WebGL使用我們的Shader繪圖
  4. 雖然我們傳入資料給Shader,但我們要處理一下傳入的方式,好讓這些資料成功在Shader裡面宣告
  5. 執行WebGL畫圖的功能

二、Javascript實現WebGL的過程

初始化我們的webGL

我們以三角形當做主角

做出三角形總共有哪些功夫呢?

  1. 我們先有一個等等用來呼叫的js function main()
function main() {// 先指定一個標籤
var canvas = document.querySelector("#c");
// 設定為WebGL,這個gl我們會很常用到
var gl = canvas.getContext("webgl");
// 如果沒有抓到webGL就結束,否則繼續讀程式碼
if (!gl) {
return;
}
// ...
}

2. 接著,我們要包一些GLSL程式碼在HTML的<script/>裡面,再從Javascript取得這些字串。當然你也可以用其他方式儲存這些純文字啦。

function main() {
// ...
// 把vertex shader程式碼文字放到HTML裡面,再從HTML抓取文字
var vertexShaderSource = document.querySelector("#vertex-shader-2d").text;
// 把fragment shader程式碼文字放到HTML裡面,再從HTML抓取文字
var fragmentShaderSource = document.querySelector("#fragment-shader-2d").text;
// ...
}

3. 我們先放下main(),先看看Shader文字內容,我們用<script>包住shader程式碼。由於Shader不是用Javascript,而是GLSL寫的。所以我們在<script>設定屬性為notjs,也就告訴HTML,裡面不是Javascript,而是一般文字。

<HTML>
<body>
// 這是vertex-shader文字資料,HTML檔裡面,設定type="notjs"
<script id="vertex-shader-2d" type="notjs">
// 我們等等在js會做一個buffer,透過這個buffer把資料傳進來這個變數。
// 這是一個attribute變數,並且型別為vec4,vec4是一個存有四筆數字的變數
// 前面加個a_代表是attribute
// 這個存有四筆數字的變數,將會有x, y, z座標資料跟w資料,w表示這個變數為位置資料或向量資料
attribute vec4 a_position;

// all shaders have a main function
void main() {

// gl_Position is a special variable a vertex shader is responsible for setting.
// gl_Position is a vertex shader outputs. a_position is a data we can access and manipulate.
gl_Position = a_position;
}

</script>
// 這是fragment-shader文字資料
<script id="fragment-shader-2d" type="notjs">

// fragment shaders don't have a default precision so we need to pick one.
// mediump determines how much precision the GPU uses when calculating floats.
// Good rule to follow: set highp for vertex positions, mediump for texture coordinates, lowp for colors.
// GPU顯示精緻程度設定為中等
precision mediump float;

void main() {
// gl_FragColor是fragment shader的終點。fragment shader是計算每個向素要呈現什麼顏色,gl_FragColor就是用來指定像素顏色的變數。
gl_FragColor = vec4(1, 0, 0.5, 1); // return reddish-purple
}

</script>
</body>
</HTML>

4. 我們做一個program。program是能儲存shader,讓webGL能使用shader的物件。先建立兩個Shader,再去建立Program,再連結Shaders給Program

function main() {
// ...
// create GLSL shaders, upload the GLSL source, compile the shaders
// 帶入webGL, vertex/fragment類型、shader的文字內容
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
// Link the two shaders into a program
var program = createProgram(gl, vertexShader, fragmentShader);
// shader裡面都有一個變數叫做a_position,我們先把a_position的位置參照到javascript,等等拿來運用
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// ...
}

我們來再次放下main(),先來看看createShader()跟createProgram()包含什麼,再繼續往下說明:

CreateShader()

function createShader(gl, type, source) {
// 使用gl提供的createShader(type)來建立shader
var shader = gl.createShader(type);
// 將我們從HTML定義好的shader文字內容放入我們剛建立的shader
gl.shaderSource(shader, source);
gl.compileShader(shader);
// shader建立成功就回傳shader,不成功就刪除剛剛做的shader
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
} else {
gl.deleteShader(shader);
}
}

CreateProgram()

function createProgram(gl, vertexShader, fragmentShader) {
// 使用gl提供的createProgram()來建立shader
var program = gl.createProgram();
// 把createShader()之後建立的shader物件放進program裡面
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 讓webGL連結我們這個program
gl.linkProgram(program);
// program建立成功就回傳program,不成功就刪除剛剛做的program
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
} else {
gl.deleteProgram(program);
}
}

5. 回到main(),由於我們要做一個形狀,所以我們要給定webGL形狀的錨點資料,這形狀資料勢必從javascript來。那js是怎麼把資料傳給shader的呢?就是用buffer。這步驟就是定義傳送資料的過程。

function main() {
// ...
// 用gl建立一個空空的buffer
var positionBuffer = gl.createBuffer();
// 把我們這個buffer參照到webGL裡面的gl.ARRAY_BUFFER。想像gl裡面有個位置叫做ARRAY_BUFFER,我們裝了一個buffer到gl.ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 這空空的buffer目前還沒有資料。我們先用javascript建立形狀位置資料
var positions = [
0, 0,
0, 0.5,
0.7, 0,
];
// 將形狀位置資料放入空空的buffer。三個參數分別為:
// 1. 將資料放在哪裡?放到gl.ARRAY_BUFFER
// 2. 我們放入什麼資料?放了float資料。由於shader不吃js原生的陣列,所以我們轉換陣列為浮點數陣列。由於浮點數有很多種,所們定義為float32,也就是GLSL在用的那種。
// 3. 我們要如何最佳化這個資料?用gl.STATIC_DRAW來標示這個資料我們不太會變。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// ...
}

6. 由於在webGL裡面的位置資訊,長寬分別是-1~1之間構成的,並不是我們常用的像素構成,我們將-1~1對照到我們Canvas的長寬

function main() {
// ...
// Canvas預設長寬400X300,但往往會被延伸成超過400x300,那這樣像素會被拉大有鋸齒狀。
// 為了防止像素被拉大有鋸齒狀,我們使用resizeCanvasToDisplaySize來讓webGL拿真實的長寬來算圖
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// 傳給webGL我們畫布大小是0到canvas寬、0到canvas高
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// ...
}

*順帶一提:沒有使用resizeCanvasToDisplaySize的canvas會有鋸齒狀,原因寫在code的註解:

7. 讓canvas為透明的畫布

function main() {
// ...
// Clear the canvas. making the RGBA 0,0,0,0 so it's transparent
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// ...
}

8. 告訴WebGL去使用我們製做的Programm,並且使用我們傳遞給他的array

function main() {
// ...
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// 我們在第三點有建立positionAttributeLocation,這個positionAttributeLocation是我們shader裡面a_position的位置,也就是我們js形狀位置資料要傳到shader,shader接收資料的變數。
// 我們讓webGL去允許使用這個attribute
gl.enableVertexAttribArray(positionAttributeLocation);

// ...
}

9. 雖然我們傳入資料給Shader,但我們要處理一下傳入的方式,好讓這些資料成功在Shader裡面宣告。我們需要定義「如何抽取資料」這件事。

function main() {
// ...
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 每個陣列都傳入兩個資料(一次拿兩個資料給a_pisition,代表xy位置)
var type = gl.FLOAT; // 資料將以浮點數傳給shader
var normalize = false; // don't normalize the data. 用normalize 會讓我們浮點數變成0~1的數值
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
// 使用vertexAttribPointer來定義傳遞資料的方式。包含傳入的終點、每次傳入的資料數量、型別等等。
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset)
// ...
}

10. 圖形就大功告成了

順帶一提,為什麼三角形在正中間而不是左上角?因為在vertex shader裡面的gl_Position,也就是vertex shader裡面定義每個錨點位置的終點,它接收的畫布範圍是-1~1。左邊為-1,右邊為1。上面為1,下面為-1。

但我們傳入的資料是0~1的範圍。所以我們傳入的0對應給gl_Position就是畫布中間。

參考資料:https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html

--

--