GLKit实战 第03话 变换
疑惑
在第02话中,已经绘制出来了一个三角形,那么可能就会有以下的疑问:
- 为什么三角形中相互垂直的两条边,长度不一致?
- 如何才能实现三角形中相互垂直的两条边长度一致?
关于这些疑问,均受到顶点位置、模型视图矩阵、投影矩阵、视口的影响,因此,在解决问题之前,会对相关的理论知识进行说明。
注:已将清除色设置为白色,glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
先上一张插图,展示的是第02话中绘制的三角形:
在这张插图中,已标注了x、y轴,两条垂直的边a和b,以及三角形顶点位置的坐标。
坐标系统
我们使用的坐标系统是默认的笛卡尔坐标系统,它有如下特点:
- 包含x、y、z三个坐标轴,且两两之间相互垂直。
- x为横坐标轴,其正方向为从左到右。
- y为纵坐标轴,其正方向为从下到上。
- z为垂直于屏幕的坐标轴,其正方向为从屏幕下方到屏幕上方,指向用户。
视景体
视景体是一个六面体,用于框定最终可以渲染到屏幕上的物体。在后面的投影变换
中,会更具体地进行说明。
视口
视口是屏幕窗口中的某个区域,用于绘制视景体中的物体,也就是说,视景体最终会被映射到屏幕窗口中的某个区域,而这个区域,就是视口。
在视口中,坐标系统还有另外两个特点:
- 在x、y、z三个方向上,范围均为[-1, 1](注意,是闭区间,没有具体的单位,因此可以将其想象为毫米、分米、千米等)。
- 原点,也即坐标为(0, 0, 0)的位置,位于视口的中心。
结合上面的插图,可能会问,原点不是位于屏幕的中心吗?下面就回答这个问题:
通常情况下,视口大小等同于窗口大小,而默认情况下,窗口大小又等同于屏幕大小(以像素为单位)。
上面的插图,就是视口大小等同于窗口大小,因此难免会有疑惑。
视口的位置及大小,可通过glViewport
函数进行修改。
变换
在物体呈现到屏幕的过程中,需要经历视图变换、模型变换、投影变换、视口变换。
视图变换相当于相机对准某个场景的过程,调整的是相机的位置及其对准的方向。
模型变换相当于对场景中的物体进行摆放的过程,调整的是场景中物体的位置、旋转角度等。
投影变换相当于调整相机镜头焦距并由胶卷将场景记录下来的过程,调整的是视角的大小、景深的大小。
视口变换相当于确定相片的最终大小的过程,最终打印出来的相片,可能是1吋的,也可能是8吋的。
视图变换、模型变换、投影变换,可以通过GLKBaseEffect
的transform
属性进行控制,更具体些,transform
中的modelviewMatrix
控制的是视图变换和模型变换,projectionMatrix
控制的是投影变换。
模型视图变换
用于将位置坐标从世界空间转换到视觉空间。
GLKBaseEffect
中transform
的modelviewMatrix
,默认是一个单位矩阵,也就意味着,观察点位于世界空间坐标原点,指向z轴的负方向,以y轴的正方向为朝上的方向。
默认情况下的视觉空间:
通过GLKMatrix4MakeLookAt
函数,可以直观地设置用于进行模型视图变换的矩阵。它用于设置观察点的位置、指定朝向哪个位置进行观看,以及朝上的方向。
函数原型如下:
1 |
|
eyeX
、eyeY
、eyeZ
,用于指定观察点的位置。
centerX
、centerY
、centerZ
,用于指定对准哪个位置进行观察,由于有了观察点的位置,因此也就指明了观察的朝向方向,也即沿着由(eyeX, eyeY, eyeZ)
和(centerX、centerY、centerZ)
所形成的直线进行观察,方向为从(eyeX, eyeY, eyeZ)
到(centerX、centerY、centerZ)
的方向。
upX
、upY
、upZ
,用于指定观察点朝上的方向。
注意,所指定的观察点的位置,是模型视图变换进行之前的位置,当变换结束后,观察点依旧位于原点(变换前后,所使用的空间,是不一样的)。
可以回顾一下中学物理知识,运动是相对的,当你朝着某个物体前进时,也就意味着物体朝向你前进。
由GLKMatrix4MakeLookAt
生成的矩阵,最终是作用于物体的,并不是作用于观察点的。比如,GLKMatrix4MakeLookAt(1, 0, 2, 1, 0, 0, 0, 1, 0)
,指定观察点位于(1, 0, 2)
,被观看的位置是(1, 0, 0)
,也就意味着,依旧是朝向z轴负方向进行观看,它所生成的矩阵如下:
1 |
|
应该已经注意到,第1列最后一行是-1
,第3列最后一行是-2
,它们分别表示,物体需要朝着x轴负方向平移1个单位,物体需要朝着z轴负方向平移2个单位。
如下图所示:
很明显,那个立方体,朝着z轴负方向平移了2个单位,朝着x轴负方向平移了1个单位(此时的视觉空间,其坐标轴为图中的红色坐标轴)
投影变换
用于将位置坐标从视觉空间转换到投影空间。
通过投影矩阵,可以定义一个由6个平面包围着的空间区域,这就是视景体,位于视景体之外的部分,最终将会被忽略掉,不会呈现到最终的画面中。
投影可分为两种:
- 正投影
- 透视投影
正投影
在正投影下,视景体是一个长方体,相交的平面是相互垂直的,相互平行的两个面是大小一致的,也就是说,视景体是正交平行的。当同一个物体完全处于视景体内,无论它位于哪个位置,最终看到的效果,物体大小都会完全一致。
正投影视景体:
通过GLKMatrix4MakeOrtho
函数,可以直观地设置用于正投影的矩阵。
函数原型:
1 |
|
这些参数,针对的是视觉空间。
left
、right
指定视景体的左右范围,也就是左右两侧的平面在x轴上的位置。
top
、bottom
指定视景体的上下范围,也就是上下两侧的平面在y轴上的位置。
nearZ
、farZ
指定视景体的前后范围,也就是近平面、远平面在z轴上的位置。
GLKBaseEffect
中transform
的projectionMatrix
,默认是一个单位矩阵,因此也就会形成正投影,其左右上下前后范围分别是-1、1、1、-1、1、-1。
透视投影
类似于视觉感官,对于同一个物体,会产生近大远小的效果。当某个物体距离近平面较近、较远时,最终呈现的大小是不一样的,较近的会大一些,而较远的会小一些。
透视投影视景体(近平面与远平面是平行的,近平面的长宽小于远平面的长宽):
前面已经提到,GLKBaseEffect
中transform
的projectionMatrix
,默认是一个单位矩阵,会形成正投影,如果想要产生透视投影,就需要改变projectionMatrix
。
通过GLKMatrix4MakeFrustum
、GLKMatrix4MakePerspective
,可以直观地设置透视投影矩阵。
函数原型如下:
1 |
|
这些参数,针对的也是视觉空间。
GLKMatrix4MakeFrustum:
left
、right
指定视景体近平面的左右范围,也就是近平面左右两侧在x轴上的位置。
top
、bottom
指定视景体近平面的上下范围,也就是近平面上下两侧在y轴上的位置。
nearZ
指定视景体近平面与观察点之间的距离。
farZ
指定视景体远平面与观察点之间的距离。
其中,nearZ
、farZ
必须为正值,且farZ
必须大于nearZ
(实际上,可以小于nearZ
,只要不相等就行。属于GLKit
代码实现中的小瑕疵)。
虽然参数很好理解,但比起GLKMatrix4MakePerspective
,在使用时,GLKMatrix4MakeFrustum
并没有很直观,因此,在大多数情况下,使用的都是GLKMatrix4MakePerspective
。
GLKMatrix4MakePerspective:
fovyRadians
指定在yz平面(由y轴和z轴所形成的平面)上的视野角度,单位为弧度。
aspect
指定视景体近平面的宽高比。
nearZ
、farZ
,与GLKMatrix4MakeFrustum
中的一样。
它所产生的视景体,总是上下对称、左右对称的(对称面分别为xz平面、yz平面)。因此,当需要产生上下不对称或左右不对称的视景体时,使用GLKMatrix4MakeFrustum
会更便利。
视口变换
经过投影变换后,为了让物体展示到窗口之中,还需要进行视口变换。
之前讲视口时,已经提到过,其坐标系统的范围,在三个方向上均为[-1, 1]。
视口变换就是把视景体内的物体,映射到视口的过程。无论近平面有多大,经过映射后,其上的位置最终都会被限制在[-1, 1]之中。
将视景体映射到方形的视口中(插图中左侧的,是透过视景体近平面所看到的效果):
将视景体映射到扁平的视口中,很明显,最终的效果发生了形变(插图中左侧的,是透过视景体近平面所看到的效果):
处理疑惑
结合第02话中的代码,以及上面的理论知识,就很容易理解,为什么三角形中相互垂直的两条边,长度是不一致的。
由于默认情况下,GLKBaseEffect
中transform
的projectionMatrix
,是一个单位矩阵,会形成正投影,视景体的范围,在[-1, 1]之间。
第02话中的代码没有设置projectionMatrix
,也就是使用的是默认值。
默认情况下,视口的大小,就是窗口的大小。在第02话中的代码中,没有通过调用glViewport
来调整视口,因此视口也就是全屏的,其宽度与高度是不一致的。
因此形成了下面的映射:
为了让最终的两条边的长度一致,可以采取以下处理方式(可任选其一):
- 直接调整顶点的位置。
- 调整模型视图矩阵,可以间接调整顶点的位置。
- 调整投影矩阵,让视景体近平面的宽高比,与视口的宽高比一致。
- 调整视口,使其与视景体近平面的宽高比一致。
因为有4种选择,因此,先定义几个枚举值,以及一个属性,用于标记所采用的是哪种选择。
1 |
|
直接调整顶点位置
通过调整顶点位置,让经过投影变换(默认情况下是正投影)后的三角形:
- 在竖直方向上扁一些(适用于视口宽度不大于高度的情况)
- 在水平方向上瘦一些(适用于视口宽度不小于高度的情况)
1 |
|
插图中,红色虚线圈定的区域,就是修改了顶点位置后,所产生的三角形。
调整模型视图矩阵
直接调整顶点位置,需要对每个顶点进行手动调整,十分笨拙,如果顶点有很多,就更麻烦了。为了方便,可以通过调整modelviewMatrix
达到间接调整顶点位置的目的:
- 在竖直方向上缩小一些(适用于视口宽度不大于高度的情况)
- 在水平方向上缩小一些(适用于视口宽度不小于高度的情况)
1 |
|
调整投影矩阵
调整投影矩阵,让视景体近平面的宽高比,与视口的宽高比一致。
如果要使用正投影,那么需要:
- 调整视景体的上下范围(适用于视口宽度不大于高度的情况)
- 调整视景体的左右范围(适用于视口宽度不小于高度的情况)
如果要使用透视投影,就需要:
当使用GLKMatrix4MakeFrustum
时,需要调整的东西和正投影一样,也是上下左右的范围。
当使用GLKMatrix4MakePerspective
时,只需要调整宽高比。
当然,在调整视景体时,注意一定要确定物体处于视景体中,否则最终的画面可能会空空如也,因此,也可能需要调整modelviewMatrix
。
为了简单起见,在代码中依旧使用正投影:
1 |
|
插图中,红色虚线圈定的区域,就是新设定的视景体近平面,它的宽高比,与视口的宽高比相同。
调整视口
调整视口,使其与视景体近平面的宽高比一致。
默认情况下,用的投影矩阵是单位矩阵,因此视景体近平面的宽高比为1(正投影视景体,范围为[-1, 1],这已经在之前提到过)。
因此,只需将视口的宽高调整为大小一致即可:
1 |
|
插图中,红色虚线圈定的区域,就是新设定的视口,它处于屏幕的中间区域,宽高相等。
最终效果
对比4种处理方式
直接调整顶点位置,前面已经说过,会显得十分笨拙,几乎不会选择这种处理方式。
考虑到要充分利用窗口空间,将视口调整为窗口上的一小部分,并不是最佳选择,因为这样的话,窗口的其他区域就不会展示内容了,因此,保持视口占用整个窗口即可。
修改模型视图矩阵、投影矩阵,才是最佳的处理方式。