[OpenGL ES 03]3D变换:模型,视图,投影与Viewport

[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世纪希腊数学家)这位几何学鼻祖,正是这位大牛创建了欧几里得几何学,他提出了基于 X,Y,Z 三轴的三维空间概念。到了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);

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,有


透视图


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(leftrightbottomtopzNearzFar);

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 的内部实现其实就是先旋转到与观察者视线相同的方向,然后再平移到观察者所在的位置。其实现伪码如下:

[csharp] view plaincopy print?
  1. Matrix4 GetLookAtMatrix(Vector3 eye, Vector3 at, Vector3 up){  
  2.     Vector3 forward, side;  
  3.     forward = at - eye;  
  4.     normalize(forward);  
  5.     side = cross(forward, up);  
  6.     normalize(side);  
  7.     up = cross(side, forward);  
  8.   
  9.     Matrix4 res = Matrix4(  
  10.                           side.x, up.x, -forward.x, 0,  
  11.                           side.y, up.y, -forward.y, 0,  
  12.                           side.z, up.z, -forward.z, 0,  
  13.                           0, 0, 0, 1);  
  14.     translate(res, Vector3(0 - eye));  
  15.     return res;  
  16. }  

上面代码中的 cross 是叉积,normalize 是规范化,Matrix4 是列主序,translate 是平移。

 

六,后记

3D 变换是对初学者来说是比较困难的,我尽量写得明白点,但效果如何就不得而知了。写这一篇花了我不少时间,但对四元数和万向节锁也只是提及而已,未详细介绍,以后再单独介绍吧。Nate Robin 写了一个3D 变换的可视化教程工具,对于理解投影,视图,模型变换非常有帮助,强烈建议下载运行该程序,并调整相关参数看看效果。下面传张截图以诱惑你去下载:点此进入下载页面(Windows 和 Mac 版本都有)

转载于:https://www.cnblogs.com/zhoug2020/p/6084717.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/253943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LOAM_velodyne学习(三)

终于到第三个模块了,我们先来回顾下之前的工作:点云数据进来后,经过前两个节点的处理可以完成一个完整但粗糙的里程计,可以概略地估计出Lidar的相对运动。如果不受任何测量噪声的影响,这个运动估计的结果足够精确&…

监控视频线种类 视频信号传输介绍及各种视频接口的传输距离

一.视频信号接口 监控视频线种类介绍: 按照材料区分有SYV及SYWV两种,绝缘层的物理材料结构不同,SYV是实心聚乙烯电缆,SYWV是高物理发泡电缆,物理发泡电缆传输性能优于聚乙烯。 S--同轴电缆 Y--聚乙烯 V--聚氯乙烯 W…

Ajax工作原理

详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt238 在这篇文章中,我将从10个方面来对AJAX技术进行系统的讲解。 1、ajax技术的背景 不可否认,ajax技术的流行得益于google的大力推广,正是由于google earth、go…

各种视频信号格式及端子介绍/VGA DVI HDMI区别

视频信号是我们接触最多的显示信号,但您并不一定对各种视频信号有所了解。因为国内用到的视频信号格式和端子非常有限,一般就是复合视频和S端子,稍高级一些的就是色差及VGA。对于那些经常接触国外电器和二手设备的朋友,就会遇到各…

LOAM_velodyne学习(四)

TransformMaintenance 来到了最后一个模块,代码不是很长,我们在看完代码之后,再详细说明这个模块的功能 依然主函数开始 int main(int argc, char** argv) {ros::init(argc, argv, "transformMaintenance");ros::NodeHandle nh;…

oracle参数文件、控制文件、数据文件、日志文件的位置及查询方法

参数文件:所有参数文件一般在 $ORACLE_HOME/dbs 下 sqlplus查询语句:show parameter spfile; 网络连接文件: $ORACLE_HOME/dbs/network/admin 目录中 控制文件:select * from v$controlfile; 数据文件:一般在oracleda…

LeGO-LOAM学习

前言 在学习了LOAM之后,了解到LeGO-LOAM(面向复杂情况的轻量级优化地面的雷达里程计),进行了一个学习整理。 Github:https://github.com/RobustFieldAutonomyLab/LeGO-LOAM 论文:https://github.com/Robu…

(一)低功耗设计目的与功耗的类型

一、低功耗设计的目的 1.便携性设备等需求 电子产品在我们生活中扮演了极其重要的作用,便携性的电子设备便是其中一种。便携性设备需要电池供电、需要消耗电池的能量。在同等电能提供下,低功耗设计的产品就能够工作更长的时间。时间的就是生命&#xff…

(转)彻底学会使用epoll(一)——ET模式实现分析

注:之前写过两篇关于epoll实现的文章,但是感觉懂得了实现原理并不一定会使用,所以又决定写这一系列文章,希望能够对epoll有比较清楚的认识。是请大家转载务必注明出处,算是对我劳动成果的一点点尊重吧。另外&#xff0…

Apache 设置http跳转至HTTPS访问

为什么80%的码农都做不了架构师&#xff1f;>>> <VirtualHost>...</VirtualHost> 中添加如下配置 <IfModule mod_rewrite.c>RewriteEngine onRewriteCond %{SERVER_PORT} 80RewriteRule ^(.*)$ https://域名/$1 [R301,L] </IfModule> 转…

(二)功耗的分析

前面学习了进行低功耗的目的个功耗的构成&#xff0c;今天就来分享一下功耗的分析。由于是面向数字IC前端设计的学习&#xff0c;所以这里的功耗分析是基于DC中的power compiler工具&#xff1b;更精确的功耗分析可以采用PT&#xff0c;关于PT的功耗分析可以查阅其他资料&#…

Hibernate创建hqll时报错

Hibernate 问题,在执行Query session.createQuery(hql) 报错误 出错截图&#xff1a; 这条语句在java运行环境下&#xff0c;直接连数据库不出错&#xff0c;如果在hiberante,struts环境下就出错 出错原因&#xff1a;jar包冲突&#xff0c;struts2和hibernate框架中都有antlr包…

.NET Core TDD 前传: 编写易于测试的代码 -- 全局状态

第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 本文是第4篇, 将介绍全局状态引起的问题. 全局状态 全局状态, 也可以叫做应用程序状态, 它是一组变量, 这些变量维护着…

(三)系统与架构级低功耗设计

前面讲解了使用EDA工具&#xff08;主要是power compiler&#xff09;进行功耗分析的流程&#xff0c;这里我们将介绍在数字IC中进行低功耗设计的方法&#xff0c;同时也结合EDA工具&#xff08;主要是Design Compiler&#xff09;如何实现。我们的讲解的低功耗设计主要是自顶向…

(四)RTL级低功耗设计

前面介绍了系统级的低功耗设计&#xff0c;换句话说就是在系统级降低功耗可以考虑的方面。系统级的低功耗设计&#xff0c;主要是由系统级设计、具有丰富经验的人员实现&#xff0c;虽然还轮不到我们设计&#xff0c;我们了解一下还是比较好的。我们前端设计人员的重点不在系统…

Unity3D 游戏前端开发技能树(思维导图)

如果做游戏也是一种游戏,那么这个游戏的自由度实在是太高了.(导图源文件链接&#xff1a;http://pan.baidu.com/s/1eSHpH5o 密码&#xff1a;qzl5) 最近要用思维导图软件Xmind把自己的思路好好捋一捋,算是温故知新吧. 转载于:https://www.cnblogs.com/qiaogaojian/p/6098962.ht…

SQL Server 死锁的告警监控

原文:SQL Server 死锁的告警监控今天这篇文章总结一下如何监控SQL Server的死锁&#xff0c;其实以前写过MS SQL 监控错误日志的告警信息&#xff0c;这篇文章着重介绍如何监控数据库的死锁&#xff0c;当然这篇文章不分析死锁产生的原因、以及如何解决死锁。死锁&#xff08;D…

关于web性能一些特性汇总

关于web性能一些特性汇总 DOMContentLoaded & load load事件是window对象上的事件。指的是网页资源已经加载完毕&#xff08;包括但不限于DOM、图片、音频、脚本、插件资源以及CSS&#xff09;。 DOMContentLoaded事件是document对象上的事件。指的是DOM已经加载完毕。IE中…

(五)门级电路低功耗设计优化

&#xff08;1&#xff09;门级电路的功耗优化综述 门级电路的功耗优化(Gate Level Power Optimization&#xff0c;简称GLPO)是从已经映射的门级网表开始&#xff0c;对设计进行功耗的优化以满足功耗的约束&#xff0c;同时设计保持其性能&#xff0c;即满足设计规则和时序的要…

Spring3向Spring4升级过程中quartz修改

为什么80%的码农都做不了架构师&#xff1f;>>> 问题 nested exception is org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [org.springframework.scheduling.quartz.CronTriggerBean] for bean with name ... 原因 org.spri…