返回

计算机图形学基础学习笔记-视角

Viewing Transformations

视口变换(Viewport Transformation)

\([-1,1]^2\)的正方形映射到屏幕上。这个屏幕宽\(n_x\)像素,高\(n_y\)像素。并且由于左下角像素中心点位置为原点,我们要有负0.5个像素,即映射到\([-0.5,n_x-0.5]\times[-0.5,n_y-0.5]\)。(注,在games101中映射到的是\([0,n_x]\times[0,n_y]\)

需要如下变换

\[\begin{bmatrix} x_{screen} \\ y_{screen} \\ 1 \end{bmatrix}= \begin{bmatrix} n_x/2 & 0 & (n_x-1)/2\\ 0 & n_y/2 & (n_y-1)/2\\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x_{canonical}\\ y_{canonical}\\ 1 \end{bmatrix} \]

三维形式有

\[M_{vp}= \begin{bmatrix} n_x/2 & 0 & 0 & (n_x-1)/2\\ 0 & n_y/2 & 0 & (n_y-1)/2\\ 0 & 0 & 1 &0\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

正交投影变换(Orthographic Projection Transformation)

将一个\([l,r]\times[b,t]\times[f,n]\)矩阵变换到\([-1,1]^3\)

其中\(l\)即left是\(x\)坐标小的平面,\(r\)即right是\(x\)坐标大的平面。

其中\(b\)即bottom是\(y\)坐标小的平面,\(t\)即top是\(y\)坐标大的平面。

其中\(f\)即far是\(z\)坐标小的平面,\(n\)即near是\(z\)坐标大的平面。

注意\(z\)可能与常识不太相同,因为我们的相机所看的方向是\(-z\)方向。

这也导致了OpenGL使用左手坐标系。

完成这个变换的矩阵是

\[\begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0\\ 0 & \frac{2}{t-b} & 0 & 0\\ 0 & 0 & \frac{2}{n-f} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -\frac{r+l}{2}\\ 0 & 1 & 0 & -\frac{t+b}{2}\\ 0 & 0 & 1 & -\frac{n+f}{2}\\ 0 & 0 & 0 & 1 \end{bmatrix}= \]

\[\begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l}\\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b}\\ 0 & 0 & \frac{2}{n-f} & -\frac{n+f}{n-f}\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

相机变换(Camera Transformation)

首先知道三个向量:

  1. \(\bm e\),相机位置(eye position)向量
  2. \(\hat{\bm g}\),相机视线(gaze)方向。
  3. \(\hat{\bm t}\),相机头顶方向。(和视线方向正交)

我们要将相机位置变换到原点,将视线方向定为\(-z\)方向,头顶方向为\(y\)方向。同时所有物体都跟随相机变换,最终结果相机看到的画面不变。

首先,显然的,将相机位置变换到原点的矩阵是

\[T_{view} \begin{bmatrix} 1 & 0 & 0 & -x_e\\ 0 & 1 & 0 & -y_e\\ 0 & 0 & 1 & -z_e\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

然后对两个另外两个向量进行旋转,直接想不太方便,可以反过来由\(z\)\(y\)变换到\(-\hat g\)\(\hat t\)\(x\)变换到\(\hat g\times \hat t\)

\[R_{view}^{-1}= \begin{bmatrix} x_{\hat g\times \hat t} & x_{\hat t} & x_{-\hat{g}} & 0\\ y_{\hat g\times \hat t} & y_{\hat t} & y_{-\hat{g}} & 0\\ z_{\hat g\times \hat t} & z_{\hat t} & z_{-\hat{g}} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

然后再得到逆变换,由于这是个正交矩阵,逆矩阵就是转置矩阵

\[R_{view}= \begin{bmatrix} x_{\hat g\times \hat t} & y_{\hat g\times \hat t} & z_{\hat g\times \hat t} & 0\\ x_{\hat t} & y_{\hat t} & z_{\hat t} & 0\\ x_{-\hat{g}} & y_{-\hat{g}} & z_{-\hat{g}} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \]

另一种算法

假设我们初始时相机在\((0,0,0)\),相机视线方向为\((0,0,1)\),相机的上方向为\((0,1,0)\)

此时相机变换到\(e\),我们保持相机上向量相对不变,而左向量\(x\)轴方向就为\(\hat r=((\hat g-e)\times (0,1,0)).normalized\),从而新的上方向则变为\(\hat t=(\hat g-e)\times\hat r\)

如果想改变上方向,则就把初始的上方向改变即可,整个变换矩阵同上。

透视投影(Perspective Projection)

由正交投影变换到透视投影,矩阵如下

\[P= \begin{bmatrix} n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ 0 & 0 & n+f & -fn\\ 0 & 0 & 1 & 0 \end{bmatrix} \]

直观上的理解就是,将视锥中远平面压小到等于近平面。

以上是games101的解释。

在Real-Time Rendering的解释中,假设我们的相机在原点,我们想将任意一个点\(p\),映射到平面\(z=-d(d>0)\)上,得到一个新点\(q=(q_x,q_y,-d)\)。显然根据相似三角形,我们有

\[\frac{q_x}{p_x}=\frac{-d}{p_z}\Rightarrow q_x = -d\frac{p_x}{p_z} \]

对于\(y\)坐标也是同样的,所以能得到矩阵

\[P_p = \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & -1/d & 0 \end{bmatrix} \]

因为

\[q=P_pp= \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & -1/d & 0 \end{bmatrix} \begin{bmatrix} p_x \\ p_y \\ p_z \\ 1 \end{bmatrix}= \begin{bmatrix} p_x \\ p_y \\ p_z \\ -p_z/d \end{bmatrix}\Rightarrow \begin{bmatrix} -dp_x/p_z \\ -dp_y/p_z \\ -d \\ 1 \end{bmatrix} \]

但我们通常要做的不是把点都映射到一个平面上,而是要把视锥压缩到一个\([-1,1]^3\)的立方体上。

这个视锥体是一个四棱椎,相机在椎顶,近平面\(n\),远平面\(f\),有\(0>n>f\)。因此四棱锥被截成了四棱台。以相机的角度,上下侧面分别为\(t,b\),左右侧面分别为\(l,r\)

对于近平面,左下角是\((l,b,n)\),右上角是\((r,t,n)\),决定了我们能看到的画面的大小,或称作视野范围。

将这样的视锥变为\([-1,1]^3\)的矩阵是

\[P_p=\begin{bmatrix} \frac{2n}{r-l} & 0 & -\frac{r+l}{r-l} & 0\\ 0 & \frac{2n}{t-b} & -\frac{t+b}{t-b} & 0\\ 0 & 0 & \frac{f+n}{f-n} & -\frac{2fn}{f-n}\\ 0 & 0 & 1 & 0 \end{bmatrix} \]

也正好和上方games101的正交投影矩阵*透视投影矩阵相同。

然后,不在\(n,f\)平面之间的物体将会被剔除,不被渲染。

如果远平面是无穷远,那么有

\[P_p=\begin{bmatrix} \frac{2n}{r-l} & 0 & -\frac{r+l}{r-l} & 0\\ 0 & \frac{2n}{t-b} & -\frac{t+b}{t-b} & 0\\ 0 & 0 & 1 & -2n\\ 0 & 0 & 1 & 0 \end{bmatrix} \]

OpenGL中的透视矩阵

虽然都是右手系,但是OpenGL中zNear和zFar都是正数,透视矩阵就变为

\[P_p=\begin{bmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0\\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0\\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n}\\ 0 & 0 & -1 & 0 \end{bmatrix} \]

另外,OpenGL默认(指glm库)有\(r=-l,t=-d\),就有

\[P_p=\begin{bmatrix} c/a & 0 & 0 & 0\\ 0 & c & 0 & 0\\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n}\\ 0 & 0 & -1 & 0 \end{bmatrix} \]

其中\(a\)是显示分辨率的宽高比,\(c\)\(1/tan(fovY/2)\)

另外,将\(z\)轴上的\([-0.1,-100]\)映射到\([-1,1]\)上,不是均匀的,不精确的说,可能\([-0.1+,1.0]\)这一段映射到了\([-1,0.5]\)上,剩下的\([1.0,100]\)映射到了\([0.5,1]\)上,这在zbuffer、深度值中可能是需要注意的事。

可以看https://learnopengl-cn.github.io/04 Advanced OpenGL/01 Depth testing/#_3了解具体的关系。

视野(Field-of-View)

通常,对于近平面,我们可以用\(l,r,b,t\)描述(假设\(l=-r,b=-t\),即平面中心位于\(-z\)轴上,且平面与\(-z\)垂直),也可以用垂直视野\(fovY\)和近平面的宽高比来表示。

首先相机到近平面的距离为\(|n|\),则有如下关系

\[tan\frac{fovY}{2} = \frac{t}{|n|} \]

\[aspect = \frac{r}{t} \]