原本打算直接写教程 04 的,可是想到3D 变换涉及的数学知识较多,每每是不少初学者的拦路虎(好比我本身)。再加上OpenGL ES 2.0 再也不提供OpenGL ES 1.0中 3D 变换相关的一些重量级函数,如 glMatrixMode(GL_PROJECTION); glMatrixMode(GL_MODELVIEW); glLoadMatrixf; glMultMatrix 等,这些函数在 OpenGL ES 2.0 中均须要咱们本身去实现。 若是不对线性代数与几何知识做一些简单介绍,恐怕很多人难以理解文中的一些步骤为何要那么作。所以今天这一篇文章将放弃原定计划,先来介绍一些 3D 数学以及 3D 变换相关的知识。BTW,原定计划的代码示例已经写好了,有兴趣的同窗能够先行浏览,代码放在这里,运行效果以下:php
咱们都学过几何学,应该都知道欧几里得(公元前3世纪希腊数学家)这位几何学鼻祖,正是这位大牛建立了欧几里得几何学,他提出了基于 X,Y,Z 三轴的三维空间概念。到了17世纪,又出了位大牛笛卡尔,咱们一般所说的笛卡尔坐标就是他的创造,笛卡尔坐标很是完美地将欧几里得几何学理论与代数学联系到一块。正是由于有了笛卡尔坐标,咱们才可以用简单的矩阵(Matrix)来表示三维变换。但用矩阵来表示三维变换操做有一个没法解决的问题-万向节锁 。什么是万向节锁呢?简单地说就是两个轴旋转到同一个方向上去了,这两个轴平行了,所以就比原来少了一维(详情可参考这里)。过了一百多年,汉密尔顿(Sir William Rowan Hamilton)建立了四元数(quaternion)解决了由于旋转而致使万向节锁的问题,而后四元数还有其余用处,但在3D数学里主要是用来处理旋转问题。html
好吧,或许你看得一头雾水,没关系,你只要知道:用矩阵来表示3D变换,但矩阵在表示旋转时可能会致使万向节锁的问题,而使用四元数能够避免万向节锁就能够了。git
在前面提到可以使用 Matrix 来表示三维变换操做,那么变换又是如何经过 Matrix 实现的呢?下面就来说这个。在这里我推荐一本3D数学入门书籍:《3D数学基础:图形与游戏开发》github
一般咱们使用 4 维向量 (x, y, z, w) 表示在3D空间中的一个点,最后一维 w 表示齐次坐标。齐次坐标的含义是两条平行线在投影平面的无穷远处相交于一点,但在 Matrix 中没有表示无穷大,因此增长了齐次坐标这一维。你能够想象下,火车轨道的两条边在无限远处看起来就相交于一点,齐次坐标详细的介绍能够参考这篇文章。编程
矩阵运算规则:数组
1) 若矩阵 A 和 B 不是互逆矩阵,则不知足乘法交换律,即 A × B 不等于 B × A;
2) M × N 阶的矩阵只能和 N × O 阶的矩阵相乘,即 N 的阶数相等,结果为 M × O 阶的矩阵;
3) 矩阵 A × B 的运算过程是 A 的每一行依次乘以 B 的每一列做为结果矩阵中的一行;
4) 矩阵 A 的逆矩阵 B 知足 A × B = B × A = 单位矩阵。
5) 单位矩阵是对角线上的值为1,其他均为 0 的矩阵。单位矩阵不影响坐标变换(你能够将下面的3D变换矩阵换成单位矩阵来思考下)。编程语言
3D空间的物体投影到2D平面上时,就须要使用到齐次坐标,所以咱们须要使用 4 × 4 的 Matrix 来表示变换。在编程语言中,这样的 Matrix 可用大小为 16 的一维数组或4 × 4 的二维数组来表示。因为矩阵乘法不知足乘法交换律,用数组表示 Matrix 又分为两种形式:行主序和列主序,它们在本质上是等价的,只不过是一个是右乘(行主序,矩阵放右边)和一个是左乘(列主序,矩阵放左边)。OpenGL 使用列主序矩阵,即列矩阵,所以咱们老是倒过来算的(左乘矩阵,变换效果是按从右向左的顺序进行): 投影矩阵 × 视图矩阵 × 模型矩阵 × 3D位置。ide
4× 4列矩阵的数组表示:数字表示数组下标对应的行列位置:函数
那么工具
平移矩阵可表示为:
平移矩阵 × 列矩阵(a, b, c, 1) = 列矩阵(a + x, b + y, c + z, 1)。
缩放矩阵可表示为:
缩放矩阵 × 列矩阵(a, b, c, 1) = 列矩阵(a × sx, b × sy, c × sz, 1)。
绕 X 轴旋转的旋转矩阵可表示为:
绕 X 轴旋转的旋转矩阵 × 列矩阵(a, b, c, 1) = 列矩阵(a, b × cos(θ) - c × sin(θ), b × -sin(θ) + c × cos(θ), 1)。
绕 Y 轴旋转的旋转矩阵可表示为:
绕 Y 轴旋转的旋转矩阵 × 列矩阵(a, b, c, 1) = 列矩阵(a × cos(θ) - c × sin(θ), b , a × -sin(θ) + c × cos(θ), 1)。
绕 Z 轴旋转的旋转矩阵可表示为:
绕 Z 轴旋转的旋转矩阵 × 列矩阵(a, b, c, 1) = 列矩阵(a × cos(θ) - b × sin(θ), a × -sin(θ) + b × cos(θ), c, 1)。
OpenGL 使用右手规则进行旋转,所以逆时针方向的选择是正角度的,而顺时针方向的旋转是负角度的。还记得中学学物理时候的右手规则么?忘记了的话,看下图:
注意:
前面说到矩阵乘法不知足乘法交换律,所以你对一个3D坐标先进行旋转,而后进行平移(平移矩阵 × 旋转矩阵 × 3D坐标);与先进行平移,而后进行旋转(旋转矩阵 × 平移矩阵 × 3D坐标)获得的效果是大为迥异的。以下图所示:
在第一种状况下,咱们一般称旋转是在 local space 中进行,由于它是绕着物体本身的中心点进行的,而在后一种状况下的旋转一般称为是在 world space 中进行的。咱们知道点是能够在坐标空间之间相互转换的,这是一个很重要的概念。OpenGL 中物体最初是在本地坐标空间中,而后转换到世界坐标空间,再到 camera 视图空间,再到投影空间,这一系列转换都是靠 matrix 计算来实现。
上面的这个过程在 OpenGL 及 OpenGL ES 1.0 中,对应的代码相似于:
glViewport (0, 0, (GLsizei) w, (GLsizei) h); a) glMatrixMode (GL_PROJECTION); b) glLoadIdentity (); glFrustum (-1.0, 1.0, -1.0, 1.0, 1.5, 20.0); c) glMatrixMode (GL_MODELVIEW); d) glClear (GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0); glLoadIdentity (); /* clear the matrix */ /* viewing transformation */ gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); e) glScalef (1.0, 2.0, 1.0); /* modeling transformation */ f) glutWireCube (1.0); g) glFlush ();
说明:
a) 是用于viewport(视口)变换,viewport 变换发生在投影到2D 投影平面以后,该变换是将投影以后归一化的点映射到屏幕上一块区域内的坐标。视口变换的目的是指定投影以后图像在屏幕上显示的区域。以下示意图所示:
视口变换 glViewport(x, y, width, height); x,y 是投影平面描绘在屏幕或窗口上的起始位置(注意屏幕坐标以左上方为原点),width和height是以像素为单位,指投影平面在屏幕上描绘的区域大小。若是投影平面的宽高比,与width/height比不相同(如上面的右图),那么描绘的场景就会扭曲。
从裁剪到屏幕的整个过程以下图所示,w 就是前面提到的齐次坐标那一维,从 Clip Space 到 Normalized Device Space 就是投影规范化的过程,从 Normalized Device Space 到 Window Space 就是 viewport 变换过程。
该转换内部计算公式为:
(xw, yw)是屏幕坐标,(x, y, width, height)是传入的参数,(xnd, ynd)是投影以后经归一化以后的点(上图中 Normalized Device Space 空间的点)。所以 viewport 变换就是将投影以后归一化的点转换为真正可用于在屏幕上进行渲染的屏幕坐标;
b) 是说明下面的 matrix 是用于投影变换的,在本例中,是经过语句 c) glFrustum 来设置透视投影变换的。投影变换有两种:正交投影和透视投影,后面会有详细介绍;
d) 是说明下面的 matrix 是用于模型视图变换,注意,OpenGL 和 OpenGL ES 都将模型变换与视图变换结合在一块儿,而不是分开为两个,这是由于模型变换等价于视图变换的逆变换。视图变换是将物体转换到观察者(通常称之为 camera)的视线空间中。你能够想象一下,照相时,你能够:A)照相机不懂,旋转本身的头找个侧面像,也能够B)本身不动,照相机旋转必定的角度来达到一样的效果。下面的两幅图分别描述了情形A)和情形B):
情形A):旋转物体,相机不动
情形B):旋转相机,物体不动
在 OpenGL 中,咱们在设置场景(scene)的时候一般是采起情形B)的作法,所以在语句 e) 处,咱们设置相机的位置和朝向,来设定视图变换,以后的语句 f) glScale 是设定在模型变换的,最后语句 g) 在本地空间描绘物体。
注意
写 OpenGL 代码时从前到后的顺序依次是:设定 viewport(视口变换),设定投影变换,设定视图变换,设定模型变换,在本地坐标空间描绘物体。而在前面为了便于理解作介绍时,说的顺序是OpenGL 中物体最初是在本地坐标空间中,而后转换到世界坐标空间,再到 camera 视图空间,再到投影空间。因为模型变换包括了本地空间变换到世界坐标空间,因此咱们理解3D 变换是一个顺序,而真正写代码时则是以相反的顺序进行的,若是从左乘矩阵这点上去理解就很容易明白为何会是反序的。
投影变换的目的是肯定 3D 空间的物体如何投影到 2D 平面上,从而造成2D图像,这
透视投影能够经过两种方式来表述,OpenGL 及 OpenGL ES 1.0 提供其中一种: glFrustum,而 glut 辅助库提供了另一种:gluPerspective。它们本质上是相同的,只不过是不一样的表述而已:
视锥体/视景体:
glFrustum(left, right, bottom, top, zNear, zFar);
left,right, bootom,top 定义了 near 裁剪面大小,而 zNear 和 zFar 定义了从 Camera/Viewer 到远近两个裁剪面的距离(注意这两个距离都是正值)。由这六个参数能够定义出六个裁剪面构成的锥体,这个锥体一般被称之为视锥体或视景体。只有在这个锥体内的物体才是能够见的,不在这个锥体内的物体就至关于再也不视线范围内,于是会被裁减掉,OpenGL 不会这些物体进行渲染。
因为 OpenGL ES 2.0 不提供此函数,所以咱们须要本身实现该函数。其计算公式以下:
假设:l = left, r = right, b = bottom, t = top, n = zNear, f = zFar,有
透视图:
glOrtho(left, right, bottom, top, zNear, zFar);
left,right, bootom,top 定义了 near 裁剪面大小,而 zNear 和 zFar 定义了从 Camera/Viewer 到远近两个裁剪面的距离(注意这两个距离都是正值)。
假设:xmax = right, xmin = left, ymax = top, ymin = bottom, zmax = far, zmin = near,正交投影的计算可分为两步:首先平移到视锥体的中心,而后缩放。
平移矩阵:(图中的2min 应为 zmin)
缩放矩阵:
正交投影矩阵 R = S × T:
视图变换的目的是为了让咱们能观察到某个角度的场景(从观察者的角度来讲)或者说是为了将物体从世界坐标转换到相机视线所在视图空间中来(从3D物体角度来讲)。这能够经过设定观察者的位置和朝向来实现的或对物体进行3D变换来实现,一般前面一种方式来实现(即设定观察者的位置与朝向)。以下图所示,xyz坐标轴表示的是世界坐标,蓝白色区域为视图空间,视图变换就是要将长方体从世界空间中转换到视图空间的坐标体系中去,而后再投影规范化,而后再经 viewport 转换映射到屏幕上渲染出来。
在 OpenGL 中,咱们能够经过工具库提供的 gluLookAt 这个函数来实现此功能。该函数的原型为:
gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);
eye 表示 camera/viewer 的位置, center 表示相机或眼睛的焦点(它与 eye 共同来决定 eye 的朝向),而 up 表示 eye 的正上方向,注意 up 只表示方向,与大小无关。经过调用此函数,就可以设定观察的场景,在这个场景中的物体就会被 OpenGL 处理。在 OpenGL 中,eye 的默认位置是在原点,指向 Z 轴的负方向(屏幕往里),up 方向为 Y 轴的正方向。在接下来的教程 04 中,使用的就是这个默认设置。
OpenGL ES 2.0 也没有提供该函数,glulookat 的内部实现其实就是先旋转到与观察者视线相同的方向,而后再平移到观察者所在的位置。其实现伪码以下:
|
上面代码中的 cross 是叉积,normalize 是规范化,Matrix4 是列主序,translate 是平移。
3D 变换是对初学者来讲是比较困难的,我尽可能写得明白点,但效果如何就不得而知了。写这一篇花了我很多时间,但对四元数和万向节锁也只是说起而已,未详细介绍,之后再单独介绍吧。Nate Robin 写了一个3D 变换的可视化教程工具,对于理解投影,视图,模型变换很是有帮助,强烈建议下载运行该程序,并调整相关参数看看效果。下面传张截图以诱惑你去下载:点此进入下载页面(Windows 和 Mac 版本都有)
1,《OpenGL 编程指南》
2,《3D数学基础:图形与游戏开发》
3,http://cse.csusb.edu/tong/courses/cs420/notes/viewing2.php