初识 WebGL:坐标系与立方体
前言
在之前的两篇文章中,我们分别学习了 WebGL 有关渲染管线的知识,并通过实战了解了 WebGL 的三大基本图元与缓冲区的概念。今天这一篇文章,让我们来了解下 WebGL 中的坐标系有关概念,并据此实现一个可动的立方体。
1. 矩形的实现
在正式开始学习坐标系之前,先让我们把上篇文章留下的问题解决了。
正如我们上篇文章所说的,在 WebGL 中基础图元只有 3 个,分别是 点,线,三角形。但是我们日常使用时,会遇上各式各样的图形,比如矩形,菱形,圆形这些,我们又该怎么去实现呢?其实很简单,回忆一下以前学习的数学几何知识,一个矩形,可以由两个等腰直角三角形拼接而成。
大家会发现,我们这里三角形的顶点连接顺序,是逆时针的。这个是 WebGL 的默认设置,当顶点顺序为逆时针时,这个平面代表正面,顺时针为背面。WebGL 有一个背面剔除的功能,开启此功能之后,背面是不会被绘制的。这个能力主要是在绘制 3D 形体时使用,对性能有一定的优化作用。
绘制的方法很简单,着色器和 js 的代码都没有什么改动,唯一重要的区别在于 positions
坐标数据数组中,我们需要传入两个三角形总计 6 个顶点的数据。但是,实际上,这 6 个顶点中,v0 - v2 这条边被传入了两次,导致这条边会被重复渲染,造成了不必要的性能和空间损耗。一个顶点算上坐标和颜色,一共有 4 * 6 = 24 个字节大小。在一个复杂的 WebGL 应用中,会有成百上千个顶点,到时候造成的损耗就十分可观了。为了解决这个问题,webGL 提供了一个根据顶点索引进行绘制的方法 gl.drawElements
。通过这个方法,我们可以避免存储重复的顶点,优化性能。当然,这个方法不仅仅可以画矩形,也可以画其他的图形。
我们在 js 代码中,新建一个缓冲区,专门用于存放索引。
最终的实现效果如图:
四边形都可以用这种绘制方法进行绘制,而圆形则可以通过数学公式中的面积推导过程得出画法:
2. WebGL 的坐标系
WebGL 在拿到顶点数据,到最终将顶点渲染在屏幕上,其中实际上经过了一系列的坐标系转化过程。
顶点坐标起始于模型坐标系,此时顶点坐标即为模型坐标。模型坐标在 CPU 中经过一系列坐标系变换,生成裁剪坐标,之后 CPU 将裁剪坐标传递给 GPU。GPU 再讲裁剪坐标系转化为 NDC 标准设备坐标系,再经过视口变化,转为屏幕坐标系,GPU 拿到屏幕坐标系坐标后,再在屏幕上展示该顶点。
这里,我们来了解下各个坐标系的概念:
-
模型坐标系:一个物体可以由很多个点组成,用于参照每个点在这个物体中的位置的坐标系,就是模型坐标系;
-
世界坐标系:模型坐标系下各个坐标在世界中的位置,就是世界坐标系的坐标;
-
观察坐标系:人眼或者摄像机看到的世界中的物体相对于他自身的位置所参照的坐标系就叫观察坐标系;
-
裁剪坐标系:观察特定范围内的顶点所对应的坐标系;
-
NDC 坐标系:标准设备坐标系;
-
屏幕坐标系:最终顶点展示在屏幕上的位置对应的坐标系。
以上各个坐标,除了裁剪坐标系与 NDC 坐标系外都轴遵循右手坐标系,即 X 轴向右,Y 轴向上,Z 轴朝向屏幕外。
这么多坐标系中,我们需要着重了解的是裁剪坐标系。裁剪坐标系由观察坐标系转化而来,它遵循的是左手坐标系规则,即 Z 轴朝向屏幕内。在之前的文章中,我们有简单介绍过它的概念;这里,我们展开来说说裁剪坐标系。
裁剪左边系会先通过投影矩阵生成观察箱,观察箱也就是平截头体内的坐标才会显示在屏幕上。通常来说,投影矩阵会有两种,分别是正射投影矩阵和透视投影矩阵。
正射投影矩阵的话,是通过制定可视区域范围,创建了一个立方体,在这个立方体内的顶点才会经过后面的转化展示在屏幕上。通过正射投影映射得出的坐标的 w 分量始终为1,最终生成的物体的轮廓比例不会发生变化,因此并不符合人眼直观的近大远小的3d物体展示效果,常用于各种建筑图纸中。
透视投影,如果有学过素描的同学,对这块会感觉很熟悉。素描有一种透视画法,来在纸上绘制立体的物体。这种画法的重点,就在于近大远小这一概念,通过近大远小来体现出立体感。透视投影矩阵在将顶点映射到裁剪空间的同时,会去对每个顶点的坐标的 w 分量进行设置,使得越靠近人眼,看起来越大的坐标的 w 分量越大。透视投影需要设置近平面、远平面、透视深度。
裁剪坐标系的顶点数据,需要进行顶点透明除法才能转化为 NDC 标准设备坐标系坐标。我们之前提到的 Gl_position
接受的坐标是裁剪坐标系的坐标,传入其中的数据是一个 4 维的向量,其中相比于一般我们熟知的三维多出来的就是 w 向量,也就是齐次坐标分量。这个分量主要应用于透明除法,将裁剪坐标系转为 NDC 坐标系。这里的算法,前面也有提到,把 x, y, z 三个值 / w 即可得到。透视除法是顶点着色器程序黑盒执行的,开发者无法通过编程手段干预。
最终,在拿到 NDC 坐标后,WebGL 后续就要将顶点绘制到屏幕视窗上。但是由于 NDC 坐标系和屏幕坐标系并不一致,需要我们再进行一次坐标转化,也就是视口转化。
视口转化算法如下:
- 屏幕 canvas 坐标系 x 轴坐标:NDC 坐标系 x 轴坐标 * canvas 视口宽度 / 2
- 屏幕 canvas 坐标系 y 轴坐标:NDC 坐标系 y 轴坐标 * canvas 视口高度 / 2
坐标系转换步骤如下:
-
计算出原坐标系的原点 O 在新坐标系的坐标。(平移变换)
-
计算出新坐标系坐标分量的单位向量在原坐标系下的长度。(缩放变换)
-
计算出原坐标系的坐标分量(基向量)的方向。(旋转变换)
这里是最简单的算法步骤,如果涉及到坐标系的旋转、缩放、Z 轴的加入、透视投影,计算过程将会更复杂。而 WebGL 主要面向的 3D 动效,都避不开这些。目前业界内已经有成熟的各个坐标系转化算法,直接使用就可以满足我们大部分的需求场景。当然,我们这里还是要学习了解一下。
为了解决这些复杂的计算,我们需要引入数学中的矩阵的概念。实际上,涉及到数据图像处理的部分,无论是信息隐藏,图像识别,还是降噪优化,频谱分析,都需要借助矩阵来处理数据,这也是为什么计算机学院专业必修课中线性代数很重要的原因。矩阵大家大学的时候应该都有学习过,这里就不带大家一起复习了。在 WebGL 中,矩阵变换一般可以分为 4 种;分别是 平移变换,缩放变换,旋转变换与斜切变换。
这里我们又需要引入一个概念:齐次坐标。它用于区分点与向量。通过齐次坐标,我们可以使用 N + 1 维向量表示N 维空间。如果第 N + 1 维数字 为 0,表示向量;非 0 则表示点。基于它,我们可以模拟透视投影效果和用矩阵来表示平移变换。
回忆下线性代数的知识,一个 n 阶的矩阵和一个 N 维的列向量相乘,得到的是一个新的列向量。实际上,我们的点的坐标就是这里的 N 维列向量,而进行变换期望的结果就是得到新位置的列向量,这里的 n 阶矩阵就是我们进行变换的工具,即变换矩阵。
但是,这种计算,我们得到的结果,只能表示缩放或者旋转变换;因为这里我们只进行了乘法操作,对应的坐标并没有在某个方向上进行平移,也就是加减操作。齐次坐标由于可以 N + 1 维向量表示 N 维空间,导致计算时的变换矩阵相比于普通的 n 阶矩阵,可以多出一行一列,而这一行可以表示 w 分量,这一列可以表示各个坐标轴上的平移常量,这样我们就可以实现坐标的平移变换。
tx,ty,tz 就是 x,y,z 轴上各自的平移常量,而 0 + 0 + 0 + 1 则是这个列向量的 w 分量。
了解了变换的过程之后,为了实现变换,我们需要得到各种情况的变换矩阵。如何求出自己需要的变换矩阵,我们也有一套通用的算法:
- 求解新坐标系基向量 U 在原坐标系下表示 U‘;
- 求解新坐标系坐标原点 O 在原坐标系下表示 O1;
- 最终结果代入下方矩阵即可得到变换矩阵。
无论是旋转,平移,缩放都可以如此操作。各个坐标系之间的转化也是如此原理。各个坐标系的如模型空间变换到世界空间的模型变换,世界空间到观察空间的视图变换等,都是在得到多个变换矩阵后,进行矩阵相乘后的结果。
3. 立方体实现
一个立方体由6个面,8个顶点组成。所以我们可以先创建一个缓冲区用于存放顶点数据。然后,由于我们是每个面分开渲染,我们需要生成对应的 24 个顶点的数据,所以需要封装一个方法,通过这个去动态生成。拿到顶点数据后,逻辑和前面的绘制平面图形一致。
完成之后,我们的最终效果如下:
为什么最终的效果会是一个长方形呢?这里是因为我们前面说到的,我们给 gl_position
传入的坐标是裁剪坐标系,裁剪坐标系坐标展示在屏幕上,还需要经过经过两次转化。这里的展示问题就出现在不同屏幕 canvas 视口大小不同。为了解决这个问题,就需要我们进行投影拉伸,来解决不同视口导致的失真问题。
除此之外,一个立方体作为 3D 图形,但是我们这里看到的效果却是一个平面图形,这是因为我们现在只能看到一个面,为了展示出立体感,需要我们让他转动起来,为此需要做模型变换。
首先,我们需要在顶点着色器声明一个变量,用于存储 js 传递过来的模型变换矩阵,在获取到变换矩阵后,需要左乘我们的顶点坐标进行转换。在这之后再计算得到正交投影矩阵,通过其进行模型变化,实现旋转效果。
最终,我们就得到如下图所示的旋转立方体了。
结语
至此,初识 WebGL 暂时告一段落。目前,WebGL 越来越火热,然而大部分前端开发者都是使用 Three.js 这样别人封装好的框架,导入模型,贴图,设置骨架动画这些来实现 3D 动画。虽说简单方便易于上手,但是如果遇到了复杂的需要修改底层的代码情况,就容易出现抓瞎的情况。学习和了解 WebGL,对于我们使用 Three.js 很有帮助。后续我也会继续学习这块内容,期待和大家的下次见面。
Q.E.D.