TypeScript中的WebGL

TypeScript中的WebGL

本文将介绍如何在TypeScript中使用WebGL。

我们将通过示例代码逐步介绍WebGL的基本概念、最小配置、渲染、扩展以及类的设计。

YouTube Video

TypeScript中的WebGL

WebGL 是一个低级API,可以让你在浏览器中直接操作GPU。

通过使用 TypeScript,你可以通过类型安全、代码补全和结构化编码,大大减少在WebGL中的“实现错误”。

TypeScript在以下方面特别有效:。

  • WebGL对象的类型变得明确。
  • 处理着色器变量时的错误可以减少。
  • 结构变得更容易追踪。

WebGL的最小配置

以下是使用WebGL进行渲染的最低要求:。

  • <canvas> 元素
  • WebGL 上下文
  • 顶点着色器
  • 片元着色器

首先,让我们创建一个可以初始化屏幕的状态。

获取Canvas和WebGL上下文

首先,获取canvas元素和WebGLRenderingContext

在这里,我们使用TypeScript代码,强调空安全

 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}
  • 到这一步,gl就成为了所有WebGL操作的入口。

清屏以确认WebGL正在工作

在渲染之前,检查是否可以填充背景色。

这是用于确认WebGL是否正常工作的初步验证。

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • 到目前为止,canvas会被填充为深灰色。

什么是着色器?

在WebGL中,绘图过程是用一种叫做GLSL的特殊语言来编写的。在 GLSL 中,你主要需要准备两种着色器:顶点着色器片段着色器

首先我们将用这两种着色器创建一个“最小可用的着色器”。

编写顶点着色器

顶点着色器决定要绘制的点或图形的位置。

下面是一段简单代码,它把一个点放在屏幕中央。

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position是从JavaScript端传递过来的坐标。

编写片元着色器

片元着色器决定显示在屏幕上的颜色。

这里它总是输出红色。

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • 在WebGL中必须始终指定precision

创建一个编译着色器的函数

由于着色器的创建处理总是一样,我们把它做成一个函数。

 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}
  • 该函数会创建指定类型的着色器,编译源码,失败时报错,成功则返回已编译的着色器。

创建程序(着色器组)

将顶点着色器和片元着色器合并为一个程序。

这被称为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}
  • 此代码将顶点着色器与片元着色器链接为一个程序,并检查它们是否可以用于渲染。

准备顶点数据

这里,我们将准备用于绘制三角形的顶点数据。

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]);
  • WebGL的坐标系统范围是-1.01.0

创建缓冲区并传输到GPU

接下来,为了让 GPU 能够使用顶点数据数组,我们通过缓冲区传递数据。

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);
  • 这段代码创建一个缓冲区用于存储顶点数据,并把内容传输到GPU。

将属性与缓冲区关联

将着色器中的a_position与之前创建的缓冲区关联起来。

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

渲染

最后,使用程序发起渲染指令。

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • 运行该程序时会显示一个红色三角形

WebGL的类设计

WebGL是一个命令式API,如果直接编写代码,很快就会变得臃肿。通过类的设计,可以清楚地划分初始化、渲染、资源管理等环节。

在这里,我们将利用TypeScript的优势,逐步演化设计,重点关注关注点分离、可复用性、可维护性

设计方针(最简但实用)

班级被分为以下角色。

  • GLApp 控制整体初始化和渲染。
  • ShaderProgram 管理着色器。
  • TriangleMesh 管理顶点数据。

首先,让我们创建一个管理全部的类。

WebGL应用的入口类

GLApp 管理canvas和WebGL上下文,并作为渲染的起点。

 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}
  • 此类负责管理WebGL的初始化和渲染流程,并使用着色器和网格在屏幕上绘制。
  • 通过分离initializerender,后续更容易支持重新初始化和动画。

管理着色器的类

接着,创建一个用于管理着色器的类,将着色器的创建、链接和使用集中到一个地方。这样渲染部分就只需关注“使用”即可。

 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}
  • 该类统一管理顶点与片元着色器的创建、链接和使用,使渲染部分安全地使用着色器。
  • 通过暴露getAttribLocation,可以让网格类安全地引用。

用于管理网格(顶点数据)的类

我们还将创建一个类来管理顶点数据。将网格类按绘制对象区分后便于扩展。

 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}
  • 此类管理三角形的顶点数据,并通过与着色器的关联,实现实际的渲染。
  • 所有渲染所需的信息都包含在TriangleMesh类中。

入口点(使用示例)

最后,组合这些类以启动应用程序。

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • 采用这种结构,添加动画或多个网格也变得容易。

使用TypeScript编写WebGL的实用技巧

TypeScript与WebGL结合使用有如下优势:。

  • 将WebGL流程转为类后,可以按职责组织,便于维护和扩展。
  • 将渲染、着色器管理等职责分离后,提升了代码的可读性。
  • 利用TypeScript的类型补全,可以减少调用WebGL API或指定参数时的错误。

总结

通过TypeScript,即使是底层的WebGL流程,也能依靠类型安全和结构化来稳定处理。理解从最小配置到渲染的流程,并通过类的设计将初始化、渲染、资源管理等角色分离,可以提升可读性和可维护性。通过一步步实现,可以将WebGL学成实用知识,而不是一个黑盒,并能应用到实际工作中。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video