初识 WebGL:渲染管线篇

1,679 阅读12分钟

初识 WebGL —— 渲染管线

1. 前置知识介绍

在正式开始讲解 WebGL 有关的知识之前,让我们先来了解两个概念。

1.1 什么是计算机图形学

首先让我们明确一个概念,什么是计算机图形学?宽泛点来讲,所谓的计算机图形学,指的是利用计算机合成和处理视觉信息的技术。它的常见应用有游戏制作,电影CG生成,VR,图像处理,GUI等。作为一个前端开发者,我们日常工作的内容,正属于它的范畴。

image-20211123230233522.png

image-20211123230248879.png

1.2 2D 与 3D

第二个概念,也是近些年来大家耳熟能详的 2D 与 3D。很多人可能会因为各种动漫游戏小说电影,认为 2D 和 3D 是世界不同等级维度,纸片人和3维空间。实际上,并非如此。更简单通俗点来说,2D 和 3D 主要是人们对于对象的不同特征的描述方式。2D 表示对象的宽度和高度这两个平面特征,而 3D 除了表示对象的平面特征之外,还展示对象空间特征的深度属性。如下面两张图,都是在一个平面上的图形而已,然而却分别描述了对象的不同特征,从而给我们带来了 2D 与 3D 这一视觉上的不同感受。

image-20211123230814377.png

image-20211123230821505.png

2. 常见绘图/动画方案对比

在前端开发中,我们可以有很多的选择来进行图像或动画的绘制。常见的自然是 HTML + CSS,SVG,Canvas 与本文的主角 WebGL。我们可以对他们进行一个简单的对比。

HTML + CSS

HTML 与 CSS 作为前端切图仔手中的两把利器,绘制图画和动画自然是信手拈来。我们可以通过使用各种 Dom 和 CSS 来绘制各种天马行空的图画。在 CSS 2 时期,我们更多的是用他们来画图;而 CSS 3 的出现,则为我们提供了很多新的 API,使我们可以更好的实现动画效果。诸如 transitionanimationtransformkeyFrame 等,都可以帮助我们实现不同的动画效果。

HTML + CSS 绘制动画的优势在于其直接通过 GPU 进行绘制,性能较好。同时,由于其实现动画主要是通过补间动画的方式,制作比较简单,易于上手。而它的缺点也正是源于此处。由于我们通过设置关键帧来实现动画,对于动画整体控制较弱,无法在动画执行过程中对其进行暂停等其他操作。同时,如果我们希望实现比较复杂酷炫的效果,代码也会变得冗长且不易阅读维护。

SVG

SVG 全称为 Scalable Vector Graphics,翻译过来就是可缩放矢量图形。它是一种 XML 应用,可以以一种简洁,可移植的形式来表示图形信息。SVG 于 2003 年正式成为 W3C 的标准。它的功能十分丰富,提供了注入 rectcircleellipse 等基本图形和 fillstroketransfrom 等属性,可以实现滤镜,动画等效果,同时也支持通过 js 和 css 进行设置。

它的优点在于作为一个矢量图,他不依赖于像素,在更改页面尺寸大小时,不会失真。同时,由于其由 DOM 驱动,事件绑定由浏览器分发到对应节点上,让我们可以更好的对动画进行控制。

但是,由于是通过 DOM 驱动,导致我们通过 SVG 实现复杂动画时,需要频繁的去操作更新 DOM,导致动画越复杂,性能越差。

Canvas

Canvas 是 HTML 5 提供的用于展示绘图效果的标签,通过 JavaScript 指令进行动态绘图。Canvas 最初是 Apple 提出的方案,最后逐渐被各大浏览器支持,相比于 SVG,Canvas 要更新一点。

Canvas,也就是画布,会在页面中开辟出一块画布区域。它提供各种用于绘图的 API,我们通过 JavaScript 调用 API 来在这块画布区域中实现绘制。

Canvas 优势在于它通过 javascript 驱动,具有灵活的逻辑处理能力,可以实现保存,恢复画布状态等操作,可定制性更强,我们可以进行很多酷炫的操作。相比于 SVG 动画需要操作多个 HTML 标签,Canvas 进行绘图时,只需要在一个画布标签上进行操作。因此,在绘制复杂图形或动画时,Canvas 性能要更好。

它的缺点则是由于其依赖于像素,无法高效保真。在画布增大时,会导致渲染耗时增加。

SVG VS Canvas

image-20211124150849029.png

性能对比

image-20211124150905745.png

这里是他们的一个性能对比折线图。

第一张图比较的是他们随着屏幕尺寸变化时的渲染耗时的变化。我们可以看到,随着屏幕尺寸增大,SVG 的渲染耗时并没有很大的变化。而 canvas 则会有很明显的耗时增加。

第二张图比较的是他们随着渲染的对象的数目变化时的渲染耗时的变化。随着渲染对象数目的增多,canvas 的渲染耗时并没有很大的变化。而 SVG 则会有很明显的耗时增加。

这也是为什么会有人说 Canvas 更适用于小面积,大数据的场景,而 SVG 更适用于大面积,小数据场景。

WebGL

WebGL,全称 Web Graphics Library,Web 图形库。有人称其为一个 JavaScript API,帮助开发者在支持 WebGL 的浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需借助诸如 Flash,SliverLight 等浏览器插件;也有人称其为一组基于 JavaScript 的图形渲染规范,浏览器厂商根据这组规范实现并提供一套 3D 图形相关的 API。实际上,以上两种说法都没有问题。

作为本文的主角,正如上面所说的,WebGL 除了可以绘制 2D 图形外,还支持绘制 3D 图形。WebGL 标准的出现使得我们可以通过一种更统一、更标准的方式绘制 3D 图形。除了可以绘制 3D 图形外,在 2D 图形的绘制上,由于 WebGL 可以直接调用底层接口,实现硬件加速,因而也有着比以上 3 种方案更好的性能表现。这也是为什么现在越来越多的前端开发者选择投入 WebGL 的开发工作中的原因。

3. WebGL 的组成和实现原理

3.1 WebGL 的发展历程

WebGL 的发展历程,我们可以参考下面的图来看。

image-20211124153108297.png

WebGL API 是基于 OpenGL ES,而 OpenGL ES 又是 OpenGL 的针对嵌入式设备图形开发的子集。可能有些同学对于 OpenGL 不是很了解,套用 wiki 的解释, OpenGL 就是个开放图形库,是用于渲染 2D,3D 矢量图形的跨平台,跨语言的应用程序编程接口。因此,WebGL 作为 OpenGL 的”子孙“,它的 API 自然可以让我们 Web 开发者通过 js 代码来操作本地 OpenGL(对于移动设备来说,操作的是 OpenGL ES) 的部分接口,直接和 显卡(GPU) 进行通信,来实现页面的图形的渲染。

3.2 WebGL 之 GLSL

WebGL 我们常用的是它暴露出来的接口,而其内部是如何实现诸如着色器,材质,动画,灯光这些核心内容的呢?这就需要借助 GLSL 的帮助。GLSL 实现着色器程序需要接收 CPU(即 WebGL 使用 JavaScript) 传递过来的来实现页面的图形的渲染数据,然后对这些数据进行流水线处理,最终显示在屏幕上,进而实现丰富多彩的 3D 应用。

GLSL,全称 OpenGL Shading Language,也就是 OpenGL 着色器语言。它是一种语言,主要用于开发在 GPU 中执行的着色器程序,代替 GPU 的固定渲染管线的一部分,使得开发者可以通过编程控制 GPU 渲染过程中的某些部分。它是 C 语言的一个超集,在 C 的基础上,增加了一些数据类型和数学函数。

下图可以让我们直观的了解 WebGL,OpenGL,GLSL 之间的关系。

image-20211124162220926.png

注:CSS Shader CSS 着色器,是由 Adobe 提出的 CSS 着色器,现在已废弃。

在进一步介绍 WebGL 和 GLSL 之前,我们需要了解两个概念:图形系统与渲染管线。

3.3 图形系统与帧缓存

图形系统,通常由我们的输入设备,CPU,GPU,存储器,帧缓存与输出设备组成。他们之间的联系如下图所示:

image20211124162814010.png

其中,我们需要着重了解图形系统三个重要组成部分的作用,分别是 CPU,GPU 和 帧缓存。

​ CPU: 加载处理场景数据,设置材质,纹理等属性的渲染状态;渲染基础图形单位,并向 GPU 发送指令。

​ GPU: 接收指令并绘制;通过顶点处理与片元处理等操作,生成像素阵列输入至帧缓存中。

​ 帧缓存: 存放像素阵列,显卡进行读取,绘制与展示。

帧缓存,又被叫做显存,它是屏幕所显示的画面的直接映像,也被叫做位映射图光栅。帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。帧缓存中像素的数目就是分辨率,帧缓存的深度表示每个像素所用的比特数,其决定了一个系统可以表示多少种颜色,例如深度为 8 比特的帧缓存可以表示 256(2的8次方)种颜色。简单点来理解呢,就是我们购买显示器时常说的色深这一参数。

image20211124163312453.png

将帧缓存中的阵列拿出来之后,转为 0 1这样的二进制数据,放入像素寄存器中,通过 DAC 将这些离散的数字信号,转化为连续变量的模拟信号,就可以在显示器上面展示出来。

3.4 渲染管线

渲染管线,其实就是图形的渲染过程。

渲染管线主要包括两个功能:一是将物体 3D 坐标转变为屏幕空间 2D 坐标,二是为屏幕每个像素点进行着色,渲染管线的一般流程如下图所示,分别是:顶点处理、裁剪和图元组装、光栅化、处理。

image-20211124164229017.png

顶点处理是针对存储于顶点缓冲区的各个输入顶点进行操作,此阶段为可编程状态。主要的操作是对顶点进行坐标转换,把对象由其所定义的坐标系下的表示,转化为照相机下的坐标系。

image20211124164359761.png

在顶点处理完之后,就需要将顶点组合成一个个单元,这个单元就是我们的图元。这一步就是组装,将顶点组合成一个个单元,如点,线,三角形。

裁剪则是指将视口以外的对象进行裁剪,这个对象也就是片元。这一步涉及到两个概念,一个是裁剪坐标系,一个是片元。我们后面都会详细说到。

image20211124164602521.png

完成图元组装和裁剪后,下一步是光栅化。光栅化是将几何图元转化为图像的过程。它决定窗口坐标中哪些整型栅格区被基本图元占用,并分配色值和深度值到对应区域。光栅化主要目的是为了将图元组装和裁剪之后的图元数据转化生成帧缓存中的像素。但是光栅化处理完之后,我们并没有直接得到像素,而是得到了片元。

这里我们就可以解释一下什么是片元。片元是一个像素大小的基本单位,但是它并非像素,而是像素的前身。片元相比于像素,除了 RGBA 之外,还会包含如深度值,法线,纹理坐标等信息。为了得到我们期望的像素,最后一步,就是对片元进行处理。

image20211124164811575.png

片元处理,主要是通过片元着色器,计算片元的最终颜色和深度。由于可能同时会有多个片元争夺一个像素,需要通过深度和模板测试后来判断当前片元是否可见,并决定是否要绘制到帧缓存中。通过筛选出合适片元,再去除法线,纹理坐标等不重要的信息后,生成像素。

image20211124164910482.png

整个渲染管线流程到这里就结束了。Unity 游戏制作,客户端渲染的最基本的流程,实际也是这样,不同的地方在于如何介入与在哪个阶段介入。

而本文的主角,WebGL 渲染管线的处理流程则是:

  1. js 处理着色器所需的顶点坐标,法线,纹理等信息,提供数据;
  2. 顶点着色器根据顶点坐标,绘制到对应坐标;
  3. 图元装配阶段根据坐标进行装配;
  4. 光栅化进行填充;
  5. 片元着色器进行上色;

image-20211124165437671.png

图中绿色的部分,就是我们开发者可以介入,通过编程进行控制的部分。顶点着色器和片元着色器就是通过上文提到的 GLSL 编写开发的。作为 Web 开发工作者而言,我们往往只需要写一个简单的 main 函数就可以得到我们需要的着色器程序了。

图中还有一点需要注意的地方就是,我们可以看到是由 JavaScript 提供数据,那顶点信息的变换操作也是通过 js 来实现吗?实际上,顶点信息的变换操作既可以在 JavaScript 中进行,也可以在着色器程序中进行。为了利用 GPU 并行计算的优势,一般来说都是在 JavaScript 中生成一个包含了所有变换的最终变换矩阵,然后将该矩阵传递给着色器,再对所有顶点执行变换。

4. 结语

整整一篇,我们对 WebGL 是如何实现图形绘制进行学习探讨。下一篇,我们就要开始进行实战,来加深我们的理解;同时,还会介绍缓冲区和坐标系变换有关的知识,搭配食用,效果更佳哦~