title: OpenGL(7)之坐标系统
date: 2020-07-11 10:34
category: 图形学
tags: opengl
项目代码可参见 6.1.coordinate_systems
0.前言
上一节学习了关于利用矩阵变换对顶点进行变换达到我们的看的物体是动态的, 但是OpenGL希望的是每次顶点着色器接收顶点数据运行后,可见的所有顶点都为标准化设备坐标内(Normalized Device Coordinate)即每个顶点的x,y,z坐标都应该是在-1.0到1.0之间。超出这个坐标范围的顶点都不可见。
关于坐标的处理:
- 自己定义一个坐标范围。
- 在顶点着色器中将这些坐标变换为标准化设备坐标。
- 将这些转换好的标准化设备坐标传入光栅器(Rasterizer)。
- 将顶点坐标变换为屏幕上的二维坐标或像素。
本文着重讲解上述过程中所需要的坐标系以及坐标空间的概念:
物体的顶点在最终转化为屏幕坐标之前还会被变换为多个坐标系统,因此衍生出了几个比较重要的坐标系统:
- 局部空间(Local Space,或物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
1.概述
将一个坐标系变换到另一个坐标系,需要使用到几个变换矩阵,
- 模型(Model)
- 视图(View)
- 投影(Projection)
顶点坐标起始于局部空间(local space)的局部坐标(local coordinate),通过模型变换变为世界坐标(World Coordinate),在经过视图变换变为视图坐标(View Coordinate),在经过投影变换变为裁剪坐标(Clip Coordinate)最后通过视口变换展示到屏幕上以屏幕坐标(Screen Coordinate)的形式结束。
流程图如下:
-
局部坐标是相对局部原点的坐标,也就是物体的坐标
-
局部坐标转换为世界空间坐标,转换之后的世界坐标相对于世界的全局原点,和其他物体相对于世界原点进行摆放。(模型变换)
-
世界坐标变换为观察空间坐标(视图空间),使得每个坐标都是从摄像机或者是观察者的角度进行观察。(视图变换)
-
坐标到达观察空间(视图空间),需要将它投影到裁剪坐标上,裁剪坐标会被处理为-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。(投影变换)
-
最后,需要将裁剪的投影坐标转换为屏幕坐标,通过一个视口变换(ViewPort Transform),该变换操作会将-1.0到1.0范围内的坐标变换到
glViewPort函数所定义的坐标范围内,最后变换出来的坐标将会被送到光栅器,之后就会转换为我们熟悉的Shader片段。(视口变换)
2.局部空间
局部空间是指的物体所在的坐标空间,即好比你画一个人物图像模型,该模型是以你选取的基点(0,0,0)为起始位置(然而它们最终位置出现在世界的不同位置),那么相对世界坐标系来说,所画的模型的所有顶点就是在一个局部空间中。
3.世界空间
物体在真实的摆放,即通过将物体的局部空间坐标通过某种手段变换到真实的世界空间中,那么物体的坐标从局部变换到世界空间,是通过**模型矩阵(Model Matrix)**实现的。 该矩阵能够通过对物体进行位移、缩放、旋转来将它置于应该在的位置以及方向。
4.视图空间
视图空间也称观察空间,被称为OpenGL的摄像机。是将世界空间坐标转换为用户的视觉前方的坐标产生的结果。观察空间是从摄像机的视角所观察的空间,而要进行这种操作,需要通过一些额外的位移和旋转组合来完成,这些位移和旋转场景可将特定的对象变换到摄像机的前方。OK,AnyWay,那么这些操作的变换同样是通过矩阵来完成,那么这些矩阵操作之后的结果是会存储在一个视图矩阵(View Matrix)。
5.裁剪空间
定义:在一个顶点着色器运行的最后,OpenGL期望的坐标点能够落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪,被裁剪的坐标会被忽略,所以剩下的坐标就是屏幕上可见的片段,这就是裁剪空间的定义。
标准化的设备坐标范围为**-1.0到1.0**之间,但是显然不是很符合实际中的坐标范围,因此我们会指定自己的坐标集(Coordinate Set),并将它变换回标准化设备坐标系,这样就可以达到OpenGL预期的那样。
为了将顶点坐标从视图空间变换到裁剪空间,需要定义一个投影矩阵(Projection Matrix),指定一个范围的坐标;
解释上述的话:比如在每个维度上的范围为-1000到1000之间,投影矩阵接着会将这个指定的-1000到1000之间范围内的坐标变换为标准化坐标的范围(-1.0,1.0)。即所有不在这个范围内的坐标不会被映射到-1.0到1.0的范围内,因此裁剪的概念就出来了。
举例:坐标(1239,299,22)将是不可见的,因为x分量超出了指定的范围,因此在转换过程中,会被转换为大于1.0的标准化设备坐标,会被裁剪掉。
平截头体(Frustum):由投影矩阵创建的观察箱;每个出现在平截头体范围内的坐标最终都会出现在用户的屏幕上;
投影:将特定范围内的坐标转换为标准化设备坐标系的过程;
透视除法:所有的顶点被变换到裁剪空间,在这个过程中将位置向量的x,y,z分量分别除以向量的齐次w分量,这样做就是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。(执行过程会在顶点着色器运行的最后自动执行)
将视图坐标变换裁剪坐标的投影矩阵可以分为两种形式,每种形式定义不同的平截头体,分别是正交投影矩阵(Orthographic Projection Matrix)和 透视投影矩阵(Perspective Projection Matrix),下面就来介绍该两种投影矩阵;
6.正交投影
正交投影矩阵定义一个类似立方体的平截头箱,定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉; 创建一个正交投影矩阵需要指定可见的平截头体的宽、高和长度。
在使用正交投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。
如上图,定义了可见的坐标,由宽、高、近平面和远平面所指定的。任何出现在近平面之前或者远平面之后的坐标都会被裁剪掉。
正交投影直接将平截头体内部的所有坐标映射为标准化设备坐标, 因为每个向量的w分量都没有进行改变。如果w分量等于1.0,则透视除法不会改变这个坐标。
使用GLM的内置函数glm:ortho创建一个正交投影矩阵:
glm::ortho(0.0f,800.0f,0.0f,600.0f,0.1f,100.0f);
参数解释:
前面两个参数指定了平截头体的左右坐标,第三和第四个参数指定了平截头体的底部和顶部(其实上述参数告知我们平截头体的近平面和远平面的大小范围一个800*600的矩形范围) 第五和第六个参数则定义了近平面(0.1f)和远平面(100.0f)的距离;
注意: 正交投影矩阵直接将坐标映射到2D平面中(屏幕上),但是实际上一个直接的投影矩阵会产生不真实的结果,原因是没有将透视考虑进行,下面来介绍什么是透视以及透视矩阵。
7.透视投影
透视的概念:生活中的实际场景,离你越来越远的东西看起来更小,这种效果就被称为透视。
透视的效果就好比我们看一条无限长的高速公路或者铁路:
如上图所示,铁轨的的两条线在很远的地方看起来会相交,这正是透视投影需要模拟的效果,可使用透视投影矩阵来完成。
-
投影矩阵将给定的平截头体范围映射到裁剪空间
-
修改每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大(映射过程中,需要除以w分量,w分量越大(即越远),从视觉上来看就显得图像很小
-
被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)
顶点坐标的每个分量都会除以它的w分量,距离观察者越远则顶点坐标就会越小。正是因为这种操作,才能呈现出透视的效果。
使用GLM内置函数创建一个透视投影矩阵
glm::mat4 proj = glm::perspective(glm::radians(45.0f),(float)width/(float)height,0.1,100.0f);
使用glm::perspective所做的其实就是创建一个定义了可视空间的大平截头体,任何这个平截头体以外的东西最后都不会出现在这个裁剪空间体积内,并且将会受到裁剪。
上述函数参数解释: 第一个参数定义了fov的值,该值是一个传入了角度,表示的是所看到的视野范围。第二个参数设置了宽高比,由视口的宽除以高。第三个和第四个参数设置了平截头体的近和远平面,近距离为0.1f,远距离设为100.f。 所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
注意: 当把透视矩阵的near值设置太大时(如10.f),OpenGL会将靠近摄像机的坐标(在0.0f和10.f之间)都裁剪掉。这导致的一个现象就是太靠近的物体视觉上有一种穿透效果。
透视和正交矩阵投影的3D效果图如下:
效果总结:使用透视投影,远处的顶点看起来比较小,而在正交投影中每个顶点距离观察者的距离都是一样的
8.组合变换矩阵
介绍完了上述的概念之后,一个物体绘制需要经历多个变换矩阵才能到达这种比较真实的3D效果。期间会经历模型矩阵、视图矩阵、投影矩阵变换,一个顶点坐标根据上述操作之后得到的是裁剪坐标,最后的顶点会被复制到顶点着色器中的gl_Position,通过这个内置变量告知OpenGL,之后OpenGL会自动进行透视除法和裁剪。
视口变换:顶点着色器的输出要求所有的顶点都在裁剪空间内,上述矩阵变换已经完成了该功能,OpenGL会对裁剪坐标执行透视除法从而使得它们变换到标准化设备坐标。
OpenGL会使用glViewPort内部参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联一个屏幕上的点,最后将视图渲染到屏幕上
9.3D初探
在绘制3D图像时候经历以下过程:
- 首先需要创建一个模型矩阵,该矩阵包含了位移、缩放、旋转操作,这些操作都会被应用到所有物体的顶点上,这样就可以将该物体变换到全局的世界空间。
同样使用GLM内置函数定义一个模型矩阵:
glm::mat4 model;
model = glm::rotate(model,glm::radians(-55.0f),glm::vec3(1.0,0.0.0.0));
然后通过顶点坐标乘以这个模型矩阵,将顶点坐标映射到世界空间中。
-
接着创建一个视图矩阵,why? 上述操作只是将物体映射到世界空间,至于它在世界空间的哪个角落,不得而知,创建视图矩阵就是为了将物体移动到我们可以观察的视角里面,使得物体可见。
如何处理物体的可见性?
接下来,我们需要创建一个视图矩阵。我们想在场景中稍微向后移动, 以使对象变得可见(在世界空间中,我们位于原点(0,0,0))
要在场景中四处移动,请考虑以下事项
向后移动相机与向前移动整个场景相同。
这正是视图矩阵的作用,我们将整个场景逆向移动到我们希望摄像机移动的位置(其实就是保证了摄像机位置不变,这样做的话其实就是保证了参照物位置不变)。 因为我们想向后移动,并且因为OpenGL是右手坐标系统,所以我们必须沿正z轴移动。 所以怎么做呢?为了保证场景向后移动,使得物体能够可见,我们通过将场景平移到负Z轴来实现,相对于OpenGL的z轴正方向来说,这种做法就好比是在往后移动。
不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动
假设您的屏幕是3轴的中心,而正的z轴穿过屏幕朝向您。
右手坐标系(Right-handed System):
OpenGL是一个右手坐标系,简单来说,就是正X轴在右手边,正Y轴朝上,而正Z轴是朝外。
右手坐标系实践:
- 沿着正Y轴伸出右手,手指指着上方
- 大拇指指向右边
- 食指指向上方
- 其他手指向下弯曲90度。
通过GLM内置函数创建一个视图矩阵:
glm::mat4 view;
//注意,根据上述分析,为了场景中的对象可见,需要对场景逆向移动,这里通过操作矩阵的形式实现
view = glm::translate(view,glm::vec3(0.0f,0.0f,-3.0f));
- 定义一个投影矩阵,在场景中使用透视投影,依然是通过GLM内置函数定义:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f),screenWidth/screenHeight,0.1f,100.0f);
- 创建好了上述变换矩阵,就需要将其传入到着色器中。通过声明一个
uniform变换矩阵(定义uniform全局变量是为了接收上述定义处理好的各类变换矩阵)然后乘以顶点坐标.
#version 330 core
layout (location =0) in vec3 aPos;
//...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main(){
//矩阵乘法要从右边向左边读(其实也就是矩阵操作的顺序)
gl_Position = projection*view*model*vec4(aPos,1.0);
//...
}
- 由于变换矩阵会经常变动,因此需要将矩阵传入着色器中,这样每次渲染中就可以处理矩阵,并更新顶点坐标
//...
int modelLoc = glGetUniformLocation(shader.ID,"model");
glUniformMatrix4fv(modelLoc,1,GL_FLASE,glm::value_ptr(model));
//...