WebGL trong TypeScript

WebGL trong TypeScript

Bài viết này giải thích về WebGL trong TypeScript.

Chúng tôi sẽ giới thiệu khái niệm về WebGL, cấu hình tối thiểu, kết xuất, mở rộng, và thiết kế lớp theo từng bước với mã ví dụ.

YouTube Video

WebGL trong TypeScript

WebGL là một API cấp thấp cho phép bạn thao tác trực tiếp với GPU trong trình duyệt.

Bằng cách sử dụng TypeScript, bạn có thể giảm đáng kể 'lỗi thực thi' trong WebGL thông qua an toàn kiểu dữ liệu, tự động hoàn thành mã và cấu trúc hóa lập trình.

TypeScript đặc biệt hiệu quả ở các điểm sau:.

  • Kiểu dữ liệu của các đối tượng WebGL trở nên rõ ràng.
  • Có thể giảm thiểu các lỗi khi xử lý biến shader.
  • Việc theo dõi cấu trúc trở nên dễ dàng hơn.

Cấu hình tối thiểu của WebGL

Dưới đây là các yêu cầu tối thiểu để kết xuất với WebGL:.

  • Phần tử <canvas>
  • Ngữ cảnh WebGL
  • Vertex shader (shader đỉnh)
  • Fragment shader (shader đoạn)

Đầu tiên, hãy tạo trạng thái cho phép khởi tạo màn hình.

Lấy Canvas và Ngữ cảnh WebGL

Trước tiên, lấy canvasWebGLRenderingContext.

Ở đây, chúng tôi sử dụng mã TypeScript với trọng tâm là an toàn với giá trị null.

 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}
  • Tại thời điểm này, gl trở thành điểm truy cập cho tất cả các thao tác WebGL.

Xóa màn hình để xác nhận rằng WebGL đang hoạt động

Trước khi kết xuất, kiểm tra xem có thể tô màu nền không.

Đây là kiểm tra ban đầu để xác nhận rằng WebGL hoạt động đúng.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Đến bước này, canvas sẽ được tô bằng màu xám đậm.

Shader là gì?

Trong WebGL, quá trình vẽ được mô tả bằng một ngôn ngữ đặc biệt gọi là GLSL. Trong GLSL, bạn chủ yếu chuẩn bị hai loại shader: shader đỉnhshader mảnh.

Đầu tiên, chúng ta sẽ tạo một 'shader hoạt động tối thiểu' sử dụng hai loại shader này.

Viết vertex shader

Vertex shader quyết định vị trí của các điểm hoặc hình sẽ được vẽ.

Sau đây là mã đơn giản đặt một điểm tại trung tâm màn hình.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position là tọa độ được truyền từ phía JavaScript.

Viết fragment shader

Fragment shader quyết định màu sắc xuất hiện trên màn hình.

Lần này, nó luôn xuất ra màu đỏ.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precision phải luôn được khai báo trong WebGL.

Tạo một hàm để biên dịch shader

Vì quá trình tạo shader luôn giống nhau, chúng tôi chuyển nó thành một hàm.

 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}
  • Hàm này tạo một shader theo loại được chỉ định, biên dịch mã nguồn, thông báo lỗi nếu thất bại, và trả về shader đã biên dịch nếu thành công.

Tạo một chương trình (tập hợp các shader)

Kết hợp vertex shader và fragment shader thành một.

Điều này được gọi là 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}
  • Đoạn mã này liên kết vertex shader và fragment shader thành một chương trình duy nhất và kiểm tra xem chúng có thể được sử dụng để kết xuất hay không.

Chuẩn bị dữ liệu đỉnh

Ở đây, chúng ta sẽ chuẩn bị dữ liệu đỉnh để vẽ một tam giác.

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]);
  • Hệ tọa độ trong WebGL nằm trong khoảng từ -1.0 đến 1.0.

Tạo buffer và truyền nó lên GPU

Tiếp theo, để mảng dữ liệu đỉnh có thể được sử dụng bởi GPU, chúng ta chuyển dữ liệu thông qua một bộ đệm.

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);
  • Đoạn mã này tạo một buffer để lưu trữ dữ liệu đỉnh và truyền nội dung của nó lên GPU.

Liên kết thuộc tính với buffer

Kết nối a_position trong shader với buffer đã được tạo trước đó.

 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);

Kết xuất

Cuối cùng, sử dụng chương trình để gửi lệnh kết xuất.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • Khi bạn chạy chương trình này, một tam giác màu đỏ sẽ được hiển thị.

Thiết kế lớp với WebGL

WebGL là một API dạng mệnh lệnh, do đó mã của bạn có thể nhanh chóng trở nên rối rắm nếu viết theo cách thông thường. Thông qua thiết kế lớp, bạn có thể tách biệt rõ ràng các phần khởi tạo, kết xuất và quản lý tài nguyên.

Ở đây, chúng ta sẽ tận dụng những lợi ích của TypeScript và dần cải tiến thiết kế để tập trung vào phân tách chức năng, tái sử dụng và dễ bảo trì.

Chính sách thiết kế (Tối giản nhưng thực tế)

Lớp học được chia thành các vai trò sau.

  • GLApp kiểm soát toàn bộ quá trình khởi tạo và kết xuất.
  • ShaderProgram quản lý các shader.
  • TriangleMesh quản lý dữ liệu đỉnh.

Đầu tiên, hãy tạo lớp kiểm soát mọi thứ.

Lớp khởi đầu cho một ứng dụng WebGL

GLApp quản lý canvas và ngữ cảnh WebGL, đồng thời là điểm bắt đầu cho quá trình kết xuất.

 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}
  • Lớp này quản lý quá trình khởi tạo và kết xuất của WebGL, chịu trách nhiệm vẽ lên màn hình bằng shader và mesh.
  • Bằng cách tách biệt initializerender, việc hỗ trợ khởi tạo lại và hoạt ảnh trong tương lai trở nên dễ dàng hơn.

Lớp quản lý shader

Tiếp theo, hãy tạo một lớp để quản lý các shader, tổng hợp việc tạo, liên kết và sử dụng shader vào một nơi. Điều này cho phép phía kết xuất chỉ cần tập trung vào việc 'sử dụng' chúng.

 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}
  • Lớp này thống nhất việc tạo, liên kết và sử dụng vertex shader và fragment shader, cho phép phía kết xuất sử dụng shader một cách an toàn.
  • Bằng cách cung cấp getAttribLocation, có thể tham chiếu an toàn từ phía mesh.

Lớp quản lý mesh (dữ liệu đỉnh)

Chúng ta cũng sẽ tạo một lớp để quản lý dữ liệu đỉnh. Bằng cách phân chia lớp mesh theo đối tượng cần vẽ, việc mở rộng trở nên dễ dàng.

 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}
  • Lớp này quản lý dữ liệu đỉnh của tam giác và thông qua việc liên kết với shader, đảm nhận việc kết xuất thực tế.
  • Tất cả thông tin cần thiết cho việc kết xuất đều nằm trong TriangleMesh.

Điểm bắt đầu (Ví dụ sử dụng)

Cuối cùng, kết hợp các lớp để khởi chạy ứng dụng.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Với cấu trúc này, việc thêm hoạt ảnh hoặc nhiều mesh trở nên dễ dàng.

Mẹo thực tế khi viết WebGL trong TypeScript

Sử dụng TypeScript với WebGL mang lại những lợi ích sau:.

  • Bằng cách chuyển các quy trình WebGL thành các lớp, chúng có thể được tổ chức theo vai trò, giúp dễ dàng bảo trì và mở rộng.
  • Bằng cách tách biệt trách nhiệm như kết xuất và quản lý shader, khả năng đọc hiểu mã được cải thiện.
  • Bằng việc sử dụng tính năng gợi ý kiểu dữ liệu của TypeScript, bạn có thể giảm thiểu sai sót khi gọi API WebGL hoặc truyền tham số.

Tóm tắt

Nhờ sử dụng TypeScript, ngay cả các quy trình WebGL cấp thấp cũng có thể được xử lý ổn định với sự an toàn kiểu dữ liệu và cấu trúc rõ ràng. Bằng cách hiểu quy trình từ cấu hình tối thiểu đến kết xuất, và áp dụng thiết kế lớp để tách biệt các vai trò như khởi tạo, kết xuất và quản lý tài nguyên, bạn có thể cải thiện khả năng đọc hiểu và bảo trì mã. Bằng cách thực hiện từng bước, bạn có thể học WebGL như kiến thức thực tiễn, không còn là hộp đen và có thể áp dụng vào công việc thực tế.

Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.

YouTube Video