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

תחילה, קבל את ה-canvas ואת ה-WebGLRenderingContext.

כאן אנו מקודדים ב-TypeScript עם דגש על בטיחות 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}
  • בשלב זה, 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

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`;
  • ב-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}
  • פונקציה זו יוצרת שאדר מסוג נתון, מקמפלת את קוד המקור, זורקת שגיאה במקרה של כישלון, ומחזירה את השאדר המקמפל במידה והצליח.

יצירת תוכנית (קבוצת שאדרים)

יש לשלב את ה-vertex וה-fragment shaders יחד.

זוהי 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)

כאן נכין נתוני קודקודים לציור משולש.

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 לאחסון נתוני הקודקודים ומעביר אותו ל-GPU.

שיוך attribute אל ה-buffer

יש לקשר את a_position בשאדר עם ה-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);

רנדרינג (ציור/הדמיה)

לבסוף, יש להפעיל את תוכנית השאדרים כדי להוציא פקודת ציור.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • כאשר תריץ את התוכנית, יוצג משולש אדום.

עיצוב מחלקות עם WebGL

WebGL היא API אימפרטיבי, ולכן קוד יכול להתנפח במהירות אם כותבים אותו כמו שהוא. בעזרת עיצוב מחלקות ניתן להפריד בברור בין אתחול, רנדרינג, וניהול משאבים.

כאן ננצל את יתרונות TypeScript ונפתח בהדרגה את העיצוב כך שיתמקד בהפרדת אחריויות, שימוש חוזר ותחזוקה נוחה.

מדיניות עיצוב (מינימלי אבל פרקטי)

הכיתה מחולקת לתפקידים הבאים.

  • GLApp מנהלת את האתחול והציור הכללי.
  • ShaderProgram מנהלת את השאדרים.
  • TriangleMesh מנהלת את נתוני הקודקודים.

נתחיל ביצירת המחלקה שמנהלת הכל.

מחלקת הכניסה לאפליקציית WebGL

GLApp מנהלת את ה-canvas וקונטקסט ה-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}
  • מחלקה זו מאחדת את יצירת, קישור ושימוש ב-vertex ו-fragment shaders, ומאפשרת לרנדרינג להשתמש בהם בצורה בטוחה.
  • על ידי חשיפת 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();
  • מבנה זה הופך הוספת אנימציות או מספר משים לפשוטה.

טיפים מעשיים לכתיבת WebGL ב-TypeScript

שימוש ב-TypeScript עם WebGL מספק את היתרונות הבאים:.

  • באמצעות הפיכת תהליכי WebGL למחלקות, ניתן לארגן אותם לפי תפקיד ולפשט תחזוקה והרחבה.
  • הפרדת אחריויות כמו רנדרינג, וניהול שאדרים, משפרת את קריאות הקוד.
  • באמצעות השלמת טיפוסים של TypeScript ניתן להפחית טעויות בקריאות ל-API של WebGL או בהגדרת פרמטרים.

סיכום

השימוש ב-TypeScript מאפשר טיפול יציב גם בתהליכי WebGL ברמה נמוכה, בזכות בטיחות טיפוסים ומבנה. הבנת התהליך מהגדרה מינימלית ועד רנדרינג, ויישום עיצוב מחלקות לחלוקה לתפקידים כמו אתחול, רנדרינג וניהול משאבים, משפרת קריאות ותחזוקה. באמצעות יישום שלב-אחר-שלב, ניתן ללמוד את WebGL כידע פרקטי שניתן ליישום מעשי, מבלי שיישאר קופסה שחורה.

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video