WebGL 是一项强大的技术。然而,对于前端开发人员来说,我们并不是很清楚在 HTML 画布中渲染图形。
在本指南中,我们将把 WebGL 分解为更简单的术语,并了解如何与 TypeScript 一起使用来渲染 Web 图形。要继续学习,您需要了解 HTML Canvas 和 TypeScript,安装 Node.js 和 TypeScript 编译器,并拥有支持 WebGL 的 IDE 和浏览器。
什么是 WebGL?
WebGL 是 Web 图形库的缩写,是一个 JavaScript API,用于在 Web 浏览器中创建硬件加速的 2D 和 3D 图形。
WebGL 基于嵌入式系统的开放图形库 (OpenGL ES 2.0),这是一个在嵌入式系统中渲染图形的 API。这些设备包括移动电话和其他资源受限的设备。
WebGL的简单示例
让我们看一个例子,以实际了解 WebGL。
在此示例中,您需要有空白 index.html
和 script.ts
文件。您还需要使用 tsc --init
该命令初始化 TypeScript 的项目。
首先,在 HTML 代码中创建一个 canvas
元素:
html
<!-- index.html -->
<canvas id="canvaselement" width="500" height="500"></canvas>
虽然我们将 TypeScript 与 WebGL 一起使用,但请记住,浏览器不会直接运行 TypeScript 代码。您需要将其编译为 JavaScript 才能运行它。因此,接下来,添加一个 script
标签,其源为 script.js
:
html
<!-- index.html -->
<script src="script.js"> </script>
之后,获取对 canvas
的 DOM 引用:
ts
// script.ts
const canvas = document.getElementById("canvaselement") as HTMLCanvasElement | null;
if (canvas === null) throw new Error("Could not find canvas element");
const gl = canvas.getContext("webgl");
if (gl === null) throw new Error("Could not get WebGL context");
确保将 DOM 引用强制转换为 HTMLCanvasElement | null
。否则,TypeScript 会认为它的类型是 HTMLElement | null
.当您尝试调用HTMLCanvasElement
中的方法时,这将导致问题。
您还需要处理 null
值的所有可能值,否则代码将无法编译。您可以通过在 tsconfig.json
文件中设置 compilerOptions.strict
为 false
来降低编译器的严格性。
接下来,将 WebGL 视口设置为以下大小:
ts
// script.ts
gl.viewport(0, 0, canvas.width, canvas.height);
最后,使用灰色清除画布视图:
ts
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT);
将 WebGL 与 TypeScript 一起使用时的注意事项
结合 WebGL 和 TypeScript 意味着您现在需要在构建项目时遵循两者的最佳实践。例如:
- 避免使用
any
类型:WebGL 应用程序可能会变得非常复杂,即使对于看似简单的操作也是如此。在代码中使用该any
类型意味着您将无法从 IDE 获得帮助。 - 不要忽略可能的
null
值:null
表示存在可能性,因为函数可能会遇到意外结果。当函数遇到意外结果时,它会返回一个null
值。如果忽略函数返回 null 值的可能性,则依赖于函数结果的其他部分代码可能会失败
以下最佳做法与渲染模型特别相关:
-
确保您的 WebGL 代码在不生成任何错误的情况下运行:WebGL 通过 JavaScript 的控制台报告它遇到的任何错误。但是在错误过多后,它会停止报告错误。此时捕获错误的唯一方法是通过 WebGL getError 方法。
-
避免使用
#ifdef GL_ES
:#ifdef
在 WebGL 中的工作方式与在 C 中的工作方式相同。它是一个预处理器,用于检查是否定义了宏。具体而言,#ifdef GL_ES
检查是否定义了GL_ES
宏。如果代码正在为着色器编译,则将定义此宏。由于将要编译的代码始终用于着色器,因此您不需要使用它,并且应避免使用它。 -
始终考虑系统限制:您能够创建在系统上运行完美的模型,但硬件较弱的系统可能会遇到困难
在考虑系统限制时,支持 WebGL 的系统必须满足以下安全:
yaml
MAX_CUBE_MAP_TEXTURE_SIZE: 4096
MAX_RENDERBUFFER_SIZE: 4096
MAX_TEXTURE_SIZE: 4096
MAX_VIEWPORT_DIMS: [4096,4096]
MAX_VERTEX_TEXTURE_IMAGE_UNITS: 4
MAX_TEXTURE_IMAGE_UNITS: 8
MAX_COMBINED_TEXTURE_IMAGE_UNITS: 8
MAX_VERTEX_ATTRIBS: 16
MAX_VARYING_VECTORS: 8
MAX_VERTEX_UNIFORM_VECTORS: 128
MAX_FRAGMENT_UNIFORM_VECTORS: 64
ALIASED_POINT_SIZE_RANGE: [1,100]
请注意,WebGL 有其缺点,尤其是与 WebGPU 相比。例如,WebGL 通常被认为比 WebGPU 更慢、更过时,后者使用更现代的 API 并支持更好的资源管理、并发和计算着色器。
然而,WebGL 有更好的浏览器支持、更大的社区和更多的可用资源,而 WebGPU 对浏览器的支持非常有限。
使用 WebGL 时需要了解的重要术语
您应该了解一些重要术语,以充分利用本指南。
Vertices(顶点)
您可以将顶点视为角。它们组合在一起勾勒出形状出外观。
一个正方形有四个角------左上角、右上角、左下角和右下角。当每个角与其相邻角的距离相等时,它们形成一个正方形。如果水平加宽它们,它们会形成一个矩形。
顶点的数量以及每个顶点之间的距离和角度共同决定了空间中几何对象的形状和大小。四个点可以组成一个正方形或矩形,三个点可以组成一个三角形。您可以移动三个顶点中的任何一个,但它们会形成一个外观不同的三角形。
WebGL 允许你通过顶点来描述每个点的位置,这是通过使用 x、y 和 z 坐标来实现的。
Indices(索引)
三角形是 WebGL 中模型的基本构建块。您可以组合多个三角形,创建任何其他形状或结构。
仅使用顶点来创建三角形,然后创建模型的形状,对您来说,会变得非常乏味,并且对计算机来说效率低下。索引是呈现复杂模型的更好方法。
索引如何帮助解决这个问题?索引是一个数组,连接三个顶点以形成三角形。换言之,您可以使用顶点绘制模型的形状,然后将三个顶点连接成三角形。
让我们看一下这个顶点数组:
arduino
[
-0.5, 0.5, // 0
-0.5, -0.5, // 1
0.5, -0.5, // 2
0.5, 0.5, // 3
]
在视觉上,我们希望它是这样的:
我们可以创建一个索引数组,将三个顶点组连接起来,形成三角形:
arduino
[
// Triangle 1
0, 1, 2,
// Triangle 2
0, 2, 3
];
两个三角形,共同形成一个正方形:
Shaders(着色)
着色器是简明的声明式代码,对于片段着色器,按像素执行,对于顶点着色器,按顶点执行。它们描述顶点和像素的特征,是图形渲染管道的一部分。WebGL 中的所有着色器都是用 OpenGL 着色语言 (GLSL) 编写的。
让我们仔细看看这两种类型的着色器。
Vertex shaders (顶点着色器)
GPU 对每个顶点运行一次顶点着色器。顶点着色器处理每个顶点的位置、纹理坐标和颜色。它们允许您变换和修改对象的几何图形。
下面是一个简单的顶点着色器示例:
GLSL
attribute vec3 coordinates;
void main(void) {
gl_Position = vec4( coordinates, 1.0 );
}
首先,第一行定义顶点着色器的属性。属性是一个入口点,您可以在其中传递顶点、颜色和其他每个顶点的数据。顶点着色器中可以有多个属性。例如:
GLSL
attribute vec3 coordinates;
attribute vec4 colors;
接下来,让我们讨论一下 main
函数。请记住,着色器是用 GLSL 编写的,GLSL 的结构与 C 编程语言类似。着色器从 main
函数开始运行。
然后是 gl_Position
,它表示放置每个顶点的最终位置。上面的顶点着色器不会以任何方式转换顶点。它只接受顶点的当前位置并将其传递给 gl_Position
,而不进行任何更改。
最后,让我们回顾一下 GLSL 中的三种向量类型:
vec2
用于存储二维向量vec3
用于存储三维向量vec4
用于存储四维向量
在上面的示例中,我们使用了 vec3
和 vec4
,这告诉我们我们正在使用三维和四维向量。
Fragment shaders (片段着色器)
片段着色器按像素执行,并处理每个像素的颜色、z 深度和透明度。顶点着色器会变换和设置对象的点,而片段着色器会更改对象的外观。
顶点着色器的输出将进入图形渲染管线中的另外两个阶段(基元装配,然后是光栅化),然后再进入片段着色器。开发人员通常不会与这些阶段进行交互,但让我们快速了解一下其中发生的情况。
在原始组装阶段,WebGL 一次连接三个顶点,形成所有的三角形:
然后,在光栅化阶段,WebGL 确定三角形应该在哪些像素上渲染。然后,片段着色器会为在光栅化阶段确定的像素着色。
片段着色器是用 GLSL 编写的,和顶点着色器一样。例如:
ts
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
在上面的代码中, gl_FragColor
是片段着色器的输出。由于片段着色器是按像素运行的, gl_FragColor
因此表示每个像素的颜色。
gl_FragColor
使用 RGBA 格式来表示颜色。要在 GLSL 中表示此格式,您需要使用 vec4
数据结构。在此数据结构中,这四个值按顺序表示红色、绿色和蓝色,以及 alpha 透明度。
Buffers (缓冲区)
缓冲区是一个内存块, 存储了各种数据。它将数据存储为连续数据的一维数组。
有不同种类的数组用于存储不同类型的数据。目前,您需要了解的是:
- 顶点缓冲区:用于存储顶点
- 索引缓冲区:用于存储索引
- 帧缓冲区:用于存储已渲染的像素颜色
在 WebGL 中渲染简单模型
现在,让我们看看如何在 WebGL 中渲染模型。为了简单起见,我将带您渲染 2D 三角形和正方形。
渲染三角形
正如我们之前所讨论的,三角形是 WebGL 中任何模型的基本构建块。让我们创建一个。
首先,我们需要获取 canvas
元素及其 WebGL 上下文:
ts
// script.ts
const canvas = document.getElementById("canvaselement") as HTMLCanvasElement | null;
if (canvas === null) throw new Error("Could not find canvas element");
const gl = canvas.getContext("webgl");
if (gl === null) throw new Error("Could not get WebGL context");
接下来,我们需要设置着色器,从顶点着色器开始:
ts
// script.ts
// Step 1: 创建一个顶点着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
if (vertexShader === null)
throw new Error("Could not establish vertex shader"); // handle possibility of null
// Step 2: 写顶点着色器代码
const vertexShaderCode = `
attribute vec2 coordinates;
void main(void) {
gl_Position = vec4(coordinates, 0.0, 1.0);
}
`;
// Step 3: 将着色器代码附加到顶点着色器上
gl.shaderSource(vertexShader, vertexShaderCode);
// Step 4: 编译顶点着色器
gl.compileShader(vertexShader);
现在,让我们转到片段着色器。它遵循与顶点着色器类似的过程:
ts
// script.ts
// Step 1: 创建一个片段着色器对象
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
if (fragmentShader === null)
throw new Error("Could not establish fragment shader"); // handle possibility of null
// Step 2: 编写片段着色器代码
const fragmentShaderCode = `
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
`;
// Step 3: 将着色器代码附加到片段着色器上
gl.shaderSource(fragmentShader, fragmentShaderCode);
// Step 4: 编译片段着色器
gl.compileShader(fragmentShader);
在此之后,下一步是将顶点和片段着色器链接到 WebGL 程序中,并将 WebGL 程序作为渲染管线的一部分启用:
ts
// script.ts
// Step 1: 创建一个WebGL程序实例
const shaderProgram = gl.createProgram();
if (shaderProgram === null) throw new Error("Could not create shader program");
// Step 2: 将顶点着色器和片段着色器附加到程序上
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// Step 3: 激活程序作为渲染管线的一部分
gl.useProgram(shaderProgram);
接下来,我们为三角形创建顶点数组,将其存储在顶点缓冲区中,并使顶点着色器的 coordinates
属性能够从顶点缓冲区接收顶点:
ts
// script.ts
// Step 1: 初始化我们三角形的顶点数组
const vertices = new Float32Array([0.5, -0.5, -0.5, -0.5, 0.0, 0.5]);
// Step 2: 创建一个新的缓冲对象
const vertex_buffer = gl.createBuffer();
// Step 3: 将对象绑定到 `gl.ARRAY_BUFFER`
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
// Step 4: 将顶点数组传递给 `gl.ARRAY_BUFFER`
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Step 5: 获取顶点着色器中 `coordinates` 属性的位置
const coordinates = gl.getAttribLocation(shaderProgram, "coordinates");
gl.vertexAttribPointer(coordinates, 2, gl.FLOAT, false, 0, 0);
// Step 6: 从顶点缓冲接收顶点数据
gl.enableVertexAttribArray(coordinates);
所有顶点都需要在 -1
to 1
范围内在画布上呈现。
最后,我们写下渲染三角形所需的最后一行:
ts
// script.ts
// Step 1: 为canvas中的WebGL设置视口
gl.viewport(0, 0, canvas.width, canvas.height);
// Step 2: 使用灰色填充画布
gl.clearColor(0.5, 0.5, 0.5, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// Step 3: 在画布上绘制模型
gl.drawArrays(gl.TRIANGLES, 0, 3);
如果一切顺利,那么应该 canvas
如下所示:
渲染正方形:方法 1
在 WebGL 中绘制正方形的过程与绘制三角形的过程类似。请记住,由于三角形是 WebGL 中模型的基本构建块,因此您可以通过将两个三角形放在一起来绘制正方形:
因此,让我们看看我们可以对代码进行哪些更改,以便我们可以呈现一个正方形。首先,让我们来看我们为三角形设置的 vertices
数组:
ts
const vertices = new Float32Array([0.5, -0.5, -0.5, -0.5, 0.0, 0.5]);
我们可以修改数组以包含两个三角形的顶点:
arduino
[
// Triangle 1
0.5, -0.5,
-0.5, -0.5,
-0.5, 0.5,
// Triangle 2
-0.5, 0.5,
0.5, 0.5,
0.5, -0.5,
]
但是,如果您运行此修改,您将只得到一个直角三角形:
这是因为 WebGL 只传递了三个顶点,而不是六个顶点。为了确保 WebGL 渲染了所有六个顶点,我们需要修改 script.ts
文件最后一行 drawArrays
的方法调用,如下所示:
ts
gl.drawArrays(gl.TRIANGLES, 0, 3);
我们将它的最后一个参数的值从 3
更改为 6
:
ts
gl.drawArrays(gl.TRIANGLES, 0, 6);
现在,你应该得到这样的东西:
渲染正方形:方法 2
构建正方形的另一种方法是通过索引(当您构建更复杂的结构时特别推荐)。
如果要使用索引来创建正方形,我们只需要进行一些修改和添加即可。除以下内容外,无需更改代码的任何部分。
首先要做的是修改 vertices
数组,使其仅包含正方形的四个顶点:
ts
const vertices = new Float32Array([
-0.5, 0.5, // 0
-0.5, -0.5, // 1
0.5, -0.5, // 2
0.5, 0.5, // 3
]);
接下来,将索引数组添加到三个顶点的分组中:
ts
const indices = new Uint16Array([
// Triangle 1
0, 1, 2,
// Triangle 2
0, 2, 3
]);
之后,准备一个索引缓冲区将 indices
数组传递到缓冲区中:
ts
// Step 1: 创建一个新的缓冲区对象
const index_buffer = gl.createBuffer();
// Step 2: 将缓冲区对象绑定到 `gl.ELEMENT_ARRAY_BUFFER`
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
// Step 3: 将缓冲区数据传递给 `gl.ELEMENT_ARRAY_BUFFER`
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
indices,
gl.STATIC_DRAW
);
您必须将前面添加的内容放在启用 coordinates
vertex 属性的上方,该属性应如下所示:
ts
const coordinates = gl.getAttribLocation(shaderProgram, "coordinates");
gl.vertexAttribPointer(coordinates, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(coordinates);
然后,您需要进行的最后修改是删除最后一行 drawArrays
的方法调用,并将其替换为以下函数调用:
ts
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
您应该在灰色画布内看到一个白色方块。