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จัดการ shaderTriangleMeshจัดการข้อมูล 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 ด้วย