WebGL ThreeJS学习总结一、二

© Young 2016-10-18 07:57
Welcome to My GitHub

概述

在写这篇总结之前,这已经是我第二次尝试学习WebGL了,第一次是在两年前,那时候正在从事Java开发相关工作,觉得没什么意思,然后平时工作中有接触到一些前端相关的东西,刚好那时HTML5很火,就稍微了解了一下Canvas,然后自然而然的知道了WebGL,刚开始学的时候感觉很吃力,然后就买了本书《WebGL入门指南》,虽然跟着书上的例子也能照葫芦画瓢弄个正方体出来,但是总感觉实现了某个例子,依然啥也不会,没多久就放弃了。

现在已经从事前端开发工作接近两年,在前不久看到一篇博客后再次燃起了学习WebGL的热情,然后采购了一本书《WebGL编程指南》,花了一周时间看了100多页之后,差点再次放弃,思考了一下午决定改进下学习方法。

结合这两次的从准备入门到放弃的经验来看,单独学习ThreeJS容易给人造成不知其然的感觉,而单独学习WebGL又太枯燥,所以我决定把二者结合起来,先根据《WebGL编程指南》学习WebGL感觉枯燥之后就参照ThreeJS官方文档以及官方源代码中的例子学习ThreeJS并找些相对容易实现的例子实现,在这过程中写一些学习总结。

总结一、二主要是了解一些WebGL的基本概念,然后通过示例程序学习WebGL程序的基本结构、GLSL语言以及编写一些简单的WebGL程序。

概念

基本概念

WebGL起源

WebGL派生于OpenGL,目前在浏览器中被广泛支持。

WebGL程序结构

WebGL程序和普通的JavaScript程序不一样,WebGL程序除了JavaScript部分之外,还包含两个使用GLSL编程语言的着色器程序,分别是顶点着色器和片元着色器;

WebGL坐标系

在WebGL中一般使用的坐标系是当你面朝计算机屏幕时,X轴是水平的(正方向为右),Y轴是垂直的(正方向为上),Z轴垂直于屏幕(正方向为外),这个坐标系也被称为右手坐标系

之所被称为右手坐标系是因为它是通过如图所示的右手手势确定的,即当你伸出右手摆出如图所示手势时,拇指指向X轴的正方向食指指向Y轴的正方向中指指向Z轴的正方向,这种确定坐标系方式也被称为右手定则

另外坐标系的取值范围为[-1.0,1.0]

WebGL颜色分量取值范围

WebGL遵循传统OpenGL颜色分量的取值范围[0.0,1.0],这和2D Canvas、CSS、SVG等常用的图形技术颜色分量的取值范围[0,255]并不一样。

三角形网格

当你在纸上手绘正方体时,其实就是先确定八个顶点,然后再这八个顶点之间相互连线,每四个顶点组成一个正方形面,最终由六个正方形面组成正方体;

在计算机中构建3D物体也是如此,只不过采用的是连接三个顶点组成三角形的方式而已,这个三角形在计算机图形学中被称为三角形网格

一般来说网格面数越多,物体越精细,但同时会消耗更多的存储空间以及计算机性能。

之所以采用三角形网格则是因为三角形网格具有很多优点,比如三角形是最简单的多边形、三角形经过多种变换之后依然是三角形等等。

材质和纹理

三角形网格只能描述物体的轮廓,但现实生活中的物体,人眼除了能看到其轮廓之外,还能看到其材质,比如一般情况下人能通过眼睛分辨出哪些金属哪些不是金属,因为金属和非金属对光的反射效果不一样;可以理解为材质主要是用来描述物体表面动态属性的对象,比如处理光照等。

着色器

在计算机中创建一个3D物体,需要轮廓材质纹理三个要素;

那么可以简单的理解为顶点着色器是用来处理物体轮廓的程序,片元着色器是用来处理物体材质和纹理的程序

片元

片元可以简单理解为像素点,也就是下图中的小方块;

在构建3D物体时通过顶点组成三角形网格,但这些三角形网格都是矢量图形,最终在屏幕上显示时还是需要转化为像素图形,这种转化过程被称为光栅化,是计算机图形学的关键技术之一。

图形装配和光栅化

  • 执行顶点着色器,传入缓冲区对象中的第一个顶点坐标,一旦赋值成功,该数据就进入了图形装配区域,并暂时存储在那里;

  • 重复执行顶点着色器直到所有顶点数据赋值完成;

  • 开始装配图形(按照一定的规则把所有顶点连接起来);

  • 将图形转化为片元,这个过程被称为光栅化,光栅化是三维图形学的关键技术之一,它负责将矢量的几何图形转变为栅格化的片元

补充解释下光栅化,其实就是把矢量图形转化成像素点的过程,因为最常用的一种绘制3D图形的方法就是使用网格(Mesh),也就是点线面属于矢量图形,而当前屏幕是像素渲染的,那么从矢量图形转化成用户所看到的像素图像必然需要光栅化这一步骤。

  • 光栅化结束之后,程序就开始逐片元调用片元着色器,每调用一次就处理一个片元,片元着色器会计算出该片元的颜色,并写入颜色缓冲区;

  • 当最后一个片元处理完成,浏览器就会显示最终结果。

另外这个过程如果我们使用当前流行的ThreeJS框架可以理解如下:

其中黄色区域是ThreeJS框架中使用JS实现的,绿色部分是THreeJS框架中使用GLSL ES实现的。

缓冲区对象

对于那些由多个顶点组成的图形,需要一次性地将图形的顶点全部传入顶点着色器,然后才能把图形绘制出来,WebGL提供了一种很方便的机制,即缓冲区对象(WebGL系统中的一块内存区域),它可以一次性地向着色器传入多个顶点数据。

使用缓冲区对象时需要遵循以下五个步骤:

创建缓冲区对象(gl.createBuffer());
绑定缓冲区对象(gl.bindBuffer());
将数据写入缓冲区对象(gl.bufferData());
将缓冲区对象分配给着色器变量(gl.vertexAttriPointer());
启动着色器变量(gl.enableVertexAttriArray())。

使用多个缓冲区对象向着色器传递多种数据,比较适合数据量不大的情况,当数据量很大时这种方式很难维护,所以WebGL允许我们把不同种类的数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区对象中不同种类的数据。

纹理坐标

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色,WebGL系统中的纹理坐标系统是二维的,为了将纹理坐标和广泛使用的x坐标和y坐标区分开来,使用s和t命名,称之为st坐标系统。

纹理映射

纹理映射的一般步骤:

  • 准备好映射到几何图形上的纹理图像;
  • 为几何图形配置纹理映射方式;
  • 加载纹理图像,对其进行一些配置,以在WebGL中使用它;
  • 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋予片元。

在配置纹理映射的时候需要注意一点,图片坐标系统和WebGL纹理坐标系统的Y轴方向是相反的,这时候你需要对纹理图像进行Y轴反转;

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

纹理单元

WebGL通过一种纹理单元的机制来同时使用多个纹理,每个纹理单元有一个单元编号来管理一张纹理图像,系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,默认情况下至少支持8个纹理单元。

使用纹理单元的一般步骤:

  • 激活纹理单元(gl.activeTexture());
  • 绑定纹理对象(gl.bindTexture());
  • 配置纹理对象的参数(gl.texParameteri());
  • 将纹理图像分配给纹理对象(gl.texImage2D());
  • 将纹理单元编号传递给取样器。

有时候渲染纹理时可能会报错:WARNING: texture bound to texture unit 0 is not renderable. It maybe non-power-of-2 and have incompatible texture filtering.;主要原因是你使用的纹理素材的尺寸不是2的幂数;
解决办法是设置纹理的填充方式为水平拉伸和垂直拉伸;

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

另外当我们想使用 WebGL Canvas 充当纹理素材时,不能直接使用 Canvas 元素,而是应该使用上下文中的 canvas 属性;

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, otherGL.canvas);

FrameBuffer

有点类似于Canvas中的离屏渲染,一般我们使用gl.drawArrays或者gl.drawElements都是将对象绘制在了默认的窗口中,但是当我们指定一个FrameBuffer时,再用这个两个方法去绘制,则会将对象会绘制于当前指定的FrameBuffer。

一般用于纹理的多重处理,比如对于一个纹理我们可以先灰度过滤,然后再模糊过滤,但是灰度过滤后还不能直接渲染到屏幕上,则可以使用FrameBuffer过渡,直到处理完成之后再渲染到屏幕上;
使用范例可以去WebGL简单实现高斯模糊这个例子中查看。

组合矩阵

在空间中存在一个三角形,红色坐标系为其自身坐标系

对三角形在绿色世界坐标系中做一些变换,该变换矩阵可以被称为模型矩阵

相机可以在某个位置以某种姿态观察这个三角形;蓝色坐标系为视图坐标系,其相对于世界坐标系的位置的变换矩阵为视图矩阵

三角形投影到相机时有很多种方式,分别对应不同的投影矩阵

这些变换矩阵共同决定了该三角形最终投影到屏幕的样子。

画家算法

画家算法也叫优先填充,画家算法首先将场景中的三角形网格根据深度进行排序,然后按照顺序进行绘制,慢绘制的会覆盖先绘制的,这样就解决了空间中物体可见性的问题;

但是当多个三角形网格相互交叉时,画家算法就不太好解决问题了,在这种情况下就需要对这些交叉的三角形网格进行切分,然后再排序;

因此对于细致场景来说,画家算法会过度消耗计算机资源,效率较低。

隐藏面消除

WebGL 在默认情况下会按照缓冲区中的顺序绘制图形,而且后绘制的图形覆盖先绘制的图形,这种做法比较高效,如果场景中的对象以及观察者状态都不发生任何变化,这种做法没有任何问题;但是如果场景中的对象或者观察者状态发生了变化,那么有可能会影响对象的显示次序,这时候还按照默认顺序绘制图形就会有问题了,为了解决这个问题,WebGL 提供了隐藏面消除功能,这个功能会自动根据对象和观察者的状态计算场景中对象的显示顺序;启动该机制只需要两行代码:

//开启隐藏面消除功能
gl.enable(gl.DEPTH_TEST);


//开启隐藏面消除功能后,每次绘制之前,除了要清除颜色缓冲区还需要清除深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

在绝大多数情况下,隐藏面消除功能都能很好的工作,然而当几何图形或者物体的两个表面极为接近时,就会出现新的问题,使得表面看上去斑斑驳驳,这种现象被称为深度冲突,主要原因在于两个面过于接近,深度缓冲区有限的精度已经不能区分哪个在前,哪个在后了。

针对上述情况WebGL提供一种被称为多边形偏移的机制来解决这个问题,该机制将自动在Z值上加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定;启动该机制同样也只需要两行代码:

//启动多边形偏移
gl.enable(gl.POLYGON_OFFSET_FILL);


//在绘制之前指定用来计算偏移量的参数
gl.polygonOffset(1.0,1.0);

通过顶点索引绘制物体

通过gl.drawElements配合gl.ELEMENT_ARRAY_BUFFER可以实现通过顶点索引绘制物体,这种绘制方式相对于gl.drawArraysgl.ARRAY_BUFFER纯顶点绘制物体方式存在一定的优势,特别是当共享顶点很多时可以节约很多内存。

按行主序和按列主序

在编程时我们通常使用数组存储矩阵,但是矩阵是二维的,数组是一维的,所以如果在数组中按行存储矩阵元素就被称为按行主序,在数组中按列存储矩阵元素就被称为按列主序;

WebGL 和 OpenGL 一样,矩阵元素是按列主序存储在数组中的。

光源类型

  • 平行光类似于自然中的太阳光,光线是相互平行的,可以用一个方向和一个颜色来定义;
  • 点光源类似于人造灯泡的光,光是从一个点向周围的所有方向发出的光,因此我们需要指定点光源的位置和颜色,光线的方向将根据点光源的位置和被照射之处的位置计算出来;
  • 环境光环境光是指那些经光源发出后,被墙壁等物体多次反射,然后照到物体表面上的光,环境光从各个角度照射物体,其强度都是一致的,环境光不需要指定位置和方向,只需要指定颜色即可。

除了上述三种基本类型的光,还有很多其它更加特殊的光源类型,可以参考《OpenGL ES 2.0 Programming Guide》。

漫反射

漫反射是指在粗糙的物体表面,反射光以不固定的角度反射出去,因此漫反射的反射光在各个方向上是均匀的。

漫反射光颜色 = 入射光颜色 * 表面基底色 * 入射角余弦;
环境反射光颜色 = 入射光颜色 * 表面基底色。

逐顶点光照

逐顶点光照简单来说就是在顶点着色器中根据顶点数据进行光照处理,确定三角形网格的顶点颜色之后,其内部片元颜色可以通过线性插值的方式得到;

这会导致内部片元颜色总是暗于顶点处片元颜色,在某些下会出现明显的棱角现象;另外线性插值的方式意味着其不适用于存在非线性计算的光照模型。

逐片元光照

逐片元光照则是在片元着色器中根据每个片元的数据分别进行光照处理,这种方式得到的光照效果更好,代价则是计算量增加。

正向渲染

正向渲染也叫正向着色法,它是我们渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推;

它非常容易理解,也很容易实现,但是同时它对程序性能的影响也很大,因为对于每一个需要渲染的物体,程序都要对每一个光源每一个需要渲染的片段进行迭代。

延迟渲染

延迟渲染也叫延迟着色法,3D场景显示到屏幕上的画面是2D的,这就意味着只能看到有限的片元,理论上我们只要对最终我们可以看到的片元进行光照和阴影处理就可以了;而正向渲染会处理所有片元,导致极大的性能浪费;

所以出现了延迟渲染技术,它区别于正向渲染的地方在于它延后了光照和阴影的处理流程,在获得对某片元进行光照和阴影处理的相关数据后,没有即时处理并渲染出来,而是把这些数据保存起来,当获得投影到屏幕上的片元的所有数据后统一进行计算;

此外延迟渲染也是有一些问题存在的,比如需要较高显存以及对透明混合模式支持较差等。

锯齿和抗锯齿

锯齿出现的和光栅器的工作方式有关,顶点坐标理论上可以取任意值,但是片元不行,因为它们受限于你窗口的分辨率,所以光栅器必须以某种方式来决定每个片元最终所在的屏幕坐标;

在上图中每个像素中心包含一个采样点,它会被用来决定这个三角形是否覆盖了某个像素点,图中红色的采样点被三角形所覆盖,在每个被覆盖的像素处都会生成一个片元;虽然三角形边缘的一些部分也遮住了某些屏幕像素,但是这些像素的采样点并没有被三角形内部所覆盖,所以它们不会被片元着色器影响。

由于屏幕像素总量的限制,有些边缘的像素能被渲染出来,有些则不会,造成的结果就是渲染的图形具有不光滑的边缘,这也就是锯齿出现的原因了。

最开始有一种超采样抗锯齿的技术用来解决这个问题,它会使用比正常分辨率更高的分辨率来渲染场景,但是这样会带来很大的性能开销;

在这项技术的基础上诞生了更为现代的技术,即多重采样抗锯齿

如图所示多重采样所做的正是将单一的采样点变为多个采样点,我们不再使用像素中心的单一采样点,取而代之的是以特定图案排列的4个采样点,我们将用这些子采样点来决定像素的覆盖度;当然这也意味着颜色缓存区的大小会随着子采样点的增加而增加;

采样点的数量并不是固定的,更多的采样点能带来更精确的覆盖率,而最终的像素颜色将由片元本身的颜色和覆盖率共同决定;

对于边缘像素来说越低的覆盖率,其颜色越浅。

GLSL

attribute、uniform、varying

attribute、uniform 和 varying 都是存储限定符,都可以用来修饰变量;

attribute 变量只能用于顶点着色器;

uniform 变量既可以用于顶点着色器,也可以用于片元着色器,用来表示不变的数据;( uniform 表示不变的数据的意思是在 GLSL 程序内部不能再被改变,但是可以再次由 JavaScript 程序赋予新的值);

varying变量意味着该变量会从顶点着色器传入到片元着色器。

gl_FragCoord

片元着色器的内置变量,其xy属性表示当前片元的窗口空间坐标,起始处再窗口左下角(如果窗口尺寸是800x600,那么一个片段的窗口坐标x值的范围就在0800之间,y值的范围就在0600之间)。

示例程序

发表评论

电子邮件地址不会被公开。 必填项已用*标注