在 TypeScript 中使用 WebGL
本文將介紹如何在 TypeScript 中使用 WebGL。
我們將透過範例程式碼,逐步介紹 WebGL 的基本概念、最小設定、繪製、延伸與類別設計。
YouTube Video
在 TypeScript 中使用 WebGL
WebGL 是一個低階 API,可以讓你在瀏覽器中直接操作 GPU。
透過使用 TypeScript,您可以藉由型別安全、自動補全以及結構化編碼,大幅降低在 WebGL 開發中的「實作錯誤」。
TypeScript 在以下幾點特別有效:。
- WebGL 物件的型別變得明確。
- 可以減少操作著色器變數時的錯誤。
- 追蹤程式結構變得更容易。
WebGL 的最小設定
以下是在 WebGL 中進行繪製的最低限度需求:。
<canvas>元素WebGL上下文- 頂點著色器
- 片段著色器
首先,讓我們先建立可以初始化畫面的狀態。
取得 Canvas 與 WebGL 上下文
首先,取得 canvas 和 WebGLRenderingContext。
在這裡,我們以強調 null 安全性 的 TypeScript 方式來編碼。
1const canvas = document.getElementById('gl') as HTMLCanvasElement | null;
2
3if (!canvas) {
4 throw new Error('Canvas not found');
5}
6
7const gl = canvas.getContext('webgl');
8
9if (!gl) {
10 throw new Error('WebGL not supported');
11}- 此時,
gl就成為所有 WebGL 操作的進入點。
清除螢幕以確認 WebGL 已經正常運作
在繪製之前,檢查是否可以填滿背景色。
這是確認 WebGL 是否正常運作的初步檢查。
1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);- 到目前為止,畫布會被填滿為深灰色。
什麼是著色器?
在 WebGL 中,繪圖邏輯是用一種稱為 GLSL 的特殊語言描述的。在 GLSL 中,主要需要準備兩種類型的著色器:頂點著色器和片段著色器。
首先,我們先用這兩種著色器建立一個「最精簡可用的著色器」。
撰寫頂點著色器
頂點著色器 決定要繪製的點與圖形的位置。
下面是將一個點顯示在畫面中央的簡單程式碼。
1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5 gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;a_position是從 JavaScript 傳遞過來的座標。
撰寫片段著色器
片段著色器 決定螢幕上顯示的顏色。
這次的著色器會一直輸出紅色。
1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;- 在 WebGL 中,必須要指定
precision。
建立一個編譯著色器的功能
由於每次產生著色器的流程都相同,我們將它寫成一個函式。
1function createShader(
2 gl: WebGLRenderingContext,
3 type: number,
4 source: string
5): WebGLShader {
6 const shader = gl.createShader(type);
7 if (!shader) {
8 throw new Error('Failed to create shader');
9 }
10
11 gl.shaderSource(shader, source);
12 gl.compileShader(shader);
13
14 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
15 throw new Error(gl.getShaderInfoLog(shader) ?? 'Shader compile error');
16 }
17
18 return shader;
19}- 這個函式會根據指定的種類建立著色器、編譯來源程式,如果失敗就拋出錯誤,成功則回傳已編譯的著色器。
建立程式(著色器的組合)
將頂點著色器與片段著色器組合為一個程式。
這稱之為 WebGLProgram。
1const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
2const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
3
4const program = gl.createProgram();
5if (!program) {
6 throw new Error('Failed to create program');
7}
8
9gl.attachShader(program, vertexShader);
10gl.attachShader(program, fragmentShader);
11gl.linkProgram(program);
12
13if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
14 throw new Error(gl.getProgramInfoLog(program) ?? 'Program link error');
15}- 這段程式碼將頂點著色器和片段著色器連結為單一程式,並確認其是否可用於繪製。
準備頂點資料
這裡我們會準備用於繪製三角形的頂點資料。
1// Vertex positions for a triangle in clip space (x, y)
2// Coordinates range from -1.0 to 1.0
3const vertices = new Float32Array([
4 0.0, 0.5, // Top vertex
5 -0.5, -0.5, // Bottom-left vertex
6 0.5, -0.5, // Bottom-right vertex
7]);- WebGL 的座標系統範圍是從
-1.0到1.0。
建立緩衝區並傳送至 GPU
接下來,為了讓 GPU 能使用這些頂點資料,我們會利用緩衝區傳送資料。
1const buffer = gl.createBuffer();
2if (!buffer) {
3 throw new Error('Failed to create buffer');
4}
5
6gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
7gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);- 這段程式碼建立了一個緩衝區來存放頂點資料,並將其內容傳送到 GPU。
將屬性綁定到緩衝區
將著色器中的 a_position 與先前建立的緩衝區連接起來。
1const positionLocation = gl.getAttribLocation(program, 'a_position');
2
3gl.enableVertexAttribArray(positionLocation);
4gl.vertexAttribPointer(
5 positionLocation,
6 2,
7 gl.FLOAT,
8 false,
9 0,
10 0
11);繪製
最後,使用此程式對畫布發出繪製指令。
1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);- 當你執行此程式時,畫面會顯示一個 紅色三角形。
WebGL 的類別設計
WebGL 是命令式 API,若用傳統方式撰寫,程式碼會急速膨脹。透過類別設計,可以明確區分 初始化、繪製、資源管理。
在這裡,我們將運用 TypeScript 的優點,循序漸進地設計以強化 職責分離、可重用性、維護性。
設計方針(最小化但實用)
班級分為以下幾個角色。
GLApp負責全體的初始化與繪製。ShaderProgram負責管理著色器。TriangleMesh負責管理頂點資料。
首先,讓我們先建立總攬全局的類別。
WebGL 應用程式的進入類別
GLApp 管理畫布與 WebGL 上下文,並作為繪製的起始點。
1export class GLApp {
2 private gl: WebGLRenderingContext;
3 private shader!: ShaderProgram;
4 private mesh!: TriangleMesh;
5
6 constructor(private canvas: HTMLCanvasElement) {
7 const gl = canvas.getContext('webgl');
8 if (!gl) {
9 throw new Error('WebGL not supported');
10 }
11 this.gl = gl;
12 }
13
14 initialize() {
15 this.gl.clearColor(0.1, 0.1, 0.1, 1.0);
16
17 this.shader = new ShaderProgram(this.gl);
18 this.mesh = new TriangleMesh(this.gl);
19 }
20
21 render() {
22 this.gl.clear(this.gl.COLOR_BUFFER_BIT);
23
24 this.shader.use();
25 this.mesh.draw(this.shader);
26 }
27}- 這個類別負責管理 WebGL 的初始化與繪製過程,用著色器與網格對螢幕進行繪圖。
- 將
initialize和render分開,可以更容易地支援未來的重新初始化與動畫。
管理著色器的類別
接著,建立一個用於管理著色器的類別,將著色器的建立、連結和使用集中管理。這樣繪製邏輯就只需專注於「使用」著色器即可。
1export class ShaderProgram {
2 private program: WebGLProgram;
3
4 constructor(private gl: WebGLRenderingContext) {
5 const vertexSource = `
6 attribute vec2 a_position;
7 void main() {
8 gl_Position = vec4(a_position, 0.0, 1.0);
9 }
10 `;
11
12 const fragmentSource = `
13 precision mediump float;
14 void main() {
15 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
16 }
17 `;
18
19 const vs = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
20 const fs = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
21
22 const program = this.gl.createProgram();
23 if (!program) throw new Error('Program create failed');
24
25 this.gl.attachShader(program, vs);
26 this.gl.attachShader(program, fs);
27 this.gl.linkProgram(program);
28
29 if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
30 throw new Error(this.gl.getProgramInfoLog(program) ?? 'Link error');
31 }
32
33 this.program = program;
34 }
35
36 use() {
37 this.gl.useProgram(this.program);
38 }
39
40 getAttribLocation(name: string): number {
41 return this.gl.getAttribLocation(this.program, name);
42 }
43
44 private createShader(type: number, source: string): WebGLShader {
45 const shader = this.gl.createShader(type);
46 if (!shader) throw new Error('Shader create failed');
47
48 this.gl.shaderSource(shader, source);
49 this.gl.compileShader(shader);
50
51 if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
52 throw new Error(this.gl.getShaderInfoLog(shader) ?? 'Compile error');
53 }
54
55 return shader;
56 }
57}- 這個類別整合了頂點著色器與片段著色器的建立、連結與使用,讓渲染邏輯能安全地善用著色器。
- 透過對外提供
getAttribLocation,可從 mesh 類別安全地引用。
管理網格(頂點資料)的類別
我們也會建立一個類別來管理頂點資料。將 mesh 類別按要繪製的物件分開後,擴充性變得很高。
1export class TriangleMesh {
2 private buffer: WebGLBuffer;
3 private vertexCount = 3;
4
5 constructor(private gl: WebGLRenderingContext) {
6 const vertices = new Float32Array([
7 0.0, 0.5,
8 -0.5, -0.5,
9 0.5, -0.5,
10 ]);
11
12 const buffer = gl.createBuffer();
13 if (!buffer) throw new Error('Buffer create failed');
14
15 this.buffer = buffer;
16
17 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
18 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
19 }
20
21 draw(shader: ShaderProgram) {
22 const gl = this.gl;
23 const positionLoc = shader.getAttribLocation('a_position');
24
25 gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
26 gl.enableVertexAttribArray(positionLoc);
27 gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
28
29 gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
30 }
31}- 這個類別管理三角形的頂點資料,並與著色器綁定,負責實際的繪製。
- 所有繪製所需資料,都包含在
TriangleMesh之中。
進入點(使用範例)
最後,將這些類別組合起來啟動應用程式。
1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();- 採用這種結構,新增動畫或多重 mesh 也很容易。
用 TypeScript 編寫 WebGL 的實用技巧
在 WebGL 中使用 TypeScript 有以下優點:。
- 把 WebGL 的處理流程轉為類別後,可以依職責分工,更容易維護與擴充。
- 將繪製、著色器管理等責任分開,可以提升程式碼的可讀性。
- 運用 TypeScript 型別補全功能,可以減少呼叫 WebGL API 或指定參數時的錯誤。
總結
利用 TypeScript,即使是底層的 WebGL 處理也能以型別安全且結構化的方式穩定執行。掌握從最小設定到繪製的整體流程,並將初始化、繪製與資源管理等職責以類別分開後,可以大幅提升可讀性與維護性。按步驟實作,你可以把 WebGL 當成可用於實務的知識學習,而不只是黑盒子知識。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。