之前在这篇文章学习了如何在OpenGL中是如何构建一个空间描述物体的信息而又是如何投射到渲染屏幕上的过程。
OpenGL采用的是相机模型,就是把视图变换操作类比为使用照相机拍摄照片的过程,具体步骤如下:
1.将准备拍摄的对象移动到场景中指定位置。(模型变换,Model Transform)
2.将相机移动到准备拍摄的位置,将它对准某个方向。(视图变换,View Transform)
3.设置相机的焦距,或者调整缩放比例。(投影变换,Projection Transform)
4.拍摄照片并变换到需要的图片大小。(这个是视口变换,glViewPort)
接下来深刻学习如何将模型坐标系->世界坐标系->视图坐标系->裁剪空间最终到屏幕空间的过程,以及在渲染引擎中的具体操作。
矩阵与坐标系
在OpenGL中所有的图形变化都可以通过矩阵来表示(通过一个4x4的矩阵来表示空间信息),下图中前三列表示一个三维空间中(x, y, z)三个向量,最后一列表示这个坐标系以哪个坐标为原点(一般为0,0,0),而最后一位表示这个三维坐标系的齐次坐标。
我们通过这个矩阵的前三列来实现对一个坐标系的旋转和缩放,最后一列来实现平移,最后通过齐次坐标来表示观察者的远近(在投影变换里面需要用到)。
那么显然一个模型的原始顶点(x, y, z)向量坐标到裁剪空间的顶点坐标可以看做是它的坐标系经过一系列的平移,旋转,缩放,投影变换得到的。
这三个变换过程又可以细分成6个过程,在世界坐标系中进行平移旋转缩放,在视图坐标系中进行平移旋转,以及投射到平面上。
我们想要得到一个最终输入的顶点坐标(裁剪空间),只需要将它的过程变换坐标系矩阵相乘得到最终坐标系矩阵再乘于原始顶点就能得到了,这也是OpenGL中坐标变换的基本原理了。
1 | /*S:缩放 R:旋转 T:平移*/ |
从模型坐标系到世界坐标系(模型变换)
模型空间(Model Space)
用于描述物体空间信息的本地坐标空间,通常也称为物体坐标空间(object space)。通常选择物体的重心或者某一特定的点建立3维空间坐标系,并用一系列顶点来描述一个3D模型。坐标原点通常不会在模型外部,物体绕其坐标原点旋转的同时产生平移是没有任何意义的。
回到最初的视角上,我们准备把拍摄的对象摆放到指定位置上(模型变换)。这个过程可以看做一个模型本身在世界坐标系中平移,旋转,缩放的过程。
我们在模型空间那些顶点都是基于模型原点所描绘出来的,那么我们只要找到这个模型的模型原点相对世界坐标系原点的位置不就可以在这个世界坐标系中描绘出这个模型了吗?
其中OpenGL中世界坐标系是固定以(0, 0, 0)为原点的,而一个模型的基本变换过程如下图所示。
其中模型可以绕x,y,z三个轴分别做旋转所以会有三个旋转矩阵可选择:
$$Rx = \left[
\begin{matrix}
1 & 0 & 0 & 0\\
0 & Cosθ & -Sinθ & 0\\
0 & Sinθ & Cosθ & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
Ry = \left[
\begin{matrix}
Cosθ & 0 & -Sinθ & 0\\
0 & 1 & 0 & 0\\
Sinθ & 0 & Cosθ & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
Rz = \left[
\begin{matrix}
Cosθ & -Sinθ & 0 & 0\\
Sinθ & Cosθ & 0 & 0\\
0 & 0 & 1 & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
$$
合成一个旋转矩阵:
最终,模型变换过程可以用下面这个数学公式来概括,需要注意的是我们使用左乘矩阵依次进行缩放->旋转->平移才能得到正确的结果(矩阵不满足交换律)。
$$
Model =
\left[
\begin{matrix}
1 & 0 & 0 & Position X\\
0 & 1 & 0 & Position Y\\
0 & 0 & 1 & Position Z\\
0 & 0 & 0 & 1
\end{matrix}
\right]
\left[Rotate
\right]
\left[
\begin{matrix}
Scale X & 0 & 0 & 0\\
0 & Scale Y & 0 & 0\\
0 & 0 & Scale Z & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
$$
从世界坐标系到视图坐标系(视图变换)
视图/观察者空间(View Space)
和观察者相关的空间,有时也称为相机空间(Camera or Eye space)。同模型空间类似,视图空间也是基于世界坐标系建立的。其x,y方向表示相机的右侧和上方,其z轴负方向表示相机的镜头朝向(右手坐标系)。
这时候我们摆好物体的位置了,准备拿出相机选个角度拍摄这个物体(视图变换)。而这个过程中我们只需要平移,旋转相机就可以了。
显然我们拍照的时候镜头肯定是对着物体的嘛,所以相机的的镜头朝向应该是在z轴的负方向。它的矩阵变换公式和上面的没有本质的区别,但是由于镜头朝向,所以把它的平移公式定义为
$$
View = \left[
\begin{matrix}
1 & 0 & 0 & -Position X\\
0 & 1 & 0 & -Position Y\\
0 & 0 & 1 & -Position Z\\
0 & 0 & 0 & 1
\end{matrix}
\right]\left[
\begin{matrix}
Rotate
\end{matrix}
\right]
$$
从视图坐标系到裁剪空间(投影变换)
裁剪空间(clip space,也叫作齐次裁剪空间)。
其矩阵是裁剪矩阵(clip matrix),也叫作投影矩阵(projection matrix),目的是将3D的物体投射到2D的平面上。
投影矩阵会将在这个指定的范围内的坐标变换为标准化设备坐标(NDC)的范围[-1.0, 1.0],超出这个范围的坐标则被舍弃掉,这也就是裁剪的过程。
接下来我们要按下相机的快门拍下一张物体的快照了(投影变换),而通过相机能观察到的范围我们称作视椎体(view frustum),然后我们按下快门后会将视椎体中的内容映射到相片上,而映射的方式可以选择透视投影(perspective projection)或者正交投影(orthographic projection)。
对于透视投影:
对于正交投影:
在经过这上诉的矩阵变换后我们终于把模型空间中的坐标送到OpenGL能接受的裁剪空间里面了。通过实践会发现在这一步我们获得的坐标已经转换成了NDC范围里面的坐标了,接着只需要再进行一步到屏幕的映射就完成了整个渲染过程。
裁剪空间到屏幕
裁剪后需要进行真正的投影,需要把视椎体投影到屏幕空间(screen space)得到像素位置。
这里将利用透视除法(Perspective Division)进行最后的映射工作,透视除法就是将位置向量的x,y,z分量分别除以位置向量的齐次w分量。
在OpenGL中使用glViewPort内部的参数来将标准化设备坐标(NDC)映射到屏幕坐标,从而让每个坐标关联上屏幕相应的点,这个过程称为视口变换。