首页 Real-Time Rendering 第四章 变换(Transforms)
文章
取消

Real-Time Rendering 第四章 变换(Transforms)

Real-Time Rendering

第四章 变换

变换(Transform)是对点、矢量 、颜色等实体以某种方式进行转换的一种操作。利用变换,你可以对物体、灯光、摄像机进行位移、缩放等操作。

变换可分为线性变换和非线性变换。

线性变换是一种保留矢量加法和标量乘法的变换。即:

$f(x)+f(y)=f(x+y)$

$kf(x) = f(kx)$

例如,$f(x)=5x$是一种变换,它取一个矢量并将每个元素乘以5。同时他也是一个线性变换,因为他满足上面的两个等式。这个函数是一种缩放变换,因为它更改对象的比例(大小)。旋转变换是另一种围绕原点旋转矢量的线性变换。缩放和旋转变换,实际上是三元素矢量的所有线性变换,可以使用3×3矩阵表示。

然而,3×3矩阵具有局限性。现在我们需要平移变换,例如$f(x)=x+(7, 3, 2)$,很显然,平移变换是非线性变换。

为了组合线性变换和平移,我们需要使用仿射变换。仿射变换通常存储为4×4矩阵。仿射变换是先执行线性变换,然后执行平移的变换。为了表示四维矢量,我们使用齐次表示法,以相同的方式表示点和方向矢量。一个方向矢量可以表示为$v=(v_x,v_y,v_z,0)^T$,一个点可以表示为$v=(v_x,v_y,v_z,1)^T$。

所有平移、旋转、缩放、反射和裁剪矩阵都是仿射矩阵。仿射矩阵的主要特点是它保留了直线的平行性,但不一定保留长度和角度。仿射变换也可以是单个仿射变换的任何串联序列。

基础变换(Basic Transforms)

下面我们介绍最基本的变换,如平移、旋转、缩放、剪切、复合变换、刚体变换和法线变换。

  • 平移变换

从一个位置到另一个位置的变化由平移矩阵$T$表示。这个矩阵可以将一个实体位移$t=(t_x,t_y,t_z)$个单位。$T$矩阵形如:

\[T=\left[\matrix{1&0&0&t_x\\0&1&0&t_y\\0&0&1&t_z\\0&0&0&1}\right]\]

我们可以使用矩阵乘法来表示对一个点进行平移变换:

\[\left[\matrix{1&0&0&t_x\\0&1&0&t_y\\0&0&1&t_z\\0&0&0&1}\right]\left[\matrix{x\\y\\z\\1}\right]=\left[\matrix{x+t_x\\y+t_y\\z+t_z\\1}\right]\]

但如果我们对一个方向矢量进行平移变换:

\[\left[\matrix{1&0&0&t_x\\0&1&0&t_y\\0&0&1&t_z\\0&0&0&1}\right]\left[\matrix{x\\y\\z\\0}\right]=\left[\matrix{x\\y\\z\\0}\right]\]

可以发现,平移变换并不会影响方向矢量。这点很容易理解,因为矢量没有位置属性,所以平移不应该对四维矢量产生影响。

平移矩阵的逆矩阵就是反向平移得到的矩阵,即:

\[\left[\matrix{1&0&0&-t_x\\0&1&0&-t_y\\0&0&1&-t_z\\0&0&0&1}\right]\]
  • 旋转变换:

我们知道,旋转操作需要指定一个旋转轴,这个旋转轴不一定是空间中的坐标轴。

如果我们需要把点绕着$x$轴旋转$\theta$度,可以使用下面的矩阵:

\[R_x(\theta)=\left[\matrix{1&0&0&0\\0&cos\theta&-sin\theta&0\\0&sin\theta&cos\theta&0\\0&0&0&1}\right]\]

绕$y$轴的旋转:

\[R_y(\theta)=\left[\matrix{cos\theta&0&sin\theta&0\\0&1&0&0\\-sin\theta&0&cos\theta&0\\0&0&0&1}\right]\]

绕$z$轴的旋转:

\[R_z(\theta)=\left[\matrix{cos\theta&-sin\theta&0&0\\sin\theta&cos\theta&0&0\\0&0&1&0\\0&0&0&1}\right]\]

旋转矩阵的逆矩阵是旋转相反角度得到的变换矩阵。

  • 缩放变换

我们可以对一个点沿空间的$x$轴、$y$轴和$z$轴进行缩放。由缩放矩阵$S$表示。$S$矩阵形如:

\[S=\left[\matrix{k_x&0&0&0\\0&k_y&0&0\\0&0&k_z&0\\0&0&0&1}\right]\]

同样,我们可以使用矩阵乘法来表示一个缩放变换:

\[\left[\matrix{k_x&0&0&0\\0&k_y&0&0\\0&0&k_z&0\\0&0&0&1}\right]\left[\matrix{x\\y\\z\\1}\right]=\left[\matrix{k_xx\\k_yy\\k_zz\\1}\right]\]

对方向矢量也可以使用同样的矩阵进行缩放:

\[\left[\matrix{k_x&0&0&0\\0&k_y&0&0\\0&0&k_z&0\\0&0&0&1}\right]\left[\matrix{x\\y\\z\\0}\right]=\left[\matrix{k_xx\\k_yy\\k_zz\\0}\right]\]

缩放矩阵的逆矩阵是使用原缩放系数的倒数来对点或方向矢量进行缩放,即:

\[\left[\matrix{1/k_x&0&0&0\\0&1/k_y&0&0\\0&0&1/k_z&0\\0&0&0&1}\right]\]
  • 剪切变换

剪切变换可以用于扭曲模型,例如方形变为平行四边形,任意一边都可以被拉伸。

一共有六种基本剪切矩阵:$H_{xy}(s)$,$H_{xz}(s)$,$H_{yx}(s)$,$H_{yz}(s)$,$H_{zx}(s)$,$H_{zy}(s)$。

第一个下标表示哪个坐标将要被变换(剪切),第二个下标表示用哪个坐标来变换(剪切)。

例如:

\[H_{xz}(s)=\left[\matrix{1&0&s&0\\0&1&0&0\\0&0&1&0\\0&0&0&1}\right]\] \[p=(p_x,p_y,p_z)^T\] \[H_{xz}(s)p=\left[\matrix{1&0&s&0\\0&1&0&0\\0&0&1&0\\0&0&0&1}\right]\left[\matrix{p_x\\p_y\\p_z\\1}\right]=\left[\matrix{p_x+sp_z\\p_y\\p_z\\1}\right]\]

剪切矩阵的逆矩阵是沿相反方向的剪切,即:

$H_{ij}^{-1}(s)=H_{ij}(-s)$

  • 复合变换

为了提高效率,我们需要将矩阵序列连接成单个矩阵。例如,假设我们有一个具有数百万个顶点的游戏场景,场景中的所有对象都必须缩放、旋转并最终平移。

现在,不是将所有顶点与三个矩阵中的每一个相乘,而是将三个矩阵连接成一个矩阵。然后将此单个矩阵应用于顶点。

我们可以把平移、旋转和缩放组合起来,来形成一个复杂的变换过程。由于矩阵乘法不满足交换律,矩阵相乘的顺序不同会导致变换的结果不同。

例如,我们现在有两个变换矩阵,一个缩放矩阵$S$和一个旋转矩阵$R$,$S(2,0.5,1)$表示将$x$坐标变为$2$倍,将$y$坐标变为$1/2$,$R_z(\pi/6)$表示沿$z$轴旋转$\pi/6$,这两个矩阵相乘有两种顺序,$RS$和$SR$,他们实现的变换是不同的:

在绝大多数情况下,我们约定变换的顺序就是先缩放,再旋转,最后平移。即复合变换矩阵为:

$C=TRS$

除了需要注意不同类型的变换顺序外,我们有时还需要小心旋转的变换顺序。

如果我们需要同时绕着3个轴进行旋转,是先绕$x$轴、再绕$y$轴最后绕$z$轴旋转还是按其他的旋转顺序呢?

首先,我们有两种旋转方法:内旋和外旋。

  • 每次旋转是绕固定轴(一个固定参考系,比如世界坐标系)旋转,称为外旋。
  • 每次旋转是绕自身旋转之后的轴旋转,称为内旋。

按照内旋方式,$Z$-$Y$-$X$旋转顺序(指先绕自身轴$Z$,再绕自身轴$Y$,最后绕自身轴$X$),可得旋转矩阵(内旋是右乘):

$R_1=R_z(\gamma)R_y(\beta)R_x(\alpha)$

按照外旋方式,$X$-$Y$-$Z$旋转顺序(指先绕固定轴$X$,再绕固定轴$Y$,最后绕固定轴$Z$),可得旋转矩阵(外旋是左乘):

$R_2=R_z(\gamma)R_y(\beta)R_x(\alpha)$

两种旋转方法按上面两种不同的顺序旋转结果是相同的。

  • 刚体变换

当一个人从桌子上拿起一个实心物体(比如一支笔)并将其移动到另一个位置,例如衬衫口袋,只有物体的方向和位置会改变,而物体的形状通常不会受到影响。这种变换仅由平移和旋转的复合组成,称为刚体变换。

刚体变换可以表示为:

\[X=T(t)R=\left[\matrix{r_{00}&r_{01}&r_{02}&t_x\\r_{10}&r_{11}&r_{12}&t_y\\r_{20}&r_{21}&r_{22}&t_z\\0&0&0&1}\right]\]

用途:摄像机LookAt

  • 法线变换

一般来说,点和绝大部分方向矢量都可以使用同一个4×4或3×3的变换矩阵把其从坐标空间A变换到坐标空间B中。但在变换法线的时候,如果使用同一个变换矩阵,可能就无法确保维持法线的垂直性。如图:

我们先来了解一下另一种方向矢量:切线(tangent),也被称为切矢量(tangent vector)。与法线类似,切线往往也是模型顶点携带的一种信息。它通常与纹理空间对齐,而且与法线方向垂直.

由于切线是由两个顶点之间的差值计算得到的,因此我们可以直接使用用于变换顶点的变换矩阵来变换切线。假设,我们使用变换矩阵$M_{A \rightarrow B}$来变换顶点,可以由下面的式子直接得到变换后的切线:

$T_B=M_{A \rightarrow B}T_A$

我们可以由数学约束条件来推出变换法线的矩阵。我们知道同一个顶点的切线$T_{A}$和法线$N_{A}$必须满足垂直条件,即$T_{A} \cdot N_{A}$。

给定变换矩阵$M_{A \rightarrow B}$,我们已经知道$T_B=M_{A \rightarrow B}T_A$。我们现在想要找到一个矩阵$G$来变换法线$N_{A}$,使得变换后的法线仍然与切线垂直,即:

$T_{B} \cdot N_{B}=(M_{A \rightarrow B} \cdot T_{A}) \cdot (GN_{A})=0$

$(M_{A \rightarrow B} \cdot T_{A}) \cdot (GN_{A})=(M_{A \rightarrow B} \cdot T_{A})^T \cdot (GN_{A})=T_{A}^T(M_{A \rightarrow B}^TG)N_{A}=0$

又$T_{A} \cdot N_{A}=0$

因此如果$M_{A \rightarrow B}^TG=I$,那么上式成立。

即$G=(M_{A \rightarrow B}^T)^{-1}$。

另外,如果变换矩阵$M_{A \rightarrow B}$为正交矩阵,即变换只包括旋转变换,那么$M_{A \rightarrow B}^T=M_{A \rightarrow B}^{-1}$,即$(M_{A \rightarrow B}^T)^{-1}=M_{A \rightarrow B}$。

特殊的矩阵变换和运算(Special Matrix Transforms and Operations)

接下来将介绍对实时渲染至关重要的几种矩阵变换和运算。

  • 欧拉变换

这种变换是一种简单的方法,可以构造一个矩阵将实体定向到某个方向。它的名字来自伟大的瑞士数学家莱昂哈德·欧拉(Leonhard Euler)(1707-1783)。

首先,我们要确定模型自身的坐标轴。

最常见的是朝向沿负$z$轴,头顶沿$y$轴,如图所示。

欧拉变换是三个矩阵的乘积,即图中所示的旋转,表示为:

$E(h,p,r)=R_z(r)R_x(p)R_y(h)$

矩阵相乘可以用24种不同的顺序,其中上面这种是最常用的。

其中,$r$代表roll角,沿$z$轴旋转;$p$代表pitch角,沿$x$轴旋转;$h$代表head角,也称为yaw角,沿$y$轴旋转。

注意欧拉变换不仅可以用于设置摄像机的朝向,也可以设置任何物体或实体的朝向。这些变换可以在全局的世界坐标系下完成,也可以在相对于某个物体的局部坐标系完成。

尽管对于小的角度或者观察者朝向改变,欧拉角很有用,但它也有一些严重的限制。同时结合使用两个欧拉角集合是困难的。例如,在两个欧拉角集合间进行插值不是简单地对两组集合的三个角度分别进行插值。因为两个不同的欧拉角集合可能给出相同的朝向,此时任何插值都不会旋转物体。这些是考虑用四元数(Quaternion)等来代替欧拉角的原因。

  • 从欧拉变换中提取参数

有时从欧拉变换矩阵,提取出欧拉角$h$,$p$,$r$是有用的,过程如下:

\[E(h,p,r)=\left[\matrix{e_{00}&e_{01}&e_{02}\\e_{10}&e_{11}&e_{12}\\e_{20}&e_{21}&e_{22}}\right]=R_z(r)R_x(p)R_y(h)\]

连接三个旋转矩阵后得到:

\[E=\left[\matrix{\cos r \cos h - \sin r \sin p \sin h & - \sin r \cos p & \cos r \sin h + \sin r \sin p \cos h\\ \sin r \cos h + \cos r \sin p & \sin h \cos r & \cos p \sin r \sin h - \cos r \sin p \cos h\\ \cos p \sin h& \sin p & \cos p \cos h}\right]\]

显然,$\sin p = e_{21}$,且:

$\frac{e_{01}}{e_{11}}=\frac{-\sin r}{\cos r}=-\tan r$

$\frac{e_{20}}{e_{22}}=\frac{-\sin h}{\cos h}=-\tan h$

因此可解出三个欧拉角分别为:

$h=atan2(-e_{20},e_{22})$

$p=arcsin(-e_{21})$

$r=atan2(-e_{01},e_{11})$

有一个特殊的情形需要处理。如果$\cos p=0$,则会出现万向节锁(gimbal lock),旋转角度$r,h$将会绕相同的轴旋转(可能方向相反,这取决于$p$的值为$-\pi/2$还是$\pi/2$),这样只有一个角度($r,h$中的一个)需要推导。如果我们任意地设$h=0$,则

\[E=\left[\matrix{\cos r & - \sin r \cos p & \sin r \sin p \\ \sin r & \cos r \cos p & - \cos r \sin p \\ 0& \sin p & \cos p}\right]\]

因为$p$不影响第一列,所以当$\cos p=0$时,我们可以使用$\sin r / \cos r = \tan r = e_{10}/e_{00}$,可以得到:$r=atan2(e_{10},e_{00})$

注意$\arcsin$的定义域为$[-\pi / 2, \pi / 2]$,这意味着如果$E$创建时包含了一个在此定义域范围外的$p$,则不能提取出原始参数。

即:不同的$h,p,r$组合可能产生相同的欧拉变换。

当使用欧拉变换时,万向锁(gimbal lock)可能会出现。当某个旋转使得三个自由度(对应$r,p,h$或者说对应$x,y,z$轴)中的一个消失时,万向锁会出现。例如,变换顺序为$xyz$,假设第一个旋转变换为绕$y$轴旋转$\pi/2$,然后再进行第二个旋转变换。但第一个旋转变换已经导致$z$轴和原始的$x$轴对齐,这样最后绕$z$轴的旋转就是冗余的了。

四元数(Quaternion)

四元数最早于1843年由Sir William Rowan Hamilton发明,作为复数(complex numbers)的扩展。直到1985年才由Shoemake把四元数引入到计算机图形学中。四元数在一些方面优于Euler angles(欧拉角)和matrices。任意一个三维空间中的定向都可以被表示为一个绕某个特定轴的旋转。给定旋转轴及旋转角度,很容易把其它形式的旋转表示转化为四元数或者从四元数转化为其它形式。四元数可以用于稳定的旋转插值,而这些在欧拉角中是很难实现的。

一个复数具有实部和虚部,每一部分由两个实数表示,其中第一个实数要乘以$\sqrt{-1}$。相似地,四元数由四部分组成,一个实部,三个虚部。三个虚部与旋转轴密切相关,而旋转角度影响四个部分。这里用向量来表示四元数,但是为了和普通矢量进行区分,我们在其上加了个“帽子”: $\hat{\mathbf{q}}$。

接下来先介绍四元数的基本数学运算,再描述四元数用于旋转等。

  • 数学背景

    • 四元数定义:

一个四元数$\hat{\mathbf{q}}$可以被定义以下形式:

$\hat{\mathbf{q}}=(\hat{\mathbf{q_v}},\hat{\mathbf{q_w}})=iq_x+jq_y+kq_z+q_w=\hat{\mathbf{q_v}}+\hat{\mathbf{q_w}}$

其中,

$\hat{\mathbf{q_v}}=iq_x+jq_y+kq_z=(q_x,q_y,q_z)$

$i^2=j^2=k^2=-1$,$jk=-kj=i$,$ki=-ik=j$,$ij=-ji=k$

$q_w$为四元数的实部,$\mathbf{q}_v$是虚部,$i,j,k$为虚数单位。

对于虚部$\mathbf{q}_v$,我们可以施加所有普通矢量运算操作,例如加法、缩放、点乘、叉积等。

根据四元数的定义,可以推导出两个四元数$\hat{\mathbf{q}}$和$\hat{\mathbf{r}}$间的乘法运算。注意,虚数单位之间的乘法不满足交换律。

    • 乘法:

$\hat{\mathbf{q}}\hat{\mathbf{r}}=(iq_x+jq_y+kq_z+q_w)(ir_x+jr_y+kr_z+r_w)$

$=i(q_yr_z-q_zr_y+r_wq_x+q_wr_x)$

$+j(q_zr_x-q_xr_z+r_wq_y+q_wr_y)$

$+k(q_xr_y-q_yr_x+r_wq_z+q_wr_z)$

$+q_wr_w-q_xr_x-q_yr_y-q_zr_z$

$=(\hat{\mathbf{q_v}}\times\hat{\mathbf{r_v}}+r_w\hat{\mathbf{q_v}}+q_w\hat{\mathbf{r_v}},q_wr_w-\hat{\mathbf{q_v}}\cdot\hat{\mathbf{r_v}})$

    • 加法:

$\hat{\mathbf{q}}+\hat{\mathbf{r}}=(\hat{\mathbf{q_v}}+\hat{\mathbf{r_v}},q_w+r_w)$

    • 共轭:

$\hat{\mathbf{q}}^\ast=(-\hat{\mathbf{q_v}},q_w)$

    • 模:

$n(\hat{\mathbf{q}})=\sqrt{\hat{\mathbf{q}}\hat{\mathbf{q}}^\ast}=\sqrt{\hat{\mathbf{q}}\ast\hat{\mathbf{q}}}=\sqrt{\hat{\mathbf{q_v}}\cdot\hat{\mathbf{q_v}}+q_w^2}=\sqrt{q_x^2+q_y^2+q_z^2+q_w^2}$

    • $\hat{\mathbf{i}}=(\mathbf{0},1)$
    • 四元数的逆:

$\hat{\mathbf{q}}^{-1}\hat{\mathbf{q}}=\hat{\mathbf{q}}\hat{\mathbf{q}}^{-1}=1$

$n(\hat{\mathbf{q}})^2=\hat{\mathbf{q}}\hat{\mathbf{q}}^\ast\Leftrightarrow\frac{\hat{\mathbf{q}}\hat{\mathbf{q}}^\ast}{n(\hat{\mathbf{q}})^2}=1$

所以四元数的逆为:

$\hat{\mathbf{q}}^{-1}=\frac{1}{n(\hat{\mathbf{q}})^2}\hat{\mathbf{q}}^\ast$

    • 四元数的一些规则:

共轭规则:

$(\hat{\mathbf{q}}^\ast)^\ast=\hat{\mathbf{q}}$

$(\hat{\mathbf{q}}+\hat{\mathbf{r}})^\ast=\hat{\mathbf{q}}^\ast+\hat{\mathbf{r}}^\ast$

$(\hat{\mathbf{q}}\hat{\mathbf{r}})^\ast=\hat{\mathbf{r}}^\ast\hat{\mathbf{q}}^\ast$

  1. 一个四元数的共轭的共轭是该四元数本身。

  2. 两个四元数的和的共轭是它们共轭的和。

  3. 两个四元数乘积的共轭是它们共轭调换顺序后的乘积。

模规则:

$n(\hat{\mathbf{q}}\hat{\mathbf{r}})=n(\hat{\mathbf{q}})n(\hat{\mathbf{r}})$

  1. 一个四元数的模等于其共轭的模。
  2. 两个四元数乘积的模等于它们模的乘积。

乘法规则:

$\hat{\mathbf{p}}(s\hat{\mathbf{q}}+t\hat{\mathbf{r}})=s\hat{\mathbf{p}}\hat{\mathbf{q}}+t\hat{\mathbf{p}}\hat{\mathbf{r}}$

$(s\hat{\mathbf{p}}+t\hat{\mathbf{q}})\hat{\mathbf{r}}=s\hat{\mathbf{p}}\hat{\mathbf{r}}+t\hat{\mathbf{q}}\hat{\mathbf{r}}$

$\hat{\mathbf{p}}(\hat{\mathbf{q}}\hat{\mathbf{r}})=(\hat{\mathbf{p}}\hat{\mathbf{q}})\hat{\mathbf{r}}$

    • 单位四元数:

模为1的四元数为单位四元数。

可推导出单位四元数$\hat{\mathbf{q}}$可以写作:

\[\hat{\mathbf{q}}=(\sin\phi\mathbf{u}_q,\cos\phi)=\sin\phi\mathbf{u}_q+\cos\phi\]

其中\(\mathbf{u}_{q}\)是一个三维向量,且$\vert\vert\mathbf{u}_{q}\vert\vert=1$。因为当且仅当\(\vert\vert\mathbf{u}_{q}\vert\vert=1\)时,

\[n(\hat{\mathbf{q}})=n(\sin\phi\mathbf{u}_q,\cos\phi)=\sqrt{\sin^2\phi(\mathbf{u}_q\cdot\mathbf{u}_q)+\cos^2\phi}=\sqrt{\sin^2\phi+\cos^2\phi}=1\]

对于复数,一个二维单位向量可以写作$\cos\phi+i\sin\phi=e^{i\phi}$,对于四元数,等价的有:

$\hat{\mathbf{q}}=\sin\phi\mathbf{u}_q+\cos\phi=e^{\phi\mathbf{u}_q}$

    • 四元数对数和指数函数:

$\log(\hat{\mathbf{q}})=\log(e^{\phi\mathbf{u}_q})=\phi\mathbf{u}_q$

$\hat{\mathbf{q}}^t=(\sin\phi\mathbf{u}_q+\cos\phi)^t=e^{\phi t \mathbf{u}_q}=\sin(\phi t)\mathbf{u}_q+\cos(\phi t)$

  • 四元数变换
    • 用四元数表示旋转:

单位四元数可以以简单的方式表示任何三维旋转。

如图为单位四元数表示的旋转:

首先,把一个点或向量$\mathbf{p}=(p_x,p_y,p_z,p_w)^{T}$的四个坐标分别放进一个四元数$\hat{\mathbf{p}}$的各个分量中,假设我们有一个单位四元数$\hat{\mathbf{q}}=(\sin\phi\mathbf{u}_q,\cos\phi)$,可以证明:

$\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1}$

会使点$\hat{\mathbf{p}}$绕轴$\mathbf{u}_p$旋转$2\phi$弧度,如上图所示。注意因为$\hat{\mathbf{q}}$是单位四元数,则$\hat{\mathbf{q}}^{-1}=\hat{\mathbf{q}}^\ast$。

注意:任何$\hat{\mathbf{q}}$的非零实数倍都和$\hat{\mathbf{q}}$表示相同的旋转变换。也就是说,$\hat{\mathbf{q}}$和$-\hat{\mathbf{q}}$表示的也是相同的旋转,这是因为我们同时对旋转轴$\mathbf{u}_p$和实部$q_w$取负。这也意味着从一个矩阵中提取的四元数可能是$\hat{\mathbf{q}}$或$-\hat{\mathbf{q}}$。

    • 两个四元数的连接:

给定两个四元数$\hat{\mathbf{q}}$和$\hat{\mathbf{r}}$(表示两个旋转变换),要实现它们的连接,即先进行$\hat{\mathbf{q}}$的变换,再进行$\hat{\mathbf{r}}$的变换,四元数点$\hat{\mathbf{p}}$是被旋转的对象,则对应的方程为:

$\hat{\mathbf{r}}(\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^\ast)\hat{\mathbf{r}}^\ast=(\hat{\mathbf{r}}\hat{\mathbf{q}})\hat{\mathbf{p}}(\hat{\mathbf{r}}\hat{\mathbf{q}})^\ast=\hat{\mathbf{c}}\hat{\mathbf{p}}\hat{\mathbf{c}}^\ast$

这里的$\hat{\mathbf{c}}=\hat{\mathbf{r}}\hat{\mathbf{q}}$是单位四元数,表示两个四元数的连接,即两个旋转变换的连接。

    • 矩阵和四元数的相互转换

很多时候我们需要结合多个变换,而它们中大部分是矩阵形式。我们需要一种方法把方程$\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1}$转换为矩阵。

一个四元数$\hat{\mathbf{q}}$可以被转换为一个矩阵$\mathbf{M}^q$:

\[\mathbf{M}^q=\left[\matrix{ 1-s(q_y^2+q_z^2) & s(q_xq_y-q_wq_z) & s(q_xq_z+q_wq_y) & 0 \\ s(q_xq_y+q_wq_z) & 1-s(q_x^2+q_z^2) & s(q_yq_z-q_wq_x) & 0 \\ s(q_xq_z-q_wq_y) & s(q_yq_z+q_wq_x) & 1-s(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 }\right]\]

这里,$s=2/(n(\hat{\mathbf{q}}))^2$。对于单位四元数,上面方程可简化为:

\[\mathbf{M}^q=\left[\matrix{ 1-2(q_y^2+q_z^2) & 2(q_xq_y-q_wq_z) & 2(q_xq_z+q_wq_y) & 0 \\ 2(q_xq_y+q_wq_z) & 1-2(q_x^2+q_z^2) & 2(q_yq_z-q_wq_x) & 0 \\ 2(q_xq_z-q_wq_y) & 2(q_yq_z+q_wq_x) & 1-2(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 }\right]\]

一旦构建好四元数,就不需要计算三角函数了。因此这个变换过程是很高效的。

把矩阵转换为四元数稍微复杂,且不太常用,此处不做记录。

  • 球面线性插值

给定两个单位四元数$\hat{\mathbf{q}}$和$\hat{\mathbf{r}}$以及一个参数$t\in[0,1]$,插值得到一个新的四元数。

这个操作的代数形式被表达为一个复合四元数$\hat{\mathbf{s}}$:

$\hat{\mathbf{s}}(\hat{\mathbf{q}},\hat{\mathbf{r}},t)=(\hat{\mathbf{r}}\hat{\mathbf{q}}^{-1})^t\hat{\mathbf{q}}$

然而为了软件实现,下面的形式更合适:

$\hat{\mathbf{s}}(\hat{\mathbf{q}},\hat{\mathbf{r}},t)=slerp(\hat{\mathbf{q}},\hat{\mathbf{r}},t)=\frac{\sin(\phi(1-t))}{\sin\phi}\hat{\mathbf{q}}+\frac{\sin(\phi t)}{\sin\phi}\hat{\mathbf{r}}$

其中,$\cos\phi=q_xr_x+q_yr_y+q_zr_z+q_wr_w$

slerp函数插值得到的一系列四元数一起组成了一个四维单位球上从$\hat{\mathbf{q}}(t=0)$到$\hat{\mathbf{r}}(t=1)$的最短弧。这个最短弧位于一个圆上,而这个圆是由$\hat{\mathbf{q}}$,$\hat{\mathbf{r}}$以及原点组成的平面与这个四维单位球的交集构成。计算得到的旋转四元数绕固定的轴以恒定的速度旋转。

slerp函数非常适合在两个方向(orientatiosns)间进行插值,并且表现得很好(固定的轴,恒定的速度)。

当多于两个方向时,比如说由n个四元数\(\hat{\mathbf{q}}_0\)到\(\hat{\mathbf{q}}_{n-1}\),我们想要从\(\hat{\mathbf{q}}_0\)插值到\(\hat{\mathbf{q}}_1\),再插值到\(\hat{\mathbf{q}}_2\),直到\(\hat{\mathbf{q}}_{n-1}\),可以简单直接地使用slerp函数,但这样插值多点四元数会造成不连续的变化,如图所示:

可以看到$\hat{\mathbf{q}}_2$到$\hat{\mathbf{q}}_3$处有一个明显的拐角,得到了不够光滑的曲线。

一个更好的方法是使用样条曲线插值(spline)。在\(\hat{\mathbf{q}}_{i}\)和\(\hat{\mathbf{q}}_{i+1}\)之间引入\(\hat{\mathbf{a}}_i\)和\(\hat{\mathbf{a}}_{i+1}\)。球面三次插值可定义在\(\hat{\mathbf{q}}_i\),\(\hat{\mathbf{a}}_i\),\(\hat{\mathbf{a}}_{i+1}\),\(\hat{\mathbf{q}}_{i+1}\),上面。引入的这两个四元数可以这样计算:

\[\hat{\mathbf{a}}_i=\hat{\mathbf{q}}_i\exp\{-[\log(\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i-1})+\log({\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i+1}})]/4\}\] \[squad(\hat{\mathbf{q}}_i,\hat{\mathbf{q}}_{i+1},\hat{\mathbf{a}}_i,\hat{\mathbf{a}}_{i+1},t)=slerp(slerp(\hat{\mathbf{q}}_i,\hat{\mathbf{q}}_{i+1},t),slerp(\hat{\mathbf{a}}_i,\hat{\mathbf{a}}_{i+1},t),2t(1-t))\]

可以看到squad函数是由重复使用球形插值slerp得到的。

  • 从一个向量旋转到另一个向量

从一个向量$\mathbf{s}$旋转到另一个向量$\mathbf{t}$也是一种常见的操作。四元数极大简化了这一过程。

首先我们把$\mathbf{s}$和$\mathbf{t}$归一化,然后计算单位旋转轴$\mathbf{u}=(\mathbf{s}\times\mathbf{t})/\vert\vert(\mathbf{s}\times\mathbf{t})\vert\vert$,然后$e=\mathbf{s}\cdot\mathbf{t}=\cos(2\phi)$,并且$\vert\vert(\mathbf{s}\times\mathbf{t})\vert\vert=\sin(2\phi)$,此处$2\phi$是$\mathbf{s}$和$\mathbf{t}$之间的夹角。则表示$\mathbf{s}$旋转到$\mathbf{t}$的四元数为:

$\hat{\mathbf{q}}=(\sin\phi \mathbf{u},\cos\phi)$

$=(\frac{1}{\sqrt{2(1+e)}}(\mathbf{s}\times\mathbf{t}),\frac{\sqrt{2(1+e)}}{2})$

但是,当$\mathbf{s}$和$\mathbf{t}$指向相反的方向时,上面的公式会有除以0的情况出现,即$e=\cos(2\phi)=-1$。当出现这种情况时,任意垂直于$\mathbf{s}$的旋转轴都可以用于旋转到$\mathbf{t}$。

有时我们也需要从$\mathbf{s}$旋转到$\mathbf{t}$的矩阵表示:

\[\mathbf{R}(\mathbf{s},\mathbf{t})=\left[\matrix{ e+hv_x^2 & hv_xv_y-v_z & hv_xv_z+v_y & 0 \\ hv_xv_y+v_z & e+hv_y^2 & hv_yv_z-v_x & 0 \\ hv_xv_z-v_y & hv_yv_z+v_x & e+hv_z^2 & 0 \\ 0 & 0 & 0 & 1 }\right]\]

其中,

$\mathbf{v}=\mathbf{s}\times\mathbf{t}$,

$e=\cos(2\phi)=\mathbf{s}\cdot\mathbf{t}$,

$h=\frac{1-\cos(2\phi)}{\sin^2(2\phi)}=\frac{1-e}{\mathbf{v}\cdot\mathbf{v}}=\frac{1}{1+e}$(当$\phi\neq0$且$\phi\neq\pi$时)。

当$\mathbf{s}$和$\mathbf{t}$平行或几乎平行时,因为$\vert\vert\mathbf{s}\times\mathbf{t}\vert\vert\approx 0$,所以无法计算$h$。如果$\phi\approx 0$,我们可以直接返回单位矩阵;而如果$\phi\approx\pi$,则绕任意轴旋转$\pi$弧度,这个轴可以通过求$\mathbf{s}$和任意不平行于$\mathbf{s}$的向量的叉积得到。

投影(Projections)

在实际渲染场景之前,场景中的所有相关对象必须投影到某种平面或某种简单体积上。然后,执行裁剪和渲染。

我们前面的变换都没有影响第四个坐标,即w分量。也就是说,点和向量在变换后保留了它们的类型。而且,前面变换矩阵的最后一行都是(0 0 0 1)。

下面,我们假设观看者沿着摄像机的负$z$轴观看,$y$轴向上,$x$轴向右,即右手坐标系,这和Unity采用的方案。而DirectX使用左手坐标系,观察者沿相机的正$z$轴观看。这两种方法都是可行的,最终都能达到同样的效果。

  • 正交投影(Orthographic Projection)

正交投影的一个特点是投影后平行线保持平行。当使用正交投影查看场景时,无论与摄影机的距离如何,对象都保持相同的大小。矩阵$\mathbf{P}_o$(如下所示)是一个简单的正交投影矩阵,它保持点的$x$和$y$分量不变,同时将$z$分量设置为零,即正交投影到平面$z=0$上:

\[\mathbf{P}_o=\left[\matrix{ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 }\right]\]

该投影矩阵的效果如下图所示。很显然,$\mathbf{P}_o$是不可逆的,因为它的行列式$\vert\mathbf{P}_o\vert=0$。换句话说,这个变换把点从三维降到了二维,而且是不可逆的。使用这种正交投影的一个问题是,它将$z$值为正值的点和$z$值为负值的点都投影到了投影平面上。但是我们通常需要将$z$值(包括$x$值和$y$值)限制在一定的范围内,例如从$n$(近平面)到$f$(远平面)。

更为常用的正交投影矩阵由六元组$(l,r,b,t,n,f)$表示,表示左、右、下、上、近和远平面。该矩阵缩放并将这些平面形成的轴对齐边界框(AABB)转换为以原点为中心的轴对齐立方体。AABB的最小角为$(l,b,n)$,最大角为$(r,t,f)$。而且,我们需要$n>f$,因为我们在这个空间中是沿着负$z$轴观看的。

在OpenGL中,轴对齐立方体的最小角为$(−1,−1,−1)$ 最大转角为$(1,1,1)$;在DirectX中,最小角为$(−1,−1,0)$ 最大转角为$(1,1,1)$。这个空间被称为规则观察体(canonical view volume),这个空间里的坐标被称为归一化的设备坐标(normalized device coordinates,简称为NDC)。转换为规则观察体是为了更方便地进行裁剪。

转换到规则观察体后,再根据该立方体剪裁要渲染的几何体的顶点,最后这些顶点再通过单位正方形映射到屏幕来进行渲染。此正交变换矩阵如下所示:

\[\mathbf{P}_o=\mathbf{S(\mathbf{s})\mathbf{T(\mathbf{t})}}= \left[\matrix{ \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{f-n} & 0 \\ 0 & 0 & 0 & 1 }\right] \left[\matrix{ 1 & 0 & 0 & -\frac{l+r}{2} \\ 0 & 1 & 0 & -\frac{t+b}{2} \\ 0 & 0 & 1 & -\frac{f+n}{2} \\ 0 & 0 & 0 & 1 }\right]\] \[=\left[\matrix{ \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}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 }\right]\]

在计算机图形学中,投影后的空间通常为左手坐标系。因为我们定义AABB的远值小于近值,所以正交投影变换将会导致镜像变换。假设定义的AABB与规则观察体大小相同,即$(l,b,n)$对应$(-1,-1,1)$,$(r,t,f)$对应$(1,1,-1)$,那么正交投影矩阵将变成:

\[\mathbf{P}_o=\left[\matrix{ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 }\right]\]

这是一个镜像矩阵,这会将右手观察坐标变换为左手标准化设备坐标。

DirectX将$z$深度映射到范围$[0,1]$,OpenGL为$[−1,1]$。 这可以通过在正交矩阵之后应用简单的缩放和平移矩阵来实现,即:

\[\mathbf{M}_{st}=\left[\matrix{ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0.5 & 0.5 \\ 0 & 0 & 0 & 1 }\right]\]

所以,在DirectX中的正交投影矩阵为:

\[\mathbf{P}_{o[0,1]}=\left[\matrix{ \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{1}{f-n} & -\frac{n}{f-n} \\ 0 & 0 & 0 & 1 }\right]\]
  • 透视投影(Perspective Projection)

透视投影比正交投影更复杂,它通常用于大多数计算机图形学应用中。透视投影后的平行线通常不平行;相反,它们可能在其极限处收敛到一个点。透视投影更接近于我们的人眼观看世界的感觉,也就是说,距离越远的物体越小。

首先,我们给定投影平面$z=-d,d>0$,假设摄影机位于原点,并且我们希望将点$p$投影到平面$z=−d,d>0$,产生一个新的点$q=(qx,qy,−d)$ 。该场景如图所示。从该图中所示的相似三角形中,我们可以得到$q$的$x$分量:

$\frac{q_x}{p_x}=\frac{-d}{p_z}\Leftrightarrow q_x=-d\frac{p_x}{p_z}$。

同样,我们也可以得到$q$的$y$分量:$q_y=-d\frac{p_y}{p_z}$。

因此我们可以得到透视投影矩阵$\mathbf{P}_p$:

\[\mathbf{P}_p=\left[\matrix{ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 }\right]\]

下面来看看这个矩阵的效果:

\[\mathbf{q}=\mathbf{P}_p\mathbf{p}=\left[\matrix{ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 }\right] \left[\matrix{p_x\\p_y\\p_z\\1}\right]=\left[\matrix{p_x\\p_y\\p_z\\-p_z/d}\right]\Rightarrow\left[\matrix{-dp_x/p_z\\-dp_y/p_z\\-d\\1}\right]\]

其中,最后一步是齐次除法(也叫透视除法),就是把齐次坐标的$x,y,z$分量分别除以$w$分量,因为我们始终约定将点$(p_x,p_y,p_z)$投影到平面$w=1$上。可以看到,变换后的点的$x,y,z$分量与我们上面计算到的结果相同。

和正交变换一样,还有一种透视变换是将视锥体(view frustum)转换为前面描述的规则观察体(canonical view volume)。我们假设视锥体从$z=n$开始,在$z=f$结束,$0>n>f$。$z=n$处的矩形在$(l,b,n)$处具有最小角点,在$(r,t,n)$处具有最大角点,如下图所示:

参数$(l,r,b,t,n,f)$确定摄像机的视锥体,$l,r$确定水平视野(horizontal field of view),$t,b$确定垂直视野(vertical field of view)。视野越大,摄像机观察到的越多。

视野(field of view,简称FOV)是提供场景感的一个重要因素。我们人眼本身就有一个视野。请看下面的式子:

$\phi=2\arctan(w/(2d))$

其中,$\phi$表示FOV,$w$表示垂直于摄像机视线的物体宽度,$d$表示物体到摄像机的距离。简单来说,$\phi$就是摄像机能够看到的左右两侧的角度。

设置更小的FOV会减少透视效果。设置更大的FOV会使物体看起来扭曲(例如广角相机镜头),特别是靠近屏幕边缘,并会夸大附近物体的比例。然而,更广阔的视野给观众一种物体更大、更令人印象深刻的感觉,并具有给用户更多关于周围环境的信息的优势。

将视锥体转换为单位立方体的透视变换矩阵如下所示:

\[\mathbf{P}_p=\left[\matrix{ \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 }\right]\]

将这个变换应用到一个点后,我们将得到另一个点$\mathbf{q}=(q_x,q_y,q_z,q_w)^T$。其中$w$分量通常不为0或1。为了得到正确的投影点,我们还要进行齐次除法:$\mathbf{q}=(q_x/q_w,q_y/q_w,q_z/q_w,1)^T$

矩阵$\mathbf{P}_p$会将$z=f$的点映射到$+1$,$z=n$的点映射到$-1$。

远平面之外的物体将被裁剪,因此不会出现在场景中。但是透视投影也可以处理无限远的平面,我们只要将$f\rightarrow\infty$,此时矩阵变为:

\[\mathbf{P}_p=\left[\matrix{ \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 }\right]\]

总结:在应用透视变换矩阵$\mathbf{P}_p$后,再通过裁剪和齐次除法,我们就能得到归一化的设备坐标(NDC),

在上面的矩阵中,$n$和$f$值都为负数,但为了方便用户使用,我们通常还要将它们转换为正数,即$0<n’<f’$。

要得到OpenGL中的透视变换矩阵,我们还要进行一次镜像变换,即乘上$\mathbf{S}(1,1,-1,1)$,但实际上摄像机还是沿着负$z$轴观察,最后在OpenGL中的透视变换矩阵为:

\[\mathbf{P}_{OpenGL}=\left[\matrix{ \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{2f'n'}{f'-n'} \\ 0 & 0 & -1 & 0 }\right]\]

但通常我们用一种更为简单的方式表达,即用户只需要提供垂直视野$\phi$(vertical field of view),宽高比$a=w/h$(aspect)($w\times h$为屏幕分辨率),以及$n’$和$f’$。最终的透视变换矩阵为:

\[\mathbf{P}_{OpenGL}=\left[\matrix{ c/a & 0 & 0 & 0 \\ 0 & c & 0 & 0 \\ 0 & 0 & -\frac{f'+n'}{f'-n'} & -\frac{2f'n'}{f'-n'} \\ 0 & 0 & -1 & 0 }\right]\]

其中,$c=1.0/\tan(\phi/2)$。这个矩阵就是旧版gluPerspective()中的矩阵,来源于OpenGL Utility Library(GLU)。

一些API(例如DirectX)把近平面映射到$z=0$(而不是$z=-1$),远平面映射到$z=1$。另外,DirectX使用左手坐标系作为观察坐标系,也就是说DirectX的摄像机从正$z$轴观察,并且$n$和$f$值都为正值,下面是DirectX中的透视变换矩阵:

\[\mathbf{P}_{p[0,1]}=\left[\matrix{ \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'}{f'-n'} & -\frac{f'n'}{f'-n'} \\ 0 & 0 & 1 & 0 }\right]\]

另外,需要注意的是,使用透视变换矩阵后,计算后的深度值(depth value)不会随输入的$p_z$值成线性变化。如图所示:

我们很容易就可以推导出原因:

$z_{NDC}=\frac{d_{p_z}+e}{-p_z}=d-\frac{e}{p_z}$

其中,

$d=−(f′+n′)/(f′−n′)$,

$e=−2f′n′/(f′−n′)$。

我们可以发现,输出的深度值$z_{NDC}$和输入的深度值$p_z$是成反比的。

因此,近平面和远平面的位置会影响z-buffer​的精度,容易产生z-flighting。

有几种方法可以增加深度值的精度:

  • reversed z,即存储$1.0-z_NDC$。
  • Kemen建议使用对数对每个顶点得深度值重新映射:

$z=w(log_2(max(10^{-6},1+w))f_c-1)$,(OpenGL)

$z=w(log_2(max(10^{-6},1+w))f_c/2)$,(DirectX)

其中$w$是投影矩阵之后的顶点的w值,常数$f_c=2/log_2(f+1)$,其中$f$是远平面。

本文由作者按照 CC BY 4.0 进行授权

Real-Time Rendering 第二章 图形渲染流水线(The Graphics Rendering Pipeline)

Real-Time Rendering 第五章 着色基础(Shading Basics)

热门标签