1、齐次坐标git
在3D世界中表示一个点的方式是:(x, y, z);然而在3D世界中表示一个向量的方式也是:(x, y, z);若是咱们只给一个三元组(x, y, z)鬼知道这是向量仍是点,毕竟点与向量仍是有很大区别的,点只表示位置,向量没有位置只有大小和方向。为了区分点和向量咱们给它加上一维,用(x, y, z, w)这种四元组的方式来表达坐标,咱们规定(x, y, z, 0)表示一个向量,(x, y, z, 1)或(x', y', z', 2)等w不为0时来表示点。这种用n+1维坐标表示n维坐标的方式称为齐次坐标。github
齐次坐标除了可以区分点和向量,在3D图形学中还有重要的意义。齐次坐标系使得咱们能够在一中特殊的方程组中求出解,这个方程组中每个方程都表示一个与系统中其余直线平行的直线。咱们知道在欧几里得空间中,对这种方程组是无解的,由于他们没有交点。然而在现实世界中咱们是能够看到两条平行线相交的。web
两条平行的铁路最终相较于无穷远处。这就说明人眼看到的世界并非欧几里得空间,而是在一个名为透视空间中的世界。因此要在2D屏幕上表示3D世界,咱们须要一个数学工具来承担这项任务,而齐次坐标很完美的承担了这项任务。算法
若是咱们知道一个三维点的齐次坐标为(X, Y, Z, w),那么它的3D空间坐标为:spring
x = X / w编程
y = Y / wcanvas
z = Z / wapi
咱们能够看到齐次坐标(1, 2, 3, 1)与(2, 4, 6, 2)表示的都是3d空间中的点(1, 2, 3);因此一般在程序设计中咱们都取w为1.数组
如今咱们再来看一下上面说的齐次坐标在一组平行线中求解,有两条直线:编程语言
Ax + By + Cz + D = 0
Ax + By + Cz + d = 0;
D不等于d;根据解析几何知识咱们能够知道这是两条在欧几里得空间中这是两条相交的平行线,它们不可能有交点。若是d = D两条直线会重合。如今咱们把他们用齐次坐标来表示:
A(X/w) + B(Y/w) + C(Z/w) + D = 0;
A (X / w) + B (Y/w) + C (Z/w) + d = 0;
方程组两边同时乘以w获得:
AX + BY + CZ + Dw = 0;
AX + BY + CZ + dw = 0;
因此在齐次空间中对于四元组(X, Y, Z, w)(想一下极限的概念)当w无限趋近于0时,欧几里得空间中的两条平行线有无穷多个解(X, Y, Z, 0);他们再无穷远处相交了。如同咱们人眼看到的现实世界中两条平行线相交同样。
2、矩阵迷宫
咱们先来看一下在2d中将一个点(x, y)绕原点旋转 α 角度获得(x', y')的过程:
对于点x,y的极坐标表示为:
x = r * cosβ
y = r * sinβ
旋转后的坐标x', y'为:
x' = r * cos(α + β) = rcosβcosα- rsinαsinβ = xcosα - ysinα
y' = r * sin(α + β) = rcosαsinβ + rsinαcosβ = ycosα + xsinα
那么咱们用n+1为齐次坐标并结合矩阵表示为:
我在学习webgl过程当中常常有一个疑问,为何矩阵能够表示空间变换,有一个大牛告诉我,表示空间变换的并非矩阵自己,而是一系列数学公式,就像上面用到的三角函数公式同样,而矩阵的运算法则可以指把公式的运算结果很好的表达出来。要想搞明白这些矩阵表示的空间变换须要本身手动的把这些变换结果推导出来。
另外有看过opengl相关矩阵运算的同窗必定会发现上文中的运算用的是行向量的形式,而opengl用的是列向量形式,从行向量到列向量只须要转支一下便可。这里也正是我想重点强调的,对于初学者来讲,行向量/列向量、行存储/列存储、以及平移旋转的表达顺序,这三者糅杂在一块儿很容易把人绕晕,由于它有2*2*2=8中状况。尤为是不一样的书籍他们使用的表达方式、存储方式、以及对运算顺序的表达是彻底相反的(好比《3D数学基础》跟《opengl权威指南》就是彻底相反的),为了统一块儿见,我建议你们按照这样的方式来思考问题:
1)webgl中使用的是列向量,对应的缩放、平移、旋转矩阵为:
2)webgl使用的是列存储
在实际编程语言中,咱们使用的一维数组来存储4x4矩阵的16个元素。所谓的行存储和列存储的区分就在于数组的前四个元素存储的是矩阵的第一列仍是第一行;表示列的称为列存储,表示行的成为行存储。以下图数组的前四个元素对应矩阵中的第一列,因此是列存储。
3)webgl中矩阵的运算顺序是从右往左进行的
当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,因此你应该从右向左读这个乘法。因此先旋转后平移的的矩阵操做是TRV而不是RTV。我发如今跟同事讨论问题时,每每就是你们在这个地方有分歧而说到两个不一样的方向最后吵起来。由于有的同事看到TRV他按照左往右读的说法是先平移后旋转,而后你们就在先平移后旋转仍是先旋转后平移里争执。(跟人讨论矩阵运算顺序时必定要写在纸上)
矩阵乘法是不遵照交换律的,这意味着它们的顺序很重要。建议您在组合矩阵时,先进行缩放操做,而后是旋转,最后才是位移,不然它们会(消极地)互相影响。好比,若是你先位移再缩放,位移的向量也会一样被缩放(好比向某方向移动2米,2米也许会被缩放成1米)!
肯定矩阵运算顺序后,接下来要肯定矩阵操做类库的api的调用顺序。向glmatrix这种类库提供的api,对先旋转后平移这种矩阵操做的实现方式是:(看api的很容易让人认为是先平移后旋转)
因此在webgl中通常api的调用顺序都是跟矩阵的运算顺序相反的,这点与opengl一致。
另外注意一下:有不少书籍会告诉你一个矩阵的某些元素表明旋转,某几个元素表明平移,好比左上角9个元素表明旋转,12-15表明平移,实际这些都不必定,一旦矩阵有了组合操做,那么这些均可能改变。
3、模型矩阵与模型视图矩阵
现实世界中咱们能够创建各类坐标系,若是咱们以一个物体原点(本身任意指定)来创建坐标系,而且这个坐标系在初始时与世界坐标系重合,那么这个物体上的全部点的坐标都是相对这个局部坐标系来。若是咱们移动或者旋转缩放物体,咱们会使用一个矩阵来编码这些变换。这个矩阵称为模型矩阵。在咱们用模型矩阵乘以咱们对象中的顶点就获得一系列新的坐标,这些坐标就是物体在世界坐标系中的顶点位置。
咱们在2D屏幕上显示三维物体,就像用相机拍摄图像同样。在三维世界中有一个假想的相机,咱们在屏幕上看到的场景都是在相机坐标系下表示的,要把世界坐标系中的点转化成相机坐标系的点,咱们就须要一个变换矩阵。这个矩阵称为模型视图矩阵,而模型视图矩阵就是相机的模型矩阵的逆矩阵。
咱们想要看到世界中的任何场景只要控制相机的移动和旋转便可。用户控制相机的过程主要是两个事情:朝向和位置。只要这两个属性肯定了,相机的模型矩阵以及模型视图矩阵均可以获得了。用3D变换的角度来讲就是旋转和平移。能够想象对于任意一个3d场景咱们均可以将相机作一个旋转而后平移到一个位置来观察到它的任意细节。
如今咱们先改变相机的朝向而后平移到一个位置,这个模型矩阵为:
C = TR
T表明平移变换,R表明旋转变换(R的前三列表明相机旋转后的三个坐标轴),那么这时候的模型视图矩阵为:
这个C-1就是咱们要的模型视图矩阵,上面说到相机旋转后的三个轴是互相垂直的,也就是正交的,而正交矩阵的逆矩阵等于矩阵的转置矩阵。因此C-1最终变为:
而T的逆矩阵很简单:
最终的模型视图矩阵为:
而咱们在三维开发中经常使用的求模型视图矩阵的方法lookAt用的就是这个原理。
这个函数主要须要三个参数:eye表明相机位置、target表明相机的目标点、up表明相机的上方向。咱们称相机模型矩阵的第一列表明相机的x轴,咱们称为right向量;第二列表明相机的y轴,咱们称为up向量,第三列表明相机的z轴,咱们称为相机轴(相机轴并非相机的朝向,而是相机朝向的负方向,另外这里咱们的相机的模型矩阵统一使用的右手系,有的资料里面用的是左手系)。
mat4.lookAt = function (eye, center, up, dest) { if (!dest) { dest = mat4.create(); } var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, eyex = eye[0], eyey = eye[1], eyez = eye[2], upx = up[0], upy = up[1], upz = up[2], centerx = center[0], centery = center[1], centerz = center[2]; if (eyex === centerx && eyey === centery && eyez === centerz) { return mat4.identity(dest); } //vec3.direction(eye, center, z); // 首先根据观察点和相机位置求得相机轴向量 z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; // normalize (no check needed for 0 because of early return) // 对相机轴作标准化 len = 1 / Math.sqrt(z0 \* z0 + z1 \* z1 + z2 \* z2); z0 \*= len; z1 \*= len; z2 \*= len; //vec3.normalize(vec3.cross(up, z, x)); // up向量叉乘z轴获得x轴,即咱们说的right向量 x0 = upy \* z2 - upz \* z1; x1 = upz \* z0 - upx \* z2; x2 = upx \* z1 - upy \* z0; len = Math.sqrt(x0 \* x0 + x1 \* x1 + x2 \* x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 \*= len; x1 \*= len; x2 \*= len; } //vec3.normalize(vec3.cross(z, x, y)); // 而后根据z轴叉乘x轴获得相机的y轴 y0 = z1 \* x2 - z2 \* x1; y1 = z2 \* x0 - z0 \* x2; y2 = z0 \* x1 - z1 \* x0; len = Math.sqrt(y0 \* y0 + y1 \* y1 + y2 \* y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 \*= len; y1 \*= len; y2 \*= len; } // 最终获得的模型视图矩阵为:R^T \* T^-1 dest[0] = x0; dest[1] = y0; dest[2] = z0; dest[3] = 0; dest[4] = x1; dest[5] = y1; dest[6] = z1; dest[7] = 0; dest[8] = x2; dest[9] = y2; dest[10] = z2; dest[11] = 0; dest[12] = -(x0 \* eyex + x1 \* eyey + x2 \* eyez); // -x轴点乘eye向量 dest[13] = -(y0 \* eyex + y1 \* eyey + y2 \* eyez); // -y轴点乘eye向量 dest[14] = -(z0 \* eyex + z1 \* eyey + z2 \* eyez); // -z轴点乘eye向量 dest[15] = 1; return dest; };
这里讲的都是经过先改变相机朝向而后改变相机位置的方式来观察三维场景中的物体,实际上也经过别的方式好比先将相机平移到一个位置,而后绕世界坐标系旋转的方式来观察场景,这种算法的效果就像是将相机固定在轨道上同样(咱们经过先改变朝向在平移也能达到这种效果),在有的资料中它把这种先平移后旋转方式称为轨道相机,把先旋转后平移称为跟踪相机。
4、透视矩阵
经过模型视图变换,3d场景中的物体已经可以用相机空间坐标来表达,接下来咱们处理的是如何来模拟人眼的近大远小效果。相机坐标系中的物体仍是处于3d世界中,要作出近大远小的效果还须要继续变换。这个变换被称为透视投影,它的特色是全部投影线都从空间一点投射,离视点近的物体投影大,离视点小的物体投影小,小到极点称为灭点。
通常将屏幕放在观察者和物体之间。投影线与屏幕的焦点就是物体点上的透视投影。这里咱们的观察点就是相机的位置。
你们对透视投影有了基本认识,如今咱们来讲一些透视除法也叫视锥体裁切,什么意思呢?你们想一下咱们人眼是否是只能看到一部分的世界内容,而不是所有,咱们视野范围以外的内容已经被过滤掉了,因此在3d图形学模拟人眼的过程当中也有一步就是将多余内容裁切掉。
在3d图形学中咱们模拟透视投影是经过一个六面体构造出投影矩阵来作透视效果:
除了穿过投影面正中心的投影线没有变形外,与其它投影线相交的点都存在变形。假设点p在相机坐标系下为(x, y, z),对应着投影面上的点为p'(x', y', z').
在相机坐标系下,一个点在投影线上的投影对应为它的z份量,那么根据三角形类似法则,咱们就能求出对应的x'和y'与z的关系:
n/|z| = y' / y
y' = n*y / |z|
由于这里在相机坐标系下,z是负数因此|z| = -z,那么上式变为:y' = n * y / (-z);
同理可求出x为:x' = n * x / (-z);
那么咱们获得的投影再近平面的坐标p'为(nx / (-z), ny / (-z), -n);
这个时候咱们发现p'的z份量永远都是-n,也就是说原来p的z在投影后已经丢失了。
让咱们先放一下,接下来咱们说一下透视除法,透视除法的目的是把投影变换后xyz任意一个不在-w与w之间的物体去除掉。然而在视锥体这里面作裁切并不容易,因此数学前辈想了一个方式,让咱们用一个立方体来作裁切,webgl中通过透视投影变换后的物体通过透视除法后会在一个xyz都是-1到1的立方体之间,这时候的坐标称为设备归一化坐标,简称ndc。
那么既然投影到近平面那部分坐标的z值已经丢了,反正后面也要变换到ndc,干脆这里直接用一种方式来表示归一化后的z;这里咱们这样设置近平面坐标p':
p' = (-nx/z, -ny/z, (az+b) / z);
能够看到x', y' 都与1/z成线性关系,因此这里让z'也与1/z保持线性关系,因此:Z(ndc) = b / z + a;
那么将p'变为齐次坐标后为:
(nx, ny, -az - b, -z);
这时候齐次坐标来源于,投影矩阵x视坐标 = p';
m * [x, y, z, 1]T = [nx, ny, -az - b, -z]T
可的矩阵m为:
前面已经说到用(az + b)/z直接对应ndc坐标,因此
当z = -n时,(az + b)/z = -1;
当z = -f时, (az + b)/z = 1;
联立方程组得:
a = (n+f)/(f-n)
b = (2nf)/(f-n)
如今的矩阵m为:
如今Zndc已经知足了-1到1的结果;咱们的x', y' 还停留在投影平面坐标中,还须要由投影面转到ndc坐标中,数学家在处理两者的关系时,选择了以下的对应关系:
(-nx/z - left)/(right - left) = X(ndc) - (-1) / (1 - (-1))获得
X(ndc) = -(2nx/z)/ (right - left) - (right+left)/(right-left);
(-ny/z - bottom) / (top - bottom) = Y(ndc) - (-1) / (1 - (-1))获得
Y(ndc) = - (2ny/z) / (top - bottom) - (top + bottom) / (top - bottom);
那么如今P(ndc) = [-(2nx/z)/ (right - left) - (right+left)/(right-left), - (2ny/z) / (top - bottom) - (top + bottom) / (top - bottom), (az+b)/z, 1]
齐次坐标为:
[2nx/(right-left) + (right+left)z/(right - left), 2ny/(top - bottom)+(top+bottom)z/(top-bottom), -az-b, -z];
而齐次坐标由矩阵运算:
m ' * [x, y, z, 1]T = [2nx/(right-left) + (right+left)z/(right - left), 2ny/(top - bottom)+(top+bottom)z/(top-bottom), -az-b, -z]T
能够获得矩阵m'为:
那么如今m'就是咱们最终直接变换到ndc坐标系下的变换矩阵。
这里能够看到这个变换矩阵彻底由视锥体的六个参数构成。咱们在转换到ndc的过程当中,对于x和y首先中间转换到投影平面坐标,因为投影后的z丢失,因此使用一个跟原来1/z成线性关系的表达式对应到Z(ndc)获得a跟b的值,而后由投影屏幕坐标与ndc的一个对应关系获得最终变换到ndc坐标系下X(ndc),Y(ndc)与视坐标系中x,y的对应关系,最终获得终极的透视矩阵。
而一般的类库api都会提供设置视锥体的方法,好比gl-matrix:
fovy对应的角度称为俯仰角:
根据关系三角函数能够算出top;
aspect为宽高比:width/height用来根据top计算出left和right;
5、屏幕坐标变换
与以前的步骤不一样,视口变换不是由矩阵变换产生的。在这里咱们使用webgl的viewport函数。变换函数为:
function fromSreenToNdc(x, y, container) { return { x: x / container.offsetWidth * 2 - 1, y: -y / container.offsetHeight * 2 + 1, z: 1 }; } function fromNdcToScreen(x, y, container) { return { x: (x + 1) / 2 * container.offsetWidth, y: (1 - y) / 2 * container.offsetHeight }; }
这里能够看到aspect最好设置为canvas.offsetWidth/canvas.offsetHeight,经过前面的图能够知道投影面多是矩形面,而ndc是正方形,因此投影过程当中会产生变形,而ndc到屏幕坐标中也是会产生变形,也就是咱们让投影面到ndc有变形,而后让ndc到屏幕在变形回去,这样就能保证最终显示在屏幕上的3d物体保持原来比例。
参考资料:
[
](https://learnopengl-cn.github.io/01%20Getting%20started/07%20Transformations/)