WebGL en TypeScript

WebGL en TypeScript

Cet article explique WebGL en TypeScript.

Nous allons présenter le concept de WebGL, sa configuration minimale, le rendu, les extensions et la conception de classes étape par étape avec du code exemple.

YouTube Video

WebGL en TypeScript

WebGL est une API de bas niveau qui permet de manipuler directement le GPU dans le navigateur.

En utilisant TypeScript, vous pouvez grandement réduire les « erreurs d'implémentation » dans WebGL grâce à la sécurité des types, l'autocomplétion et la programmation structurée.

TypeScript est particulièrement efficace sur les points suivants :.

  • Les types des objets WebGL deviennent clairs.
  • Les erreurs de gestion des variables de shader peuvent être réduites.
  • Il devient plus facile de suivre la structure.

Configuration minimale de WebGL

Voici les exigences minimales pour effectuer un rendu avec WebGL :.

  • Élément <canvas>
  • Contexte WebGL
  • Vertex shader
  • Fragment shader

Commençons par créer un état permettant d’initialiser l’écran.

Acquisition du Canvas et du contexte WebGL

Commencez par obtenir le canvas et le WebGLRenderingContext.

Ici, nous utilisons la programmation TypeScript en mettant l’accent sur la sécurité contre les valeurs nulles.

 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}
  • À ce stade, gl devient le point d’entrée pour toutes les opérations WebGL.

Effacer l’écran pour confirmer que WebGL fonctionne

Avant de faire le rendu, vérifiez si la couleur d’arrière-plan peut être appliquée.

Ceci est la vérification initiale pour confirmer que WebGL fonctionne correctement.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Jusqu’à présent, le canvas sera rempli avec une couleur gris foncé.

Qu’est-ce qu’un shader ?

Dans WebGL, les processus de dessin sont décrits à l’aide d’un langage spécial appelé GLSL. En GLSL, vous préparez principalement deux types de shaders : les shaders de vertex et les shaders de fragment.

Nous allons d’abord créer un « shader minimal fonctionnel » en utilisant ces deux shaders.

Écriture du vertex shader

Les vertex shaders déterminent les positions des points ou des formes à dessiner.

Voici un simple code qui place un point au centre de l’écran.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position est la coordonnée transmise depuis JavaScript.

Écriture du fragment shader

Les fragment shaders déterminent les couleurs qui apparaissent à l’écran.

Ici, il produit toujours la couleur rouge.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precision doit toujours être spécifié dans WebGL.

Création d’une fonction pour compiler les shaders

Comme la création des shaders est toujours identique, nous en faisons une fonction.

 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}
  • Cette fonction crée un shader du type spécifié, compile le code source, génère une erreur en cas d’échec, et retourne le shader compilé en cas de succès.

Création d’un programme (un ensemble de shaders)

Combinez le vertex et le fragment shader en un seul.

Ceci s’appelle un 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}
  • Ce code relie les vertex et fragment shaders dans un programme unique et vérifie qu’ils peuvent être utilisés pour le rendu.

Préparation des données de sommets

Ici, nous allons préparer les données de sommets pour dessiner un triangle.

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]);
  • Le système de coordonnées de WebGL va de -1.0 à 1.0.

Création d’un buffer et transfert vers le GPU

Ensuite, pour rendre le tableau de données de sommets utilisable par le GPU, nous transférons les données à l'aide d'un tampon (buffer).

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);
  • Ce code crée un buffer pour stocker les données de sommets et transfère son contenu vers le GPU.

Associer l’attribut avec le buffer

Connectez a_position dans le shader avec le buffer précédemment créé.

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

Rendu

Enfin, utilisez le programme pour effectuer le rendu.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • Lorsque vous exécutez ce programme, un triangle rouge apparaît à l’écran.

Conception en classes avec WebGL

WebGL étant une API impérative, le code peut vite devenir volumineux si on l’écrit tel quel. Grâce à la conception en classes, vous pouvez séparer clairement l’initialisation, le rendu et la gestion des ressources.

Nous allons ici exploiter les avantages de TypeScript et faire évoluer progressivement la conception en mettant l’accent sur la séparation des responsabilités, la réutilisabilité et la maintenabilité.

Politique de conception (minimale mais pratique)

La classe est divisée selon les rôles suivants.

  • GLApp gère l’initialisation générale et le rendu.
  • ShaderProgram gère les shaders.
  • TriangleMesh gère les données de sommets.

Créons d’abord la classe qui contrôle l’ensemble.

Classe d’entrée pour une application WebGL

GLApp gère le canvas, le contexte WebGL et sert de point de départ pour le rendu.

 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}
  • Cette classe gère l’initialisation et le rendu WebGL, et s’occupe d’afficher à l’écran à l’aide des shaders et des maillages.
  • En séparant initialize et render, il devient plus facile de gérer la réinitialisation et l’animation à l’avenir.

Classe de gestion des shaders

Ensuite, créez une classe pour gérer les shaders, en regroupant la création, la liaison et l'utilisation des shaders en un seul endroit. Cela permet à la partie rendu de se concentrer uniquement sur leur usage.

 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}
  • Cette classe unifie la création, la liaison et l’utilisation des vertex et fragment shaders, ce qui permet au rendu de les utiliser de façon sécurisée.
  • En exposant getAttribLocation, il peut être référencé en toute sécurité depuis la classe de mesh.

Classe de gestion des meshes (données de sommets)

Nous allons également créer une classe pour gérer les données de sommets. En divisant les classes de mesh par objet à dessiner, il devient facile d’ajouter de nouvelles formes.

 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}
  • Cette classe gère les données de sommets du triangle et, en les reliant au shader, effectue le rendu effectif.
  • Toutes les informations nécessaires au rendu sont contenues dans TriangleMesh.

Point d’entrée (exemple d’utilisation)

Enfin, combinez les classes pour lancer l’application.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Avec cette structure, ajouter des animations ou plusieurs meshes devient facile.

Astuces pratiques pour écrire du WebGL en TypeScript

L’utilisation de TypeScript avec WebGL offre les avantages suivants :.

  • En transformant les procédures WebGL en classes, celles-ci peuvent être organisées par rôle, ce qui facilite la maintenance et l’extension.
  • En séparant les responsabilités comme le rendu et la gestion des shaders, la lisibilité du code est améliorée.
  • En profitant de la complétion des types de TypeScript, vous réduisez les erreurs lors de l’appel des API WebGL ou dans la spécification des paramètres.

Résumé

Avec TypeScript, même les processus WebGL bas niveau peuvent être sécurisés et structurés avec les types. En comprenant le flux allant de la configuration minimale au rendu, et en appliquant la conception en classes pour séparer les rôles comme l’initialisation, le rendu et la gestion des ressources, vous améliorez la lisibilité et la maintenabilité. En procédant étape par étape, vous pouvez apprendre WebGL comme une connaissance pratique, non comme une boîte noire, et l’appliquer dans des projets réels.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video