一个坐标转换的心路历程。git
在前面绘制基本图形中,遇到了很明显的问题,圆形不像圆形,正多边形不像正多边形?就像下面图形同样:github
好好的正五边形却东倒西歪的,这就是由于咱们前面的绘制都是把它当成 二维 的绘制,而在 OpenGL 中倒是绘制 三维的。在二维和三维之间还有个转换,而以前为了方便学习则忽略了这个转换,如今就要开始理解它了 —— 坐标系统
!!spring
在立体几何的坐标系里面定义一个点的位置,须要 x、y、z 三个坐标轴的值,而在 OpenGL 中绘制 3D 物体也是须要的。bash
在绘制基本形状时,只是定义了 x、y 轴的坐标,这样 z 轴的坐标就默认为 0 了。微信
OpenGL 将定义好的坐标轴的值转换为实际绘制的坐标,须要通过五个坐标系统的转换。函数
以下图所示:post
这里面涉及到了五个坐标空间和三个转换矩阵:学习
空间:ui
矩阵:spa
根据流程图,每一个坐标空间的转换都须要一个转换矩阵来完成。
最后裁剪空间到屏幕空间的转换,就是将通过这一系列转换后的坐标映射到屏幕的坐标上,这一过程就不须要转换矩阵了。
在进入不一样的坐标空间以前,须要先了解 OpenGL 的坐标系:
OpenGL 是一个右手坐标系,正 X 轴在右手边,正 Y 轴朝上,正 Z 轴穿过屏幕朝向你。
与之相对的就是左手坐标系,其正 Z 轴穿过屏幕朝向里面了。
局部空间坐标是 OpenGL 绘制坐标的起点,接下来全部的转换操做都是在局部空间坐标基础上进行的。
局部空间坐标就是咱们本身定义的起始坐标点,是相对于原点 的。
此时所在的空间就是局部空间,也就是说咱们在局部空间里面定义物体的起始坐标。
咱们定义每个坐标点都是在局部空间,相对于 的。这样一来,当多个物体同时绘制时,就会扎堆了。
而世界空间就是当全部物体一块儿绘制、仍然相对于原点的、更大的一个坐标系。
局部空间和世界空间有点相像,能够在局部空间定义坐标系时就考虑到世界坐标系,避免多个物体绘制时出现扎堆现象。
固然还有更好的方法,就是使用模型矩阵(Model Matrix)。
使用模型矩阵,能够对物体进行位移、缩放、旋转。
这样的话就能够将物体从坐标原点移开,而且还可以进行一些相关操做,不用去考虑在局部空间来定义世界空间的坐标了。
横当作岭侧成峰 远近高低各不一样
当物体在世界空间中就位了,接下来就是要考虑从哪一个方向和角度来观察物体了。
观察空间,又是 OpenGL 的摄像机,是将世界空间的坐标转化为摄像机的视角所观察到的空间坐标。
也就是说,在观察空间里,坐标原点再也不是世界空间的坐标原点了,而是以摄像机的视角做为场景原点,这就再也不是简单地进行平移、旋转了,而是切换到另外一种坐标系里。
OpenGL 自己是没有摄像机的概念的,不过能够经过把场景中的全部物体往相反的方向移动来模拟出摄像机。这样就场景没动,而摄像机在移动。
要定义一个摄像机,或者说要定义一个摄像机视角为坐标原点的坐标系,须要:
如图,最终创建了一个以摄像机位置为原点的坐标系。
其中,蓝色箭头为摄像机坐标系中的 Z 轴,绿色箭头为摄像机坐标系中的 Y 轴,红色箭头为摄像机坐标系中的 X 轴。
而接下来要作的就是将物体在世界空间中的坐标转换到以摄像机视角为原点的观察空间坐标中。
这其中也须要用到一个转换矩阵:视图矩阵(View Matrix)。经过视图矩阵来切换坐标系。
当物体坐标都位于观察空间后,接下来要作的就是裁剪。根据咱们的须要来裁剪必定范围内的物体,而在这个范围以外的坐标就会被忽略掉。
裁剪空间实质上仍是进行坐标的操做。
从观察空间到裁剪空间,须要用到:投影矩阵(Projection Matrix)。
投影矩阵会指定一个坐标范围,这个范围内的坐标将变换为归一化设备坐标
,不在这个范围内的坐标就会被裁剪掉。
观察空间中的坐标通过投影矩阵的变换以后称为投影坐标,又叫作裁剪坐标
。
说是裁剪坐标,实际上是待裁剪,接下来的裁剪过程将由 OpenGL 来完成的。投影矩阵的变换,只是筛选出那些不须要被裁剪的坐标。
由投影矩阵建立的范围,是一个封闭的空间几何体,被称为视景体
。
投影矩阵有两种不一样的形式,建立的视景体也有两种样式。
正交投影会建立一个相似立方体的视景体。它由左、上、右、下 四个方向距离和近平面距离、远平面距离组成。四个方向距离定义了近平面和远平面的大小。而在近平面和远平面以外的坐标点就会被裁剪掉了。
在场景中处于视景体内的物体会被投影到近平面上,而后再将近平面上投影出的内容映射到屏幕上。
它所用到的矩阵是正交投影矩阵。
因为正交投影是平行投影的一种,其投影线是平行的,因此投影到近平面上的图形不会产生真实世界中的近大远小
的效果。由于正交投影没有把透视考虑进去,因此,远处的物体不会变小,这适用于一些特定的场合。
透视投影是可以产生近大远小
效果的,就像咱们人眼同样,看远处的物体就变得很小了。
它所用到的矩阵就是透视投影矩阵。
透视投影也会建立一个视景体,相似于锥形。它一样也有着近平面距离和远平面距离,并且也是将近平面的内容映射到屏幕视口中,但不一样与正交投影近平面和远平面大小相同,因此它的左、上、右、下距离都是相对于近平面的。
能够看到,透视投影的投影线互不平行,都相交于视点。所以,一样尺寸的物体,才会近处的投影出来大,远处的投影出来小。
当坐标通过投影矩阵的变换到裁剪空间以后,紧接着就会进行透视除法
的操做。
透视除法是在三维绘制中产生近大远小
效果很是关键重要的一步。
在此以前要先来了解一下 OpenGL 中的 w 份量
。
OpenGL 坐标系中除了 x、y、z 坐标外,还有 w 份量,默认状况下都是 1 。而通过透视投影变换以后,w 份量再也不是 1 了,正交投影不改变 w 份量。
而 OpenGL 进行裁剪,实质上是 GPU 进行裁剪的过程,就是将 x、y、z 坐标的绝对值与 w 份量绝对值进行比较,只要有一个份量的绝对值大于 w 的绝对值,就认为不在视景体内,会被裁剪掉。
通过裁剪以后,再进行透视除法。就是将 x、y、z 坐标分别除以 w 份量,获得新的 x、y、z 坐标。因为 x、y、z 坐标的绝对值都小于 w 的绝对值,因此获得新的坐标值都是位于 的区间内的。此时获得的坐标,也就是
归一化设备坐标
。
归一化设备坐标是独立于屏幕的,并且它的坐标系用的是左手坐标系。
通过透视投影矩阵变换以后,每一个坐标的 w 份量都不相同了,这样再通过透视除法操做,就会使得远处的物体看起来变小了。
有了归一化设备坐标,最后一步就是将坐标投射到屏幕上,这一步是由 OpenGL 来完成的。
OpenGL 会使用 glViewPort
函数来将归一化设备坐标映射到屏幕坐标,每一个坐标都关联了屏幕上的一个点,这个过程称为视口变换
。这一步操做再也不须要变换矩阵了。
就这样,一个点的坐标就完成了从局部空间坐标 到屏幕坐标
的转变。
点的坐标能够看做是一个向量,用 表示,而矩阵用
表示。
那么,从 局部空间 -> 世界空间 -> 观察空间 -> 裁剪空间 ,四个空间的转换,须要用到三个转换矩阵,点从某个坐标系变换到另外一个坐标系的时候都要左乘某个变换矩阵,最后裁剪空间的坐标能够表示以下:
而在着色器脚本中,gl_Position
对应的也是 裁剪坐标。
有了裁剪空间坐标后,接下来的事情就交个 OpenGL 去完成裁剪和透视除法就行了。
在文章一开始提到的,绘制的圆形变成了椭圆,绘制的正多边形却东倒西歪的,如今也能给出缘由了。
默认状况下,局部空间、世界空间、观察空间、裁剪空间的坐标系都是重合的,都是以为坐标原点。一开始只是给出了理想状态下的平面坐标点,而且定义着色器脚本以下:
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
复制代码
那么它通过一系列转换后,最后 OpenGL 用来裁剪的坐标仍是咱们定义的基于平面的坐标,只有值,而
坐标默认为 0,
坐标默认为 1 。通过透视除法后的归一化设备坐标依旧是
。
而归一化设备坐标假定的坐标空间是一个正方形,但手机屏幕的视口倒是一个长方形,这样的话,就会有一个方向被拉伸。一样的份数,但长度越长,致使每一份的长度也增长了,因此也就被拉伸了。
要解决这种问题,能够在归一化设备坐标上进行操做,将较长的一边乘以相应的比例系数,转化到一样的长度比上。
// 1280 * 720 的宽高比
aspect = width / height ;
x = x * aspect
y = y
复制代码
这样一来,将较长的一边的比例放大了,取较短的那一边做为 1 的标准。
固然也能够在坐标转换成归一化设备坐标以前,也就是在投影时就把拉伸的状况考虑进去。
使用正交投影,再将物体的宽高投影到近平面上时,就把屏幕的宽高比例系数考虑进去,这样在转换成归一化设备坐标以前就已经完成了图形的宽高比适应。
这样的话,就须要修改着色器脚本语言,把投影矩阵考虑在内。
attribute vec4 a_Position;
uniform mat4 u_Matrix;
void main(){
gl_Position = u_Matrix * a_Position;
}
复制代码
具体代码详情,能够参考个人 Github 项目: https://github.com/glumes/AndroidOpenGLTutorial
最后,若是以为文章不错,欢迎关注微信公众号:【纸上浅谈】