时间:2026-03-09 00:14
人气:
作者:admin
一个变换(Transform)是一个操作,它会用某种方式转变点、向量、颜色这种实体。对于计算机图形学的实践者,掌握变换是极其重要的。
这个变换是一个直观的方法,被用来改变物体的朝向,它的名字来自瑞士数学家欧拉。首先,我们需要确定一个默认的观察方向。就如同下图那样

欧拉变换是三个旋转矩阵的乘积,一般来说,我们记其为\(\mathbf{E}\),它的公式如下
\[\mathbf{E}(h,p,r)=\mathbf{R}_z(r)\mathbf{R}_x(p)\mathbf{R}_y(h) \]当然了,\(\mathbf{E}\)的表达有许多种,我们展示这一个,因为它是通常被使用的那一个。式中的\(h\)、\(p\)、\(r\)被统称为欧拉角,每个量描述了绕对应坐标轴的旋转角度。我们可以把它理解为先绕\(y\)轴旋转,再绕\(x\)轴旋转,最后绕\(z\)轴旋转。这么做自然就能让物体有一个朝向,你可以拿上方这个图像的例子想象一下。
虽然欧拉变换对小角度的改变或是观察者的观察朝向很有用,但是它也有一些严格的限制。首先,组合两个欧拉角会有难度。例如,在某些情况下进行简单的插值是不合理的,因为两个欧拉角可能指代相同的朝向。此外就是万向节锁定(Gimbal Lock),以之前的公式作为出发点,考虑当\(p=90^\circ\)时,你会发现\(\mathbf{R}_z(r)\)和\(\mathbf{R}_y(h)\)对于被旋转物体来说,实际上有相同的旋转效应,这样就失去了一个自由度。就拿上图来说,当\(p=90^\circ\)时,\(\mathbf{R}_z(r)\)和\(\mathbf{R}_y(h)\)表示的都是雕像的头向左偏或向右偏的旋转。用之前所说的先绕\(y\)轴旋转,再绕\(x\)轴旋转,最后绕\(z\)轴旋转想象一下可以印证这一点。这些以及其它的原因导致了别的例如四元数的替代方法更被推崇使用,而下个部分我们将会介绍四元数。
在某些情况下,需要从变换\(\mathbf{E}(h,p,r)\)提取欧拉角,在这里我们直接给出如下的公式
\[\begin{align*} \mathbf{E}(h,p,r) &= \begin{pmatrix} e_{00} & e_{01} & e_{02} \\ e_{10} & e_{11} & e_{12} \\ e_{20} & e_{21} & e_{22} \end{pmatrix}\\ &= \begin{pmatrix} \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) \end{pmatrix} \end{align*} \]由上述公式可以得到
\[\begin{align*} h &= \mathrm{atan2}(-e_{20},e_{22}) \\ p &= \arcsin(e_{21}) \\ r &= \mathrm{atan2}(-e_{01},e_{11}) \end{align*} \]此外我们还可以由上述公式知晓为什么会发生万向节锁定。当\(p=\pi/2+2k\pi\)(\(k\)为整数)时,我们可以得到
\[\begin{align*} \mathbf{E}(h,p,r) &= \begin{pmatrix} \cos(r)\cos(h)-\sin(r)\sin(h) & 0 & \cos(r)\sin(h)+\sin(r)\cos(h) \\ \sin(r)\cos(h)+\cos(r)\sin(h) & 0 & \sin(r)\sin(h)-\cos(r)\cos(h) \\ 0 & 1 & 0 \end{pmatrix} \\ &= \begin{pmatrix} \cos(r+h) & 0 & \sin(r+h) \\ \sin(r+h) & 0 & -\cos(r+h) \\ 0 & 1 & 0 \end{pmatrix} \end{align*} \]这个时候的变换只与\(p\)和\(r+h\)有关。当\(p=-\pi/2+2k\pi\)(\(k\)为整数)时同理可证得变换只与\(p\)和\(r-h\)有关。
尽管四元数作为复数的扩展由Sir William Rowan Hamilton在1843年就被开发出来了,直到1985年它才被Shoemake引入到计算机图形学这一领域中。四元数是被用来表示旋转和定向的,它在很多方面比欧拉角和矩阵有优势。任意的三维旋转可以用绕一个特定轴的旋转来表示。在这种轴加上角度的表示和四元数之间进行转换其实很直接,而欧拉角在任意方向上的转换却很难做到。四元数可以被用于稳定和连续的朝向的插值,这是欧拉角做不到的事。
一个复数有一个实部和一个虚部,它们都是使用两个实数表示的,不过第二个实数要乘以\(\sqrt{-1}\)。相似的,四元数有四个部分,前三个值和旋转轴密切相关,最后一个值和旋转的角度有关且影响着所有四个部分。因为四元数有四个部分,我们选择使用向量来表示它们,但是为了进行区分,我们在符号上放个“帽子”,就比如\(\hat{\mathbf{q}}\)。接下来,我们首先介绍下有关四元数的数学背景,然后会利用四元数构造一些有用的变换。
\(\hat{{\mathbf{q}}}\)的定义如下
\[\begin{align*} \hat{\mathbf{q}} = (\mathbf{q}_v,q_w) = iq_x + jq_y+kq_z+q_w = \mathbf{q}_v+q_w\\ \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 \end{align*} \]其中,\(q_w\)为四元数的实部,\(\mathbf{q}_v\)为四元数的虚部,\(i\)、\(j\)、\(k\)被称为虚单元。
对于虚部\(\mathbf{q}_v\),我们可以使用所有的一般类型的向量操作,比如相加、缩放、点乘、叉乘等等。利用四元数的定义,我们能得到两个四元数\(\hat{\mathbf{q}}\)和\(\hat{\mathbf{r}}\)的乘积为
这里要注意虚部的相乘不满足交换律。通过四元数的定义,我们还能得到
加法:$$ \hat{\mathbf{q}}+\hat{\mathbf{r}} = (\mathbf{q}_v,q_w)+(\mathbf{r}_v,r_w) = (\mathbf{q}_v+\mathbf{r}_v,q_w+r_w) $$
共轭:$$\hat{\mathbf{q}}^* = (\mathbf{q}_v,q_w)^*=(-\mathbf{q}_v,q_w)$$
范数:
单位元:$$\hat{\mathbf{i}}=(\mathbf{0},1)$$
求逆:$$n(\hat{\mathbf{q}})^2 = \hat{\mathbf{q}}\hat{\mathbf{q}}^* \Rightarrow \hat{\mathbf{q}}^{-1} = \frac{1}{n(\hat{\mathbf{q}})^2} \hat{\mathbf{q}}^* $$
以下是通过四元数的定义得到的一些规则
共轭规则:
范数规则:
乘法法则:
线性:
\[\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}} \]一个单位四元数\(\hat{\mathbf{q}} = (\mathbf{q}_v,q_w)\)可以被写作
\[\hat{\mathbf{q}} = (\sin\phi\mathbf{u}_q,\cos\phi) = \sin\phi\mathbf{u}_q + \cos\phi \]其中三维向量\(\mathbf{u}_q\)的模为\(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} = 1 \Rightarrow ||\mathbf{u}_q|| = 1 \]正如下个部分我们会了解的那样,单位四元数极其适合创造旋转和朝向。但是在那之前,我们还需要一些用于单位四元数的额外操作。
对于单位复数\(\cos\phi+i\sin\phi\)来说我们可以使用\(e^{i\phi}\)来表示它。相似的,对于四元数来说有
对于单位四元数的对数函数和指数函数分别有
\[\mathrm{ln}(\hat{\mathbf{q}}) = \mathrm{ln}(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) \]此外对于实部为\(0\)的纯虚四元数的乘法有
\[\hat{\mathbf{q}}\hat{\mathbf{r}} = (\mathbf{q}_v \times \mathbf{r}_v,-\mathbf{q}_v \cdot \mathbf{r}_v) = \mathbf{q}_v \times \mathbf{r}_v - \mathbf{q}_v \cdot \mathbf{r}_v \]假设有\(\hat{\mathbf{p}}=(p_x,p_y,p_z,0)\)和\(\hat{\mathbf{q}}=(\sin\phi\mathbf{u}_q,\cos\phi)\),那么\(\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1}\)表示\(\hat{\mathbf{p}}\)(点\(\mathbf{p}\))绕轴\(\mathbf{u}_q\)旋转\(2\phi\)的变换,我们接下来证明这一点。因为\(\hat{\mathbf{q}}\)为单位四元数,所以
\[\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1} = \hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^* \]接下来我们把\(\mathbf{p}\)分解成垂直于\(\mathbf{u}_q\)和平行于\(\mathbf{u}_q\)的两部分,因此公式变成了
\[\hat{\mathbf{q}}(\hat{\mathbf{p}}_\parallel+\hat{\mathbf{p}}_\perp) \hat{\mathbf{q}}^* \]接下来我们分别计算两个部分被变换的情况,首先是平行部分的情况
\[\begin{align*} \hat{\mathbf{q}} \hat{\mathbf{p}}_\parallel \hat{\mathbf{q}}^* &= (\cos\phi+\mathbf{u}_q\sin\phi) \hat{\mathbf{p}}_\parallel (\cos\phi-\mathbf{u}_q\sin\phi)\\ &= \hat{\mathbf{p}}_\parallel \cos^2\phi - \hat{\mathbf{p}}_\parallel \mathbf{u}_q \sin\phi \cos\phi + \mathbf{u}_q \hat{\mathbf{p}}_\parallel \sin\phi \cos\phi - \mathbf{u}_q \hat{\mathbf{p}}_\parallel \mathbf{u}_q \sin^2\phi\\ &= \hat{\mathbf{p}}_\parallel \cos^2\phi + (\mathbf{u}_q \hat{\mathbf{p}}_\parallel - \hat{\mathbf{p}}_\parallel \mathbf{u}_q) \sin\phi \cos\phi - \mathbf{u}_q \hat{\mathbf{p}}_\parallel \mathbf{u}_q \sin^2\phi\\ &= \hat{\mathbf{p}}_\parallel \cos^2\phi + (-\mathbf{u}_q \cdot \mathbf{p}_{\parallel v}+\mathbf{u}_q \cdot \mathbf{p}_{\parallel v}) \sin\phi \cos\phi - \mathbf{u}_q \hat{\mathbf{p}}_\parallel \mathbf{u}_q \sin^2\phi\\ &= \hat{\mathbf{p}}_\parallel \cos^2\phi - \mathbf{u}_q \hat{\mathbf{p}}_\parallel \mathbf{u}_q \sin^2\phi\\ &= \hat{\mathbf{p}}_\parallel \cos^2\phi + \hat{\mathbf{p}}_\parallel \sin^2\phi\\ &= \hat{\mathbf{p}}_\parallel(\cos^2\phi+\sin^2\phi)\\ &= \hat{\mathbf{p}}_\parallel \end{align*} \]接着是垂直部分的情况
\[\begin{align*} \hat{\mathbf{q}} \hat{\mathbf{p}}_\perp \hat{\mathbf{q}}^* &= (\cos\phi+\mathbf{u}_q\sin\phi) \hat{\mathbf{p}}_\perp (\cos\phi-\mathbf{u}_q\sin\phi)\\ &=(\hat{\mathbf{p}}_\perp \cos\phi+\mathbf{u}_q \hat{\mathbf{p}}_\perp \sin\phi)(\cos\phi-\mathbf{u}_q\sin\phi) \\ &= (\hat{\mathbf{p}}_\perp \cos\phi+(\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v}) \sin\phi)(\cos\phi-\mathbf{u}_q\sin\phi) \\ &= \hat{\mathbf{p}}_\perp \cos^2\phi - ((\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v}) \times \mathbf{u}_q) \sin^2\phi + (\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v})(2\sin\phi\cos\phi)\\ &= \hat{\mathbf{p}}_\perp \cos^2\phi- \hat{\mathbf{p}}_\perp \sin^2\phi + (\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v})(2\sin\phi\cos\phi)\\ &= \hat{\mathbf{p}}_\perp \cos(2\phi) + (\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v})\sin(2\phi) \end{align*} \]观察垂直部分的计算结果能发现\(\hat{\mathbf{p}}_{\perp v}\)和\(\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v}\)实际上位于过零点且垂直于轴\(\mathbf{u}_q\)的平面上,而且两者相互垂直,因此实际上表示\(\hat{\mathbf{p}}_{\perp v}\)绕轴\(\mathbf{u}_q\)的旋转。结合这两个部分从而得到
\[\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1} = \hat{\mathbf{p}}_\parallel + \hat{\mathbf{p}}_\perp \cos(2\phi) + (\mathbf{u}_q \times \hat{\mathbf{p}}_{\perp v})\sin(2\phi) \]\(\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1}\)因此表示点\(\mathbf{p}\)绕轴\(\mathbf{u}_q\)旋转\(2\phi\)的变换。
有时我们需要结合多个四元数变换,假如给定两个单位四元数\(\hat{\mathbf{q}}\)和\(\hat{\mathbf{r}}\),首先应用\(\hat{\mathbf{q}}\)表示的变换,然后应用\(\hat{\mathbf{r}}\)表示的变换,那么整个变换可以表示为
在很多情况下,我们只能用矩阵来表示变换,这时候你可能会想到用单位四元数实现的绕轴变换实际上是线性变换,那么有没有办法把单位四元数转化为它表示的线性变换所对应的矩阵呢?实际上对于单位四元数\(\hat{\mathbf{q}}=(q_x,q_y,q_z,q_w)\)我们可以把它转化为
\[\mathbf{M}^q = \begin{pmatrix} 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 \end{pmatrix} \]从另一个方向转化则会复杂些,从上方的矩阵我们能得到
\[\begin{align*} m_{21}^q-m_{12}^q &= 4q_wq_x\\ m_{02}^q-m_{20}^q &= 4q_wq_y\\ m_{10}^q-m_{01}^q &= 4q_wq_z \end{align*} \tag{1} \]如果\(q_w\)是已知的那么我们就能计算出\(\mathbf{q}_v\)进而计算出\(\hat{\mathbf{q}}\)。矩阵\(\mathbf{M}^q\)的迹是用如下公式计算的
\[\begin{align*} \mathrm{tr}(\mathbf{M}^q) &= 4-4(q_x^2+q_y^2+q_z^2)\\ &= 4(1-\frac{q_x^2+q_y^2+q_z^2}{q_x^2+q_y^2+q_z^2+q_w^2})\\ &= \frac{4q_w^2}{q_x^2+q_y^2+q_z^2+q_w^2}\\ &= \frac{4q_w^2}{n(\hat{\mathbf{q}})^2}\\ &= 4q_w^2 \end{align*} \]这样看,\(\hat{\mathbf{q}}\)的四个分量的计算公式分别为
\[\begin{align*} &q_w = \frac{1}{2} \sqrt{\mathrm{tr}(\mathbf{M}^q)}, &q_x= \frac{m_{21}^q-m_{12}^q}{4q_w}\\ &q_y = \frac{m_{02}^q-m_{20}^q}{4q_w}, &q_z = \frac{m_{10}^q-m_{01}^q}{4q_w} \end{align*} \tag{2} \]为了有个数值上稳定的求解流程,除以一个很小的数应该被避免。因此,我们首先令\(t=q_w^2-q_x^2-q_y^2-q_z^2\),那么
\[\begin{align*} m_{00} &= t + 2q_x^2\\ m_{11} &= t + 2q_y^2\\ m_{22} &= t + 2q_z^2\\ \end{align*} \]此外我们令\(u = m_{00} + m_{11} + m_{22} = t + 2q_w^2\)。\(m_{00}\)、\(m_{11}\)、\(m_{22}\)、\(u\)中最大的那一个会决定\(q_x\)、\(q_y\)、\(q_z\)、\(q_w\)中哪个最大,如果\(u\)即\(q_w\)是最大的那么就用公式\((2)\)中的四个小式子分别计算即可。如果不是,那么就要用下方的公式,先把\(q_x\)、\(q_y\)、\(q_z\)中最大的计算出来
\[\begin{align*} &4q_x^2 = +m_{00}-m_{11}-m_{22}+m_{33}\\ &4q_y^2 = -m_{00}+m_{11}-m_{22}+m_{33}\\ &4q_z^2 = -m_{00}-m_{11}+m_{22}+m_{33}\\ &4q_w^2 = \mathrm{tr}(\mathbf{M}^q) \end{align*} \]接着再用公式\((1)\)计算出\(\hat{\mathbf{q}}\)的剩余分量。
球面线性插值是一个操作,给定两个单位四元数\(\hat{\mathbf{q}}\)和\(\hat{\mathbf{r}}\),还有一个参数\(t \in [0,1]\),计算出一个插值后的四元数。对于物体的动画来说很有用,但是对相机朝向的插值不太有用。因为这么做会让相机的“向上”向量(Up Vector)倾斜,而这通常是不想要的一个效果。
这个操作的代数形式如下
而它的软件实现如下,被称作slerp
\[\hat{\mathbf{s}}(\hat{\mathbf{q}},\hat{\mathbf{r}},t) = \mathrm{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}} \]为了计算式中的\(\phi\),我们可以利用以下的事实
\[\cos\phi = q_xr_x+q_yr_y+q_zr_z+q_wr_w \]当\(t \in [0,1]\)且\(\hat{\mathbf{q}}\)和\(\hat{\mathbf{r}}\)不相反时,slerp函数会沿着在四维单位球上的从\(\hat{\mathbf{q}}\)到\(\hat{\mathbf{r}}\)的最短弧进行速度恒定的插值,正如下图所示

一个这样有着常速度的曲线被称为测地线(Geodesic)。当我们要在多个方向进行插值,就比如\(\hat{\mathbf{q}}_0\)、\(\hat{\mathbf{q}}_1\)、...、\(\hat{\mathbf{q}}_{n-1}\),我们可以先从\(\hat{\mathbf{q}}_0\)线性插值到\(\hat{\mathbf{q}}_1\),再从\(\hat{\mathbf{q}}_1\)线性插值到\(\hat{\mathbf{q}}_2\),就这么进行下去直到插值完毕。但是这么做会导致轨迹不平滑,而且会有速度上的突变,一个更好的方式是借助样条。具体的做法是先在相邻的两个四元数\(\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 \mathrm{exp} \left[ - \frac{\log(\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i-1})+\log(\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i+1})}{4} \right] \]三次插值是用如下的方法实现的
\[\mathrm{squad}(\hat{\mathbf{q}}_i,\hat{\mathbf{q}}_{i+1},\hat{\mathbf{a}}_i,\hat{\mathbf{a}}_{i+1},t) =\\ \mathrm{slerp}(\mathrm{slerp}(\hat{\mathbf{q}}_i,\hat{\mathbf{q}_{i+1}},t),\mathrm{slerp}(\hat{\mathbf{a}}_i,\hat{\mathbf{a}}_{i+1},t),2t(1-t)) \]改良后的插值过程就变成了先使用\(\hat{\mathbf{q}}_0\)、\(\hat{\mathbf{a}}_0\)、\(\hat{\mathbf{a}}_1\)、\(\hat{\mathbf{q}}_1\)进行三次插值,再使用\(\hat{\mathbf{q}}_1\)、\(\hat{\mathbf{a}}_1\)、\(\hat{\mathbf{a}}_2\)、\(\hat{\mathbf{q}}_2\)进行三次插值,就这么进行下去直到插值完毕。
一个常见的操作是从一个方向\(\mathbf{s}\)变换到另一个方向\(\mathbf{t}\),在四元数的帮助下这个过程能被极大地简化。首先归一化\(\mathbf{s}\)和\(\mathbf{t}\),接着计算单位旋转轴\(\mathbf{u}\),具体的公式为\(\mathbf{u} = (\mathbf{s} \times \mathbf{t})/||\mathbf{s} \times \mathbf{t}||\),然后令\(e = \mathbf{s} \cdot \mathbf{t} = \cos(2\phi)\),式中的\(2\phi\)是\(\mathbf{s}\)和\(\mathbf{t}\)之间的角度。那么四元数$\hat{\mathbf{q}}=\left( \sin\phi \mathbf{u} , \cos\phi \right) = (\frac{\sin\phi}{\sin 2 \phi}(\mathbf{s} \times \mathbf{t}),\cos\phi) $ 就表示了从\(\mathbf{s}\)到\(\mathbf{t}\)的旋转,使用三角函数的半角公式我们可以得到
\[\hat{\mathbf{q}} = (\mathbf{q}_v,q_w) = \left( \frac{1}{\sqrt{2(1+e)}}(\mathbf{s} \times \mathbf{t}), \frac{\sqrt{2(1+e)}}{2} \right) \]以这种方式实现的旋转避免了当\(\mathbf{s}\)和\(\mathbf{t}\)过于接近时带来的数值不稳定性。但是当\(\mathbf{s}\)和\(\mathbf{t}\)几乎相对时还是会带来不稳定的问题,在这种情况下\(e\)会接近\(-1\)从而带来除以\(0\)的问题。当这种情况被侦测到时,我们可以使用任意一个与\(\mathbf{s}\)垂直的轴把\(\mathbf{s}\)旋转到\(\mathbf{t}\)。
在很多情况下我们会需要旋转的矩阵表达形式,经过代数和三角函数的简化后,旋转矩阵为
其中
\[\begin{align*} \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} \end{align*} \]可以看到的是,所有的开根号和三角函数在化简后都没了,因此这是一个高效的方法来创建矩阵。
要注意的是,当\(\mathbf{s}\)和\(\mathbf{t}\)平行或者近乎平行时,\(||\mathbf{s} \times \mathbf{t}||\)会约等于\(0\),在这个时候我们可以返回单位矩阵。而当\(2\phi\)约等于\(\pi\)时,我们可以通过\(s\)与任意一个不与它平行的向量叉乘来得到旋转轴,旋转角度为\(\pi\)弧度。
想象一下数字角色的一条手臂是用两个部分进行动画的,包括前臂和上臂,如下图所示

模型可能使用刚体变换来进行动画。然而关节处将不会和真实的肘部相似,这是因为使用了两个分离的物体,因此关节是由两个物体重叠的部分构成的。
顶点混合(Vertex Blending)是一个受欢迎的解决方案,它也被称为线性混合蒙皮(Linear-blend Skinning)、包络蒙皮(Enveloping)、骨骼子空间变形(Skeleton-subspace Deformation)。虽然这个算法的确切起源是未知的,但是定义骨骼并让皮肤响应骨骼的变动是计算机动画中的一个古老的话题。在它的最简单的形式中,前臂和上臂是单独动画的。但是在关节处,两个部分会通过弹性“皮肤”连接到一起。因此,弹性部分会有一系列被前臂矩阵变换的顶点和另一系列被上臂的矩阵变换的顶点。这会让三角形的顶点被不同的矩阵变换,与使用一个矩阵变换三角形的顶点相反,如下图所示那样。

更进一步,可以让每个顶点被不同的矩阵变换,接着给予变换后的位置权重并混合起来。这是通过让被动画的物体有骨骼骨架做到的,在这之中每个骨骼对顶点施加的变换会有一个被用户定义的权重。因为整个手都有可能是“弹性的”,所有的顶点有可能被不止一个矩阵影响,整个网格因此通常被称为皮肤(在骨骼之上的),如下图所示那样

许多商用建模系统都有相同的骨骼骨架建模特性。尽管叫这个名字,骨骼并不一定要是刚体。例如Mohr和Gleicher展示了增加额外的关节来实现肌肉隆起。
从数学上说,这是使用下方的公式表示的,式中的\(\mathbf{p}\)是原始的顶点,\(\mathbf{u}(t)\)表示在\(t\)时刻被变换后的顶点。
总计有\(n\)个骨骼影响着\(\mathbf{p}\),\(\mathbf{p}\)是使用世界坐标表示的。\(w_i\)是骨骼\(i\)对于\(\mathbf{p}\)的权重。矩阵\(\mathbf{M}_i\)表示从骨骼的初始坐标系到世界坐标系的变换。每个骨骼一般都有在它的坐标系原点的控制关节。例如,一个前臂骨骼会让它的肘关节处于原点位置,一个随时间变化的旋转矩阵会让前臂绕着肘关节运动。\(\mathbf{B}_i(t)\)矩阵是第\(i\)个骨骼的被用来让物体动起来的世界变换矩阵,它通常是一些矩阵的组合,比如先前的骨骼变换层级结构和局部的动画矩阵。
一个用来维护和更新\(\mathbf{B}_i(t)\)的方法被Woodland深入讨论过了。每个骨骼把顶点都根据它自己的参考帧对顶点进行位置变换,最终的位置通过在计算出的位置中插值得到。矩阵\(\mathbf{M}_i\)在一些关于蒙皮的讨论中没有直接出现,而被认为是\(\mathbf{B}_i(t)\)的一部分。我们在这里展示它是因为它是一个有用的矩阵,几乎总是矩阵组合过程的一部分。
在实践中,矩阵\(\mathbf{B}_i(t)\)和\(\mathbf{M}_i^{-1}\)在动画的每一帧都会被组合起来,每个组合后的矩阵都会被用来变换顶点。顶点\(\mathbf{p}\)会被不同的骨骼的组合矩阵变换,接着使用权重进行混合,这个过程就叫顶点混合(Vertex Blending)。混合使用到的权重一般都是非负的而且总和一般为\(1\)。正是这样,变换后的\(\mathbf{u}\)点会位于\(\mathbf{B}_i(t)\mathbf{M}_i^{-1}\mathbf{p}\)构成的凸包(Convex Hull)上。法线一般也能使用上方的公式进行计算,取决于具体的使用情况,一般用到的应该是\(\mathbf{B}_i(t)\mathbf{M}_i^{-1}\)逆的转置。
顶点混合非常适合在GPU上使用。网格中的顶点可以存储于静态缓冲中,接着被发送到GPU上被重复利用。在每一帧,只有骨骼矩阵在变化,顶点着色器会为被存储的网格计算骨骼矩阵的效果。通过这种方法,被处理的和来自CPU被传输的数据总量会最小化,从而允许GPU高效地渲染网格。如果模型所有的骨骼矩阵可以被一起使用那么将会是最轻松的,否则模型必须被分开,并且一些骨骼需要被复制。或者,骨骼变换可以被存储到纹理上用于顶点访问,这样可以避免到达寄存器存储限制。每个变换可以通过使用四元数仅仅被存储到两个纹理上。如果可以使用无序访问视图,那么可以让蒙皮结果被重复利用。
权重可以位于\([0,1]\)之外,或者总和不为\(1\)。但是,这只被一些混合算法使用,例如形态目标(Morph Targets)。
基础的顶点混合会带来不被期望的折叠、扭曲、自相交,如下图所示那样

一个更好的解决方案是使用双四元数,这个技巧能在进行蒙皮时保持原始变换的刚体性,避免了在肢体上的“糖纸”扭曲。它的计算开销小于线性皮肤混合开销的\(1.5\)倍而处理结果又比较好,这个技术因此迅速被业界采纳了。然而,双四元数蒙皮会导致鼓胀效应,Le和Hodgins提出了旋转中心的蒙皮这一更好的替代方案。这个方案依赖于一个假设,即局部变换应该是刚体的,有着相似权重\(w_i\)的顶点应该有着相似的变换。旋转中心会为每个顶点预计算,正交约束会被施加来避免肘部塌陷和糖纸扭曲伪影。在运行时,这个算法会和线性混合蒙皮相似,GPU为旋转中心执行线性混合蒙皮后会紧跟着一次四元数混合步骤。
从一个三维模型到另一个三维模型的形态变形在进行动画时很有用。想象一下有一个模型在\(t_0\)时刻被显示,我们希望它在\(t_1\)时刻变化到另外一个模型。对于\(t_0\)到\(t_1\)的所有时刻,我们获得的是一个使用了某种插值后“被混合的”模型,一个例子如下图所示

形态变形涉及解决两个主要的问题,即顶点对应关系(Vertex Correspondence)问题和插值问题。给定两个模型,这两个模型有着可能不一样的拓扑、不同的顶点数量、不同的网格网络连接,一般一开始得设置顶点的对应关系,这是一个难以解决的问题,现在已经有许多对于这个领域的研究。
然而,如果在两个模型之间已经有一对一的顶点对应关系,那么插值可以逐顶点完成。比如,我们可以使用线性插值直接作用于顶点。对于给定的时间\(t \in [t_0,t_1]\),为了计算一个形态变形后的顶点,我们首先计算\(s=(t-t_0)/(t_1-t_0)\),接着进行线性顶点混合,即
式中的\(\mathbf{p}_0\)和\(\mathbf{p}_1\)对应着相同的顶点,不过分别位于\(t_0\)和\(t_1\)时刻。
一个形态变形的变种可以给予用户更直观的控制,它被称为形态目标(Morph Targets)或混合形状(Blend Shapes)。它的基本想法可以用下图来解释

注解:给定两个嘴部姿态,一组不同的差分向量可以被计算用来控制插值,或者甚至是推断。在形态目标中,差分向量会被用来在中性表情的脸上“增加”动作。为差分向量使用正的权重,我们可以得到一个微笑的嘴巴,反之则有相反的效果。
我们从一个中性模型(脸)开始,使用\(\mathcal{N}\)来表示这个模型。此外,我们也有一组不同的面部姿态。在上图所示的例子中只有一个姿态,也就是笑脸。假如有\(k\)(\(k \geq 1\)),我们可以使用\(\mathcal{P}_i,i \in [1,...,k]\)来表示这些姿态。“差分脸”应该在预处理中通过\(\mathcal{D}_i=\mathcal{P}_i-\mathcal{N}\)被计算出来。
现在,我们已经有了一个中性模型\(\mathcal{N}\),以及一组不同的姿态\(\mathcal{D}_i\)。一个进行形态变形后的模型\(\mathcal{M}\)接着可以通过下方的公式来获得
以上图为例,使用权重\(w_1=1\)会得到笑脸,而使用\(w_1=0.5\)会得到半笑的脸。
对于这个简单的脸模型,我们可以增加另一个有着"悲伤的"眉毛的脸。使用一个负的权重我们能得到“开心的”眉毛。因为位移是可叠加的,眉毛的姿态可以同其它姿态比如微笑的嘴巴一起使用。
形态目标是一个强大的技术,它能给予动画师非常多的控制,因为模型的不同特征可以被独立地操纵。Lewis等人引入了姿态空间变形(pose-space deformation),它结合了顶点混合和形态目标。Senior使用了预计算的顶点纹理来存储和读取目标姿态之间的位移。支持流输出和每个顶点的ID的硬件能让更多的目标在单个模型中被使用,而且效果可以单独在GPU上被计算。使用一个低分辨率的网格,接着通过镶嵌细分阶段和位移映射能避免在高度精细的模型中蒙皮每个顶点的开销。
一个现实中的同时使用蒙皮和形态变形的例子如下图所示

在过场动画中使用极其高质量的动画可以带来非常好的观看体验,对于那些不能使用之前提过的方法来实现的动作,一个简单的方法是为所有的帧存储顶点,在运行时从硬盘读取并更新网格。然而,一个有着\(30000\)顶点左右的简单模型在短动画中就能消耗\(50\)MB/s左右的带宽,Gneiting介绍了一些方法来降低内存使用到简单方法的\(10\%\)。
首先,量化是需要被使用的。例如,位置和纹理坐标会使用16比特整数来存储。这个步骤是有损的,在压缩后就不能还原出原始的数据。为了进一步减少数据的使用量,空间上的信息和时间上的信息会被预测,差异会被编码。对于空间上的压缩来说,平行四边形预测可以被使用。比如对于一个三角形条带来说,下一个顶点的预测位置可以通过将当前三角形围绕当前边在三角形所在平面内进行反射得到,接着编码预测位置与真实位置之间的差异。使用好的预测,绝大多数的数值将会接近\(0\),这对于许多压缩方法是理想的一个情况。和MPEG压缩类似,预测也会在时间的维度上进行。也就是说,每隔\(n\)帧会进行一次空间压缩。在这之间,预测会在时间维度上进行。如果某个顶点从第\(n-1\)帧到第\(n\)帧有一些位移\(\Delta\),那么从第\(n\)帧到第\(n+1\)帧可能有相似的一些位移\(\Delta\)。这些技术可以很好地减少存储空间的使用,这个系统因此能被用来进行实时的流式传输。
本文来自博客园,作者:TiredInkRaven,转载请注明原文链接:https://www.cnblogs.com/TiredInkRaven/p/19524454
下一篇:遥感劝退帖中的悲观主义