聊城网站建设 推广聊城博达,珠海网站建设王道下拉強,哈尔滨企业建网站推广,自己搭建网站[OpenGL ES 03]3D变换#xff1a;模型#xff0c;视图#xff0c;投影与Viewport 罗朝辉 (http://blog.csdn.net/kesalin) 本文遵循“署名-非商业用途-保持一致”创作公用协议 系列文章#xff1a;[OpenGL ES 01]OpenGL ES之初体验[OpenGL ES 02]OpenGL ES渲染管线与着色器…[OpenGL ES 03]3D变换模型视图投影与Viewport 罗朝辉 (http://blog.csdn.net/kesalin) 本文遵循“署名-非商业用途-保持一致”创作公用协议 系列文章 [OpenGL ES 01]OpenGL ES之初体验 [OpenGL ES 02]OpenGL ES渲染管线与着色器 前言 本来打算直接写教程 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原定计划的代码示例已经写好了有兴趣的同学可以先行浏览代码放在这里运行效果如下 一3D数学历史 我们都学过几何学应该都知道欧几里得公元前3世纪希腊数学家这位几何学鼻祖正是这位大牛创建了欧几里得几何学他提出了基于 XYZ 三轴的三维空间概念。到了17世纪又出了位大牛笛卡尔我们通常所说的笛卡尔坐标就是他的创造笛卡尔坐标非常完美地将欧几里得几何学理论与代数学联系到一块。正是因为有了笛卡尔坐标我们才能够用简单的矩阵Matrix来表示三维变换。但用矩阵来表示三维变换操作有一个无法解决的问题-万向节锁 。什么是万向节锁呢简单地说就是两个轴旋转到同一个方向上去了这两个轴平行了因此就比原来少了一维详情可参考这里。过了一百多年汉密尔顿Sir William Rowan Hamilton创建了四元数quaternion解决了因为旋转而导致万向节锁的问题四元数还有其他用处但在3D数学里主要是用来处理旋转问题。 好吧或许你看得一头雾水不要紧你只要知道用矩阵来表示3D变换但矩阵在表示旋转时可能会导致万向节锁的问题而使用四元数可以避免万向节锁就可以了。 二矩阵变换 在前面提到可使用 Matrix 来表示三维变换操作那么变换又是如何通过 Matrix 实现的呢下面就来讲这个。在这里我推荐一本3D数学入门书籍《3D数学基础图形与游戏开发》 通常我们使用 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位置。 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 中的实现 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 变换的整体概念下面来详细说说投影变换与视图变换。 四投影变换 投影变换的目的是确定 3D 空间的物体如何投影到 2D 平面上从而形成2D图像这些 2D 图像再经视口变换就被渲染到屏幕上。前面提到投影变换有两种正交投影和透视投影。透视投影用的比较广泛它与真实世界更相近近处的物体看起来要比远处的物体大而正交投影没有这个效果正交投影通常用于CAD或建筑设计。下面是正交投影与透视投影效果示意图 正交投影 透视投影 透视投影可以通过两种方式来表述OpenGL 及 OpenGL ES 1.0 提供其中一种 glFrustum而 glut 辅助库提供了另外一种gluPerspective。它们本质上是相同的只不过是不同的表述而已 视锥体/视景体 glFrustum(left, right, bottom, top, zNear, zFar); leftright bootomtop 定义了 near 裁剪面大小而 zNear 和 zFar 定义了从 Camera/Viewer 到远近两个裁剪面的距离注意这两个距离都是正值。由这六个参数可以定义出六个裁剪面构成的锥体这个锥体通常被称之为视锥体或视景体。只有在这个锥体内的物体才是可以见的不在这个锥体内的物体就相当于不再视线范围内因而会被裁减掉OpenGL 不会这些物体进行渲染。 由于 OpenGL ES 2.0 不提供此函数因此我们需要自己实现该函数。其计算公式如下 假设l left, r right, b bottom, t top, n zNear, f zFar有 透视图 gluPerspective(fovy, aspect, zNear, zFar); fovy 定义了 camera 在 y 方向上的视线角度介于 0 ~ 180 之间aspect 定义了近裁剪面的宽高比 aspect w/h而 zNear 和 zFar 定义了从 Camera/Viewer 到远近两个裁剪面的距离注意这两个距离都是正值。这四个参数同样也定义了一个视锥体。 在 OpenGL ES 2.0 中我们也需要自己实现该函数。我们可以通过三角公式 tan(fovy/2) (h / 2)/zNear 计算出 h 然后再根据 w h * aspect 计算出 w这样就可以得到 left, right, top, bottom, zNear, zFar 六个参数代入在介绍视锥体时提到的公式即可。 正交投影在 OpenGL 及 OpenGL ES 1.0 中是由 glOrtho 来提供的我们可以把正交投影看成是透视投影的特殊形式即近裁剪面与远裁剪面除了Z 位置外完全相同因此物体始终保持一致的大小即便是在远处看上去也不会变小。 glOrtho(left, right, bottom, top, zNear, zFar); leftright bootomtop 定义了 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 的内部实现其实就是先旋转到与观察者视线相同的方向然后再平移到观察者所在的位置。其实现伪码如下 [csharp] view plaincopy print? Matrix4 GetLookAtMatrix(Vector3 eye, Vector3 at, Vector3 up){ Vector3 forward, side; forward at - eye; normalize(forward); side cross(forward, up); normalize(side); up cross(side, forward); Matrix4 res Matrix4( side.x, up.x, -forward.x, 0, side.y, up.y, -forward.y, 0, side.z, up.z, -forward.z, 0, 0, 0, 0, 1); translate(res, Vector3(0 - eye)); return res; } 上面代码中的 cross 是叉积normalize 是规范化Matrix4 是列主序translate 是平移。 六后记 3D 变换是对初学者来说是比较困难的我尽量写得明白点但效果如何就不得而知了。写这一篇花了我不少时间但对四元数和万向节锁也只是提及而已未详细介绍以后再单独介绍吧。Nate Robin 写了一个3D 变换的可视化教程工具对于理解投影视图模型变换非常有帮助强烈建议下载运行该程序并调整相关参数看看效果。下面传张截图以诱惑你去下载点此进入下载页面Windows 和 Mac 版本都有 转载于:https://www.cnblogs.com/zhoug2020/p/6084717.html