图形学——Viewing

上次图形学的博客中介绍了转换,所以我们可以从世界坐标转换到相机坐标了。不过虽然我们学的是三维模型,不过我们看到的都是二维的。近大远小是小学生都明白的道理,而一个物品的距离等等都会影响它在我们眼中,以及拍摄出来照片的样子。因此这次讲得内容是观察(Viewing)。 ## 正交投影(Orthographic Projection) ## 正交投影是最简单的一个投影方式。它实际上就是三维坐标中的点丢弃一个坐标轴,如我们需要将物体投影到xy平面上,我们就需要丢弃掉z轴。

它的特点:原来平行的线保持平行。这个特点使得它在很多工程制图中非常有用。

这个博客会介绍OpenGL中的正交投影(gluOrtho)实现。

在OpenGL中,gluOrtho做的实际上是将物体转换到一个中心位于坐标轴中心的正方体上。物体原来是个长方体,所以gluOrtho需要提供的是left,right;up,bottom;near,far.

为什么要这么做?这个是三维pipeline的一步,先映射到正方体上,最后方便投影到真正的屏幕上,也就是映射到平面像素上。

而映射到中心正方体的边长是2,左右(上下前后)坐标分别为-1,1. 因此如何映射?

假如提供的left,right;up,bottom;near,far分别值为l,r;u,b;n,f;既然要映射到正方体上,那么需要两部:一个平移,一个缩放。

首先是平移,平移向量很容易: \[ t = \begin{bmatrix} -\frac{l+r}{2}\\ -\frac{u+b}{2}\\ -\frac{n+f}{2} \end{bmatrix} \]

再一个是缩放。既然要缩放,比如左右距离的缩放,是从\(r - l\)缩放到2.因此缩放比例为:\(\frac{2}{r-l}\).

同样的道理,我们可以得到缩放矩阵: \[ S = \begin{bmatrix} \frac{2}{r - l}&0&0\\ 0&\frac{2}{u-b}&0\\ 0&0&\frac{2}{f - n} \end{bmatrix} \]

需要注意的是缩放的这些值都是正值。

然后通过齐次坐标将上面两个结合起来得到转换矩阵: \[ M = \begin{bmatrix} \frac{2}{r - l}&0&0&-\frac{r+l}{r-l}\\ 0&\frac{2}{u-b}&0&-\frac{u+b}{u-b}\\ 0&0&\frac{2}{f - n}&-\frac{f+n}{f-n}\\ 0&0&0&1 \end{bmatrix} \]

不过事情还没完。要知道,在OpenGL中,规定我们观察的方向是Z轴的负向(也就是在视点坐标中,x,y的坐标都是有正有负的,但是我们往前看到的东西的z坐标都一定是负的)。所以上面的式子就要有点变化了,我们仍然希望远的投影到+1,而近的投影到-1,这就要求实际上不光要平移到原点,在缩放时候还要将远近两个面颠倒。这时候平移大小变为:\(\frac{f+n}{2}\)(因为实际坐标是-f,-n),而为了让远的投影到1,而近的投影到-1,这个缩放尺度就要变成负数,使得位置颠倒,因此缩放尺度变为:\(-\frac{2}{f-n}\),最后乘进去后,变化的只有一小部分: \[ M = \begin{bmatrix} \frac{2}{r - l}&0&0&-\frac{r+l}{r-l}\\ 0&\frac{2}{u-b}&0&-\frac{u+b}{u-b}\\ 0&0&-\frac{2}{f - n}&-\frac{f+n}{f-n}\\ 0&0&0&1 \end{bmatrix} \]

也就是,实际上,只有一项变化了。需要注意的是这里的f和n都是正值。

透射投影(Perspective Projection)

透射投影中,远处的景色总是更近一点。实际上这就是透射投影。

下面说的这个东西和SLAM中说的针孔模型很相似:假如有一个点坐标为\(X,Y,Z\),而面前有一个屏幕,到针孔的距离为d(d>0),那么在屏幕上这一点的投影为: \[ X' = -d\frac X Z\\ Y' = -d\frac Y Z \]

这里负号的存在,还是因为z坐标都是负的。

而我实际上我们可以将透射投影转换写成这样: \[ P = \begin{bmatrix} 1&0&0&0\\ 0&1&0&0\\ 0&0&1&0\\ 0&0&-\frac {1}{d}&0 \end{bmatrix} \]

这个矩阵乘起来之后之前的坐标都没有改变,除了最后一项1变成了\(-\frac Z d\). 而齐次坐标如果将最后一个转化为1,则之前的X,Y,Z变成了:\(-d\frac X Z, -d\frac Y Z,-d\).这是个很巧妙的转换。

而OpenGL中的透投影函数会更复杂一点。我们还是通过说明gluPerspective,来理解透射映射。

首先我们需要定义一个新的名词,叫做Viewing Frustum(视锥体)。一个视锥体如下图:

任何近裁面近的点或者比远裁面远的点都会被遮挡。

gluPerspective的参数需要:fovy,aspect,zNear,zFar(zNear,zFar>0,后文简写为\(Z_n,Z_f\)). fovy为视野,可以理解为眼睛睁得大小程度,而aspect定义了视锥的高宽比。

gluPerspective依然是将这个视锥体的投影结果转换到坐标轴的中心正方体(边长为2),使得近截面的z坐标为1,远截面的z坐标为-1.

而zNear和zFar代表了我们需要透射投影的最近距离和最远距离。 投影到的"屏幕"由下图确定,(其中投影屏幕高为两个单位): 因为要映射到最后的中心正方体(变长为2),所以这个“屏幕”的高已经已经确定了,所以d的距离由\(\theta\)确定,而\(\theta = \frac {fovy}2,d = \cot \theta\).另一方面,高确定为2, 因此aspect实际上改变最终投影的宽窄,由之前的基础,我们先这样写下这个式子: \[ P = \begin{bmatrix} \frac 1 {aspect}&0&0&0\\ 0&1&0&0\\ 0&0&1&0\\ 0&0&-\frac {1}{d}&0 \end{bmatrix} \]

既然齐次坐标最终最后一项要转化为1,也就是同时乘以某个数不会影响齐次坐标的大小,我们可以将上面个的矩阵写成: \[ P = \begin{bmatrix} \frac d {aspect}&0&0&0\\ 0&1&0&0\\ 0&0&A&B\\ 0&0&-1&0 \end{bmatrix} \]

因为我们最后要影响Z坐标,所以需要改变的值是A和B的位置,而不能让他们为0.从上式求得坐标: \[ p' = \begin{bmatrix} \frac d {aspect}&0&0&0\\ 0&d&0&0\\ 0&0&A&B\\ 0&0&-1&0 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} = \begin{bmatrix} \frac {dx}{aspect} \\ dy\\ Az+B\\ -z \end{bmatrix} = \begin{bmatrix} -\frac {xd}{aspect*z} \\ -\frac{yd}{z}\\ -A-\frac B z\\ 1 \end{bmatrix} \]

因为我们要让远裁剪面在-1,近裁剪面在+1,因此: \[ \left \{ \begin{matrix} -A-\frac B {-Z_f} = 1\\ -A - \frac B {-Z_n} = =-1 \end{matrix} \right . \]

得到: \[ A =-\frac{Z_f+Z_n}{Z_f-Z_n} \\ B = -\frac{2 Z_n Z_f}{Z_f - Z_n} \]

因此将A,B带入后就是最后gluPerspective得到的矩阵。

Note

  • 在这里我们不能将\(Znear\)设置为0,如果那样的话,会导致深度信息无法解析。
  • fovy视野越大,我们看到的对象变得越小,这是因为屏幕大小是固定的。
  • 我不明白为什么openGL要将这个映射到立方体上做的这么复杂,更远的地方(z值更小)映射到1。不过gluPerspective只是一部分,除了透射投影以外,还要得到得到平面坐标,然后映射到屏幕上。
  • 传入函数的near,far,计算的到的d等都是距离,也就是都是正值,但是为了处理负的坐标值,多了很多麻烦。