WebGL in TypeScript
Questo articolo spiega WebGL in TypeScript.
Introdurremo il concetto di WebGL, la configurazione minima, il rendering, le estensioni e il design delle classi passo dopo passo con esempi di codice.
YouTube Video
WebGL in TypeScript
WebGL è un'API di basso livello che consente di manipolare direttamente la GPU nel browser.
Utilizzando TypeScript, puoi ridurre notevolmente gli 'errori di implementazione' in WebGL grazie alla sicurezza dei tipi, al completamento del codice e alla programmazione strutturata.
TypeScript è particolarmente efficace nei seguenti aspetti:.
- I tipi di oggetti WebGL diventano chiari.
- Si possono ridurre gli errori nella gestione delle variabili degli shader.
- Diventa più facile seguire la struttura.
Configurazione minima di WebGL
I seguenti sono i requisiti minimi per il rendering con WebGL:.
- Elemento
<canvas> - Contesto di
WebGL - Vertex shader
- Fragment shader
Per prima cosa, creiamo uno stato in cui lo schermo possa essere inizializzato.
Acquisizione del Canvas e del Contesto WebGL
Per prima cosa, ottieni il canvas e il WebGLRenderingContext.
Qui utilizziamo la programmazione TypeScript con enfasi sulla sicurezza dei 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}- A questo punto,
gldiventa il punto di ingresso per tutte le operazioni WebGL.
Pulisci lo schermo per confermare che WebGL funziona
Prima del rendering, verifica se è possibile riempire il colore di sfondo.
Questo è il controllo iniziale per verificare che WebGL funzioni correttamente.
1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);- Fino a questo punto, il canvas verrà riempito con un colore grigio scuro.
Che cos'è uno shader?
In WebGL, i processi di disegno sono descritti utilizzando un linguaggio speciale chiamato GLSL. In GLSL, si preparano principalmente due tipi di shader: vertex shader e fragment shader.
Innanzitutto, creeremo uno 'shader minimo funzionante' utilizzando questi due shader.
Scrittura del vertex shader
I vertex shader determinano le posizioni dei punti o delle figure da disegnare.
Segue un codice semplice che posiziona un unico punto al centro dello schermo.
1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5 gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;a_positionè la coordinata passata dal lato JavaScript.
Scrittura del fragment shader
I fragment shader determinano i colori che appaiono sullo schermo.
In questo caso, emette sempre il colore rosso.
1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;- La
precisiondeve sempre essere specificata in WebGL.
Creazione di una funzione per compilare gli shader
Poiché il processo di creazione degli shader è sempre lo stesso, lo trasformiamo in una funzione.
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}- Questa funzione crea uno shader del tipo specificato, compila il codice sorgente, lancia un errore se fallisce e restituisce lo shader compilato se il processo ha successo.
Creazione di un programma (un insieme di shader)
Combina i vertex e i fragment shader in uno solo.
Questo è chiamato 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}- Questo codice collega i vertex e i fragment shader in un unico programma e verifica se possono essere utilizzati per il rendering.
Preparazione dei dati dei vertici
Qui prepareremo i dati dei vertici per disegnare un triangolo.
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]);- Il sistema di coordinate in WebGL va da
-1.0a1.0.
Creazione di un buffer e trasferimento alla GPU
Successivamente, per rendere l'array dei dati dei vertici utilizzabile dalla GPU, trasferiamo i dati utilizzando un 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);- Questo codice crea un buffer per memorizzare i dati dei vertici e ne trasferisce il contenuto alla GPU.
Associazione dell'attributo con il buffer
Collega a_position nello shader con il buffer creato in precedenza.
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
Infine, usa il programma per inviare il comando di rendering.
1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);- Quando esegui questo programma, verrà visualizzato un triangolo rosso.
Progettazione di classi con WebGL
WebGL è un'API imperativa, quindi il codice può diventare rapidamente ingombrante se scritto così com'è. Attraverso la progettazione a classi, puoi separare chiaramente inizializzazione, rendering e gestione delle risorse.
Qui sfrutteremo i vantaggi di TypeScript e faremo evolvere il design concentrandoci su separazione delle responsabilità, riusabilità e manutenibilità.
Politica di Design (Minima ma Pratica)
La classe è divisa nei seguenti ruoli.
GLAppcontrolla l'inizializzazione generale e il rendering.ShaderProgramgestisce gli shader.TriangleMeshgestisce i dati dei vertici.
Per prima cosa, creiamo la classe che controlla tutto.
Classe principale di un'applicazione WebGL
GLApp gestisce il canvas e il contesto WebGL e funge da punto di partenza per il 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}- Questa classe gestisce i processi di inizializzazione e rendering di WebGL ed è responsabile del disegno sullo schermo utilizzando shader e mesh.
- Separando
initializeerender, è più facile supportare la re-inizializzazione e l'animazione in futuro.
Classe per la gestione degli shader
Poi, crea una classe per gestire gli shader, raggruppando la creazione, il collegamento e l'utilizzo degli shader in un unico posto. Questo permette al lato rendering di concentrarsi solo sull''utilizzo' degli 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}- Questa classe unifica la creazione, il collegamento e l'uso di vertex e fragment shader, consentendo al lato rendering di utilizzare gli shader in sicurezza.
- Esponendo
getAttribLocation, può essere referenziata in sicurezza dal lato mesh.
Classe per la gestione delle mesh (dati dei vertici)
Creeremo anche una classe per gestire i dati dei vertici. Suddividendo le classi mesh in base all'oggetto da disegnare, diventa facile estendere.
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}- Questa classe gestisce i dati dei vertici del triangolo e, collegandola allo shader, gestisce il rendering effettivo.
- Tutte le informazioni necessarie per il rendering sono contenute all'interno di
TriangleMesh.
Punto di ingresso (Esempio d'uso)
Infine, combina le classi per avviare l'applicazione.
1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();- Con questa struttura, aggiungere animazioni o più mesh diventa facile.
Suggerimenti pratici per scrivere WebGL in TypeScript
L'utilizzo di TypeScript con WebGL offre i seguenti vantaggi:.
- Convertendo le procedure WebGL in classi, possono essere organizzate per ruolo, rendendo più semplice la manutenzione e l'estensione.
- Separando responsabilità come rendering e gestione degli shader, la leggibilità del codice è migliorata.
- Sfruttando il completamento dei tipi di TypeScript, puoi ridurre gli errori nelle chiamate API di WebGL o nella specifica dei parametri.
Riepilogo
Utilizzando TypeScript, anche i processi WebGL di basso livello possono essere gestiti stabilmente con sicurezza dei tipi e struttura. Comprendendo il flusso dalla configurazione minima al rendering e applicando il design a classi per separare ruoli come inizializzazione, rendering e gestione delle risorse, puoi migliorare leggibilità e manutenibilità. Implementando passo dopo passo, puoi apprendere WebGL come conoscenza pratica non più una scatola nera e applicabile nel lavoro reale.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.