视口
窗口是以像素为单位度量. 在开始在窗口中绘制点,线,形状之前,必须告诉OpenGL 如何把指定坐标映射为屏幕坐标.
坐标系统必须从逻辑笛卡尔坐标映射到物理屏幕像素坐标. 这个映射是通过一种叫做视口(viewPort)
的设置来指定.
在显示器的屏幕窗口上定义一个对齐的矩形的视口,OpenGL会自动建立世界窗口和视口的变换(包括缩放和平移)。当世界窗口中所有对象都被绘制时,对象在世界窗口中的部分会被自动地映射到视口中————换句话说,被映射到屏幕坐标中,即像素在显示器上的坐标。
视口就是窗口内部用于绘制裁剪区域的客户区域
// 视口的设定通过glViewport()函数,它的原型是:
void glViewport(GLint x,GLint y,GLint width,GLint ehignt);
//它设置窗口的左下角,以及宽度和高度。
左右手坐标系
OpenGL
坐标系中(物体、世界、照相机坐标系)都属于右手坐标系
而规范化设备坐标系使用的是左手坐标系
坐标系
OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。每个顶点的x,y,z坐标都应该在−1.0到1.0之间,超出这个坐标范围的顶点将是不可见的。
通常情况下我们会自己设定一个坐标范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后这些标准化设备坐标传入光栅器(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)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:
1.局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
2.下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
3.接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
4.坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
5.最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport
函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
我们之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间变换到裁剪空间的变换矩阵,但那样会失去很多灵活性。
结合一个实际例子来理解一下
想想在现实世界中,我们怎样能看到一样东西,比如说就是我们常用的电脑?首先,电脑会在工厂中叮叮当当地装配起来,然后,通过飞机火车汽车之类的东西,将它送到我们手中。我们拿到之后,要把包装拆掉,然后放到一个合适的地方,这样我们才能看到。
让我们一步一步拆解这个过程,看看我们要显示一个3D的物体是有多么复杂!
局部空间(Local Space)/ 物体空间(Object Space)
局部空间是指物体所在的坐标空间,即对象最开始所在的地方。
电脑在工厂中制造的时候,工人需要去考虑这颗螺丝需要装到东经多少度,北纬多少度的位置上吗?当然不可能,他只需要考虑需要装到电脑的什么位置!这个就是局部空间。在局部空间中,物体位于空间的原点,所有的调整都是基于物体的相对位置去调整的。
世界空间 (World Space)
如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。
电脑装配好之后,自然是要卖出去才有价值。这时候,就需要把它运到专卖店里,或者直接快递到买家手里。于是,它被装到火车上,从生产地(A)送到了销售点(B),卫星定位起始点位于东经xx度,北纬xx度。这就是世界空间。物体首先要有一个初始位置,然后才能从一个位置移动到另一个位置。
在OpenGL中,我们使用模型矩阵
(Model Matrix)将物体放到世界空间中的某个位置上。它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。
这时,我的手机响了,快递员告诉我电脑已经到了,让我签收。但是,我没看到啊?于是我就起床去拿快递了……到了楼下,环顾四周,发现快递员正好在我左前方40米的距离,于是我转个弯就直直地朝他走过去。我终于看到我的电脑了!
看上去非常流畅的过程,在OpenGL中就需要分成两个操作:1、人眼的视线转换到正对-z轴的方向,并将物体转换到以人眼为原点的位置上。2、物体必须在人眼的视野之内。这就是观察空间和裁剪空间的概念。可能第一步很难察觉,我们从OpenGL的角度来解释一下。
观察空间 (View Space) / 视觉空间(Eye Space)
观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是以摄像机为位置为原点,将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵
(View Matrix)里,
裁剪空间 (Clip Space)
摄像机有朝向,也有拍摄的视野范围,所有在视野范围之外的东西都看不到,都被剔除了。
在每个顶点着色器运行的最后,OpenGL希望所有的坐标都能落在一个特定的范围内,所有超出范围的点都应该被裁剪掉
,被裁剪掉的坐标就会被忽略,剩下的坐标才会进入片元着色阶段然后显示到你的屏幕上。这就是裁剪空间
名字的由来。
因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL期望的那样。
将顶点坐标从观察空间转换到裁剪空间的操作叫做投影变换(perspective projection),我们需要定义一投影矩阵
(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是由于它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。
还是从现实世界中看到东西的角度去分析。
人眼在看东西时,左右宽度上有一定的范围,上下高度上也有一定的范围。放到OpenGL中,就是摄像机的左右和上下方向上都有一定的视野角度(FOV),只有在这个角度范围内的东西,才可能被看到。
从上面的图中可以看出,h和w就确定了摄像机上下左右可以看到的范围大小。通常我们会设置上下左右的视野都是90度。为了方便计算(这点非常重要!想把所有的物体都渲染出来,世界上所有的计算机的运算能力加起来都不够,所以,不要显示的就坚决剔除。),我们也会设置一个近裁剪面和一个远裁剪面。比近裁剪面更近的物体被剔除,比远裁剪面更远的物体被剔除。我们需要把两个裁剪面之间的所有物体都映射到投影平面上(投影平面可以在裁剪面和远裁剪面之间,图上只是一种情况),其他的物体都被剔除!
由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。
一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。
将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
视口空间 (Screen Space)
视口空间,可以简单地理解成应用窗口。投影平面上的东西和窗口上的像素通过一一对应的方式映射到窗口,在窗口上显示!
这一步由OpenGL完成,我们不管。
投影方式
不管我们觉得自己的眼睛看到的三维立体图像多么真实.屏幕上像素实际上只有二维的.
那么OpenGL 是如何将笛卡尔坐标系映射成可以在屏幕上显示的二维坐标的?
在这里需要用到投影.我们需要指定投影空间,指定在窗口显示的视景体(Viewing Volume).并指定如何对它进行变换.
2D+透视 = 3D
正投影(Orthographics Projection)或平行投影
正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:
上面的平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。
要创建一个正射投影矩阵,我们可以使用GLM的内置函数glm::ortho:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。
正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视
(Perspective)考虑进去。所以我们需要透视投影矩阵
来解决这个问题。
透视投影(Perspective Projection).
透视投影矩阵
将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:
顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。
在GLM中可以这样创建一个透视投影矩阵:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
同样,glm::perspective
所做的其实就是创建了一个定义了可视空间的大平截头体,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:
它的第一个参数定义了fov
的值,它表示的是视野
(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
当你把透视矩阵的 near 值设置太大时(如10.0f),OpenGL会将靠近摄像机的坐标(在0.0f和10.0f之间)都裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候你的视线会直接穿过去。
当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。某些如 Blender 等进行三维建模的软件有时在建模时也会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。
将坐标系统组合在一起
我们为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。
然后呢?
顶点着色器的输出要求所有的顶点都在裁剪空间内,这正是我们刚才使用变换矩阵所做的。OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换
。
3D图形学中的常见的坐标系
1.世界坐标系
世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上所有的点的坐标都可以在该坐标系的原点来确定各自的位置.世界坐标系始终是固定不变的。
它是一个特殊的坐标系,它建立了描述其他坐标系所需要的参考系。也就是说,可以用世界坐标系去描述其他所有坐标系或者物体的位置。所以有很多人定义世界坐标系是“我们所关心的最大坐标系”,通过这个坐标系可以去描述和刻画所有想刻画的实体。
世界坐标系又称全局坐标系
或者宇宙坐标系
。
2.物体坐标系
物体坐标系与特定的物体关联,每个物体都有自己特定的坐标系。不同物体之间的坐标系相互独立,可以相同,可以不同,没有任何联系。同时,物体坐标系与物体绑定,绑定的意思就是物体发生移动或者旋转,物体坐标系发生相同的平移或者旋转,物体坐标系和物体之间运动同步,相互绑定。
这个立方体的物体坐标系如图所示,不论该立方体位于世界坐标系的任何位置,处于什么角度,物体坐标系与物体都是绑定在一起。
物体坐标系又称模型坐标系
3.摄像机(照相机)坐标系
在坐标系的范畴里,摄像机坐标系和照相机坐标系都是一样的意义。照相机坐标系是和观察者密切相关的坐标系。照相机坐标系和屏幕坐标系相似,差别在于照相机坐标系处于3D空间中,而屏幕坐标系在2D平面里。
4.惯性坐标系
惯性坐标系是为了简化世界坐标系到惯性坐标系的转化而产生的。惯性坐标系的原点与物体坐标系的原点重合,惯性坐标系的轴平行于世界坐标系的轴。引入了惯性坐标系之后,物体坐标系
转换到惯性坐标系
只需旋转
,从惯性坐标系
转换到世界坐标系
只需平移
。
坐标转换
OpenGL最终的渲染设备是2D的,我们需要将3D表示的场景转换为最终的2D形式,前面使用模型变换和视变换将物体坐标转换到照相机坐标系后,需要进行投影变换,将坐标从相机—>裁剪坐标系,经过透视除法后,变换到规范化设备坐标系(NDC),最后进行视口变换后,3D坐标才变换到屏幕上的2D坐标,这个过程如下图所示
在上面的图中,注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系,它们的关系如下图所示
图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成;
图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。