視覺化學習:WebGL的基礎使用

beckyye發表於2023-12-07

引言

繼續複習視覺化的學習。WebGL和其他Web端的圖形系統存在很大的不同,是OpenGL ES規範在瀏覽器的實現,它最大的不同就在於它更接近底層,可以由開發者直接操作GPU來實現繪圖,效能很好,可以充分利用GPU平行計算的能力,並且WebGL還支援3D物體的渲染;WebGL最大的缺點應該就在於它的使用比較複雜,不易掌握,不同於一般的Web API使用,想要掌握好WebGL,還需要了解與WebGL相關的GLSL語言。

著色器

想要在WebGL中繪圖,離不開著色器的使用,著色器是什麼呢,我覺得可以簡單理解為,著色器定義瞭如何去處理畫布上的座標點。在WebGL中有兩類著色器,一類是頂點著色器,一類是片元著色器(或者也可以說片段著色器)。

頂點著色器可以認為是,宣告瞭需要處理的座標點;片元著色器就是定義了將這個待處理的座標點渲染為什麼顏色。當然這只是我目前學下來的一個理解,一種感覺,不一定準確。

這兩類著色器都是由GPU呼叫的。

GPU會根據著色器程式,以及傳入的資料,對批次的座標點並行進行處理,最後渲染為一個圖形。WebGL最大的特點就在於對批次的座標點應用同一個著色器程式。所以通常來說,要繪製的座標點和繪製的顏色,尤其是座標點,一般不會直接寫在GLSL程式碼中,而是由GPU從快取中讀取相關資訊;所以在GPU讀取之前,我們需要透過程式碼將資料寫入快取。

基礎使用

接下來我們透過繪製一個簡單的三角形來體會WebGL的使用,來瞭解如何使用WebGL來繪圖。

首先我們在頁面上準備一個canvas畫布。

<canvas width="512" height="512"></canvas>
canvas {
  width: 512px;
  height: 512px;
  border: 1px solid #eee;
}

接下來我們就開始編寫JavaScript程式碼。

1. 獲取WebGL上下文

首先是最基本的,獲取WebGL上下文。

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

2. 編寫著色器程式

然後是最關鍵的,編寫著色器程式。

  • 編寫著色器程式的第一步,是編寫GLSL程式碼,來定義兩個著色器。

定義著色器有兩種方式,可以直接透過一段字串定義,也可以透過使用自定義type屬性的script元素把GLSL程式碼包含在網頁中;以下我們透過字串來定義。

const vertex = `
	attribute vec2 position;
	
	void main() {
		gl_Position = vec4(position, 0.0, 1.0);
	}
`;

vertex變數定義的是頂點著色器的GLSL程式碼。

attribute表明變數是專門用於接收頂點資料的;

vec2是變數型別,表示變數是一個包含兩個元素的陣列,兩個元素分別代表x和y座標;

position不用說,就是變數的標識。

在執行著色器程式時,會對每一個待處理的頂點執行main函式,將position透過vec4建立一個包含4個元素的陣列,把2D座標轉換為3D座標,並賦值給gl_Position。gl_Position是內建變數,是四維向量,為什麼3D座標要用四維向量表示呢?這是因為頂點有可能會需要做一些座標變換的操作。

gl_Position就是最終要渲染的點的座標。

const fragment = `
	precision mediump float;
	
	void main() {
		gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
	}
`;

fragment變數定義的是片元著色器的GLSL程式碼。

precision mediump float;這是對精度的描述,不新增會報錯;

gl_FragColor也是內建變數,是四維向量,代表待渲染的點的顏色資訊。vec4中的四個元素分別對應RGB三個顏色通道的色階和alpha通道,與CSS中的RGBA不同的點在於,這裡的RGB的取值在0.0到1.0之間。

  • 接下來我們就來建立著色器程式

先是分別定義兩個GLSL程式碼對應的shader物件,並把GLSL程式碼傳遞給shader物件,然後編譯這兩個shader物件,這兩個著色器。

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);

然後是建立Program物件並關聯這兩個shader物件,將兩個shader物件連結到著色器程式。

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

最後使WebGL上下文使用這個program程式。

gl.useProgram(program);

至此,GPU就可以透過這個著色器程式來使用兩個著色器。

3. 將頂點資料存入緩衝區

現在就可以使用著色器程式來繪製我們的三角形了。

對於三角形,我們知道用三個頂點就可以確定一個三角形。

在定義三角形的頂點之前,我們需要先了解WebGL中的座標系。和Canvas2D不同,在預設情況下,WebGL的座標系原點(0, 0)在畫布的中心,並且畫布的左下角是(-1, -1),右上角是(1, 1),也就是說x和y座標的取值範圍都是-1到1。

接著我們還需要了解,WebGL中頂點資訊是儲存在TypedArray中的,TypedArray翻譯過來可以叫做定型陣列或者型別陣列,我們知道在JavaScript中,普通陣列中的元素並沒有限制,我們可以透過push方法,插入任意型別的值,但是在定型陣列中就不能這麼做了,定型陣列可以簡單認為就是陣列中所有元素的型別是被指定了的。

JavaScript透過定型陣列向GPU傳輸資料,某種程度上也是防止GPU接收到意外型別的資料吧,並且這樣GPU也不用花費額外的時間去進行資料型別的判斷和轉換,效能效率更高。

好了,瞭解完這些之後,我們就來定義三角形的三個頂點

const points = new Float32Array([
  -1, -1,
  0, 1,
  1, -1,
]);

points變數引用了一個Float32Array型別的陣列,Float32Array就是定型陣列的其中一種,代表陣列中的元素都是32位的浮點數。

可以看到,我們在這個陣列中放了6個元素,每兩個元素代表一個點的x和y座標。

然後我們就將這些點寫入WebGL的緩衝區。我的理解是,這三個點其實並不是獨立的點,WebGL會將這三個點在後續用於劃定要處理的範圍;後續還會透過繪圖模式來進一步確定要處理的點。

定義好頂點後,我們就將這些頂點資料寫入緩衝區提供給WebGL使用

首先在使用前我們需要先建立buffer物件,也就是緩衝區物件。

const bufferId = gl.createBuffer();

然後將這個緩衝區指定為WebGL的操作物件,gl.ARRAY_BUFFER代表這個緩衝區儲存的是頂點資料。

gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);

最後將資料寫入緩衝區,以供GPU讀取。

gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

gl.STATIC_DRAW代表資料載入一次,可以在多次繪製中使用。

4. 將緩衝區資料讀取到GPU

完成了資料的寫入後,GPU就可以從緩衝區讀取資料,所以我們需要告訴GPU去哪裡讀取資料

因為上面緩衝區中儲存的是頂點資料,所以這些資料是在頂點著色器中使用;又因為在頂點著色器的GLSL程式碼中,我們指定了變數position用於接收頂點資料,所以我們需要先獲取position變數的地址

const vPosition = gl.getAttribLocation(program, 'position');

接著建立一個指標,指向剛剛繫結到gl.ARRAY_BUFFER的緩衝區,並儲存到vPosition中。

gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);

2表示頂點資料是2個為一組讀取,這是因為在頂點著色器的GLSL程式碼中,我們是透過vec2型別接收的;

gl.FLOAT代表讀取的資料型別;

最後2個0,代表的是從緩衝區中讀取資料時的偏移量,這個例子中資料都是連續寫入的,所以不用管,都設定為0就可以。

最後是啟用這個變數,這樣在頂點著色器中就能透過變數position讀取到points定型陣列中對應的值了。

gl.enableVertexAttribArray(vPosition);

5. 執行著色器程式完成繪製

著色器程式和資料都準備好了之後,GPU就可以呼叫著色器程式並完成圖形的繪製了。

首先,我們先呼叫gl.clear將當前畫布的內容清除,就類似於Canvas2D中的clearRect。

gl.clear(gl.COLOR_BUFFER_BIT);

我們可以直接使用gl.COLOR_BUFFER_BIT,也可以自己指定顏色。

然後我們就可以開始繪製了。

gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);

drawArrays的第一個引數是繪圖模式,代表繪圖時所使用的圖元,圖元我理解就是圖形的單元,就一個圖形是由若干個單元組成的,比如這個程式碼中的gl.TRIANGLES就代表這個圖形由三角形組成,它就是一個三角形,那麼WebGL所要渲染的點就是整個三角形區域內的點。

如果我們設定為gl.LINE_LOOP,就代表這個圖形由封閉的線段組成,會形成一個封閉圖形,它預設最後一個點和第一個點連線在一起,那麼WebGL所要渲染的點就是頂點所形成的封閉線段上的點。

如果我們設定為gl.LINES,就代表這個圖形由一個個線段組成,那麼WebGL所要渲染的點就是每一條線段上的點。

drawArrays的第二個引數是緩衝區的起始偏移量,這裡我們從0開始讀取。

最後一個引數是頂點的個數,由於points中每兩個元素一組作為一個頂點座標,所以陣列長度除以2就是頂點的個數。

至此,就完成了三角形的繪製。

可以看到,這個三角形是紅色的,這是因為我們在片元著色器中的定義,就是gl_FragColor,我們寫死了vec4(1.0, 0.0, 0.0, 1.0);,這就相當於我們在CSS中寫的RGBA(255, 0, 0, 1);在WebGL中,RGB色階通道的取值也和透明度一樣,在0.0到1.0之間。我們可以透過修改gl_FragColor來修改三角形的顏色。

由於我們在程式碼中把gl_FragColor寫死了,所以在對所有點並行執行片元著色器時,渲染了同樣的顏色,所以三角形整體是紅色的。

相關文章