WebGL en TypeScript
Este artículo explica WebGL en TypeScript.
Presentaremos el concepto de WebGL, su configuración mínima, renderizado, extensiones y diseño de clases paso a paso con código de ejemplo.
YouTube Video
WebGL en TypeScript
WebGL es una API de bajo nivel que permite manipular la GPU directamente en el navegador.
Al utilizar TypeScript, puedes reducir en gran medida los 'errores de implementación' en WebGL gracias a la seguridad de tipos, la autocompletación del código y una codificación estructurada.
TypeScript es especialmente efectivo en los siguientes puntos:.
- Los tipos de objetos de WebGL se vuelven claros.
- Se pueden reducir los errores al manejar las variables del shader.
- Se vuelve más fácil rastrear la estructura.
Configuración mínima de WebGL
Los siguientes son los requisitos mínimos para renderizar con WebGL:.
- Elemento
<canvas> - Contexto de
WebGL - Shader de vértices
- Shader de fragmentos
Primero, creemos un estado donde la pantalla pueda ser inicializada.
Obteniendo el Canvas y el Contexto WebGL
Primero, obtén el canvas y el WebGLRenderingContext.
Aquí usamos la programación TypeScript con énfasis en la seguridad de nulos.
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}- En este punto,
glse convierte en el punto de entrada para todas las operaciones de WebGL.
Limpiando la pantalla para confirmar que WebGL está funcionando
Antes de renderizar, verifica si se puede rellenar el color de fondo.
Esta es la comprobación inicial para verificar que WebGL funciona correctamente.
1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);- Hasta este punto, el canvas se rellenará con un color gris oscuro.
¿Qué es un shader?
En WebGL, los procesos de dibujo se describen utilizando un lenguaje especial llamado GLSL. En GLSL, principalmente preparas dos tipos de shaders: shaders de vértices y shaders de fragmentos.
Primero, crearemos un 'shader funcional mínimo' usando estos dos shaders.
Escribiendo el shader de vértices
Los shaders de vértices determinan las posiciones de los puntos o figuras a dibujar.
El siguiente es un código simple que coloca un solo punto en el centro de la pantalla.
1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5 gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;a_positiones la coordenada pasada desde el lado de JavaScript.
Escribiendo el shader de fragmentos
Los shaders de fragmentos determinan los colores que aparecen en la pantalla.
En este caso, siempre produce rojo.
1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;- En WebGL, siempre debe especificarse
precision.
Creando una función para compilar shaders
Como el proceso de creación de shaders es siempre el mismo, lo convertimos en una función.
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}- Esta función crea un shader del tipo especificado, compila el código fuente, lanza un error si falla y devuelve el shader compilado si tiene éxito.
Creando un programa (un conjunto de shaders)
Combina el shader de vértices y el de fragmentos en uno solo.
Esto se llama 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}- Este código enlaza el shader de vértices y el de fragmentos en un solo programa y comprueba si pueden ser usados para renderizar.
Preparando los datos de los vértices
Aquí, prepararemos los datos de los vértices para dibujar un triángulo.
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]);- El sistema de coordenadas en WebGL va de
-1.0a1.0.
Creando un buffer y transfiriéndolo a la GPU
A continuación, para que el array de datos de los vértices pueda ser usado por la GPU, transferimos los datos usando un búfer.
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);- Este código crea un buffer para almacenar los datos de los vértices y transfiere su contenido a la GPU.
Asociando el atributo con el buffer
Conecta a_position en el shader con el buffer creado previamente.
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);Renderización
Finalmente, utiliza el programa para emitir el comando de renderización.
1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);- Cuando ejecutes este programa, se mostrará un triángulo rojo.
Diseño de clases con WebGL
WebGL es una API imperativa, por lo que el código puede volverse rápidamente voluminoso si se escribe tal cual. Gracias al diseño de clases, puedes separar claramente la inicialización, el renderizado y la gestión de recursos.
Aquí, aprovecharemos las ventajas de TypeScript y evolucionaremos el diseño paso a paso para centrarnos en la separación de responsabilidades, la reutilización y el mantenimiento.
Política de diseño (Mínima pero Práctica)
La clase se divide en los siguientes roles.
GLAppcontrola la inicialización general y el renderizado.ShaderProgramgestiona los shaders.TriangleMeshgestiona los datos de vértices.
Primero, creemos la clase que lo controla todo.
Clase principal para una aplicación WebGL
GLApp gestiona el canvas y el contexto WebGL y sirve como el punto de partida para el renderizado.
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}- Esta clase gestiona los procesos de inicialización y renderizado de WebGL y es responsable de dibujar en la pantalla usando shaders y mallas.
- Al separar
initializeyrender, es más fácil soportar la reinicialización y la animación en el futuro.
Clase para la gestión de shaders
Luego, crea una clase para gestionar los shaders, consolidando la creación, vinculación y uso de los shaders en un solo lugar. Esto permite que la parte de renderizado se centre solo en 'usarlos'.
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}- Esta clase unifica la creación, vinculación y uso de los shaders de vértices y fragmentos, lo que permite que el lado de renderizado los use de forma segura.
- Al exponer
getAttribLocation, puede ser referenciado de manera segura desde el lado de la malla.
Clase para gestionar mallas (datos de vértices)
También crearemos una clase para gestionar los datos de los vértices. Dividiendo las clases de mallas según el objeto a dibujar, es fácil extenderlo.
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}- Esta clase gestiona los datos de los vértices del triángulo y, al vincularlo con el shader, se encarga del renderizado real.
- Toda la información necesaria para el renderizado se encuentra dentro de
TriangleMesh.
Punto de entrada (Ejemplo de uso)
Finalmente, combina las clases para lanzar la aplicación.
1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();- Con esta estructura, agregar animaciones o múltiples mallas se vuelve fácil.
Consejos prácticos para escribir WebGL en TypeScript
Usar TypeScript con WebGL ofrece las siguientes ventajas:.
- Al convertir los procedimientos de WebGL en clases, se pueden organizar por roles, lo que facilita el mantenimiento y la extensión.
- Al separar responsabilidades como el renderizado y la gestión de shaders, la legibilidad del código mejora.
- Al utilizar la autocompletación de tipos de TypeScript, puedes reducir los errores al llamar las APIs de WebGL o al especificar parámetros.
Resumen
Al usar TypeScript, incluso los procesos de bajo nivel de WebGL pueden manejarse de manera estable gracias a la seguridad de tipos y la estructura. Al comprender el flujo desde la configuración mínima hasta el renderizado, y al aplicar el diseño de clases para separar roles como inicialización, renderizado y gestión de recursos, puedes mejorar la legibilidad y el mantenimiento. Al implementar paso a paso, puedes aprender WebGL como un conocimiento práctico que no es una caja negra y se puede aplicar en trabajos reales.
Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.