本节内容来自于小册 WebGL 入门与实践。javascript
上一节咱们讲了 WebGL 坐标系的分类以及它们之间的转换方式,本节开始详细介绍坐标系基本变换的算法实现,图形学中实现变换的主要数学工具是矩阵
,因此在讲解坐标系变换以前,咱们先温习一下矩阵。前端
舒适提示:java
在学习矩阵变换时,必定要搞清楚如下三点:算法
行向量
仍是列向量
。
行向量
,按照数学领域
中矩阵相乘的规则,向量要放在左侧
相乘。列向量
,向量要放在右侧
相乘。行主序
仍是列主序
。
第一行
第一列
在多个矩阵变换时,不一样的相乘顺序会致使不一样的结果,因此咱们要保证矩阵相乘的顺序是咱们指望的。假设有三个变换矩阵:旋转矩阵 R,平移矩阵 T,缩放矩阵 S,以及顶点向量 P,那么 P 变换到 P1 的顺序通常是这样的:canvas
即先缩放
,再旋转
,最后平移
。数组
3D 学习过程当中的一大难点就是矩阵变换,咱们常常看到矩阵左乘一个列向量就可以实现平移、旋转、缩放等效果。框架
那么,矩阵背后的神秘力量是什么呢?函数
其实矩阵并不神秘,只是矩阵能够对一些数字按照矩阵的规则执行一系列运算操做,简化了咱们使用+
、 -
、 *
、 /
进行变换运算的步骤而已。工具
一个矩阵能够理解为一种变换,多个矩阵相乘表明多个变换。学习
常见的矩阵变换有以下几种:
可是在坐标系转换中,最常应用的是前三种。咱们看一下如何用矩阵表示这些变换。在讲解矩阵变换以前,咱们先从头捋一下点
和向量
的表示。
前面章节咱们讲了齐次坐标
,它用来区分点
和向量
,齐次坐标使用 N+1
维向量表示 N 维空间,第 N+1 维数字若是是 0 的话,则表明 N 维空间中的向量
,以下用 4 维向量表示 3 维空间中的一个向量:
第 N+1
维数字若是是非0数字
的话,则表明 N 维空间下的点
:
使用 N+1 维数字表示 N 维空间中的点或向量的方式就是齐次坐标。
齐次坐标除了可以区分点
和向量
,还有两大用处:
模拟透视效果咱们上一节已经介绍了,在裁剪坐标系中,w 值越大,通过透视除法后的坐标越小,因而也就有了近大远小的投影效果。
前面章节已经讲过,n 阶矩阵只能和 n 维列向量相乘,获得一个新的 n 维列向量。
乘得的结果只能表示缩放和旋转变换,没有办法表示平移变换,由于平移是在原向量的基础上加上一个常量位移,属于加法操做,可是 n 阶矩阵和 n 维列向量相乘的话,每一步都是相乘操做,没有加法运算,因此没法用 n 阶矩阵和 n 维列向量表示 n 维列向量的平移。
要注意:上面所说的列向量指的是坐标,不是数学意义上的向量。
能够看到 n 维矩阵和 n 维向量相乘,不能实现 n 维向量和一个常量进行加减的操做。
咱们期待的是获得这样一个向量:
其中 p 是常数,表明平移的大小。
咱们看一下齐次坐标是如何帮助咱们解决这个问题的。
顶点 P 用齐次坐标表示以下:
由于 3 维坐标用齐次坐标的话须要增长到 4 维,因此表示平移变换的矩阵也要相应地变成 4 阶矩阵,咱们看下这个 4 阶矩阵如何构成:
在原来基础上增长一行和一列,其中第四行前三个份量为 0,第四个份量为 1,这样矩阵和向量的乘积获得的新的向量的第四个份量也是 1,因此也是表明点。
第四列t一、t二、t3分别表明沿 x 轴、y 轴、 z 轴方向上的平移量。
咱们推算验证一下:
转换后的向量的每个份量都实现了ax + by + cz + 常数
的格式,也就是说,向量能够经过乘以一个矩阵实现平移操做。
首先咱们要知道,对物体(顶点)作平移、旋转、缩放的变换操做至关于对原来的坐标系作平移、旋转、缩放变换,获得一个新坐标系。了解了这一点,咱们就能够学习一种求解变换矩阵的简单方式:
基向量是指坐标系中各个坐标轴正方向的单位向量,假设 Ux 表明 X 轴的单位向量,那么 Ux = (1, 0, 0),同理, Uy = (0, 1, 0),Uz = (0, 0, 1)。
基向量是坐标系变换的基础,咱们求解坐标变换矩阵关键就是要找到原坐标系的基向量在新坐标系中的表示。
这是一个简单易于理解的求解思路,掌握了这个思路,无论进行什么样的变换,咱们都能很快地求出来变换矩阵,只须要找到这些值,而后将其代入矩阵框架
就行啦。
下面是一个坐标系变换的例子,坐标系 oxyz 绕 Z 轴旋转 β 角度后造成了新坐标系 ox'y'z':
你们必定要分清,新坐标系是 ox'y'z'
,原坐标系是 oxyz
,新坐标系的基向量
在原坐标系下的表示咱们利用三角函数运算便可求出,如上图所示,因此按照求解思路的第一步,新坐标系的基向量在原坐标系表示为:
原坐标系的坐标原点和新坐标系重合,因此新坐标系原点在原坐标系下的表示:
代入通用矩阵框架后得出变换矩阵为:
按照上面的矩阵变换求解思路来寻找平移矩阵:
一、求出原坐标系的基向量在新坐标系的表示。
因为没有进行旋转和缩放操做,因此新坐标系的基向量和原坐标系同样:
二、新坐标系坐标原点的坐标:
将这些值代入变换矩阵框架
咱们用 JavaScript 实现上述平移矩阵。
还记得吗?WebGL 矩阵是列主序的,每隔 4 个数表明一列。
function translation(tx, ty, tz, target){
target = target || new Float32Array(16);
// 第一列
target[0] = 1;
target[1] = 0;
target[2] = 0;
target[3] = 0;
// 第二列
target[4] = 0;
target[5] = 1;
target[6] = 0;
target[7] = 0;
// 第三列
target[8] = 0;
target[9] = 0;
target[10] = 1;
target[11] = 0;
// 第四列
target[12] = tx;
target[13] = ty;
target[14] = tz;
target[15] = 0;
return target;
}
复制代码
平移矩阵的生成算法很简单,按照数学关于矩阵的定义,在指定位置设置正确的值便可。
以后咱们就能够用该算法生成一个平移矩阵实现顶点的平移变换了。
平移矩阵的演示
咱们绘制两个半径为 5 的球体,第一个球体在世界坐标系中心,第二个球体沿着 X 轴偏移 10 个单位,为了演示方便,咱们先设置一个正射投影矩阵,左平面位于 -15 处,右平面位于 15 处,上平面位于 15 处,下平面位于 -15 处,远平面位于 1000,近平面位于 -1000。
//获取视口宽高比
var aspect = canvas.width / canvas.height;
//获取正射投影观察箱
var perMatrix = matrix.ortho(-aspect * 15, aspect * 15, -15, 15, 1000, -1000);
// 获取平移矩阵
var translationMatrix = matrix.translation(10, 0, 0);
// 将矩阵传往 GPU。
gl.uniformMatrix4fv(u_Matrix, translationMatrix);
复制代码
一、gl.uniformMatrix4fv该方法的做用是 JavaScript 向着色器程序中的`u_Matrix`属性传递一个 4 阶`列主序`矩阵。
二、ortho 方法是生成正射投影矩阵的方法,讲到投影变换时咱们再对它的实现作讲解。
复制代码
右侧球体是平移后的效果:
能够看出,平移矩阵可以正常工做。
缩放是将组成物体的各个顶点沿着对应坐标轴缩小或者放大,一种方法是:使用顶点向量乘以缩放向量便可实现。请注意数学领域向量和向量只有点乘和叉乘,并无一种运算能够实现向量与向量各个份量相乘获得一个新的向量。 不过在上一节咱们使用 JavaScript 实现了这样一个算法,在这里就能够用到了:
Vector3 vec = new Vector3(3, 2, 1);
Vector3 scale = new Vector3(2, 2, 1);
vec = vec.multiply(scale);
复制代码
可是这里咱们要实现的是经过向量和矩阵相乘的方式来实现。
咱们要构建一个缩放矩阵,缩放矩阵也比较简单,按照上面的求解思路:
一、新坐标系基向量在原坐标系下的表示:
沿着 X 轴缩放 sx 倍,至关于将原来的基向量放大了 sx 倍,因此新坐标系下一个单位的长度至关于原来坐标系下的 sx 个长度,以此类推,咱们很容易地推导出 Y 轴和 Z 轴的基向量
二、原坐标系原点在新坐标系下的坐标:
因为缩放操做没有改变原点位置,因此,原点坐标在新坐标系下仍然是(0,0,0)。
将这些值代入变换矩阵框架,能够得出:
上面这个图就是一个典型的缩放矩阵:
function scale(sx, sy, sz, target){
target = target || new Float32Array(16);
// 第一列
target[0] = sx;
target[1] = 0;
target[2] = 0;
target[3] = 0;
// 第二列
target[4] = 0;
target[5] = sy;
target[6] = 0;
target[7] = 0;
// 第三列
target[8] = 0;
target[9] = 0;
target[10] = sz;
target[11] = 0;
// 第四列
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
复制代码
调用该方法须要指定三个方向的缩放比例,可是有时咱们可能只缩放某个方向,因此须要再衍生三个缩放函数
function scaleX(sx){
return scale(sx, 1, 1);
}
function scaleY(sy){
return scale(1, sy, 1);
}
function scaleZ(sz){
return scale(1, 1, sz);
}
复制代码
相比平移和缩放,旋转矩阵相对复杂一些,咱们从 2D 平面上一个顶点的旋转提及。
点 P(x, y) 旋转 β 角度后,获得一个新的顶点 P1(x1, y1) , P1 和 P 之间的坐标关系:
P 点坐标:
旋转后的 P1 点坐标:
将 P 点坐标带入 P1点能够获得:
咱们使用齐次坐标和矩阵表示:
扩展到 3D 空间,咱们一样能推导出下面三种旋转矩阵。
JavaScript 的实现,相信你们已经熟记于心了,咱们只须要在矩阵的各个位置指定对应数字便可。
function rotationX(angle, target){
target = target || new Float32Array(16);
let sin = Math.sin(angle);
let cos = Math.cos(angle);
target[0] = 1;
target[1] = 0;
target[2] = 0;
target[3] = 0;
target[4] = 0;
target[5] = cos;
target[6] = sin;
target[7] = 0;
target[8] = 0;
target[9] = -sin;
target[10] = cos;
target[11] = 0;
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
复制代码
只要你理解了矩阵的运算规则,并推导出变换矩阵,以后只需将各个位置的元素赋值到一个类型化数组中便可。
算法和 X 轴旋转极其类似,就不在这里写了,具体实现请看这里。
具体实现请看这里。
请注意:以上每一种旋转都是单一旋转,但每每咱们须要既沿 X 轴旋转,又要沿 Y 轴旋转,这种状况,咱们只须要将旋转矩阵相乘,获得的新的矩阵就是包含了这两种旋转的变换矩阵。
上面三种是绕坐标轴进行旋转,但实际上咱们每每须要绕空间中某一根轴旋转,绕任意轴旋转的矩阵求解比较复杂。
这里咱们采用过原点的任意轴旋转,不考虑平移状况,若是是绕一个不过原点的任意轴旋转的话,咱们能够利用一个旋转矩阵和一个平移矩阵来完成。
咱们看下如何推导,以下图所示:
点 C 旋转 β 角以后,来到 C' 点。
假设这个变换矩阵为 M,那么 M 和 角度 β以及旋转轴 有关,
以下图所示:
咱们如今须要求得如何用点C、 旋转角β以及旋转轴 表示 C'。
用数学公式表示:
一、向量点乘
,求出向量
。
二、求出
三、经过向量叉乘
求得
四、利用三角函数求出
五、利用向量加法求出
六、将1-4步代入第5步,得出:
假设旋转轴向量表示为:
新坐标系基向量U(Ux,Uy,Uz)在原坐标系中的坐标位置求解以下:
利用向量点乘、叉乘规则最终推导出:
同理,将Y 轴基向量 (0, 1, 0) 代入上面公式,推导可得:
这样咱们就求出了新坐标系的基向量在原坐标系的表示。
接下来,咱们找出新坐标系的原点在原坐标系下的坐标,由于是绕原点旋转,因此坐标不变,仍然是(0,0,0)。将这些值代入矩阵框架,得出绕任意旋转轴的变换矩阵:
有了变换矩阵,那么咱们就能够实现JavaScript的任意轴旋转矩阵了:
function axisRotation(axis, angle, target){
var x = axis.x;
var y = axis.y;
var z = axis.z;
var l = Math.sqrt(x * x + y * y + z * z);
x = x / l;
y = y/ l;
z = z /l;
var xx = x * x;
var yy = y * y;
var zz = z * z;
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var oneMCos = 1 - cos;
target = target || new Float32Array(16);
target[0] = xx + (1 - xx) * cos;
target[1] = x * y * oneMCos + z * sin;
target[2] = x * z * oneMcos - y * sin;
target[3] = 0;
target[4] = x * y * oneMCos - z * sin;
target[5] = yy + (1 - yy) * cos;
target[6] = y * z * oneMCos + x * sin;
target[7] = 0;
target[8] = x * z * oneMCos + y * sin;
target[9] = y * z * oneMCos - x * sin;
target[10] = zz + (1 - zz) * cos;
target[11] = 0;
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
复制代码
以上就是绕任意轴进行旋转的矩阵。
咱们利用上面的算法演示一下,使用四根旋转轴:
// 中间立方体绕 X 轴旋转。
var axisX = {x: 1, y: 0, z: 0}
//右边立方体绕 Y 轴旋转
var axisY = {x: 0, y: 1, z: 0}
// 左边立方体绕 Z 轴旋转
var axisZ = {x: 0, y: 0, z: 1}
// 上边立方体绕对角线轴旋转。
var axisXYZ = {x: 1, y: 1, z: 1}
复制代码
效果以下:
绕任意轴旋转的推导比较复杂,涉及到立体几何以向量点乘叉乘等运算,不过它的使用方法仍是很简单的。
本节主要讲解坐标系变换过程当中涉及到的基本变换的原理与实现,涉及几何和三角函数的运算比较多,你们看一遍可能不能明白,不妨多看几遍,拿纸笔写写画画,很快就会豁然开朗。
虽说这些 API 只要能看懂、会用就足够了,没有必要去掌握推导过程,但我仍然建议你们尝试推导一遍,掌握推导过程对巩固学过的数学知识颇有帮助,也能够培养本身利用数学知识解决疑难问题的能力。
下一节咱们学习如何利用这些基本变换实现各个坐标系之间的变换。
这一系列的内容来自于小册 WebGL 3D 入门与实践,若是你们对进阶知识感兴趣,能够到小册中去学习:小册:WebGL 3D 入门与实践。
小册内容除了包含 WebGL 相关的基础练习,还包括 3D 图形概念与相关数学的原理与推导,旨在帮助你们创建图形学的技术轮廓。这部分图形学知识独立于 WebGL,除了能够适用于 WebGL,还适用于 OpenGL 等。
固然,本小册主要目的仍是帮助 Web 前端同窗学习 3D 技术,除了介绍适用 WebGL 实现 3D 效果之外,还对 CSS3 中的 3D 技术相关属性进行了深刻剖析,并演示了与数学库的结合使用,但愿可以让前端同窗不只仅局限于通常的二维平面开发,也可以在 3D 开发上更近一步~