vlambda博客
学习文章列表

【前端可视化】 OpenGL / WebGL 入门和实践

希望通过这篇文章,大家能够对 OpenGL/WebGL 有一个基础的认识~~

OpenGL

定义

OpenGL 是一套规范,不是接口,学习这套规范,就可以在支持 OpenGL 的机器上正常使用这些规范,在显示器上看到绘制的结果。

这套接口是 Khronos 这个组织在维护。怎么维护呢?就是写一个说明书,指导各个 GPU 厂家,如果他们要支持 OpenGL 的话,要怎么实现一个具体的 OpenGL 库。比如 Khronos 说要实现 glDrawArray这 个接口,那么硬件厂家就得在它的库里实现这个接口。如果不实现,那么就不算支持 OpenGL。当然也有一些接口不一定要实现。

厂家实现的 OpenGL 库的内容,其实就是厂家自己的团队整合自己的图形知识以及 GPU 硬件的指令,这些 OpenGL 的实现通常被称为“驱动”,它们负责将 OpenGL 定义的 API 命令翻译为 GPU 指令。因此使用时只需要安装显卡的驱动。

既然是在 GPU 上运行的 OpenGL,那么接下来我们来了解一下 GPU ~

GPU

概念

显卡处理器称为图形处理器(即 GPU ),它是显卡的“心脏”,与 CPU 类似,只不过 GPU 是专为执行复杂的数学和几何计算而设计的,这些计算是图形渲染所必需的。一些最快速的 GPU 集成的晶体管数甚至超过了普通 CPU。

GPU 的工作

现代的 GPU 功能涵盖了图形显示的方方面面,这里只取一个简单的方向作为例子。这个立方体渲染的例子,会有助于理解接下来会讲到的 GLSL(OpenGL着色器) 语言。

大家可能都见过上面这张图,这是老版本 Direct X (是由微软公司创建的一系列专为多媒体以及游戏开发的应用程序接口)的一项测试,就是一个旋转的立方体。显示出一个这样的立方体要经过很多步骤,我们先考虑简单一点的,想象一下他只是一个线框,没有侧面的“X”图像。再简化一点,连线都没有,就是八个点(立方体有八个顶点的)。那么问题就简化成如何让这八个点转起来。

首先,在创造这个立方体的时候,肯定有八个顶点的坐标,坐标都是用向量表示的,因而至少也是个三维向量。然后“旋转”这个变换,在线性代数里面是用一个矩阵来表示的。向量旋转,是用向量乘以这个矩阵。把这八个点转一下,就是进行八次向量与矩阵的乘法而已。

这种计算并不复杂,拆开来看无非就是几次乘积加一起,就是计算量比较大。八个点就要算八次,2000个点就要算2000次。这就是 GPU 工作的一部分,顶点变换,这也是最简单的一部分。

通过这个例子可以先思考一下,想要渲染出一个图形,就需要告诉 GPU 图形的顶点(即坐标向量),如果需要变化(如:平移、旋转、缩放等),就需要告知对应的矩阵,这也就是文章后面要说的 GLSL 语言核心需要做的事情~

CPU 与 GPU 区别大揭秘

CPU 和 GPU 因为最初用来处理的任务就不同,所以设计上有很大的区别。它们分别针对了两种不同的应用场景。

CPU 需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。这些都使得 CPU 的内部结构异常复杂。而 GPU 面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。

于是 CPU 和 GPU 就呈现出非常不同的架构(示意图):

【前端可视化】 OpenGL / WebGL 入门和实践

其中绿色的是计算单元,橙红色的是存储单元,橙黄色的是控制单元。

GPU 采用了数量众多的计算单元和超长的流水线,但只有非常简单的控制逻辑并省去了 Cache。而 CPU 不仅被 Cache 占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是 CPU 很小的一部分。

而 GPU 的工作大部分就是这样,计算量大,但没什么技术含量,而且需要重复很多次。就像你有个工作需要算几亿次一百以内加减乘除一样,最好的办法就是雇上几十个小学生一起算,一人算一部分,反正这些计算也没什么技术含量,纯粹体力活,人海战术而已。而 CPU 则像老教授,积分微分都会算,一个老教授资顶二十个小学生。GPU 就是这样,用很多简单的计算单元去完成大量的计算任务。不过这种策略基于一个前提,就是每个小学生工作没有什么依赖性,是互相独立的,即 GPU 的计算单元所做的事情是互相独立的。

还有一些任务涉及到步骤的问题,不能把执行顺序颠倒了。这种比较复杂的问题都是 CPU 来做的。

GPU 的运算速度取决于雇了多少小学生,CPU 的运算速度取决于请了多厉害的教授。教授处理复杂任务的能力是碾压小学生的,但是对于没那么复杂的任务,还是人多力量大。不过现在的 GPU 也能做一些稍微复杂的工作,但还是需要 CPU 把数据给 GPU 才能开始干活,因此还是靠 CPU 来管的。

至此为止,GPU 的内容先了解到这里,接下来我们继续回到 OpenGL。

OpenGL ES

OpenGL ES 与 WebGL 有关,WebGL 是基于 OpenGL ES 2.0 的 Javascript API,因此我们在这里先来了解一下OpenGL ES。

OpenGL ES 是 OpenGL 的子集,专门针对手机/PDA(掌上电脑,如: 条形扫码器,POS机等)/游戏主机等嵌入式设备设计的。OpenGL ES 主要直接提供 C api,各自平台根据习惯提供一层包装(比如Android提供了Java的包装,iOS提供了obj-c的包装)。

虽然 OpenGL ES 是 OpenGL 的子集,但是 OpenGL 与 OpenGL ES 还是有一点区别,比如他们的数据类型会存在一些不一样:

  1. OpenGL ES 没有 double 型(浮点)数据类型,而是加入了高性能的定点小数数据类型;

  2. OpenGL ES 没有 glBegin/glEnd/glVertex,只能用 glDrawArrays/glDraw......;

  3. 没有实时将非压缩图片数据转成压缩贴图的功能,程序必须直接提供压缩好的贴图;

  4. ...

可实现滤镜效果🌰

这里可以简单看一些直接使用 OpenGL 实现的滤镜效果

  1. 缩放、出窍、抖动、闪白、毛刺

  2. 灰度、旋涡、马赛克

  3. 分屏

注意:这些直接使用 OpenGL 实现滤镜效果的例子可以了解一下,但是团队项目中使用到的滤镜效果是通过 effect sdk 统一支持(Windows/Mac/Android/iOS),因此如果需要实现 effect creator for web,则需要在浏览器支持 effect sdk 的使用,以保证多端统一,且支持现有功能效果。

至此,除了 GLSL 语言以及具体API,OpenGL 的基础知识就这么多了。OpenGL 是在移动端/桌面端使用,那么在 Web 端呢?就是大家熟悉的 WebGL 了,我们一起来看一下~

WebGL

一些 WebGL 应用场景🌰

  • 3D 的数据可视化

  • kaspersky

  • 3D游戏开发

  • WebCam Mesh

  • webgl games

  • Cube Slam

  • 打造炫酷的交互

  • WebGL bookcase

  • H5宣传页面 & 广告

  • 淘宝双11VR邀请函 H5页面

  • 进行 3D 产品 / 物体展示

  • WebGL reflection

  • ...

概念

WebGL 是一种 3D 绘图标准,这种绘图技术标准把 JavaScript 和 OpenGL ES 2.0 结合在一起,通过 HTML5 的 Canvas 来和 DOM 打交道,为HTML5 Canvas 提供硬件 3D 加速渲染。WebGL 技术标准免去了开发网页专用渲染插件的麻烦,可被用于创建具有复杂 3D 结构的网站页面,甚至可以用来设计 3D 网页游戏等。

与 OpenGL 的关系

通过上述概念可以看出,WebGL 将 JavaScript 和 OpenGL ES 2.0 结合在一起,因此也会使用 GLSL(OpenGL Shader Language) 作为 Shading Language(一种顶点计算和着色的语言,缓存编译到 GPU,由 GPU 来执行)。

说白了,就是通过浏览器提供的接口,我们能够直接和底层的 OpenGL 库打交道。由于能直接调用底层接口,并且有硬件加速,因此 WebGL 要比普通的 Canvas 2D Api 性能要高出不少。这里有一个WebGL 和 Canvas 2D Api 性能的对比实验结果,横坐标是绘制任务数量,纵坐标是页面的 FPS(画面每秒传输帧数)。

【前端可视化】 OpenGL / WebGL 入门和实践

从结果中可见,当需要执行大量绘制任务时,WebGL 的性能远远超越了 Canvas 2D Api,达到了后者的3~5倍。

Three.js

为什么会介绍一下这个库,是因为在学习 WebGL知识时 总会看到一个库:Three.js,那我们这里也来简单的了解一下。Three.js 是一个用于在浏览器中绘制3D图形的JS库,其底层实际是对浏览器提供的 WebGL Api 进行了封装,类似于 JS 与 JQuery 的关系,甚至不需要 WebGL 基础就能够上手使用,但是由于是以 WebGL 为基础,所以遇到问题还得回来查看 WebGL,而 WebGL 的基础又是 OpenGL ES,因此 OpenGL 就显得至关重要了。

OpenGL 很重要,而 OpenGL 还有一个重要部分就是前面多次提到的 GLSL(OpenGL 着色器语言),接下来我们就来看看这个着色器语言究竟是什么吧~~

GLSL着色器语言

首先要明白,着色器(Shader)是运行在 GPU 上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序,比如我们要画一个三角形,着色器只是通过读取我们传给它的顶点,颜色,变化等输入,然后经过一系列计算,最终输出图形。

着色器主要分为顶点着色器和片段(像素)着色器,这也是主要的两种着色器,还有一种是几何着色器。每个着色器是非常独立的程序,它们之间不能相互通信,唯一的沟通只能通过输入和输出。通常一个WebGL 应用会有多个着色程序。我们可以根据着色起的名字来思考一下他们的作用。顶点着色器,顾名思义就是为了渲染图形的顶点所使用的,回想一下我们刚才讲的 GPU 的工作,一个立方体的渲染,肯定是先要找到立方体的顶点,这个就是顶点着色器的作用了。顶点找到后,就会连接成线,以及形成平面,那么线段/平面的颜色等就是片段着色器的工作了。

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。数据类型:

【前端可视化】 OpenGL / WebGL 入门和实践

修饰符:

【前端可视化】 OpenGL / WebGL 入门和实践

这里只是简单介绍了一下常用概念,关于 GLSL 概念的详解,可以看一下这里

我们在 GPU 的工作一节提到过,坐标都是向量表示,变化(比如:旋转/平移/缩放等)都是通过矩阵表示,回到大学线性代数知识,向量(a1=[x1, y1, z1])与矩阵相乘,就会得到另一个向量(a2=[x2, y2, z2]),a1通过矩阵运算,得到了a2,这样就改变了坐标的位置。看到这里就明白了,如何通过计算得出我们想要的结果,就需要线性代数的知识了。(PS:矩阵真的很神奇,几乎一切变化都从这里来,在最后的例子中带大家来看看矩阵带来的魔法吧)

【前端可视化】 OpenGL / WebGL 入门和实践

看完着色器的基本知识后,我们就可以看一下渲染的过程了。

WebGL 渲染过程

WebGL API 在了解一门新技术前,我们都会先看看它的开发文档或者API。于是,我们查看WebGL绘图API,发现:

【前端可视化】 OpenGL / WebGL 入门和实践

是的,它只能画点、线、三角形。就算是像下面这样的复杂模型,也是一个个三角形画出来的。

【前端可视化】 OpenGL / WebGL 入门和实践

简单绘制流程

简单说来,WebGL绘制过程包括以下三步:

  1. 获取顶点坐标(使用顶点着色器)

  2. 图元装配(这里画出一个个三角形,gl.TRIANGLES)

  3. 光栅化(生成片元/片段,即一个个像素点,使用片段/像素着色器)

【前端可视化】 OpenGL / WebGL 入门和实践

接下来,我们分步讲解每个步骤。

顶点坐标

顶点坐标从何而来呢?一个立方体还好说,但如果是像上边复杂的茶壶呢?想一下,每个三角形都有三个顶点,而一个茶壶就会有成千上万个顶点,而且还需要精密的计算,显然人的肉眼以及精力是不允许一个一个写这些坐标的。往往它使用三维软件(C4D、MAYA、3DS max等)导出,或者是框架生成。

获取顶点坐标过程图:

【前端可视化】 OpenGL / WebGL 入门和实践

前面两个步骤都很好理解,但是第三步写入缓存区是什么意思呢?由于顶点数据往往成千上万,在获取到顶点坐标后,我们通常会将它存储在缓存区内,方便 GPU 更快的读取。

图元装配

我们已经知道,图元装配就是由顶点生成一个个图元(即点/线/三角形)。那这个过程是自动完成的吗?答案是并非完全如此。WebGL 需要我们先处理顶点,那怎么处理呢?我们先看下图:

【前端可视化】 OpenGL / WebGL 入门和实践

第一步就是将上面缓存中的顶点坐标传入了顶点着色器,顶点着色器根据传入的gl.POINTS/gl.LINES/gl.TRIANGLES参数,进行图元装配(通俗一点讲,就是要画点,还是线,还是三角形)

下面是一段顶点着色器代码:

 
   
   
 
  1. attribute vec4 position;

  2. void main() {

  3. gl_Position = position;

  4. }

这里具体解释一下:attribute 修饰符,是由浏览器(javascript)传输给顶点着色器的变量值修饰符;vec4 就是包含4个元素的浮点型向量(坐标);position 即我们定义的顶点坐标,传入到着色器的;gl_Position 是一个内建的传出变量。

这段代码其实就是 GPU 通过传入的数据找顶点的过程。

光栅化

和图元装配类似,光栅化也是可控的。

【前端可视化】 OpenGL / WebGL 入门和实践

在图元生成完毕之后,我们需要给模型“上色”,模型看起来是什么质地(颜色、漫反射、贴图等)、灯光等,而完成这部分工作的,则是运行在 GPU 的“片元着色器”来完成。如下是一段简单的片元着色器代码:

 
   
   
 
  1. precision mediump float;

  2. void main(void) {

  3. gl_FragColor = vec4(1.0, 0.5, 1.0, 1.0);

  4. }

precision 是指向一个整数的指针,返回的该整数是对应格式的精度的位数,用 log2 取对数的值,暂不做深究。gl_FragColor 是一个内建的传出变量,即输出的颜色值,这段代码就是紫粉色。

片元着色器处理流程

片元着色器具体是如何控制颜色生成的呢?

【前端可视化】 OpenGL / WebGL 入门和实践

如上图,顶点着色器是有多少顶点,运行了多少次,而片元着色器则是,有多少片元(像素),运行多少次。

整体详细绘制流程

至此,实质上,WebGL经历了如下处理流程(这里我们涉及到的前面没讲到的名词稍微多一点,但是大概涵盖了所有涉及到的内容):

  1. 准备数据阶段 在这个阶段,我们需要提供顶点坐标、索引(三角形绘制顺序)、uv(决定贴图坐标)、法线(决定光照效果),以及各种矩阵(比如投影矩阵)。

如何传入?

  • 顶点数据存储在缓存区(因为数量巨大),以修饰符attribute传递给顶点着色器;

  • 矩阵则以修饰符uniform传递给顶点着色器。

  1. 生成顶点着色器 根据我们需要,由Javascript定义一段顶点着色器程序的字符串,生成并且编译成一段着色器程序传递给 GPU。传入的顶点着色器程序,是一个字符串,这是 WebGL API 所要求的,会进行编译成着色器语言。我们来大致看一下看一下。

 
   
   
 
  1. <script id="2d-vertex-shader" type="notjs">

  2. attribute vec4 a_position;

  3. void main() {

  4. gl_Position = a_position;

  5. }

  6. </script>

  7. <script id="2d-fragment-shader" type="notjs">

  8. precision mediump float;

  9. void main() {

  10. gl_FragColor = vec4(1, 0.5, 1, 1);

  11. }

  12. </script>


  13. function onload() {

  14. ...

  15. // 着色器字符串程序

  16. const vertexShaderSource = document.getElementById("2d-vertex-shader").text;

  17. const fragmentShaderSource = document.getElementById("2d-fragment-shader").text;

  18. // start 将字符串传入,创建顶点以及片段着色器

  19. const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);

  20. const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

  21. // end 创建着色器

  22. ...

  23. }

  1. 图元装配 GPU根据顶点数量,挨个执行顶点着色器程序,生成顶点最终的坐标,完成坐标转换。

  2. 生成片元着色器 这一步则是解决我们最终绘制出来的效果,它的模型是什么颜色,看起来是什么质地,光照效果,阴影(流程较复杂,需要先渲染到纹理,可以先不关注),都在这个阶段处理。

  3. 光栅化 通过第4步生成了片元着色器,因此 GPU 内部已经确定好了每个片元的颜色,然后根据深度缓存区判断哪些片元被挡住了,不需要渲染,最终将片元信息存储到颜色缓存区,最终完成整个渲染。

WebGL 入门实例

通过一些小例子,学会使用 WebGL 基础知识

例1:简单的画一个三角形,学会从 WebGL 到着色器的全过程 [可参看这里] 步骤:

  1. 获取canvas,以及 webgl context

  2. 编写着色器(字符串形式)

  3. 创建顶点/片段着色器

  4. 将顶点/片段着色器链接在一起

  5. 将位置的坐标放入buffer 中,因为着色器从 buffer 读取数据

  6. 传入绘制需要的数据(比如2D/3D 缓冲位置等)

  7. 开始绘制

例2:通过例1进行简单的修改,一个变色三角形 [可参看这里https://webglfundamentals.org/webgl/lessons/zhcn/webgl-how-it-works.html] 例3:只能画 点/线/三角形 的 WebGL 如何画一个矩形呢?[可参看这里https://webglfundamentals.org/webgl/lessons/zhcn/webgl-fundamentals.html] 例4:绘制一张图片呀,再加一点魔法 [可参看这里https://webglfundamentals.org/webgl/lessons/zhcn/webgl-image-processing.html] 例5:给图片施更多的魔法(矩阵的神奇力量) [可参看这里https://webglfundamentals.org/webgl/lessons/zhcn/webgl-image-processing-continued.html] 例6:二维平移/旋转/缩放(二维矩阵的由来看这里https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-2d-matrices.html) 其实最重要的就是顶点坐标,因为片段着色器只是将顶点按照所需图元连线,因此 平移/旋转/缩放 只需计算出变化后的顶点坐标即可

WebGL 入门篇大概就讲到这里,相信大家对基础已经有了一定的了解,但是 WebGL 还有很多知识,比如 投影/光源/相机/三维呈现 等,有兴趣的可以接下来了解。

相关资料

OpenGL中文教程OpenGL ESOpenGL书籍:

  1. 红宝书 OpenGL Programming Guide,出到第八版

  2. 蓝宝书 OpenGL SuperBible,出到第六版

  3. 橙宝书 OpenGL Shading Language,出到第三版

  4. 更多

一个易懂的WebGL教程:WebGLFundamentalsWebGL MDN

相关文章图解WebGL与Threejs工作原理与CPU的区别是什么,渲染是否是并行计算的?