WebGL ใน TypeScript

WebGL ใน TypeScript

บทความนี้อธิบายเกี่ยวกับ WebGL ใน TypeScript

เราจะนำเสนอแนวคิดของ WebGL การตั้งค่าขั้นต่ำ การแสดงผล ส่วนขยาย และการออกแบบคลาสทีละขั้นตอนพร้อมโค้ดตัวอย่าง

YouTube Video

WebGL ใน TypeScript

WebGL เป็น API ระดับต่ำที่ช่วยให้คุณสามารถควบคุม GPU ได้โดยตรงภายในเบราว์เซอร์

ด้วยการใช้ TypeScript คุณสามารถลด 'ข้อผิดพลาดในการพัฒนา' ใน WebGL ได้อย่างมากผ่าน การตรวจสอบชนิดข้อมูลอัตโนมัติ, การเติมโค้ดอัตโนมัติ และโครงสร้างโค้ดที่เป็นระบบ

TypeScript มีประสิทธิภาพเป็นพิเศษในจุดต่อไปนี้:

  • ชนิดข้อมูลของออบเจกต์ WebGL ชัดเจนขึ้น
  • สามารถลดข้อผิดพลาดในการจัดการตัวแปรของ shader ได้
  • การติดตามโครงสร้างทำได้ง่ายขึ้น

การตั้งค่าขั้นต่ำของ WebGL

ต่อไปนี้คือข้อกำหนดขั้นต่ำสำหรับการแสดงผลด้วย WebGL:

  • องค์ประกอบ <canvas>
  • บริบทของ WebGL
  • Vertex shader
  • Fragment shader

ก่อนอื่น มาสร้างสถานะที่สามารถเริ่มต้นหน้าจอได้

การรับค่า Canvas และ WebGL Context

ก่อนอื่น ให้รับค่า canvas และ WebGLRenderingContext

ที่นี่ เราจะเขียนโค้ด TypeScript โดยเน้นที่ null safety

 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);
  • จนถึงตรงนี้ canvas จะถูกเติมสีเทาเข้ม

Shader คืออะไร?

ใน WebGL กระบวนการวาดจะอธิบายด้วยภาษาพิเศษที่ชื่อว่า GLSL ใน GLSL โดยหลักแล้วคุณจะต้องเตรียมเชดเดอร์สองประเภทคือ vertex shader และ fragment shader

ก่อนอื่น เราจะสร้าง 'shader ขั้นต่ำที่ใช้งานได้' ด้วย shader ทั้งสองนี้

การเขียน vertex shader

Vertex shaders จะกำหนดตำแหน่งของจุดหรือรูปร่างที่จะวาด

ต่อไปนี้เป็นโค้ดง่ายๆ สำหรับวางจุดเดียวไว้กลางหน้าจอ

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position คือพิกัดที่ส่งมาจากฝั่ง JavaScript

การเขียน fragment shader

Fragment shaders จะกำหนดสีที่แสดงบนหน้าจอ

ครั้งนี้จะส่งออกเป็นสีแดงเสมอ

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • ต้องระบุ precision เสมอใน WebGL

สร้างฟังก์ชันสำหรับ compile shader

เนื่องจากกระบวนการสร้าง shader เหมือนกัน เราจึงแปลงเป็นฟังก์ชัน

 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}
  • ฟังก์ชันนี้จะสร้าง shader ตามประเภทที่ระบุ ทำการ compile โค้ดต้นฉบับ แจ้ง error หากล้มเหลว และคืนค่า shader ที่ compile สำเร็จถ้าสำเร็จ

การสร้างโปรแกรม (ชุดของ shaders)

ผสมผสาน vertex shader และ fragment shader เข้าด้วยกัน

สิ่งนี้เรียกว่า 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}
  • โค้ดนี้จะเชื่อมโยง vertex กับ fragment shaders เข้าด้วยกันในโปรแกรมเดียว และตรวจสอบว่าสามารถใช้แสดงผลได้หรือไม่

เตรียมข้อมูลจุดยอด (vertex data)

ที่นี่ เราจะเตรียมข้อมูลเวอร์เท็กซ์สำหรับวาดรูป สามเหลี่ยม

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

สร้าง buffer และส่งไปยัง 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);
  • โค้ดนี้สร้าง buffer เพื่อเก็บ vertex data แล้วส่งข้อมูลไปยัง GPU

เชื่อมโยง attribute กับ buffer

เชื่อมโยง a_position ใน shader เข้ากับ buffer ที่สร้างไว้ก่อนหน้านี้

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

การแสดงผล (Rendering)

สุดท้าย ใช้โปรแกรมเพื่อสั่งการแสดงผล

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • เมื่อเรียกใช้โปรแกรมนี้ จะปรากฏ สามเหลี่ยมสีแดง

การออกแบบคลาสด้วย WebGL

WebGL เป็น API แบบ imperative ทำให้โค้ดอาจรกได้อย่างรวดเร็วหากเขียนแบบเดิม ด้วยการออกแบบเป็นคลาส คุณสามารถแยกส่วน การเริ่มต้น การแสดงผล และการจัดการทรัพยากร ได้อย่างชัดเจน

ที่นี่ เราจะใช้ประโยชน์จาก TypeScript เพื่อพัฒนาโครงสร้างโดยเน้น การแบ่งหน้าที่ การนำกลับมาใช้ซ้ำ และการดูแลรักษา

นโยบายการออกแบบ (เรียบง่ายแต่ใช้งานได้จริง)

ชั้นเรียนถูกแบ่งออกเป็นบทบาทต่อไปนี้

  • GLApp ควบคุมการเริ่มต้นและการแสดงผลโดยรวม
  • ShaderProgram จัดการ shader
  • TriangleMesh จัดการข้อมูล vertex data

ก่อนอื่น สร้างคลาสที่ควบคุมทุกอย่าง

คลาสเริ่มต้นของแอปพลิเคชัน WebGL

GLApp จัดการ canvas และ WebGL context และเป็นจุดเริ่มต้นของการแสดงผล

 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 รวมถึงรับผิดชอบการวาดบนหน้าจอด้วย shader และ mesh
  • โดยแยก initialize และ render จะสามารถรองรับการเริ่มต้นใหม่และแอนิเมชันในอนาคตได้ง่ายขึ้น

คลาสสำหรับจัดการ shaders

ต่อไป สร้างคลาสเพื่อจัดการเชดเดอร์ เพื่อรวมขั้นตอนการสร้าง, เชื่อมโยง และการใช้งานเชดเดอร์ไว้ในที่เดียวกัน สิ่งนี้ทำให้ฝั่งการแสดงผลมุ่งเน้นเฉพาะการ 'ใช้' shader เท่านั้น

 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}
  • คลาสนี้รวมการสร้าง การเชื่อมโยง และการใช้ vertex และ fragment shaders เพื่อให้ฝั่งแสดงผลใช้ shader ได้อย่างปลอดภัย
  • โดยการเปิดเผย getAttribLocation จะสามารถอ้างอิงได้อย่างปลอดภัยจากฝั่ง mesh

คลาสสำหรับจัดการ mesh (vertex data)

เรายังจะสร้างคลาสเพื่อจัดการข้อมูลเวอร์เท็กซ์ด้วย โดยแบ่ง 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}
  • คลาสนี้จัดการ vertex data ของสามเหลี่ยม และเชื่อมโยงกับ shader เพื่อทำการแสดงผลจริง
  • ข้อมูลทั้งหมดที่จำเป็นสำหรับการแสดงผลถูกรวมไว้ใน TriangleMesh

จุดเริ่มต้น (ตัวอย่างการใช้งาน)

สุดท้าย รวมคลาสต่างๆ เพื่อเปิดแอป

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • ด้วยโครงสร้างนี้ การเพิ่มแอนิเมชันหรือ mesh หลายๆ ชิ้นก็เป็นเรื่องง่าย

เคล็ดลับเชิงปฏิบัติสำหรับการเขียน WebGL ใน TypeScript

การใช้ TypeScript กับ WebGL มีข้อดีดังต่อไปนี้:

  • การเปลี่ยนกระบวนการ WebGL ให้เป็นคลาส สามารถจัดระเบียบตามหน้าที่ จึงทำให้ดูแลและต่อยอดได้ง่ายขึ้น
  • โดยแยกความรับผิดชอบเช่น การแสดงผลและการจัดการ shader ทำให้โค้ดอ่านง่ายขึ้น
  • โดยใช้ประโยชน์จากการเติมชนิดข้อมูลอัตโนมัติของ TypeScript จะช่วยลดข้อผิดพลาดในการเรียกใช้ WebGL API หรือการระบุพารามิเตอร์

สรุป

ด้วยการใช้ TypeScript แม้แต่กระบวนการ WebGL ระดับต่ำก็สามารถจัดการได้อย่างมีเสถียรภาพด้วยความปลอดภัยของชนิดข้อมูลและโครงสร้าง ด้วยการเข้าใจกระบวนการตั้งแต่การตั้งค่าขั้นต่ำจนถึงการแสดงผล และการออกแบบคลาสเพื่อแยกบทบาทหน้าที่ เช่น การเริ่มต้น การแสดงผล และการจัดการทรัพยากร จะช่วยปรับปรุงความอ่านง่ายและการดูแลรักษาโค้ด ด้วยการทำตามขั้นตอนทีละขั้น คุณจะได้เรียนรู้ WebGL อย่างเป็นรูปธรรม ไม่ใช่แค่ 'กล่องดำ' และสามารถนำไปใช้ในการทำงานจริงได้

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video