1. OpenGL坐标系统概述
OpenGL希望每次顶点着色后,我们的可见顶点都为标准化设备坐标(Normalized Device Coordinate,NDC)。也就是说每个顶点的x,y,z都应该在−1到1之间,超出这个范围的顶点将是不可见的。
通常情况下我们会自己设定一个坐标范围,之后再在顶点着色器中将这些坐标变换为表转化设备坐标。然后这些标化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标和像素。
将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。
物体顶点的起始坐标再局部空间(Local Space),这里称它为局部坐标(Local Coordinate),它在之后会变成世界坐标(world Coordinate),观测坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Corrdinate)的形式结束。
3D场景转换为最终的2D形式渲染到屏幕的大致流程:
局部坐标系使用模型变换转换到世界坐标系,世界坐标系经过视变换转换到照相机坐标系后,需要进行投影变换,将相机坐标系转换到裁剪坐标系,再经过透视除法后,变换到规范化设备坐标系(NDC),最后进行视口变换后,3D坐标才变换到屏幕上的2D坐标。
有以下几点需要注意:
- 局部坐标是对象相对于原点的坐标,也是物体的起始坐标。
- 下一步将局部坐标转化为世界空间坐标,世界空间坐标是一个处于更大空间范围内的。这些坐标相对于世界的全局原点,它们会和其他物体一起相对于世界原点进行摆放。
- 接下来将世界坐标转化为观测坐标,使得每个坐标都是从摄像机或者说观察者角度进行观察的。
- 坐标到达观测空间后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理从−1.0到1.0范围内,并判断哪些点将会出现在屏幕上。
- 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于−1.0到1.0范围的坐标变换到由glViewport()函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
在OpenGL中,模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成。而透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。
你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。
2. 各种坐标系
2D笛卡尔坐标
2D笛卡尔坐标是一个直角坐标系,它由一个X轴和一个Y轴组成,X轴Y轴在同一个平面上互相垂直且有公共原点。在二维绘图中,最为常用的坐标系统是笛卡尔坐标系统。
通常,两条数轴分别置于水平位置与垂直位置,取向右与向上的方向分别为两条数轴的正方向。水平的数轴叫做x轴或横轴,垂直的数轴叫做y轴或纵轴,x轴y轴统称为坐标轴,它们的公共原点称为坐标系的原点。关于正负方向问题,我们可以根据实际需求自己定义。
3D笛卡尔坐标系
三维笛卡尔坐标系是在二维笛卡尔坐标系的基础上根据右手定则增加第三维坐标(即Z轴)而形成的。Z轴代表了深度分量,它同时垂直于X,Y轴,是一条从屏幕中心朝向读者的直线。
为了更好的观察,我们将Y轴向做旋转,把X轴向下和后渲染.否则Z轴将直面我们,我们无法具体观察到Z轴存在.我们可以用3个坐标(X,Y,Z)来指定三维空间中的任意一个位置.
右手系
右手系(right-hand system)是在空间中规定直角坐标系的方法之一。此坐标系中x轴,y轴和z轴的正方向是如下规定的:把右手放在原点的位置,使大姆指,食指和中指互成直角,把大姆指指向x轴的正方向,食指指向y轴的正方向时,中指所指的方向就是z轴的正方向。
也可以按如下方法确定右手(左手)坐标系:如果当右手(左手)的大拇指指向第一个坐标轴(x轴)的正向,而其余手指以第二个轴(y轴)绕第一轴转动的方向握紧,就与第三个轴(z轴)重合,就称此坐标系为右手(左手)坐标系。
- OpenGL坐标系(物体、世界、照相机坐标系)属于右手坐标系
- 设备坐标系使用的是左手坐标系
世界坐标系
它是一个特殊的三维坐标系,它建立了描述其他坐标系所需要的参考系。也就是说,可以用世界坐标系去描述其他所有坐标系或者物体的位置。所以有很多人定义世界坐标系是“我们所关心的最大坐标系”,通过这个坐标系可以去描述和刻画所有想刻画的实体。世界坐标系又称全局坐标系或者宇宙坐标系。
世界坐标系是系统的绝对坐标系,始终是固定不变的。如果我们将所有的物体导入到世界坐标系中,默认它们有可能会全挤在世界的原点(0, 0, 0)上,但是我们布置场景的时候一般希望物体分散到世界的各处,那么这时需要通过模型矩阵(Model Matrix)来实现。将物体变换到世界空间中,相当于就是建立物体的顶点相对于世界原点的关系。
物体坐标系
物体坐标系与特定的物体关联,每个物体都有自己特定的坐标系。不同物体之间的坐标系相互独立,可以相同,可以不同,没有任何联系。同时,物体坐标系与物体绑定,绑定的意思就是物体发生移动或者旋转,物体坐标系发生相同的平移或者旋转,物体坐标系和物体之间运动同步,相互绑定。
物体坐标系是以物体本身而言,比如,我先向你发指令,“向前走一步”,是向您的物体坐标体系指令。我并不知道你会往哪个绝对的方向移动。比如说,当你开车时,有人会说向左转,有人说向东。但是,向左转是物体坐标系的概念,而向东则是世界坐标系中的。
在某种情况下,我们可以理解物体坐标系为模型坐标系。因为模型顶点的坐标都是在模型坐标系中描述的。
摄像机/照相机坐标系
摄像机坐标系。摄像机坐标系是和观察者密切相关的坐标系。摄像机坐标系和屏幕坐标系相似,只不过一个处在三维环境中,一个处在二维环境中。摄像机坐标系也是一种特殊的物体坐标系。一般的摄像机坐标系都是x轴向右,y轴向上,z轴向里。
观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。
惯性坐标系
这种坐标系是在世界坐标系和物体坐标系之间的一种坐标系。惯性坐标系的原点和物体坐标系的原点重合,但惯性坐标系的坐标轴是和世界坐标系的坐标轴平行的。为什么要引入惯性坐标系呢?三维系统在运行时,不可避免的引入外部工具如3DMax做得模型,每个模型又有很多三角面,点组成。这些三角面和点得坐标都是物体坐标。所以系统中会由大量的从物体坐标向世界坐标的转换。有了惯性坐标系,可以简化他们之间的转换过程。从物体坐标系转换到惯性坐标系只需要旋转操作。从惯性坐标系到世界坐标系只需要平移操作。
齐次坐标系
齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示。在齐次坐标下,旋转/平移/仿射变换/透视变换都可以用同一个矩阵实现,这在传统笛卡尔坐标系下是不可能的。
4D向量是由3D坐标(x,y,z)和齐次坐标w组成,写作(x,y,z,w)。
在3D世界中为什么需要3D的齐次坐标呢?简单地说明一下,在一维空间中的一条线段上取一点x,然后我们想转移x的位置,那我们应该是x'=x+k,但我们能使用一维的矩阵来表示这变换吗?不能,因为此时一维的矩阵只能让x点伸缩。但如果变成了一维的齐次空间[k 1]就很容易地做到。同样地,在二维空间中,某一图形如果不使用二维的齐次坐标,则只能旋转和伸缩,确不能平移。
因此,我们在3D坐标中使用齐次坐标,是为了物体在矩阵变换中,除了伸缩旋转,还能够平移,如下运算:
齐次坐标w是什么意义: 设w=1,此时相当于我们把3D的坐标平移搬去了w=1的平面上,4D空间的点投影到w=1平面上,齐次坐标映射的3D坐标是(x/w,y/w,z/w),也就是(x,y,z)。(x,y,z)在齐次空间中有无数多个点与之对应。所有点的形式是(kx,ky,kz,k),其轨迹是通过齐次空间原点的“直线”(其实每个点相当于3D的坐标世界)。
当w=0时,有很大的意义,可解释为无穷远的“点”,其意义是描述方向。这也是平移变换的开关,当w=0时,
此时不能平移变换了。这个现象是非常有用的,因为有些向量代表“位置”,应当平移,而有些向量代表“方向”,如表面的法向量,不应该平移。从几何意义上说,能将第一类数据当作"点",第二类数据当作"向量"。可以通过设置w的值来控制向量的意义。
3. 坐标系相关
裁剪空间过程
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。因为将所有可见的坐标都指定在−1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL期望的那样。
为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的−1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(−1.0,1.0)。所有在范围外的坐标不会被映射到在−1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250,500,750)将是不可见的,这是由于它的坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。
如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。
由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。
一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。
将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
正射投影
正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:
上面的平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的分量都没有进行改变;如果分量等于1.0,透视除法则不会改变这个坐标。
要创建一个正射投影矩阵,我们可以使用glOrtho函数:
glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些,,值范围内的坐标变换为标准化设备坐标。
正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视(Perspective)考虑进去,使用正投影所以实际大小相同的物体在屏幕上都具有相同的大小,所以从屏幕上看不出来观测点到物体的距离有多远。正投影比较适合平面图形/2D图形渲染时使用。
透视投影
如果需要渲染结果看起来真实,就需要透视投影矩阵来解决这个问题。
在生活中,我们会注意到离你越远的东西看起来越小。这个奇怪的效果称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上,就是每个点的分量除以w进行转换:
out = (x/w, y/w, x/w);
顶点坐标的每个分量都会除以它的分量,距离观察者越远顶点坐标就会越小。这是也是分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。
创建一个透视投影矩阵:
SetPerspective(float fFov, float fAspect, float fNear, float fFar)
同样,SetPerspective所做的其实就是创建了一个定义了可视空间的大平截头体,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:
它的第一个参数定义了fFov的值,它表示的是视野(Field of View),其值通常是垂直方向上的视野角度大小,它设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。某些如 Blender 等进行三维建模的软件有时在建模时也会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。
使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。下面给出两种投影方式的对比:
视口
窗口是以像素为单位度量。 在开始在窗口中绘制点,线,形状之前,必须告诉OpenGL如何把指定坐标映射为屏幕坐标.
坐标系统必须从逻辑笛卡尔坐标映射到物理屏幕像素坐标. 这个映射是通过一种叫做视口(viewPort)的设置来指定.> 在我们代码中,我们会通过glViewPort函数来实现视口的设计. 视口就是窗口内部用于绘制裁剪区域的客户区域.
glViewport (GLint x, GLint y, GLsizei width, GLsizei height);
视口就是窗口中用来显示图形的一块矩形区域,它可以和窗口等大,也可以比窗口大或者小。只有绘制在视口区域中的图形才能被显示,如果图形有一部分超出了视口区域,那么那一部分是看不到的。
裁剪区域:就是视口矩形区域的最小最大x坐标(left, right)和最小最大y坐标(bottom, top),而不是窗口的最小最大x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近最远z坐标,形成一个立体的裁剪区域。
glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);