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.0到1.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的初始化和渲染流程,并使用着色器和网格在屏幕上绘制。
- 通过分离
initialize和render,后续更容易支持重新初始化和动画。
管理着色器的类
接着,创建一个用于管理着色器的类,将着色器的创建、链接和使用集中到一个地方。这样渲染部分就只需关注“使用”即可。
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频道。