1. 摄像机
摄像机就好像是我们的眼睛,我们从摄像机的方向观察世界空间中的模型。摄像机远离模型,模型自然就变小了(透视投影下),然而,在GL中事实上并没有摄像机的概念。但是我们可以通过移动世界空间远离我们的摄像机来模拟摄像机远离世界的感觉
。这也正是在上一章中,我们的观察矩阵是(0,0,-3)的原因。我们通过将世界矩阵向屏幕里移动3个单位模拟摄像机向屏幕外移动三个单位。
上一章中,我们只是很简单的设置了一个观察矩阵,这一章,我们来仔细说一下如何设置这个观察矩阵,也就是,如何确定摄像机所在的坐标系。
2. 视察矩阵
事实上我们所要做的事就是确定一个观察矩阵,就可以完成从世界空间到观察空间的转换。而这在三维空间内,实际就是确定观察空间坐标系相对于世界空间坐标系的位置及各轴的夹角即可
。如果你空间感足够好的话,或者相对位置足够简单的话,你可能很快的就可以写出观察坐标系的表达式。但情况比较复杂的时候你可能需要耗费大量时间去完成这个转换,下面,我将尝试用一个通俗易懂的方式来介绍一下如何去生成这个坐标。
3. 摄像机的位置
这个很好理解,比如你规定了地面上一个物体作为世界的原点,那么随着你眼睛相对远点的位置改变时,你所看到的物体的样子也会随之改变。所以眼睛相对于远点的位置会直接影响物体的样子,同理,我们也需要确定摄像机相对于世界空间坐标原点的位置
。这里你可以看下上面图中的第一个图片来理解。坐标系就是世界空间,我们要确定的就是摄像机在世界空间中的位置。
4. 摄像机的方向
同样,即是你站在一个位置不动,你将目光集中在物体上不同的点时,你所看到的物体也不同。或者说,你目光的方向改变时,物体也跟着改变。同理,我们还需要确定摄像机观察的方向
。第二张图中就显示了摄像机在当前位置看向世界空间远点的示例。
5. 摄像机的滚转角
好了,现在你站在一个位置不动,目光也一直盯着物体的中心点不动,你还可以让你看到的物体改变。除了闭眼睛,你还可以歪一下头,你看到的东西是不是斜过来了(你非说没变那是因为强大的大脑已经帮你转换回来了又,你可以把眼睛换成手机摄像头然后倾斜手机再看看,手动滑稽)。所以,我们要确定摄像机在世界空间中摆放的夹角
。这么表述可能不清楚,稍微借一点坐标系的概念。我们摄像机的方向就是观察坐标系的Z轴。但是一个Z轴确定却并不能确定一个坐标系,我们至少要确定两个坐标轴,才能通过两个坐标轴确定第三个坐标轴从而建立一个坐标系。第四张图就显示了确定三个轴夹角后的坐标系。
6. 视察坐标系
经过上面的论述,我们知道了,我们只要知道摄像机的位置
、摄像机的方向
和x或y轴中任意一个轴的方向
即可确定。
如果我们不改变滚转角保证了摄像机坐标系的x轴与世界坐标系的y轴总是空间垂直,我们就可以通过摄像机方向向量与世界坐标系的y轴的方向向量叉乘从而获得摄像机坐标系的x轴
(两向量叉乘将获得同时垂直于两个向量的第三个向量)。
Look At
我们已经知道如何去构建一个摄像机坐标系了,不过怎么通过这些元素构建出观察矩阵呢?glm为我们提供了LookAt(position,target,up)函数。它含有三个参数:
- position,第一个参数就是我们摄像机在世界坐标系的位置了
- target,第二个参数是我们观察的点的位置,就是我们目光汇聚的那个点了,为什么是目标点呢?因为通过position减去target我们就可以获得摄像机方向的向量了
- up,第三个参数是一个与摄像机坐标系x轴垂直的向量。为什么是这个向量呢?因为我们可以通过position和target确定摄像机的方向,也就是摄像机坐标系z轴。再找到一个也与x轴垂直的向量即可确定x轴的方向向量了。
所以,上一章中我们生成的观察矩阵可以通过lookAt函数这样生成:
上面函数中,描述了我们的摄像机在世界坐标系的(0,0,3)位置,我们观察的点就是世界坐标原点,这个up向量就是世界坐标系的y轴的方向向量。
7. 圆周运动
那么现在,我们将我们的target保持在(0,0,0)这个点上,通过改变position来改变我们的lookAt矩阵。我们可以大概猜想一下结果就应该是我们围着一个东西转圈一直盯着这个东西的样子。
下面这段代码在渲染循环中:
8. 水平运动
接下来我们来模拟一下我们日常生活中的视角。我们想一下,一定是我们相对世界的position一直是改变的,我们眼睛的焦距是不变的,始终看我们自身位置前的某一个位置。
那么lookAt函数变成了这样:
那么现在我们还是只要改变我们的position就好了。
这里我们用键盘来接收我们想要做的移动的输入,代码如下:
上述左右移动时,我们看到我们用front向量与up向量相乘后标准化获取了right向量。这里之所以直接使用世界坐标系中的front向量和up向量,是因为我们当前观察角度的摄像机坐标系与世界坐标是的各轴完全是平行的,只是原点不同而已。
这样,我们就在这个世界中可以前后左右自由移动了。
9. 移动速度
我们看到,我们处理键盘输入是在渲染循环中处理的。试想如果我们的渲染循环循环一次的事件长,那么我们改变position经过的时间间隔就长,反之就短。那么如果一次渲染循环的时间是在一个范围内浮动的,那么物体运动的速度看起来也就是一个浮动的过程,我们应该监测每次渲染循环(更准确的应该是每次键盘事件处理)的时间间隔,通过这个时间间隔决定我们这一次position改变的数量
。
这里我们只要将我们之前定义的摄像机的速度在乘上一个时间间隔系数即可:
10. 视角移动
目前为止,我们实现了在世界中水平自由移动了,但是我们还不能转头也不能抬头。我们只要改变我们观察点的位置即可。这里,为了保证我们的焦距是不变的,所以我们要将front向量标准化。
现在我们的摄像机坐标系与世界坐标系的各轴是平行的。我们想抬头呢,我们就以x轴旋转坐标系,想左右牛头就以y轴旋转坐标系即可。
11. 欧拉角
我们先来看一下坐标系旋转角的概念和图示。
欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll)。
俯仰角
是描述我们如何往上或往下看的角,可以在第一张图中看到。第二张图展示了偏航角
,偏航角表示我们往左和往右看的程度。滚转角
代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。
我们来看一下如何计算这个向量。
12. 鼠标输入
首先,我们要告诉GL,如果捕捉光标的话,我们不应该展示光标(当然你也可以展示)。
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
然后我们应该设置鼠标事件的回调:
glfwSetCursorPosCallback(window, mouse_callback);
接下来就是回调函数了:
13. 缩放
目前为止,我们可以自由在世界中移动,改变视角了已经。但是我们还想给摄像机添加一个缩放功能,你可能说我走近点东西不就大了么。的确是这样的,不过我们也可以在原地做到这件事。我们可以利用透视的特性完成这件事
。在同样的距离观察一个物体,视野越小时,我们能观察到的物体的部分越小,但是我们的屏幕是不变的,也就是屏幕上显示的物体的部分越小,这样就好像物体被放大了一样。所以我们只要改动这个fov值就可以完成缩放。