一起来玩玩WebGL--第一弹

avatar
前端 @阿里巴巴

本文分享来自淘系前端智能化团队-赋行


上一篇文章说到我从客户端转前端的历程,短短一年的时间就打开了前端世界的大门,简直就是有无穷多的东西可玩,以前酷爱Java的我终于见识到什么都可以写的JavaScript的厉害了,不仅仅可以写Web,客户端,后端,系统应用,还可以在神经网络、物联网,甚至嵌入式都可以,简直就是一个万能的语言,可以说能编程的地方理论上都可以用JS来写!

转前端后,我就发现了不少新奇好玩的东西,WebGL,WebAssembly(后面再起这一个系列),前端还能干这么东西啊,完全出乎我意料,一直以为Web就是性能不好,所以才一直在Native上玩。以前我玩客户端,为了追求极致的性能,巧合的就干了这里两件事情,第一件就是自学了ARM,用汇编来实现我的功能(这就和WebAssembly契合),第二件就是自学了OpenGLES,利用GPU来加速优化我的代码(这就和WebGL契合)。既然来到了前端大陆,为什么我就不玩玩这两个东西呢?

由于平时上班实在太忙了,不能一下子肝出一大篇干货了,而且这货也是不简单呢,一篇文章也学不深入,于是就计划分开几弹来一起玩玩这货。其实我也是刚学,不敢说教啥的,也就是借助下班业余时间凭借兴趣学习玩玩,总结一下心得罢了,和大家一起交流交流。曾经在Android上用OpenGLES写了一个红蓝3D播放器和实现了弹幕SDK,那么也就以此为目标,学习一下WebGL,然后写一个网页版的红蓝3D播放器和实现弹幕SDK,虽然不知道能否实现,反正理论上来说是可以,就朝着这个方向去努力尝试就好了。

什么是WebGL?


那么到底啥是WebGL?当我们要学习或者了解一个东西的时候,通常做的第一件事情就是使用搜索引擎,找找资料。这里给你摘了百科上的介绍:

WebGL(全写Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。显然,WebGL技术标准免去了开发网页专用渲染插件的麻烦,可被用于创建具有复杂3D结构的网站页面,甚至可以用来设计3D网页游戏等等。

乍一看上面的描述,JavaScript谁不会啊?Canvas也会啊(除了我这种刚入门前端的小白以为,不过在客户端也是有Canvas的哦),剩下也就是一个OpenGL ES 2.0罢了,那么只要学习这个就可以了。再看百科描述:

WebGL 1.0基于OpenGL ES 2.0,并提供了3D图形的API。它使用HTML5Canvas并允许利用文档对象模型接口。WebGL 2.0基于OpenGL ES 3.0,确保了提供许多选择性的WebGL 1.0扩展,并引入新的API。可利用部分Javascript实现自动存储器管理。


原来WebGL就是基于OpenGLES的嘛,那太好了,我以前学习的不就是OpenGLES了么?这就已经事半功倍了哈哈哈!我的学习风格是慢慢的,循序渐进的,从最基本的理解入门,再到最简单的helloworld,最后才是用一个实际的例子来练习;而不是上来就直接教你怎么使用了,可能我这样会比较慢,但是我觉得理解确实最深刻的,而且对于没基础的我来说更加容易上手,而不会入门就放弃了!毕竟我还是觉得慢慢培养对这货的兴趣最重要。

图形引子


其实在我学习OpenGLES的时候,确实有点痛苦,明明我就是学习计算机科班出身的,怎么我就这么难理解呢?怎么那么多的概念和我以前学习的不太一样呢?确实需要颠覆一下之前的惯性思维,最好就是抛开重新学习。还是一步一步的来,先理解什么是图形编程呗。

理解图形编程


我们知道计算机的发展历史,从最简单的加法器,到图灵机,到冯诺依曼计算机,再到今天的智能机,相信你和我一样对此很痴迷,非常崇拜两位偶像,比尔盖茨和乔布斯,并读过所有关于他们的故事,想必就清楚知道图形发展的重要性了。苹果电脑的MacOS、Windows系统的绚丽UI,还有今天各种绚丽的画面,游戏,等等。最简单的,我们肯定知道组装电脑的时候,要选一块好的显卡,才能玩更好的游戏。而实际上,我们对计算机的了解并不够多。

还没写过一行代码的我就已经玩过了无数绚丽画面的游戏了,然而,当我学习编程的时候,最想解密的就是一个软件、一个这么牛逼的游戏画面,到底是怎么通过这些代码写出来的呢?还记得刚学习C语言的第一个代码吗?盯着控制台,看到输出“Hello world!”,人人都说会为这一伟大的成就欢呼。然而,这不过是几十年前的技术罢了;我想要知道的是,到底怎么样编写图形界面,而不是在控制台输出文字!

#include<stdio.h>
void main()
{
 printf("Hello World!\n");
}


admindeMacBook-Pro-191:develop fuxing$ gcc hello_world.c
admindeMacBook-Pro-191:develop fuxing$ ./a.out
Hello World!

不管是学习哪一门语言,C、Java,还是JS,实际上我们都是针对CPU来编程,所有的逻辑计算都是由CPU来计算完成的。在我们学习计算机组成原理的时候,我们知道这些计算其实都是CPU的指令集在操作寄存器而已。对于输出的结果显示,基本上都是文字字母,简单的在阴极射线管显示器即可显示。对CPU的操作和对输出结果的显示,这一切都是操作系统和编译器完成的事情,简单的说,编译器把高级语言编译成了01指令,而操作系统则是把底层硬件管理给封装成了系统接口调用,我们只需要调用系统接口就可以了,所以我们调用一下printf()就可以在显示器上进行展示了。
image.png
(图片来自于网络)

而其实系统对大部分硬件的操作都是通过驱动程序完成的,每一个硬件都有对应的驱动程序,驱动程序要遵循统一的规范和接口,然后实现即可。所以每次我们重装完系统,都要安装各种各样的驱动程序。对于硬件的开发,是嵌入式的领域了,虽然不用深入探讨,但是从这里可以理解到,硬件也是有处理单元的!
image.png
(图片来自于网络)

我们终于知道了其实显卡是有图形处理单元的,也就是GPU(Graphic Process Unit),和CPU一样的意义,用于渲染画面。那么问题又来了,图形是怎么绘制的呢?其实我们都知道,那就是像素,我们现在是电子设备爆炸的年代,谁不知道分辨率这东西呢?一个显示器的分辨率是1920x1080,意思就是横向有1920个像素,纵向有1080个像素,我们可以理解,像素就是一个很小很小的发光点,所以说,屏幕其实是汇聚了超密集的像素点,当然,这会涉及到材料学领域了,如液晶屏等等,我们只要抽象来学习理解就好。学过物理,我们也知道三原色,只要红绿蓝即可调出所有颜色,也就是说,其实每个像素点都是有不同量的红绿蓝三色。当然啦,显示器肯定不是那么简单,原生的数据是YUV,这又涉及到深入图形学领域了,我们现在还是只要抽象理解就好了。

在计算机里面一个字节是8位,取值范围也就是0-255,如果用一个字节表示一种原色量的取值范围,那么红绿蓝就是三个字节,再加上一个透明度Alpha,RGBA刚好是四个字节,通常是用一个整形int来表示。这样就可以把一个图形给量化成数字,既然已经是数字化了,就可以给计算机处理了。现在我们就能理解到,实际上一张二维的图片,就是一个二维整形矩阵,这些都是我们在CPU和内存都可以操作的逻辑了。最后操作系统把数据交换给显卡,通常是复制到显存,交给了GPU来渲染显示,本质上GPU也是处理了这些数据,根据这些数据来控制显示器在不同的点放射不同量度的原色,这样就能展示图形了。

图形编程API


通过上面的历史了解,我们十分清晰,图形领域是十分重要和可发展的,介于操作系统和硬件(驱动)的中间层,可以做很多事情,提供重要的图形编程接口,方便开发二维和三维的图形。于是,这个世界基本上又出现了两大阵型,OpenGL和DirectX。
image.png
(图片来自于网络)

DirectX太熟悉了,我们打游戏,一定要安装这个东西,童年啊!它就是用于Windows的游戏开发,一统天下!而OpenGL是跨平台的,不管哪里都能用(不然怎么会有今天的WebGL呢)!并且不仅用于游戏开发,几乎什么领域都可以用。当然了,很少人会直接用这两个东西直接开发游戏,而是用来开发游戏引擎,然后基于引擎开发游戏,例如Unity3D!

OpenGL的全称是Open Graphics Library,关于OpenGL的开发,我们用到的是着色语言(Shading Language),只是一个新语言而已,学习语法就好了,只不过我们重点要理解的是渲染的流程是怎么样的,不然我们也不知道怎么用着色语言进行编写呀。

既然图形编程都需要用到OpenGL和DirectX,那么为啥我们日常开发写那么多的UI,却从来没有涉及到这两货的开发呢?我们回想一下,日常的开发中涉及UI有哪些?Image、Button、Text、EditText、InputText等等,基本上都是利用这些组件进行拼装,似乎很少涉及到复杂的图形,最多也就是涉及到一些动画效果而已。其实这些组件,或者说我们日常用到的这些API,都是已经封装好了底层怎么去渲染,对于我们来说,根本不需要了解底层怎么去渲染,或许其实都是在CPU上进行处理,再从内存复制到显存进行了显示了,又或许可以开启硬件加速,其实所有的渲染处理都是丢给了GPU去处理。再说日常我们开发中,并不需要那么高的性能,压根就不需要去写什么GL的东西啦。

曾经在Windows上玩游戏,如果没有安装DirectX,其实也是可以玩的,只不过可能没那么流畅,这就说明游戏开发者也不会直接调用DirectX的API,而是基于上层的游戏引擎开发游戏,有DirectX就说明有了硬件加速,没有的话,也不会游戏的运行,还是可以用CPU进行渲染处理。

当我们发现日常的开发中,涉及到图像相关的,CPU的处理已经不行了,性能成为了瓶颈,那么我们就要自己去实现底层的渲染逻辑,这时候就要去写GL了。例如,播放一个视频,每一帧都要实时去处理某些像素,这个时候如果用CPU去处理的话,就会很慢了,我们可以用GPU来处理像素逻辑,而CPU尽管处理播放同步逻辑就好了。

什么是OpenGL ES


我们已经了解到要学习WebGL,其实就是要学习OpenGLES了,可以理解为WebGL就是在用JS调用OpenGLES的API,那么OpenGLES又是啥呢?学习这个之前是否又需要先学习OpenGL呢?答案是不需要的。

由于移动设备的快速发展,于是出现了针对这些嵌入式设备的一套API子集出台了,OpenGL for Embedded Systems。显然,因为是子集,所以就是对于OpenGL进行了功能的裁剪。然后OpenGL ES现在已经发展到了3.0版本,每一个版本都是巨大的飞跃。1.0和2.0之间的巨大区别在于,2.0是支持自定义渲染管线编程的,也就是可以支持着色语言了,意思就是,我们除了在高级语言那里调用OpenGL的API,还能在管线里面编写着色语言程序!这就是2.0的最大区别,而3.0基于2.0并没有巨大的变化,因此两者的学习递进即可,不需要转变。

OpenGL ES 1.0的渲染管线


GPU内部有许多处理图形信号的并行处理单元,所以它比CPU的串行执行效率高很多。我们理解的CPU的能力无非就是有很多的指令集,如果我们要调用这些指令集的话,就需要编写汇编语言,而在高级语言层面调用的API就是系统或者平台为我们封装好的能力。而GPU,也是会有相应的指令,硬件开发商会开发相应的驱动程序,提供标准的API供系统调用。OpenGL和DirectX就是基于此提供了相应的渲染能力,实际上,它们两个才是标准大佬,反推硬件提供相应的能力。**渲染管线实际上就是通过处理单元处理的一系列绘制过程。**管线如下图所示:

image.png
(图片来自于网络)
重点理解几点:

  • 什么是图元,其实就是图像单元;OpenGL绘制图形的时候,是有一个个的图元组合而成的。绘制方式有点、线和三角形,分别对应三种图元。
  • 什么是光栅化,图元在数学上是连续的量,但是在显示器就是离散的像素,所以,光栅化就是把顶点数据转换为片元的过程。
  • 什么是片元,为什么不叫像素?像素是屏幕上的点,那是二维的,但是一个屏幕上的像素在三维中,可能覆盖了很多个像素,于是在三维中不能叫像素,应该叫片元。

OpenGL ES 2.0的渲染管线


2.0的渲染管线如下图所示:
image.png
(图片来自于网络)
2.0的最大区别就是多了顶点着色器和片元着色器,方便程序员进行开发,需要学习着色语言了,而1.0就只能调用上层API。

顶点着色器


什么是顶点?就是几何图形的顶点的意思,例如三角形有3个顶点,矩形有4个顶点,线段只有两个顶点。顶点着色器(Vertex Shader)就是一个可编程的处理单元,图形的每一个顶点都会经过顶点着色器进行处理转换,产生纹理坐标,颜色,点位置等所需的顶点属性信息。工作原理图如下:
image.png
(图片来自于网络)

其中Attribute是每个顶点各自不同信息所属的变量,一般包含顶点坐标和顶点纹理坐标,通过高级语言传输下来的;Uniform是全局变量,是通过高级语言传输下来的数据;Varying是产生输出到片元着色器的数据,一般是纹理坐标。然后就是你编写代码的临时变量和gl_xxx的内置变量了。

一个顶点着色器例子代码如下(不需要理解这段代码的意思,感受即可):

uniform mat4 uMVPMatrix; //总变换矩阵
attribute vec3 aPosition;  //顶点位置
attribute vec2 aTexCoor;    //纹理坐标
varying vec2 vTextureCoord;  //用于传递给片元着色器的变量
void main()     
{                              
   gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置
   vTextureCoord = aTexCoor;//将接收的纹理坐标传递给片元着色器
}

可以看到,GL语言并不复杂,类似于C语言,这些uniform、attribute、varying都是关键字,标识数据的来源,mat4、vec3、vec2是数据类型,分别对应是4维矩阵、3维向量和2维向量的意思。

片元着色器


光栅化后的每个片元都会执行一次片元着色器(Fragment Shader),可以理解为每个像素都执行一次(二维的角度理解),主要的功能是纹理的采样和颜色的汇总。

什么是纹理,直接理解就是图形的表面、皮肤之类的,也就是图像、颜色、花纹等等。例如,在Android中,把一张图片Bitmap直接映射到OpenGLES中成为一张纹理,这时候纹理就是一张图片了,Bitmap是可以回收的了,已经传输到显存了。

工作原理图如下:
image.png
(图片来自于网络)

其中,Varying数据是从顶点着色器传来的;Uniform通常是纹理的数据,gl_FragColor就是输出的结果。
一个片元着色器例子代码如下:

precision mediump float;//告诉精度是float
varying vec2 vTextureCoord; //接收从顶点着色器过来的参数
uniform sampler2D sTexture;//纹理内容数据
void main()                         
{           
   //给此片元从纹理中采样出颜色值            
   gl_FragColor = texture2D(sTexture, vTextureCoord); 
}

实际上,绘制一个矩形是通过绘制两个三角形合成的,也就是有6个顶点,每个顶点执行一次顶点着色器,然而顶点着色器输出的用于传递给片元着色器的坐标变量并没有直接传递给片元着色器,而是在光栅化以后,通过插值计算,得出每个片元的坐标再传递给片元着色器,于是,片元着色器是执行处理每一个片元(像素)的。例如,把一张图片绘制满1920x1080的屏幕,则每个像素都执行一遍片元着色器。

着色语言


学到这里,基本上已经能够理解OpenGLES的原理了,着色语言就是又是一大块知识,学习它就能在上面两个着色器上编写代码了,这里不可能完全列举出所有知识,学习这个就跟学习一门语言差不多,而且不难理解。先简单知道几个向量类型,mat4是四维矩阵,vec4是4维向量,vec3是3维向量,vec2是2维向量,后续一遍学习一遍总结就可以了。

总结


这一弹就先学习这么多了,主要以介绍和理解WebGL为主,暂时不上手写代码,即使上面有一些例子代码,主要还是以阅读理解为主,先感受感受这个历史进程,培养一下兴趣,一步一个脚印,慢慢学习,目前先重点学习了一下着色器,后续如果要编写高级的功能,还有各种转换、数学知识要学习,下一弹再一起上手简单的例子学习。以上仅是个人的学习心得,难免会有一些错误的,对于理解有误的地方欢迎指出。