WebGL in TypeScript

WebGL in TypeScript

Dit artikel legt WebGL in TypeScript uit.

We introduceren het concept van WebGL, de minimale configuratie, rendering, extensies en klassendesign stap voor stap met voorbeeldcode.

YouTube Video

WebGL in TypeScript

WebGL is een low-level API waarmee je direct de GPU in de browser kunt aansturen.

Door TypeScript te gebruiken, kun je 'implementatiefouten' in WebGL sterk verminderen door typeveiligheid, code-aanvulling en gestructureerd coderen.

TypeScript is vooral effectief op de volgende punten:.

  • De types van WebGL-objecten worden duidelijk.
  • Fouten in het omgaan met shader-variabelen kunnen verminderd worden.
  • Het wordt eenvoudiger om de structuur te volgen.

Minimale configuratie van WebGL

De volgende zijn de minimale vereisten voor rendering met WebGL:.

  • <canvas>-element
  • WebGL-context
  • Vertex shader
  • Fragment shader

Laten we beginnen met een situatie waarin het scherm kan worden geïnitialiseerd.

Canvas en WebGL-context verkrijgen

Verkrijg eerst het canvas en de WebGLRenderingContext.

Hier gebruiken we TypeScript-code met de nadruk op 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}
  • Op dit punt wordt gl het toegangspunt voor alle WebGL-bewerkingen.

Het scherm wissen om te bevestigen dat WebGL werkt

Controleer vóór het renderen of de achtergrondkleur kan worden gevuld.

Dit is de eerste controle om te verifiëren dat WebGL correct functioneert.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Tot nu toe wordt het canvas gevuld met een donkergrijze kleur.

Wat is een shader?

In WebGL worden tekenprocessen beschreven met een speciale taal genaamd GLSL. In GLSL bereid je voornamelijk twee soorten shaders voor: vertex shaders en fragment shaders.

We maken eerst een 'minimaal werkende shader' met deze twee shaders.

De vertex shader schrijven

Vertex shaders bepalen de posities van de te tekenen punten of figuren.

De volgende eenvoudige code plaatst een enkel punt in het midden van het scherm.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position is de coördinaat die vanuit JavaScript wordt doorgegeven.

De fragment shader schrijven

Fragment shaders bepalen de kleuren die op het scherm verschijnen.

Deze keer wordt altijd rood uitgegeven.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precision moet altijd worden opgegeven in WebGL.

Een functie maken om shaders te compileren

Omdat de shader-creatie altijd hetzelfde verloopt, zetten we het om in een functie.

 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}
  • Deze functie maakt een shader van het opgegeven type, compileert de broncode, geeft een foutmelding bij falen, en retourneert de gecompileerde shader bij succes.

Een programma maken (een set shaders)

Combineer de vertex en fragment shaders tot één geheel.

Dit wordt een WebGLProgram genoemd.

 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}
  • Deze code koppelt de vertex en fragment shaders tot één programma en controleert of deze gebruikt kunnen worden voor rendering.

Vertexgegevens voorbereiden

Hier zullen we vertexgegevens voorbereiden om een driehoek te tekenen.

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]);
  • Het coördinatensysteem in WebGL loopt van -1.0 tot 1.0.

Een buffer maken en deze naar de GPU sturen

Vervolgens maken we de array met vertexgegevens bruikbaar voor de GPU door de gegevens over te dragen via een 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);
  • Deze code maakt een buffer om vertexgegevens op te slaan en stuurt deze naar de GPU.

De attribute koppelen aan de buffer

Koppel a_position in de shader aan de eerder gemaakte 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

Gebruik tenslotte het programma om het rendercommando uit te geven.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • Als je dit programma uitvoert, wordt een rode driehoek weergegeven.

Klassendesign met WebGL

WebGL is een imperatieve API, dus de code kan snel omvangrijk worden als je die rechtstreeks schrijft. Door klassendesign kun je duidelijk initialisatie, rendering en resource management scheiden.

Hier benutten we de voordelen van TypeScript en verbeteren we het ontwerp stap voor stap richting scheiding van verantwoordelijkheden, herbruikbaarheid en onderhoudbaarheid.

Ontwerpbeleid (minimaal maar praktisch)

De klas is verdeeld in de volgende rollen.

  • GLApp beheert de algemene initialisatie en rendering.
  • ShaderProgram beheert de shaders.
  • TriangleMesh beheert de vertexgegevens.

Laten we eerst de klasse maken die alles aanstuurt.

Startklasse voor een WebGL-toepassing

GLApp beheert het canvas en de WebGL-context en fungeert als het startpunt voor rendering.

 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}
  • Deze klasse regelt de initialisatie- en renderprocessen van WebGL en is verantwoordelijk voor het tekenen op het scherm via shaders en meshes.
  • Door initialize en render te scheiden, wordt het later eenvoudiger om opnieuw te initialiseren en animatie toe te voegen.

Klasse voor het beheren van shaders

Maak vervolgens een klasse om shaders te beheren, waarbij het aanmaken, linken en gebruiken van shaders op één plek wordt samengebracht. Dit betekent dat de renderende kant zich alleen maar hoeft te richten op het 'gebruik' ervan.

 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}
  • Deze klasse brengt de creatie, koppeling en het gebruik van vertex- en fragment-shaders bij elkaar, zodat de renderzijde shaders veilig kan gebruiken.
  • Door getAttribLocation beschikbaar te maken, kan deze veilig vanuit de mesh-kant worden aangeroepen.

Klasse voor het beheren van meshes (vertexgegevens)

We zullen ook een klasse maken om de vertexgegevens te beheren. Door mesh-klassen per te tekenen object te scheiden, wordt uitbreiding eenvoudig.

 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}
  • Deze klasse beheert de vertexgegevens van de driehoek en verzorgt via een koppeling aan de shader de daadwerkelijke rendering.
  • Alle informatie die nodig is voor rendering is opgenomen in TriangleMesh.

Startpunt (voorbeeld van gebruik)

Combineer tot slot de klassen om de app te starten.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Met deze structuur is het toevoegen van animaties of meerdere meshes eenvoudig.

Praktische tips voor het schrijven van WebGL in TypeScript

TypeScript gebruiken met WebGL biedt de volgende voordelen:.

  • Door WebGL-procedures om te zetten naar klassen kun je ze per rol structureren, wat het onderhoud en uitbreiden vereenvoudigt.
  • Door verantwoordelijkheden zoals rendering en shaderbeheer te scheiden, wordt de leesbaarheid van code verbeterd.
  • Door TypeScript’s automatische type-aanvulling te gebruiken, verminder je fouten in het aanroepen van WebGL-API's of bij het opgeven van parameters.

Samenvatting

Zelfs low-level WebGL-processen kunnen met TypeScript stabiel beheerd worden, dankzij typeveiligheid en structuur. Door het proces van minimale configuratie tot rendering te begrijpen en klassendesign toe te passen om rollen als initialisatie, rendering en resource management te scheiden, verhoog je de leesbaarheid en onderhoudbaarheid. Door stap voor stap te implementeren, kun je WebGL leren als praktische kennis die geen 'black box' is en ook echt in het werk toepasbaar is.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video