相信作技术的同窗,特别是作客户端开发的同窗,都据说过OpenGL。要想对客户端的渲染机制有一个深刻的了解,不对OpenGL了解一番恐怕是作不到的。并且,近年来客户端开发中对于图像和视频处理的需求,成上升趋势,要想胜任这些稍具「专业性」的工做,对于OpenGL的学习也是必不可少的。然而,OpenGL的学习曲线相对来讲比较陡峭,尤为是涉及到一些计算机图形学方面的专业知识,难免会让不少人望而生畏。git
要想熟练地掌握OpenGL,有两方面相关的知识是须要重点关注的。程序员
本文所要探讨的主题,将主要围绕上述第二个方面的知识,也就是坐标变换。这部分涉及到一点数学知识,显得更难理解一些,而且网上的资料也散落在各处,不多有系统而详尽的描述。严格来讲,这部分理论知识并不彻底属于OpenGL规范所规定的范围,但却与之有着很是密切的关系。接下来,就坐标变换这个主题,我会写一个小系列,由多篇技术文章组成,将坐标变换相关的资料整理在一块儿,并尽力用通俗易懂的语言表达出来,但愿能为学习OpenGL和图像处理的同窗扫清理论上的障碍。github
本着理论联系实际的原则,咱们将结合Android系统上的API介绍相关的理论。之因此选择Android环境,是由于上手简单,大部分程序员都能很快地跑起一个Android程序,而且OpenGL相关的编程环境在Android上是现成的,几乎不用太多的配置。在Android上,实际普遍使用的是OpenGL ES 2.0,它能够当作是OpenGL对应版本的一个子集。咱们在接下来的讨论中,也以OpenGL ES 2.0为准。编程
另外,不少实际中的开发任务只涉及到2D图像的处理,而不会涉及3D的处理。使用OpenGL ES作2D的图像处理,确实处理流程会简化一些,然而,我的认为,搞清3D的渲染机制,对于理解整件事有相当重要的做用。理解了3D,便能理解2D,反之则不成立。并且,只有在3D的语境下,坐标变换的概念才能被完整地理解。所以,咱们一开始便从3D开始,等介绍完3D空间中的坐标变换以后,咱们再回到2D的特殊状况加以讨论。c#
不少OpenGL的入门文章,都以画一个三角形开始。可是,对于讨论坐标变换这件事来讲,画一个三角形的例子并不太合适,由于三角形是一个平面图形,对它应用了完整的坐标变换以后,会获得看似很奇怪的结果,反而让初学者比较迷惑。因此,本篇给出的例子程序画的是立方体(cube)。程序下载地址:markdown
下面是程序输出截图:oop
没错,程序画了三个立方体的木箱子,它们的位置、大小、角度各不相同。但实际上,上面的大木箱子和下面的小木箱子都是由中间的那个木箱子通过必定的坐标变换(缩放、旋转、平移)以后获得的。而中间的木箱子所在的位置是原始的位置,即世界坐标的原点处(世界坐标的概念咱们立刻就会介绍)。学习
在本篇中,咱们先不过早地深刻到代码细节,而是留到后面的文章再讨论。接下来,咱们先把坐标变换的整个过程作一个概览。测试
咱们前面提到过,坐标变换的目标,简单来讲,就是把一个3D空间中的对象最终投射到2D的屏幕上去(严格来讲,OpenGL ES支持离屏渲染,因此最终未必是绘制到一个「可见」的屏幕上,不过在本文中咱们忽略这一细节)。这也正是计算机图形学(computer graphics)所要解决的其中一个基础问题。当咱们观察3D世界的时候,是经过一块2D的屏幕,咱们真正看到的实际是3D世界在屏幕上的一个投影。坐标变换就是要解决在给定的观察视角下,3D世界的每一个点最终对应到屏幕上的哪一个像素上去。固然,对于一个3D对象的坐标变换,实际中是经过对它的每个顶点(vertex)来执行相同的变换获得的。最终每一个顶点变换到2D屏幕上,再通过后面的光栅化(rasterization)的过程,整个3D对象就对应到了屏幕的像素上,咱们看到的效果就至关于透过一个2D屏幕「看到了」3D空间的物体(3D对象)。spa
下面的图展现了整个坐标变换的过程:
咱们先来简略地了解一下图中各个过程:
为了更好地理解以上各个步骤,下面咱们来看几张图。
上面这张图展现的是本地坐标。3D对象是一个立方体,本地坐标的原点(0, 0, 0)位于立方体的中心。红色、绿色、蓝色的坐标轴分别表示x轴、y轴、z轴。
上面这张图展现的是世界坐标。能够这样认为,最初,世界坐标系和立方体的本地坐标系是重合的,但立方体通过了某些缩放、旋转和平移以后,两个坐标系再也不重合。图中虚线表示的坐标轴,就是原来的本地坐标系。
上面这张图展现的是相机坐标。左下实线表示的坐标轴便是相机坐标系,右边虚线表示的坐标轴是世界坐标系。相机坐标系能够当作是相机(或眼睛)看向3D空间中的某一点造成的一个观察视角,以上图为例,相机观察的方向正对着世界坐标系的(0,2,0)这一点。相机坐标系的原点正是相机(或眼睛)所在的位置。这里须要注意的一点细节是,按照OpenGL ES的定义习惯,相机坐标系的z轴方向与观察方向正好相反。也就是说,相机(或眼睛)看向z轴的负方向。
咱们前面提到的view变换,指的就是在世界坐标系中的各个顶点(vertex),通过这样一个变换,就到了相机坐标系下,也就是各个顶点的坐标变成了以相机坐标的值来表示了。
仔细观察的话,咱们会发现,相机坐标系实际上能够当作是由世界坐标系通过旋转和平移操做获得的。这在后面咱们还会详细讨论。
至此,咱们已经转换到了相机坐标系下了。接下来是很是关键的一步变换,要将3D坐标(以相机坐标表示)投射到2D屏幕上。如前所述,这个变换是经过投影变换(projection)获得的。为了使得投射到2D屏幕上的图像看起来像是3D的,咱们须要让这个变换知足人眼的一些直觉。根据实际经验,咱们眼中看到的东西,离咱们越远,显得越小;反之,离咱们越近,显得越大。就像咱们正对着一列铁轨或一个走廊看过去的那种效果同样,以下图:
因此,投影变换也要保持这种效果。通过投影变换后,咱们就获得了裁剪坐标,在此基础上再附加一个perspective division的过程,就变换到了NDC坐标。像前面所讲的同样,perspective division的细节咱们先不追究,咱们暂且认为相机坐标通过了投影变换就获得了NDC坐标。这个投影的过程,是经过从相机出发构建一个视锥体(frustum)获得的,以下图所示:
上图中,从相机所在位置(也就是相机坐标系原点)沿着相机坐标系的z轴负方向望出去,同时指定一个近平面(N)和远平面(F),在两个平面之间就截出一个视锥体。它由6个面组成,近平面(N)和远平面(F)分别是先后两个面,另外它还有上下左右四个面。其中,近平面(N)对应着最终要投影的2D屏幕。落在视锥体内部的顶点坐标,最终将投影到2D屏幕上;而落在视锥体外部的顶点坐标,则被裁剪掉。并且,落在视锥体内部的3D对象,它的位置越是靠近近平面,这个3D对象在近平面上的投影越大;相反越是远离近平面,则投影越小。
以视锥体中的某点为原点,创建一个坐标系,就获得了NDC坐标,也就是上图中位于右上部的实线红、绿、蓝坐标轴。视锥体的6个面正好对应着NDC坐标每一个维度的最大取值(-1和1)。
有两个细节须要注意一下:
上面左图是左手坐标系,右图是右手坐标系。到底应该用左手坐标系仍是右手坐标系,是一种约定俗成的习惯,不一样的图形系统和规范极可能选择不同的坐标系类型。但按照OpenGL的习惯,咱们应该使用如前面所讲的坐标系类型。
OpenGL ES涉及到的主要的坐标变换过程,咱们把大概的概况已经讨论清楚了。在这个系列后面的文章中,咱们将逐步讨论各个变换过程的细节,包括理论推导,以及在Android上如何用代码来实现。
(完)
最后,借这个地发则招聘小广告,方向是计算机图形学、计算机视觉和AR,坐标北京。不占用更多篇幅介绍了,以避免影响无关的读者,任何感兴趣的同窗欢迎到公众号后台勾搭我^-^
其它精选文章: