WebGL em TypeScript

WebGL em TypeScript

Este artigo explica o WebGL em TypeScript.

Vamos apresentar o conceito de WebGL, sua configuração mínima, renderização, extensões e design de classes passo a passo com código de exemplo.

YouTube Video

WebGL em TypeScript

WebGL é uma API de baixo nível que permite manipular a GPU diretamente no navegador.

Ao usar TypeScript, você pode reduzir significativamente os 'erros de implementação' em WebGL através da segurança de tipos, autocompletar de código e programação estruturada.

O TypeScript é especialmente eficaz nos seguintes pontos:.

  • Os tipos dos objetos WebGL se tornam claros.
  • Os erros ao manipular variáveis dos shaders podem ser reduzidos.
  • Fica mais fácil acompanhar a estrutura.

Configuração mínima do WebGL

A seguir estão os requisitos mínimos para renderização com WebGL:.

  • Elemento <canvas>
  • Contexto do WebGL
  • Shader de vértice
  • Shader de fragmento

Primeiro, vamos criar um estado onde a tela pode ser inicializada.

Obtendo o Canvas e o Contexto WebGL

Primeiro, obtenha o elemento canvas e o WebGLRenderingContext.

Aqui, usamos codificação em TypeScript com ênfase em segurança contra 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}
  • Neste ponto, gl se torna o ponto de entrada para todas as operações do WebGL.

Limpando a tela para confirmar que o WebGL está funcionando

Antes de renderizar, verifique se é possível preencher a cor de fundo.

Esta é a verificação inicial para garantir que o WebGL está funcionando corretamente.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Até este ponto, o canvas será preenchido com uma cor cinza escura.

O que é um shader?

No WebGL, o processo de desenho é descrito usando uma linguagem especial chamada GLSL. No GLSL, você prepara principalmente dois tipos de shaders: vertex shaders e fragment shaders.

Primeiro, vamos criar um 'shader mínimo funcional' usando esses dois tipos de shaders.

Escrevendo o shader de vértice

Vertex shaders determinam as posições dos pontos ou figuras a serem desenhados.

O código a seguir coloca um ponto único no centro da tela.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position é a coordenada passada do lado do JavaScript.

Escrevendo o shader de fragmento

Fragment shaders determinam as cores que aparecem na tela.

Desta vez, ele sempre produz a cor vermelha.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • precision deve ser sempre especificado no WebGL.

Criando uma função para compilar shaders

Como o processo de criação de shaders é sempre o mesmo, vamos transformá-lo em uma função.

 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 função cria um shader do tipo especificado, compila o código-fonte, lança um erro se falhar e retorna o shader compilado em caso de sucesso.

Criando um programa (um conjunto de shaders)

Combine o shader de vértice e o shader de fragmento em um só.

Isso é chamado de 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 liga o shader de vértice e o shader de fragmento em um único programa e verifica se eles podem ser usados para renderização.

Preparando dados de vértice

Aqui, vamos preparar os dados dos vértices para desenhar um 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]);
  • O sistema de coordenadas no WebGL vai de -1.0 a 1.0.

Criando um buffer e transferindo para a GPU

Em seguida, para tornar o array de dados dos vértices utilizável pela GPU, transferimos os dados usando um 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);
  • Este código cria um buffer para armazenar os dados de vértice e transfere seu conteúdo para a GPU.

Associando o atributo ao buffer

Conecte o a_position no shader ao buffer criado anteriormente.

 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);

Renderização

Por fim, use o programa para emitir o comando de renderização.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • Quando você rodar este programa, um triângulo vermelho será exibido.

Design de classes com WebGL

O WebGL é uma API imperativa, portanto o código pode rapidamente ficar inchado se escrito como está. Através do design de classes, é possível separar claramente inicialização, renderização e gerenciamento de recursos.

Aqui, vamos aproveitar as vantagens do TypeScript e evoluir progressivamente o design para focar em separação de responsabilidades, reutilização e manutenibilidade.

Política de Design (Mínima, mas Prática)

A turma é dividida nos seguintes papéis.

  • GLApp controla a inicialização geral e a renderização.
  • ShaderProgram gerencia os shaders.
  • TriangleMesh gerencia os dados de vértice.

Primeiro, vamos criar a classe que controla tudo.

Classe de entrada para uma aplicação WebGL

GLApp gerencia o canvas e o contexto WebGL, servindo como o ponto de partida para a renderização.

 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 classe gerencia os processos de inicialização e renderização do WebGL e é responsável por desenhar na tela usando shaders e malhas.
  • Ao separar initialize e render, fica mais fácil dar suporte à reinicialização e à animação no futuro.

Classe para gerenciar shaders

Depois, crie uma classe para gerenciar os shaders, consolidando a criação, ligação e uso deles em um só lugar. Isso permite que o lado da renderização foque apenas em 'usá-los'.

 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 classe unifica a criação, o link e o uso dos shaders de vértice e fragmento, permitindo que o lado da renderização utilize os shaders com segurança.
  • Ao expor getAttribLocation, ele pode ser referenciado com segurança pelo lado da malha.

Classe para gerenciar malhas (dados de vértice)

Também iremos criar uma classe para gerenciar os dados dos vértices. Dividindo as classes de malha por objeto a ser desenhado, fica fácil expandir.

 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 classe gerencia os dados de vértice do triângulo e, vinculando-os ao shader, realiza a renderização real.
  • Todas as informações necessárias para a renderização estão contidas em TriangleMesh.

Ponto de entrada (exemplo de uso)

Por fim, combine as classes para iniciar o aplicativo.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Com essa estrutura, adicionar animações ou múltiplas malhas fica fácil.

Dicas práticas para escrever WebGL em TypeScript

Usar TypeScript com WebGL oferece as seguintes vantagens:.

  • Ao converter procedimentos WebGL em classes, eles podem ser organizados por função, facilitando a manutenção e a extensão.
  • Ao separar responsabilidades como renderização e gerenciamento de shaders, a legibilidade do código melhora.
  • Utilizando a conclusão de tipos do TypeScript, você pode reduzir erros ao chamar APIs WebGL ou ao especificar parâmetros.

Resumo

Ao usar TypeScript, mesmo processos WebGL de baixo nível podem ser tratados de forma estável com segurança de tipos e estrutura. Ao entender o fluxo desde a configuração mínima até a renderização, e aplicar design de classes para separar funções como inicialização, renderização e gerenciamento de recursos, você pode melhorar a legibilidade e a manutenção. Ao implementar passo a passo, você pode aprender WebGL como conhecimento prático, que não é uma caixa preta e pode ser aplicado em trabalhos reais.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video