WebGL 是 Web 3D 渲染引擎的基础,它作为非常底层的 API,学习上手难度非常大,这是因为 WebGL 要求的背景知识比较多。而网上的教程一般没有过多介绍直接就介绍 API 开始渲染了,容易让人云里雾里,很容易被劝退,就算学到了 API 使用,也是只懂表面知识,没有了解背后的原理,很容易就忘记了。
《从 0 实现 3D 渲染引擎》系列教程将从最基本知识开始,渐进的讲解 WebGL 使用和 WebGL 背后原理还有必不可少的数学知识,真正的从 0 开始,只要了解 JS 就行,不需要要任何图形学或者数学基础。学完之后除了能够自己从 0 实现自己的 3D 渲染引擎还能熟悉 three.js 的源码,因为本系列文章实现的 3D 渲染引擎和 three.js 很相似。
什么是 WebGL?
WebGL(Web Graphics Library)是一个 Web 标准 JavaScript API,通过 HTML5 的 canvas 元素进行暴露,无需使用插件,即可在浏览器中渲染高性能的交互式 3D 和 2D 图形。目前是由非营利 Khronos Group 设计和维护。
使用 WebGL 的方式和 canvas 2d 类似,都是通过 getContext
方法获取渲染上下文,如下所示。
const canvas = document.createElement('canvas')const gl = (
canvas.getContext('webgl2') ||
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl')
)
上面代码中是按照 webgl2
、webgl
、experimental-webgl
的顺序获取 WebGL 渲染上下文。webgl2
是最新版本,它几乎完全兼容 WebGL1。experimental-webgl
用来兼容老浏览器,如 IE 11。
兼容性
大多数浏览器都支持 WebGL1。也有很多现代浏览器支持 WebGL2,但是苹果还不支持 WebGL2,所以编写 WebGL 程序时,需要向下降级到 WebGL1。
OpenGL
在深入 WebGL 之前,我们还需要先了解 OpenGL,因为 WebGL 是基于 OpenGL 的。OpenGL(Open Graphics Library) 是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口,常用于CAD、虚拟现实、科学可视化程序和电子游戏开发。OpenGL 通常是显卡生产商根据规范来实现的。
OpenGL 前身是 SGI 的 IRIS GL API 它在当时被认为是最先进的科技并成为事实上的行业标准,后由 SGI 转变为一项开放标准 OpenGL。1992年 SGI 创建 OpenGL架构审查委员会,2006年将 OpenGL API 标准的控制权交给 Khronos Group。
OpenGL 是跨平台的,在移动设备上是使用 OpenGL ES(OpenGL for Embedded Systems), 它是 OpenGL 的子集。下图展示了 OpenGL 和 OpenGL ES 的时间线。
WebGL 基于 OpenGL,是 OpenGL 的子集。WebGL1 基于 OpenGL ES 2.0。WebGL2 基于 OpenGL ES 3.0。
GPU
WebGL 性能高的原因是它使用到了 GPU。GPU 和 CPU 针对的是两种不同的应用场景,大家可以把 CPU 想象为一个切图专家,而 GPU 是一群初级切图仔,现在有一大堆非常简单的页面,大街上随便抓个人都能切。那么对于这个任务不用想就知道一群初级切图仔更快,切图专家当然厉害,但是也奈不了对面人多。所以对于大量简单计算 GPU 的执行速度是远大于 CPU 的。
上面图片中,第一个是 CPU,第二是 GPU,CPU 只有一杆枪,而 GPU 有一大捆枪。CPU 要一下一下的打,就像切图专家一个一个的切,而 GPU 一次性全打了,就像一群初级切图仔,没人切一个,一次性全切完了。
上图是显卡 3090 的配置参数,我们可以看到它有 1 万多个核心,24G 显存。支持 3D API,DirectX 12 Ultimate 和 OpenGL 4.6 (DirectX 是微软的图形 API)。
坐标系
WebGL 的坐标系和 canvas 2d 中的是不太一样的。因为 WebGL 是 OpenGL 子集,所以 WebGL 坐标系和 OpenGL 坐标系性值一样。
canvas 2d 中的坐标系如下所示。
代码语言:javascript复制const canvas = document.createElement('canvas')const ctx = canvas.getContext('2d')
canvas 2d 的坐标原点在左上角,X 轴和 Y 轴的正值分别向右和向下。
而 WebGL 的坐标系和 OpenGL 一样,它更符合我们的常识一点。
原点在正中间,右边为 X 轴正方向,上面为 Y 轴正方向,就和数学中的一样。
需要注意的是 WebGL 中坐标值的范围是 -1
到 1
,而 canvas 2d 是根据 canvas 的宽高大小来的。如果 canvas 宽度为 500
,那么 WebGL 中的 1
就相当于 500
,0.5
就相当于 250
,这样的好处是我们无需关心 canvas 的宽高,无论 canvas 多大对于渲染图形来说范围都是 -1
到 1
。
当然 WebGL 中还有一个 Z 轴。Z 轴有两种形式,一种是正值朝外,另一种是正值朝内。
当 Z 轴正值朝外时,我们称为右手坐标系,当 Z 轴正值朝内时称为左手坐标系。可以伸出双手像下图一样比划下,就知道为什么称为左手坐标系和右手坐标系了。
那么 WebGL 是左手坐标系还是右手坐标系呢?答案为都不是。但是在实际开发中是使用 右手坐标系,当然并不是右手坐标系比左手坐标系好,而是右手坐标系是 OpenGL 的惯例。例如微软的 DirectX 中惯用的是左手坐标系。
这里为什么说 WebGL 既不是左手坐标系也不是右手坐标系,原因将在后续文章中讲解,现在只用知道 WebGL 中使用的是右手坐标系,也就是 Z 轴正值朝外。
三角形
WebGL 算是比较底层的图形 API,不同于 canvas 2d,WebGL 只能用它来渲染点,线和三角形。那些复杂的 3D 模型其实都是由一个个三角形组成。
比如上方这辆汽车模型,它其实是由 267300 个三角形组成。
点击这个链接查看模型详情https://sketchfab.com/3d-models/the-argonaut-4982efe9a03e42e6a867c33afd863ca5 。
可能有同学会问了,为什么就是三角形,而不是 5 边形,6 边形呢?
因为三角形有很多的优势,比如三角形一定在一个平面上,任何多边形都可以使用三角形组成等性值。
渲染一个三角形
了解了这么多背景知识,现在让我们来实际使用 WebGL 来渲染一个最简单的三角形吧。
代码语言:javascript复制const canvas = document.createElement('canvas')
canvas.width = canvas.height = 300document.body.append(canvas)const gl = canvas.getContext('webgl')
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)// 设置 webgl 视口,将 -1 到 1 映射为 canvas 上的坐标const vertexShader = gl.createShader(gl.VERTEX_SHADER) // 创建一个顶点着色器gl.shaderSource(vertexShader, `
attribute vec4 a_position;
void main() {
gl_Position = a_position; // 设置顶点位置
}
`) // 编写顶点着色器代码gl.compileShader(vertexShader) // 编译着色器const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) // 创建一个片元着色器gl.shaderSource(fragmentShader, `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color; // 设置片元颜色
}
`) // 编写片元着色器代码gl.compileShader(fragmentShader) // 编译着色器const program = gl.createProgram() // 创建一个程序gl.attachShader(program, vertexShader) // 添加顶点着色器gl.attachShader(program, fragmentShader) // 添加片元着色器gl.linkProgram(program) // 连接 program 中的着色器gl.useProgram(program) // 告诉 webgl 用这个 program 进行渲染const colorLocation = gl.getUniformLocation(program, 'u_color') // 获取 u_color 变量位置gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1) // 设置它的值const positionLocation = gl.getAttribLocation(program, 'a_position')
// 获取 a_position 位置const positionBuffer = gl.createBuffer()
// 创建一个顶点缓冲对象,返回其 ID,用来放三角形顶点数据,gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0, 0.5, 0.5, 0,
-0.5, -0.5]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据gl.STATIC_DRAW // 表示缓冲区的内容不会经常更改)// 将顶点数据加入的刚刚创建的缓存对象gl.enableVertexAttribArray(positionLocation);// 开启 attribute 变量,使顶点着色器能够访问缓冲区数据gl.vertexAttribPointer( // 告诉 OpenGL 如何从 Buffer 中获取数据
positionLocation, // 顶点属性的索引
2, // 组成数量,必须是1,2,3或4。我们只提供了 x 和 y
gl.FLOAT, // 每个元素的数据类型
false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效
0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让OpenGL决定具体步长
0 // offset 字节偏移量,必须是类型的字节长度的倍数。)
gl.clearColor(0, 1, 1, 1) // 设置清空颜色缓冲时的颜色值gl.clear(gl.COLOR_BUFFER_BIT) // 清空颜色缓冲区,也就是清空画布gl.drawArrays( // 从数组中绘制图元
gl.TRIANGLES, // 渲染三角形
0, // 从数组中哪个点开始渲染
3 // 需要用到多少个点,三角形的三个顶点)
渲染结果如下所示。
可以发现 WebGL 的代码非常复杂繁琐,一个非常简单的三角形就需要编写这么多的代码。
上面实例代码中有详细的注释,不过相信大家看了也还是满头问号。我们再来看看 WebGL 渲染的整个流程,一般 WebGL 程序是 JS 提供数据(在 CPU 中运行),然后将数据发送到显存中,交给 GPU 渲染,我们可以使用着色器控制 GPU 渲染管线部分阶段。
代码语言:javascript复制// CPUconst vertexShader = `shader source code` // 顶点着色器代码const fragmentShader = `shader source code` // 片段着色器代码const points = [{ x: 1, y: 1, z: 1 }/* ... */] // 准备数据gl.draw(points, vertexShader, fragmentShader) // 将数据和着色器发送给 GPU// GPUconst positions = data.map(point => vertexShader(point)) // 运行顶点着色器const frags = Rasterization(positions) // 光栅化const colors = frags.map(frag => fragmentShader(frag)) // 运行片段着色器Display(colors) // 渲染到屏幕
上面的伪代码,简单展示了 WebGL 程序的执行流程。OpenGL 中着色器是使用 GLSL 编写,WebGL 中也是使用的 GLSL 着色器语言,它的语法有点类似 C 语言,我们可以通过顶点着色器和片段着色器控制 GPU 渲染的部分环节。
WebGL 中有两个着色器分别是顶点着色器和片段(也可称为“片元”)着色器。顶点着色器用于处理图形的每个点,也就是上面例子中三角形的三个顶点。处理完毕后会进行光栅化,大家可以把光栅化理解成把图形变成一个个像素,我们显示器屏幕是一个个像素组成的,要显示图形就需要计算出图形中的每个像素点。片段着色器可以先理解成像素着色器,也就是将光栅化中的每个像素拿过来,给每个像素计算一个颜色。整个流程如下所示。
上图中顶点数据传送给 GPU 后,顶点着色器计算出每个点的位置,光栅化计算出图形的每个像素,片段着色器计算出每个像素的颜色,然后就可以渲染到显示器上了。(可以忽略上图的几何着色器,WebGL 中没有这个着色器)着色器先简单介绍到这里,还不了解着色器也没有关系,下篇文章会更加详细的讲解。
其实 WebGL 是一个非常大的状态机,它提供的方法都是改变 WebGL 的某个状态。我们需要在 CPU 中使用 JS 设置 WebGL 的状态,准备数据和着色器程序,然后发送给 GPU 执行。
上方代码可以分为如下几步。
- 因为 WebGL 的坐标是 -1 到 1,所以首先我们使用 viewport 设置视口大小信息。
- 创建顶点和片段着色器(关于着色器情况下篇文章),然后创建一个程序,来连接顶点和片段着色器。
- 然后获取着色器中的变量,设置如何将值传递给着色器。三角形是由 3 个顶点组成,所以准备了 3 个点的坐标。
- 设置清屏颜色,并清屏,和坐标类似,WebGL 中的颜色是 0 到 1,而不是 0 到 255。
- 将数据发送给 GPU 来渲染三角形
例子
上面这个简单的三角形一点都不炫酷,其实 WebGL 可以做出非常炫酷的效果,下面列举一些不错例子,大家感兴趣可以看一看。
ThreeJS
https://threejs.org/
WebGL Samples
http://webglsamples.org/
Experiments with Google
https://experiments.withgoogle.com/
Adult Swim
https://www.adultswim.com/etcetera/
Evan Wallace
http://madebyevan.com/
总结
这篇文章讲解了什么是 WebGL,了解了 WebGL 的大致轮廓,并且完成了一个最简单的 WebGL 应用。
如果觉得不错欢迎点赞转发分享。
系列文章目录:《从 0 实现 3D 渲染引擎》