WebGL w TypeScript
Ten artykuł wyjaśnia WebGL w TypeScript.
Przedstawimy koncepcję WebGL, jego minimalną konfigurację, renderowanie, rozszerzenia oraz projektowanie klas krok po kroku z przykładowym kodem.
YouTube Video
WebGL w TypeScript
WebGL to niskopoziomowe API umożliwiające bezpośrednią manipulację GPU w przeglądarce.
Korzystając z TypeScript, możesz znacznie ograniczyć „błędy implementacji” w WebGL dzięki bezpieczeństwu typów, podpowiedziom kodu oraz strukturalnemu podejściu do kodowania.
TypeScript jest szczególnie skuteczny w następujących kwestiach:.
- Typy obiektów WebGL stają się jasne.
- Można zredukować błędy w obsłudze zmiennych shaderów.
- Śledzenie struktury staje się łatwiejsze.
Minimalna konfiguracja WebGL
Oto minimalne wymagania do renderowania za pomocą WebGL:.
- Element
<canvas> - Kontekst
WebGL - Shader wierzchołków
- Shader fragmentów
Najpierw utwórzmy stan, w którym ekran może zostać zainicjowany.
Uzyskiwanie Canvas i kontekstu WebGL
Najpierw uzyskaj canvas oraz WebGLRenderingContext.
Tutaj stosujemy kodowanie TypeScript z naciskiem na bezpieczeństwo typu 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}- W tym momencie
glstaje się punktem wejścia dla wszystkich operacji WebGL.
Czyszczenie ekranu w celu potwierdzenia działania WebGL
Przed renderowaniem sprawdź, czy można wypełnić kolor tła.
To początkowe sprawdzenie, czy WebGL działa poprawnie.
1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);- Do tego momentu canvas zostanie wypełniony ciemnoszarym kolorem.
Czym jest shader?
W WebGL procesy rysowania opisuje się za pomocą specjalnego języka o nazwie GLSL. W GLSL głównie przygotowuje się dwa typy shaderów: shadery wierzchołków oraz shadery fragmentów.
Najpierw utworzymy 'minimalny działający shader' używając tych dwóch shaderów.
Pisanie shaderu wierzchołków
Shader wierzchołków określa pozycje punktów lub figur do narysowania.
Poniżej znajduje się prosty kod umieszczający pojedynczy punkt na środku ekranu.
1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5 gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;a_positionto współrzędna przekazywana ze strony JavaScript.
Pisanie shaderu fragmentów
Shader fragmentów określa kolory pojawiające się na ekranie.
Tym razem zawsze zwraca kolor czerwony.
1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;- W WebGL należy zawsze określić
precision.
Tworzenie funkcji do kompilacji shaderów
Ponieważ proces tworzenia shaderów jest zawsze taki sam, zamieniamy go w funkcję.
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}- Ta funkcja tworzy shader określonego typu, kompiluje kod źródłowy, zgłasza błąd w przypadku niepowodzenia i zwraca skompilowany shader przy sukcesie.
Tworzenie programu (zestawu shaderów)
Połącz shader wierzchołków i shader fragmentów w jeden program.
To nazywa się 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}- Ten kod łączy shader wierzchołków i fragmentów w jeden program oraz sprawdza, czy można ich użyć do renderowania.
Przygotowywanie danych wierzchołków
Tutaj przygotujemy dane wierzchołków do narysowania trójkąta.
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]);- Układ współrzędnych w WebGL zawiera się w zakresie od
-1.0do1.0.
Tworzenie bufora i przesyłanie go do GPU
Następnie, aby dane wierzchołków były dostępne dla GPU, przesyłamy je za pomocą bufora.
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);- Ten kod tworzy bufor na dane wierzchołków i przesyła je do GPU.
Powiązanie atrybutu z buforem
Połącz a_position w shaderze z wcześniej utworzonym buforem.
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);Renderowanie
Na końcu użyj programu do wydania polecenia renderowania.
1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);- Po uruchomieniu tego programu pojawi się czerwony trójkąt.
Projektowanie klas z WebGL
WebGL to API imperatywne, więc kod może szybko stać się rozbudowany, jeśli pisze się go wprost. Dzięki projektowaniu klas można wyraźnie oddzielić inicjalizację, renderowanie i zarządzanie zasobami.
Tutaj wykorzystamy zalety TypeScript i stopniowo rozwiniemy projekt z myślą o separacji odpowiedzialności, możliwościach ponownego użycia i łatwości utrzymania.
Polityka projektowa (Minimalistyczna, ale praktyczna)
Klasa jest podzielona na następujące role.
GLAppzarządza ogólną inicjalizacją i renderowaniem.ShaderProgramzarządza shaderami.TriangleMeshzarządza danymi wierzchołków.
Najpierw stwórzmy klasę, która nadzoruje wszystko.
Klasa wejściowa aplikacji WebGL
GLApp zarządza elementem canvas i kontekstem WebGL oraz służy jako punkt wyjścia dla renderowania.
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}- Ta klasa zarządza procesami inicjalizacji i renderowania WebGL, a także odpowiada za rysowanie na ekranie przy użyciu shaderów i siatek (meszy).
- Oddzielenie metod
initializeirenderułatwia ponowną inicjalizację i dodawanie animacji w przyszłości.
Klasa do zarządzania shaderami
Następnie utwórz klasę do zarządzania shaderami, łącząc w jednym miejscu ich tworzenie, łączenie i używanie. Dzięki temu część renderująca skupia się tylko na ich 'używaniu'.
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}- Ta klasa jednoczy tworzenie, łączenie i używanie shaderów wierzchołków i fragmentów, umożliwiając bezpieczne używanie shaderów po stronie renderowania.
- Udostępniając
getAttribLocation, można bezpiecznie odwoływać się do niej ze strony obiektu mesh.
Klasa do zarządzania meshami (danymi wierzchołków)
Stworzymy także klasę do zarządzania danymi wierzchołków. Dzieląc klasy meshy według rysowanych obiektów, rozszerzanie staje się łatwiejsze.
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}- Ta klasa zarządza danymi wierzchołków trójkąta i poprzez powiązanie z shaderem obsługuje rzeczywiste renderowanie.
- Wszelkie informacje potrzebne do renderowania są zawarte w
TriangleMesh.
Punkt wejścia (Przykład użycia)
Na koniec połącz klasy, aby uruchomić aplikację.
1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();- Dzięki tej strukturze dodawanie animacji lub wielu meshów jest łatwe.
Praktyczne wskazówki dotyczące pisania WebGL w TypeScript
Użycie TypeScript z WebGL daje następujące korzyści:.
- Przekształcając procedury WebGL w klasy, można je uporządkować według ról, co ułatwia utrzymanie i rozbudowę.
- Oddzielając odpowiedzialności, takie jak renderowanie i zarządzanie shaderami, zwiększa się czytelność kodu.
- Dzięki użyciu podpowiedzi typów TypeScript można ograniczyć błędy w wywołaniach API WebGL lub określaniu parametrów.
Podsumowanie
Korzystając z TypeScript, nawet niskopoziomowe procesy WebGL można stabilnie obsługiwać dzięki bezpieczeństwu typów i strukturze. Poznając przepływ od minimalnej konfiguracji do renderowania oraz stosując projektowanie klas do oddzielenia ról takich jak inicjalizacja, renderowanie i zarządzanie zasobami, można zwiększyć czytelność i łatwość utrzymania. Wdrażając krok po kroku, możesz poznać WebGL jako praktyczną wiedzę, która nie jest czarną skrzynką i może być stosowana w prawdziwych projektach.
Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.