WebGL 的第一步
原文:https ://aralroca.com/blog/first-steps-in-webgl
在本文中,我们将了解什么是 WebGL,以及如何通过与图形处理单元 (GPU) 交互来绘制三角形。虽然这个简单的例子可以用更好的方法解决,例如使用带有 2D 上下文的画布,甚至使用 CSS,但我们的目标是从 WebGL 入手,就像“Hello World”一样,了解它的工作原理。
我们将介绍以下内容:
什么是 WebGL?
WebGL 的字面定义是“Web 图形库”。然而,它并不是一个提供简单易用的 API 的 3D 库,无法让我们像在这儿放个光源,在那儿放个摄像头,在这儿画个角色等等。
它处于底层,将顶点转换为像素。我们可以将 WebGL 理解为一个光栅化引擎。它基于 OpenGL ES 3.0 图形 API(WebGL 2.0,不同于基于 ES 2.0 的旧版本)。
网络上现有的 3D 库(例如THREE.js或Babylon.js)在下面使用了 WebGL。它们需要一种方式与 GPU 通信,以指示要绘制的内容。
这个例子也可以直接用 THREE.js 来解决,使用。你可以在这里THREE.Triangle
看到一个例子。然而,本教程的目的是理解其底层工作原理,即这些 3D 库如何通过 WebGL 与 GPU 通信。我们将在不使用任何 3D 库的情况下渲染一个三角形。
创建 WebGL 画布
为了绘制三角形,我们需要定义通过 WebGL 渲染的区域。
我们将使用 HTML5 的元素画布,以 的形式检索上下文webgl2
。
import { useRef, useEffect } from 'preact/hooks'
export default function Triangle() {
const canvas = useRef()
useEffect(() => {
const bgColor = [0.47, 0.7, 0.78, 1] // r,g,b,a as 0-1
const gl = canvas.current.getContext('webgl2') // WebGL 2.0
gl.clearColor(bgColor) // set canvas background color
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT) // clear buffers
// @todo: Render the triangle...
}, [])
return <canvas style={{ width: '100vw', height: '100vh' }} ref={canvas} />
}
该clearColor
方法使用 RGBA(值从 0 到 1)设置画布的背景颜色。
此外,该clear
方法会将缓冲区清空为预设值。使用的常量值将取决于您的 GPU 容量。
一旦我们创建了画布,我们就可以使用 WebGL 渲染内部三角形了...让我们看看如何操作。
顶点坐标
首先,我们需要知道所有这些向量的范围都是从 -1 到 1。
画布的角落:
- (0, 0) -中心
- (1, 1) - 右上角
- (1,-1) - 右下角
- (-1, 1) - 左上角
- (-1,-1) - 左下角
我们要绘制的三角形有以下三个点:
(-1,-1),(0,1)和(1,-1)。因此,我们将三角形坐标存储到一个数组中:
const coordinates = [-1, -1, 0, 1, 1, -1]
GLSL 和着色器
着色器是计算机图形学中用于计算渲染效果的一种程序,具有高度的灵活性。这些着色器使用 OpenGL ES 着色语言 (GLSL ES)(一种类似于 C 或 C++ 的语言)编写,并在 GPU 上运行。
我们要运行的每个 WebGL 程序都由两个着色器函数组成:顶点着色器和片段着色器。
几乎所有的 WebGL API 都是以不同的方式运行这两个函数(顶点和片段着色器)。
顶点着色器
顶点着色器的作用是计算顶点的位置。GPU 根据计算结果 ( gl_Position ) 来定位视口上的点、线和三角形。
为了写入三角形,我们将创建这个顶点着色器:
const vertexShader = `#version 300 es
precision mediump float;
in vec2 position;
void main () {
gl_Position = vec4(position.x, position.y, 0.0, 1.0); // x,y,z,w
}
`
我们现在可以将其作为模板字符串保存在我们的 JavaScript 代码中。
第一行(#version 300 es
)告诉我们正在使用的 GLSL 版本。
第二行(precision mediump float;
)决定 GPU 计算浮点数时使用的精度。可用选项包括highp
、mediump
和lowp
,但某些系统不支持highp
。
在第三行(in vec2 position;
)中,我们为GPU定义了一个二维的输入变量(X,Y)。三角形的每个向量都是二维的。
该main
函数在程序初始化后(类似 C/C++)启动时调用。GPU 将gl_Position = vec4(position.x, position.y, 0.0, 1.0);
通过将gl_Position
当前顶点的位置保存到 来运行其内容( )。第一个和第二个参数是 和x
,y
距离我们的vec2
位置。第三个参数是z
轴,在本例中是 ,0.0
因为我们是在二维空间中创建几何体,而不是三维空间。最后一个参数是w
,默认情况下应设置为1.0
。
GLSL 内部识别并使用的值gl_Position
。
一旦我们创建了着色器,我们就应该编译它:
const vs = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vs, vertexShader)
gl.compileShader(vs)
// Catch some possible errors on vertex shader
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(vs))
}
片段着色器
在“顶点着色器”之后,执行“片段着色器”。该着色器的作用是计算每个位置对应的每个像素的颜色。
对于三角形,让我们用相同的颜色填充:
const fragmentShader = `#version 300 es
precision mediump float;
out vec4 color;
void main () {
color = vec4(0.7, 0.89, 0.98, 1.0); // r,g,b,a
}
`
const fs = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fs, fragmentShader)
gl.compileShader(fs)
// Catch some possible errors on fragment shader
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(fs))
}
语法与上一个非常相似,尽管vect4
我们这里返回的 指的是每个像素的颜色。由于我们想用 填充三角形rgba(179, 229, 252, 1)
,因此我们将通过将每个 RGB 数字除以 255 来平移它。
从着色器创建程序
一旦我们编译了着色器,我们就需要创建运行 GPU 的程序,并添加两个着色器。
const program = gl.createProgram()
gl.attachShader(program, vs) // Attatch vertex shader
gl.attachShader(program, fs) // Attatch fragment shader
gl.linkProgram(program) // Link both shaders together
gl.useProgram(program) // Use the created program
// Catch some possible errors on program
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program))
}
创建缓冲区
我们将使用缓冲区为 GPU 分配内存,并将该内存绑定到用于 CPU-GPU 通信的通道。我们将使用此通道将三角形坐标发送到 GPU。
// allowcate memory to gpu
const buffer = gl.createBuffer()
// bind this memory to a channel
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
// use this channel to send data to the GPU (our triangle coordinates)
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(coordinates),
// In our case is a static triangle, so it's better to tell
// how are we going to use the data so the WebGL can optimize
// certain things.
gl.STATIC_DRAW
)
// desallocate memory after send data to avoid memory leak issues
gl.bindBuffer(gl.ARRAY_BUFFER, null)
将数据从 CPU 链接到 GPU
在顶点着色器中,我们定义了一个名为的输入变量position
。但是,我们尚未指定此变量应采用通过缓冲区传递的值。我们必须按以下方式指定它:
const position = gl.getAttribLocation(program, 'position')
gl.enableVertexAttribArray(position)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.vertexAttribPointer(
position, // Location of the vertex attribute
2, // Dimension - 2D
gl.FLOAT, // Type of data we are going to send to GPU
gl.FALSE, // If data should be normalized
0, // Stride
0 // Offset
)
绘制三角形
一旦我们为三角形创建了带有着色器的程序,并创建了链接缓冲区以将数据从 CPU 发送到 GPU,我们最终就可以告诉 GPU 渲染三角形!
gl.drawArrays(
gl.TRIANGLES, // Type of primitive
0, // Start index in the array of vector points
3 // Number of indices to be rendered
)
此方法根据数组数据渲染图元。图元可以是点、线或三角形。我们来指定gl.TRIANGLES
。
所有代码放在一起
我已将文章代码上传至 CodeSandbox,以便您探索。
结论
WebGL 只能绘制三角形、直线或点,因为它只能进行光栅化,所以你只能做矢量能做的事情。这意味着 WebGL 的概念很简单,但处理过程却相当复杂……而且根据你想要开发的内容,它会变得越来越复杂。光栅化一个二维三角形和处理一个包含纹理、变量、变换等元素的 3D 电子游戏是不一样的……
希望本文能帮助您理解 WebGL 的工作原理。建议您阅读以下参考资料。
参考
- https://webglfundamentals.org
- https://webgl2fundamentals.org/
- https://developer.mozilla.org/es/docs/Web/API/WebGL_API/Tutorial/
- https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices
- http://vispy.org/modern-gl.html
- https://github.com/subhasishdash/webgliinternals