WebGL i TypeScript

Den här artikeln förklarar WebGL i TypeScript.

Vi introducerar konceptet WebGL, dess minimala konfiguration, rendering, tillägg och klassdesign steg för steg med exempelkod.

YouTube Video

WebGL i TypeScript

WebGL är ett lågnivå-API som låter dig styra GPU:n direkt i webbläsaren.

Genom att använda TypeScript kan du kraftigt minska 'implementeringsmisstag' i WebGL genom typsäkerhet, kodkomplettering och strukturerad kodning.

TypeScript är särskilt effektivt på följande punkter:.

  • Typen på WebGL-objekt blir tydlig.
  • Misstag vid hantering av shader-variabler kan minskas.
  • Det blir enklare att följa strukturen.

Minimal konfiguration av WebGL

Följande är minimikraven för rendering med WebGL:.

  • <canvas>-element
  • WebGL-kontext
  • Vertexshader
  • Fragmentshader

Låt oss först skapa ett tillstånd där skärmen kan initieras.

Hämta Canvas och WebGL-kontext

Först, hämta canvas och WebGLRenderingContext.

Här använder vi TypeScript-kodning med fokus på nullsäkerhet.

 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}
  • Vid denna punkt blir gl ingångspunkten för alla WebGL-operationer.

Rensa skärmen för att bekräfta att WebGL fungerar

Innan rendering, kontrollera om bakgrundsfärgen kan fyllas.

Detta är den ursprungliga kontrollen för att verifiera att WebGL fungerar korrekt.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Fram till denna punkt kommer canvasen att fyllas med en mörkgrå färg.

Vad är en shader?

I WebGL beskrivs ritningsprocesser med ett speciellt språk som kallas GLSL. I GLSL förbereder du huvudsakligen två typer av shaders: vertex shaders och fragment shaders.

Först skapar vi en 'minimalt fungerande shader' med dessa två shaders.

Skriva vertexshadern

Vertexshaders bestämmer positioner för punkter eller figurer som ska ritas.

Följande är enkel kod som placerar en enda punkt i mitten av skärmen.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position är koordinaten som skickas från JavaScript-sidan.

Skriva fragmentshadern

Fragmentshaders bestämmer färgerna som visas på skärmen.

Den här gången matas rött alltid ut.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precision måste alltid anges i WebGL.

Skapa en funktion för att kompilera shaders

Eftersom skapandet av shaders alltid sker på samma sätt, gör vi det till en funktion.

 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}
  • Denna funktion skapar en shader av angiven typ, kompilerar källkoden, kastar ett fel om det misslyckas och returnerar den kompilerade shadern om det lyckas.

Skapa ett program (en uppsättning shaders)

Kombinera vertex- och fragmentshadern till en.

Detta kallas för ett 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}
  • Den här koden länkar vertex- och fragmentshadern till ett enda program och kontrollerar om de kan användas för rendering.

Förbereda vertexdata

Här kommer vi att förbereda vertexdata för att rita en triangel.

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]);
  • Koordinatsystemet i WebGL sträcker sig från -1.0 till 1.0.

Skapa en buffert och överföra den till GPU:n

Därefter, för att göra arrayen av vertexdata användbar för GPU:n, för vi över datan med hjälp av en 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);
  • Den här koden skapar en buffert för att lagra vertexdata och överför dess innehåll till GPU:n.

Koppla attributet till bufferten

Koppla a_position i shadern till bufferten som skapats tidigare.

 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

Slutligen, använd programmet för att utföra rendering-kommandot.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • När du kör detta program kommer en röd triangel att visas.

Klassdesign med WebGL

WebGL är ett imperativt API, så koden kan snabbt bli svåröverskådlig om den skrivs som den är. Genom klassdesign kan du tydligt separera initiering, rendering och resurs­hantering.

Här utnyttjar vi fördelarna med TypeScript och utvecklar successivt designen med fokus på ansvarsseparering, återanvändbarhet och underhållbarhet.

Designpolicy (Minimal men praktisk)

Klassen är uppdelad i följande roller.

  • GLApp styr övergripande initiering och rendering.
  • ShaderProgram hanterar shaders.
  • TriangleMesh hanterar vertexdata.

Låt oss först skapa klassen som styr allt.

Startklass för en WebGL-applikation

GLApp hanterar canvas och WebGL-kontext och fungerar som startpunkt för 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}
  • Den här klassen hanterar WebGL:s initierings- och renderingsprocesser och ansvarar för att rita på skärmen med shaders och mesh.
  • Genom att separera initialize och render blir det enklare att stödja ominitiering och animation i framtiden.

Klass för hantering av shaders

Skapa sedan en klass för att hantera shaders och samla skapande, länkning och användning av shaders på ett ställe. Detta gör att rendringssidan enbart kan fokusera på att 'använda' dem.

 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}
  • Den här klassen samlar skapande, länkning och användning av vertex- och fragmentshaders, så att renderingssidan kan använda shaders på ett säkert sätt.
  • Genom att exponera getAttribLocation kan det refereras säkert från mesh-sidan.

Klass för hantering av mesh (vertexdata)

Vi kommer också att skapa en klass för att hantera vertexdatan. Genom att dela upp mesh-klasser efter objekt att rita, blir det enkelt att bygga ut.

 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}
  • Den här klassen hanterar vertexdata för triangeln och genom att koppla det till shadern, hanterar den själva rendering.
  • All information som behövs för rendering finns i TriangleMesh.

Startpunkt (Användningsexempel)

Slutligen, kombinera klasserna för att starta appen.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Med denna struktur blir det lätt att lägga till animationer eller flera mesh.

Praktiska tips för att skriva WebGL i TypeScript

Att använda TypeScript med WebGL ger följande fördelar:.

  • Genom att omvandla WebGL-processer till klasser kan de organiseras efter roll, vilket förenklar underhåll och vidareutveckling.
  • Genom att separera ansvar som rendering och shader-hantering förbättras kodens läsbarhet.
  • Genom att använda TypeScripts typkomplettering kan du minska misstag vid anrop av WebGL-API:er eller vid parameterangivelser.

Sammanfattning

Med TypeScript kan till och med lågnivå-WebGL-processer hanteras stabilt med typsäkerhet och struktur. Genom att förstå flödet från minimal konfiguration till rendering, och genom att tillämpa klassdesign för att separera roller såsom initiering, rendering och resurshantering, kan du förbättra läsbarhet och underhållbarhet. Genom att implementera steg för steg kan du lära dig WebGL som praktisk kunskap, som inte är en svart låda och som kan användas i verkliga projekt.

Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.

YouTube Video