“What if angry vectors veer
Round your sleeping head, and form.
There’s never need to fear
Violence of the poor world’s abstract storm.”
——Robert Penn Warren
(译者注:译者完全看不懂…大概解读一下,诗者的意思应该是表达数学里的抽象概念是客观的,当你理解时他们就将存在于你的脑中,如果你学得很混乱的话,可能就会产生所谓的“抽象风暴abstract storm”。诗者没有说相应的解决办法,只说了不要去恐惧。译者觉得,所谓的解决办法,其实就是梳理脑中的概念,夯实基础,可消弭风暴于无形。)
变换就是一种用某种方式对实体(如点、向量、颜色等)进行转换的操作。对计算机图形从业者来说,掌握变换是及其重要的。有了他们的帮助,你可以对物体、灯光和相机执行位置、形状和动画等相关的操作。你也可以保证所有的计算都是在统一坐标系统下执行的,并且将物体以不同的方式投影到平面上。这些只是变换可以执行的操作的一小部分,但这也足以表明变换在实时图形学中的重要性,甚至于在任何计算机图形学中的重要性。
线性变换可以保留了向量加法和标量乘法。即,
f
(
x
)
+
f
(
y
)
=
f
(
x
+
y
)
,
(
4.1
)
\ f(x) + f(y) = f(x+y),(4.1)
f(x)+f(y)=f(x+y),(4.1)
k
f
(
x
)
=
f
(
k
x
)
,
(
4.2
)
\ kf(x) = f(kx),(4.2)
kf(x)=f(kx),(4.2)
例如,
f
(
x
)
=
5
x
\ f(x) = 5x
f(x)=5x是对向量中每个元素乘以5的一个变换。为了证明这是一个线性变换,上面两中情况(公式4.1和4.2)都需要被满足。第一个条件是成立的,因为任何两个向量乘以5然后相加,与向量相加然后相乘结果是一致的。而标量乘法的条件显然是符合的。这个函数就被称为是缩放变换,因为它改变的是物体的缩放(尺寸)。旋转变换是另一种线性变换,它是基于原点对向量进行旋转。缩放和旋转变换,以及所有的针对三个元素向量的线性变换,都可以用一个3x3的矩阵来表示。
然而,这个大小的矩阵往往并不足够大。三元向量x的一个函数,例如 f ( x ) = x + ( 7 , 3 , 2 ) \ f(x) = x + (7,3,2) f(x)=x+(7,3,2)就不是线性的。在两个单独的向量上执行这个函数将会将 ( 7 , 3 , 2 ) (7,3,2) (7,3,2)中的每个值做两次加法到最终结果中。将一个固定的向量加到另一个向量中可以执行平移操作,例如,它将所有的位置移动相同的量。这是一种有用的变换,并且我们还可以对各种变换进行组合,例如,将物体缩小到原先的一半大小,然后把它移动到一个不同的位置。继续保持函数以这种简单的方式呈现将会使组合操作变得有些困难。
组合线性变换和平移可以通过仿射变换(affine transform)来实现,通常是用一个4x4的矩阵来存储。仿射变换即是在执行一个线性变换和再执行一个平移变换。为了表示四元向量,我们使用了齐次符号(homogeneous notation),可以将点和方向以相同的方式进行表示(使用粗体小写字母)。方向向量表示为 v = ( v x v y v z 0 ) T \textbf{v} = (v_x\ v_y\ v_z\ 0)^T v=(vx vy vz 0)T,一个点可以表示为 v = ( v x v y v z 1 ) T \textbf{v} = (v_x\ v_y\ v_z\ 1)^T v=(vx vy vz 1)T。这整个章节,我们将会充分使用到各种术语和操作,你可以在线性代数附录中的找到更加详尽的解释(可以访问realtimerendering.com获取)。
所有的平移、旋转、缩放、反射和剪切矩阵都是仿射变换。仿射矩阵的主要特点就是它保留了线的并行性,而长度和角度都不是必须的。仿射变换也可能是相互独立的仿射变换的组合的结果。
本章将会从最主要的、最基础的仿射变换开始阐述。这个部分也可以作为简单变换的参考手册。后面会去描述更加特定的矩阵,以及讨论另一个强大的变换工具——四元数。然后就是顶点混合和变形,他们是两个简单而有效的表现网格动画的方法。最后,就是投影矩阵。所有这些相关的变换、符号、函数以及属性都能够参考表4.1,其中正交矩阵的逆矩阵是转置矩阵。
表 4.1. 本章节将要讨论的大部分的变换
变换是操纵(译者注:玩弄)几何的基本工具。大部分的图形应用编程接口都允许用户设置任意矩阵,有些时候也会可能会使用一些矩阵操作的库,库内实现了大部分本章接下来会讨论的变换。但是,去理解函数调用背后的真正的矩阵和他们之间的相互作用任然是有价值的。知道了函数调用背后的矩阵的运算仅仅是一个开始,而明白矩阵本身的属性会使你走得更远。例如,拥有了这样的理解会让你在处理一个正交矩阵,明白它的逆即是它的转置矩阵,从而加速矩阵的求逆过程。这样的知识越多,会使你的代码的质量和效率更高。
4.1 Basic Transform(基础变换)
这一节描述了最基本的变换,例如平移、旋转、缩放、剪切、变换组合、刚体变换、法线变换(译者注:normal transform,并不是真的普通(normal))以及逆的计算。对于以及有经验的读者,这一节可以用作简单变换的参考手册,而对于新人来讲,这可以作为进入正题的一个介绍。本章节的内容材料是本章节余下内容以及其他章节的必要基础。我们将会从最简单的变换开始讲起——平移变换。
4.1.1 Translation(平移)
从一个位置到另一个位置的变化可以用一个平移矩阵
T
\textbf{T}
T来表示。这个矩阵将一个实体通过向量
t
=
(
t
x
,
t
y
,
t
z
)
\textbf{t}=(t_x,t_y,t_z)
t=(tx,ty,tz)来进行平移。
T
\textbf{T}
T由下面的方程4.3给出:
T(t)
=
T
(
t
x
,
t
y
,
t
z
)
=
(
1
0
0
t
x
0
1
0
t
y
0
0
1
t
z
0
0
0
1
)
.
(
4.3
)
\textbf{T(t)} = \textbf{T}(t_x,t_y,t_z)=\left( \begin{matrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.3)
T(t)=T(tx,ty,tz)=⎝⎜⎜⎛100001000010txtytz1⎠⎟⎟⎞.(4.3)
图4.1中展示了平移变换的效果的一个例子。点
P
=
(
p
x
,
p
y
,
p
z
,
1
)
\textbf{P}=(p_x,p_y,p_z,1)
P=(px,py,pz,1)经过了变换矩阵
T(t)
\textbf{T(t)}
T(t)(乘法)可以得到一个新的点
P’
=
(
p
x
+
t
x
,
p
y
+
t
y
,
p
z
+
t
z
,
1
)
\textbf{P'}=(p_x+t_x,p_y+t_y,p_z+t_z,1)
P’=(px+tx,py+ty,pz+tz,1),很容易可以看出这是做了一次平移变换。需要注意,向量
V
=
(
v
x
,
v
y
,
v
z
,
0
)
\textbf{V}=(v_x,v_y,v_z,0)
V=(vx,vy,vz,0)在与
T(t)
\textbf{T(t)}
T(t)做乘法后的结果不会受到其影响,因为一个方向向量是不可能被平移的(译者注:方向向量的起始点总是在坐标原点)。相对的,在其余的仿射变换中,点和向量两者都会被变换矩阵所影响。平移矩阵的逆是
T
−
1
(
t
)
=
T
(
−
t
)
\textbf{T}^{-1}(\textbf{t}) = \textbf{T}(-\textbf{t})
T−1(t)=T(−t),即,对向量
t
\textbf{t}
t取反。
图 4.1. 左侧的正方形经由平移矩阵
T
(
5
,
2
,
0
)
\textbf{T}(5,2,0)
T(5,2,0)进行变换,正方形向右移动了5个单位距离,向上移动了2个单位距离
在这一点上,我们应该提到的是,有时在计算机图形学中也可以看到的另一种有效的符号表示方法是使用底端为平移向量的矩阵。例如,DirectX即是使用这种形式。在这种方案中,矩阵的顺序将会是相反的,例如,去应用矩阵计算的顺序将会是从左向右。这种表示法下的向量和矩阵被认为是以行为主(in row-major form),因为向量是以行的形式呈现。本书中,我们将会使用以列为主的形式(column-major form)。当然,不论使用哪种,这仅仅是符号表示上的区别。当矩阵存储到内存中时,16个值中的最后四个值即是平移变换的三个值外加一个1。
4.1.2 Rotation(旋转)
旋转变换将一个向量(位置或者方向)绕着给定的穿过原点的轴旋转一个给定的角度。与平移矩阵类似,它也是刚体变换,即,它保持了变换的点之间的距离,并且保留了惯用手(即它从不会导致左右换边)(译者注:译者也不懂)。这两种类型的变换在计算机图形学中对物体进行位置和转向操作极为有用。方向矩阵(orientation matrix)是与相机视图或对象相关联的旋转矩阵,用于定义其在空间中的方向,即其向上和向前的方向。
在两个维度上,旋转矩阵比较容易去推演。假设我们有一个向量,
v
=
(
v
x
,
v
y
)
\textbf{v}=(v_x,v_y)
v=(vx,vy),我们可以将其参数化为
v
=
(
v
x
,
v
y
)
=
(
r
cos
θ
,
r
sin
θ
)
\textbf{v}=(v_x,v_y)=(r \cos \theta,r \sin \theta)
v=(vx,vy)=(rcosθ,rsinθ)。如果我们是要将向量以
ϕ
\phi
ϕ弧度进行旋转(逆时针),那我们可以得到
u
=
(
r
cos
(
θ
+
ϕ
)
,
r
sin
(
θ
+
ϕ
)
)
\textbf{u}=(r \cos(\theta+\phi),r \sin( \theta+\phi))
u=(rcos(θ+ϕ),rsin(θ+ϕ))。这也可以表示为
u
=
(
r
cos
(
θ
+
ϕ
)
r
sin
(
θ
+
ϕ
)
)
=
(
r
(
cos
θ
cos
ϕ
−
sin
θ
sin
ϕ
)
r
(
sin
θ
cos
ϕ
+
cos
θ
sin
ϕ
)
)
=
(
cos
θ
−
sin
ϕ
)
sin
ϕ
cos
θ
)
(
r
cos
θ
r
sin
θ
)
=
R
(
ϕ
)
v
(
4.4
)
\textbf{u}=\left( \begin{matrix} r \cos(\theta+\phi) \\ r \sin( \theta+\phi) \end{matrix} \right)=\left( \begin{matrix} r (\cos\theta\cos\phi-\sin\theta\sin\phi) \\ r (\sin\theta\cos\phi+\cos\theta\sin\phi) \end{matrix} \right)=\left( \begin{matrix} \cos\theta & -\sin\phi) \\ \sin\phi & \cos\theta \end{matrix} \right)\left( \begin{matrix} r \cos\theta \\ r \sin\theta \end{matrix} \right)=\textbf{R}(\phi)\textbf{v} \qquad(4.4)
u=(rcos(θ+ϕ)rsin(θ+ϕ))=(r(cosθcosϕ−sinθsinϕ)r(sinθcosϕ+cosθsinϕ))=(cosθsinϕ−sinϕ)cosθ)(rcosθrsinθ)=R(ϕ)v(4.4)
其中,我们使用了角度的求和关系来将
cos
(
θ
+
ϕ
)
\cos(\theta+\phi)
cos(θ+ϕ)和
sin
(
θ
+
ϕ
)
\sin( \theta+\phi)
sin(θ+ϕ)进行展开。在三维中,通常使用的旋转矩阵是
R
x
(
ϕ
)
\textbf{R}_x(\phi)
Rx(ϕ)、
R
y
(
ϕ
)
\textbf{R}_y(\phi)
Ry(ϕ)和
R
z
(
ϕ
)
\textbf{R}_z(\phi)
Rz(ϕ),其分别表示将实体沿着x、y和z轴旋转
ϕ
\phi
ϕ弧度。参考公式4.5-4.7:
R
x
(
ϕ
)
=
(
1
0
0
0
0
cos
ϕ
−
sin
ϕ
0
0
sin
ϕ
cos
ϕ
0
0
0
0
1
)
.
(
4.5
)
\textbf{R}_x(\phi) =\left( \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & \cos\phi & -\sin\phi & 0 \\ 0 & \sin\phi & \cos\phi & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.5)
Rx(ϕ)=⎝⎜⎜⎛10000cosϕsinϕ00−sinϕcosϕ00001⎠⎟⎟⎞.(4.5)
R
y
(
ϕ
)
=
(
cos
ϕ
0
sin
ϕ
0
0
1
0
0
−
sin
ϕ
0
cos
ϕ
0
0
0
0
1
)
.
(
4.6
)
\textbf{R}_y(\phi) =\left( \begin{matrix} \cos\phi & 0 & \sin\phi &0 \\ 0 & 1 & 0 &0 \\ -\sin\phi & 0 & \cos\phi & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.6)
Ry(ϕ)=⎝⎜⎜⎛cosϕ0−sinϕ00100sinϕ0cosϕ00001⎠⎟⎟⎞.(4.6)
R
z
(
ϕ
)
=
(
cos
ϕ
−
sin
ϕ
0
0
sin
ϕ
cos
ϕ
0
0
0
0
1
0
0
0
0
1
)
.
(
4.7
)
\textbf{R}_z(\phi)=\left( \begin{matrix} \cos\phi & -\sin\phi & 0 & 0 \\ \sin\phi & \cos\phi & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.7)
Rz(ϕ)=⎝⎜⎜⎛cosϕsinϕ00−sinϕcosϕ0000100001⎠⎟⎟⎞.(4.7)
如果去掉4x4矩阵中最底部的行和最右边的列,可以得到一个3x3的矩阵。对于每一个3x3的旋转矩阵,
R
\textbf{R}
R,其表示绕任意轴旋转
ϕ
\phi
ϕ弧度,其矩阵的迹(trace,表示矩阵对象线元素的和)是和轴无关的常量,可以由下式计算得到:
t
r
(
R
)
=
1
+
2
cos
ϕ
(
4.8
)
tr(\textbf{R})=1+2\cos\phi\qquad(4.8)
tr(R)=1+2cosϕ(4.8)
旋转矩阵的效果可以参考65页的图4.4。旋转矩阵
R
i
(
ϕ
)
\textbf{R}_i(\phi)
Ri(ϕ)的特征,除了它绕轴
i
i
i旋转
ϕ
\phi
ϕ弧度外,是它保持旋转轴
i
i
i上的所有点不变。注意
R
\textbf{R}
R也可以用来表示绕任意轴的旋转矩阵。上面给定的旋转矩阵可以被任意组合以表示绕任意轴的旋转。这个过程在4.2.1小节中会进行讨论。直接绕任意轴进行旋转这个问题将会在4.2.4中提到。
所有的旋转矩阵都有一个唯一的秩,且是正交的。即便是将数个变换进行组合这一性质依然保持不变。有另一种方法来获得矩阵的逆: R i − 1 ( ϕ ) = R i ( − ϕ ) \textbf{R}^{-1}_i(\phi) = \textbf{R}_i(-\phi) Ri−1(ϕ)=Ri(−ϕ),即以相反的方向绕同一个轴旋转。
例子:绕点旋转。假设我们希望将物体绕z轴旋转
ϕ
\phi
ϕ弧度,旋转的中心是一个特定的点
p
\textbf{p}
p。变换该怎样进行?可以参照图4.2中显示的内容。由于围绕一个点的旋转的特点是该点本身不受旋转的影响(译者注:点本身没有“朝向”这一属性,即其只有一个点/位置属性,所以才会说“该店本身不受旋转影响”),因此变换开始先做平移,从而使
p
\textbf{p}
p与坐标原点重合(通过平移矩阵
T
(
−
p
)
\textbf{T}(-\textbf{p})
T(−p))。然后应用旋转矩阵
R
z
(
ϕ
)
\textbf{R}_z(\phi)
Rz(ϕ)。最终,物体需要移动回去它原先的位置(通过平移矩阵
T
(
p
)
\textbf{T}(\textbf{p})
T(p))。得到的变换矩阵
X
\textbf{X}
X可以表示为:
X
=
T
(
p
)
R
z
(
ϕ
)
T
(
−
p
)
(
4.9
)
\textbf{X}=\textbf{T}(\textbf{p})\textbf{R}_z(\phi)\textbf{T}(-\textbf{p})\qquad(4.9)
X=T(p)Rz(ϕ)T(−p)(4.9)
需要注意我们之前提到的矩阵计算的顺序。
图4.2. 绕特定点
p
\textbf{p}
p旋转的例子
4.1.3 Scaling(缩放)
缩放矩阵
S
(
s
)
=
S
(
s
x
,
s
y
,
s
z
)
\textbf{S}(\textbf{s})=\textbf{S}(s_x,s_y,s_z)
S(s)=S(sx,sy,sz),将实体沿着x、y和z轴以因子
s
x
s_x
sx、
s
y
s_y
sy和
s
z
s_z
sz进行缩放。这意味着缩放矩阵可以用来放大和缩小物体。
s
i
,
i
∈
{
x
,
y
,
z
}
s_i,i\in\{x,y,z\}
si,i∈{x,y,z}越大,物体在该方向就会被放的越大。设置
s
\textbf{s}
s的所有组件元素都为1可以天然的避免在对应方向的缩放。
S
\textbf{S}
S的公式为:
S
(
s
)
=
(
s
x
0
0
0
0
s
y
0
0
0
0
s
z
0
0
0
0
1
)
.
(
4.10
)
\textbf{S}(\textbf{s})=\left( \begin{matrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0&0 \\ 0 & 0& s_z & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.10)
S(s)=⎝⎜⎜⎛sx0000sy0000sz00001⎠⎟⎟⎞.(4.10)
后面的图4.4展示了缩放矩阵的效果。缩放操作可以分为两种:如果是
s
x
=
s
y
=
s
z
s_x= s_y=s_z
sx=sy=sz的话就称为是均匀(uniform)缩放;否则就是非均匀(nonuniform)缩放。有时候也会使用术语各项同性缩放(isotropic scaling)和各项异性缩放(anisotropic scaling),其实讲的是一个意思(译者注:学过材料尤其是晶体材料的同学应该理解特别深,很不幸译者也学过)。其矩阵的逆
S
−
1
(
s
)
=
S
(
1
/
s
x
,
1
/
s
y
,
1
/
s
z
)
\textbf{S}^{-1}(\textbf{s})=\textbf{S}(1/s_x,1/s_y,1/s_z)
S−1(s)=S(1/sx,1/sy,1/sz)。
使用齐次坐标系,另一个创建均匀缩放矩阵的方式是对处于位置
(
3
,
3
)
(3,3)
(3,3)的元素(即矩阵右下角的元素)进行乘法操作。这个值会影响齐次坐标的
w
w
w部分,从而缩放被矩阵变换的点(而不是方向向量)的每个坐标轴。例如,要以5为因子进行缩放,缩放矩阵中位置
(
0
,
0
)
(0,0)
(0,0),
(
1
,
1
)
(1,1)
(1,1)和
(
2
,
2
)
(2,2)
(2,2)的元素可以设置为5,或者位置
(
4
,
4
)
(4,4)
(4,4)的元素设置为
1
/
5
1/5
1/5。两个矩阵分别为:
S
=
(
5
0
0
0
0
5
0
0
0
0
5
0
0
0
0
1
)
,
S
′
=
(
1
0
0
0
0
1
0
0
0
0
1
0
0
0
0
1
/
5
)
.
(
4.10
)
\textbf{S}=\left( \begin{matrix} 5 & 0 & 0 & 0 \\ 0 & 5 & 0&0 \\ 0 & 0& 5 & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right),\textbf{S}'=\left( \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0&0 \\ 0 & 0& 1 & 0 \\ 0 & 0 & 0 & 1/5 \end{matrix} \right).\qquad\qquad(4.10)
S=⎝⎜⎜⎛5000050000500001⎠⎟⎟⎞,S′=⎝⎜⎜⎛1000010000100001/5⎠⎟⎟⎞.(4.10)
不使用
S
\textbf{S}
S来进行均匀缩放,而是采用
S
′
\textbf{S}'
S′的话需要再进行一步均质化(homogenization)操作。这可能比较低效,因为它涉及到均质化过程中的除法操作;如果右下角元素(位置
(
3
,
3
)
(3,3)
(3,3))是1的话,那么就没有做除法的必要。当然,如果系统总是在不进行1测试的情况下进行该除法,则不会产生任何额外费消耗。
如果
s
\textbf{s}
s的其中一个或者三个成员取负值的话,那么会得到一类反射矩阵(reflection matrix),也被称为镜像矩阵(mirror matrix)。如果仅有两个缩放因子为
−
1
-1
−1的话,那么我们会旋转
π
\pi
π个弧度。需要注意,旋转矩阵和反射矩阵的组合依然时一个反射矩阵。即,下面即是一个反射矩阵:
(
cos
(
π
/
2
)
sin
(
π
/
2
)
sin
(
π
/
2
)
cos
(
π
/
2
)
)
(
1
0
0
−
1
)
=
(
0
−
1
−
1
0
)
.
(
4.12
)
\left( \begin{matrix} \cos(\pi/2) & \sin(\pi/2) \\ \sin(\pi/2) & \cos(\pi/2) \end{matrix}\right) \left( \begin{matrix} 1 & 0 \\ 0 &-1 \end{matrix}\right) = \left(\begin {matrix}0&-1\\ -1&0 \end{matrix}\right).\qquad(4.12)
(cos(π/2)sin(π/2)sin(π/2)cos(π/2))(100−1)=(0−1−10).(4.12)
当检测到反射矩阵时,通常需要进行特殊处理。例如,当顶点由反射矩阵进行转换处理时,具有逆时针顶点顺序的三角形将得到顺时针序。这个顺序的改变可能会导致不正确光照和背面裁剪的出现。为了检测给定的矩阵以何种方式进行反射,要计算聚会着呢左上的
3
∗
3
3*3
3∗3的矩阵的秩。如果值是负的,那么矩阵就是反射性的(译者注:即表示可用于反射操作)。例如,公式4.12中的矩阵的秩为
0
⋅
0
−
(
−
1
)
⋅
(
−
1
)
=
−
1
0·0-(-1)·(-1)=-1
0⋅0−(−1)⋅(−1)=−1。
**示例:以特定方向进行缩放。**缩放矩阵
S
\textbf{S}
S沿着
x
x
x、
y
y
y和
z
z
z轴进行缩放。如果需要以其他的方向进行的话,那么需要进行复合变换。假设缩放是以正交坐标轴进行的,即向右的向量
f
x
\textbf{f}^x
fx、
f
y
\textbf{f}^y
fy和
f
z
\textbf{f}^z
fz(译者注:由这三个正交向量构建正交坐标系)。首先,构建矩阵
F
\textbf{F}
F,如下:
F
=
(
f
x
f
y
f
z
0
0
0
0
1
)
.
(
4.13
)
\textbf{F}=\left( \begin{matrix} \textbf{f}^x & \textbf{f}^y& \textbf{f}^z & \textbf{0} \\ 0 & 0 & 0 & 1 \end{matrix} \right).\qquad(4.13)
F=(fx0fy0fz001).(4.13)
想法是使由三个轴确定的坐标系与标准轴重合,然后应用标准缩放矩阵,随后再变换回去。需要执行的第一步即是乘以
F
\textbf{F}
F的转置,也就是逆。随后依次执行缩放和变换回去的操作。变换公式为:
X
=
FS
(
s
)
F
T
.
(
4.14
)
\textbf{X}=\textbf{F}\textbf{S}(\textbf{s})\textbf{F}^T.\qquad(4.14)
X=FS(s)FT.(4.14)
4.1.4 Shearing(拉伸)
还有一类变换就是拉伸矩阵组(译者注:注意到国内会使用的另一个术语“剪切”)。例如,他们可以应用到游戏中去将整个场景扭曲掉来创建某种迷幻效果或以其他方式扭曲模型的外观。共有六个基础的拉伸矩阵,他们可以表示为
H
x
y
(
s
)
\textbf{H}_{xy}(s)
Hxy(s),
H
x
z
(
s
)
\textbf{H}_{xz}(s)
Hxz(s),
H
y
x
(
s
)
\textbf{H}_{yx}(s)
Hyx(s),
H
y
z
(
s
)
\textbf{H}_{yz}(s)
Hyz(s),
H
z
x
(
s
)
\textbf{H}_{zx}(s)
Hzx(s)和
H
z
y
(
s
)
\textbf{H}_{zy}(s)
Hzy(s)。其中第一个下标表示的是被拉伸矩阵改变的坐标轴,而第二个下标指出了执行拉伸的坐标轴。公式4.15中即是一个拉伸矩阵
H
x
z
(
s
)
\textbf{H}_{xz}(s)
Hxz(s)的实例。可以观察到,下标可以用来定位参数
s
s
s的在矩阵中的位置;其中
x
x
x(其数字索引是0)表示第0行,
z
z
z(其数字索引是2)表示是第2列,因而
s
s
s的位置即是第0行第2列:
H
x
z
(
s
)
=
(
1
0
s
0
0
1
0
0
0
0
1
0
0
0
0
1
)
.
(
4.15
)
\textbf{H}_{xz}(s)=\left(\begin{matrix}1&0&s&0\\ 0&1&0&0\\ 0&0&1&0\\ 0&0&0&1 \end{matrix}\right).\qquad(4.15)
Hxz(s)=⎝⎜⎜⎛10000100s0100001⎠⎟⎟⎞.(4.15)
将这个矩阵和一点
p
\textbf{p}
p相乘的结果会返回一个点:
(
p
x
+
s
p
z
p
y
p
z
)
T
(p_x+sp_z\quad p_y\quad p_z)^T
(px+spzpypz)T。图4.3展示了单位正方形的变换过程。
H
i
j
(
s
)
\textbf{H}_{ij}(s)
Hij(s)的逆(依据
j
j
j坐标轴对
i
i
i坐标轴进行拉伸,其中
i
≠
j
i\not=j
i=j),即是以反方向进行拉伸,即
H
i
j
−
1
(
s
)
=
H
i
j
(
−
s
)
\textbf{H}^{-1}_{ij}(s)=\textbf{H}_{ij}(-s)
Hij−1(s)=Hij(−s)。
图4.3. 应用
H
x
z
(
s
)
\textbf{H}_{xz}(s)
Hxz(s)对单位正方形进行拉伸的效果。
y
y
y和
z
z
z的值都不会受到变换的影响,而
x
x
x的值则是
x
x
x和
s
s
s和
z
z
z值的乘积的和,从而使正方形变得倾斜。这个变换不会改变区域的面积,即覆盖的区域的大小是没有改变的。
你可以对拉伸矩阵稍稍得做一些改变:
H
x
y
′
(
s
,
t
)
=
(
1
0
s
0
0
1
t
0
0
0
1
0
0
0
0
1
)
.
(
4.16
)
\textbf{H}'_{xy}(s,t)=\left(\begin{matrix} 1&0&s&0\\ 0&1&t&0\\ 0&0&1&0\\ 0&0&0&1 \end{matrix}\right).\qquad(4.16)
Hxy′(s,t)=⎝⎜⎜⎛10000100st100001⎠⎟⎟⎞.(4.16)
这里,两个下标都被用来表示被拉伸的坐标轴(被第三个轴拉伸)。这两种描述之间的联系可以表示为
H
i
j
′
(
s
,
t
)
=
H
i
k
(
s
)
H
j
k
(
t
)
\textbf{H}'_{ij}(s,t)=\textbf{H}_{ik}(s)\textbf{H}_{jk}(t)
Hij′(s,t)=Hik(s)Hjk(t),其中
k
k
k被用来表示第三个坐标轴的索引。具体使用那一种表示取决于你的偏好。最后需要注意,因为任意拉伸矩阵的秩
∣
H
∣
=
1
|\textbf{H}|=1
∣H∣=1,故而这个变换并不会改变体积,可以参照图4.3。
4.1.5 Concatenation of Transforms(级联变换)
(译者注:可以称为是变换的组合和连接,其中顺序是一个重要的影响因素,本质是矩阵不满足交换律。译者在众多翻译中选取了“级联”一词,是想既体现“组合”的概念,又强调组合过程中的“顺序”和相互依赖关系。)
由于矩阵乘法运算的非可交换性(noncommutativity),故而其中矩阵的顺序就很重要了。因此,级联变换可以说是对顺序有依赖。
一个体现顺序依赖性的例子是:试想两个矩阵,
S
\textbf{S}
S和
R
\textbf{R}
R。
S
(
2
,
0.5
,
1
)
\textbf{S}(2,0.5,1)
S(2,0.5,1)将
x
x
x放大2倍,将
y
y
y缩小为其一半。
R
z
(
π
/
6
)
\textbf{R}_z(\pi/6)
Rz(π/6)表示逆时针绕
z
z
z轴旋转
π
/
6
\pi/6
π/6个弧度(想象将书页看作是
x
y
xy
xy平面,在右手系中,
z
z
z轴方向垂直于书页向外)。这两个矩阵可以以两种方式做乘法,而其结果是截然不同的。图4.4中展示了两种情况。
图4.4. 这幅图展示了做矩阵乘法时候的顺序依赖性。在上面一行中,先做旋转
R
z
(
π
/
6
)
\textbf{R}_z(\pi/6)
Rz(π/6),然后再做缩放
S
(
s
)
\textbf{S}(\textbf{s})
S(s),其中
s
=
(
2
,
0.5
,
1
)
\textbf{s}=(2,0.5,1)
s=(2,0.5,1)。组合起来即是
S
(
s
)
R
z
(
π
/
6
)
\textbf{S}(\textbf{s})\textbf{R}_z(\pi/6)
S(s)Rz(π/6)。在下面那一行中,则是以相反的顺序,得到
R
z
(
π
/
6
)
S
(
s
)
\textbf{R}_z(\pi/6)\textbf{S}(\textbf{s})
Rz(π/6)S(s)。结果有着明显的区别。通常认为对于任意矩阵
M
\textbf{M}
M和
N
\textbf{N}
N有
MN
≠
NM
。
\textbf{M}\textbf{N}\not=\textbf{N}\textbf{M}。
MN=NM。
将一些列矩阵组合(级联)成一个单独的矩阵,这样做的原因是可以以此来提高效率。例如,试想一下你的游戏场景中有上百万个顶点,而其中所有场景中的物体需要缩放、旋转,然后平移。现在,将三个矩阵组合成一个单独的矩阵再应用于顶点上,而不是将待变换的顶点分别与这三个矩阵相乘。这个组合起来的矩阵即是 C = TRS \textbf{C}=\textbf{T}\textbf{R}\textbf{S} C=TRS。注意这里的顺序。缩放矩阵 S \textbf{S} S应该首先与顶点相乘,然后出现在公式的最右侧。这个顺序即是 TRSp = ( T ( R ( Sp ) ) ) \textbf{T}\textbf{R}\textbf{S}\textbf{p}=(\textbf{T}(\textbf{R}(\textbf{S}\textbf{p}))) TRSp=(T(R(Sp))),其中 p \textbf{p} p是待变换的点。顺带一提, TRS \textbf{T}\textbf{R}\textbf{S} TRS即是场景图像系统中常用的变换顺序。
值得注意的是,矩阵级联存在顺序依赖的同时,矩阵也可以依据需要进行分组(译者注:不满足交换律,但是满足结合律)。例如,假设使用 TRSp \textbf{T}\textbf{R}\textbf{S}\textbf{p} TRSp,您只想计算一次刚体运动变换 TR \textbf{T}\textbf{R} TR。将这两个矩阵进行组合(( TR ) ( Sp ) \textbf{T}\textbf{R})(\textbf{S}\textbf{p}) TR)(Sp))是完全有效可行的,然后可以替换为中间结果。因此,矩阵级联是可以结合的/满足结合律的(associative)。
4.1.6 The Rigid-Body Transform(刚体变换)
当一个人去抓握一个固态的物体,假设是一个桌子上面的一支笔,然后移动到另外一个位置,也许是移到口袋里,那么只有物体的朝向和位置会发生改变,而物体的形状通常不会受到影响。这样的仅仅只是平移、和旋转的级联组合的变换,称为是刚体变换。它的特性是会保持物体的长度、角度以及惯用手系。
任意刚体矩阵
X
\textbf{X}
X,可以写作是平移矩阵
T
(
t
)
\textbf{T}(\textbf{t})
T(t)和旋转矩阵
R
\textbf{R}
R的级联。因此
X
\textbf{X}
X可写作如下的公式:
X
=
T
(
t
)
R
=
(
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
)
.
(
4.17
)
\textbf{X}=\textbf{T}(\textbf{t})\textbf{R}=\left(\begin{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\\ \end{matrix}\right).\qquad(4.17)
X=T(t)R=⎝⎜⎜⎛r00r10r200r01r11r210r02r12r220txtytz1⎠⎟⎟⎞.(4.17)
X
\textbf{X}
X的逆的计算为
X
−
1
=
(
T
(
t
)
R
)
−
1
=
R
−
1
T
(
t
)
−
1
=
R
T
T
(
−
t
)
\textbf{X}^{-1}=(\textbf{T}(\textbf{t})\textbf{R})^{-1}=\textbf{R}^{-1}\textbf{T}(\textbf{t})^{-1}=\textbf{R}^T\textbf{T}(-\textbf{t})
X−1=(T(t)R)−1=R−1T(t)−1=RTT(−t)。因此为了计算逆,需要对
R
\textbf{R}
R左上角的
3
∗
3
3*3
3∗3的矩阵进行转置,并对
T
\textbf{T}
T的平移值的符号进行改变。这两个新的矩阵以与原先相反的顺序进行矩阵乘法,从而得到逆。另一个计算
X
\textbf{X}
X的方法是以下面的表示法来重新认识矩阵
R
\textbf{R}
R和
X
\textbf{X}
X:
R
ˉ
=
(
r
,
0
r
,
1
r
,
2
)
=
(
r
0
,
T
r
1
,
T
r
2
,
T
)
,
X
=
(
R
ˉ
→
t
0
T
1
)
(
4.18
)
\bar\textbf{R}=(\textbf{r}_{,0}\quad\textbf{r}_{,1}\quad\textbf{r}_{,2})=\left(\begin{matrix} \textbf{r}_{0,}^T\\ \textbf{r}_{1,}^T\\ \textbf{r}_{2,}^T\\ \end{matrix}\right),\\ \textbf{X}=\left(\begin{matrix} \overrightarrow{\bar\textbf{R}}&\textbf{t}\\ \textbf{0}^T&1\\ \end{matrix}\right) \qquad\qquad(4.18)
Rˉ=(r,0r,1r,2)=⎝⎛r0,Tr1,Tr2,T⎠⎞,X=(Rˉ0Tt1)(4.18)
其中
r
,
0
\textbf{r}_{,0}
r,0代表着旋转矩阵的第一竖列(即逗号表示从0到1的数字,而第二个下标是0),
r
0
,
T
\textbf{r}_{0,}^T
r0,T是列矩阵的第一行。注意
0
\textbf{0}
0(译者注:加粗的0)代表着一个
3
∗
1
3*1
3∗1的列向量,其中的所有值都为0。通过计算可以得到其逆矩阵:
X
−
1
=
(
r
0
,
r
1
,
r
2
,
−
R
ˉ
T
t
0
0
0
1
)
.
(
4.19
)
\textbf{X}^{-1}=\left(\begin{matrix} \textbf{r}_{0,}&\textbf{r}_{1,}&\textbf{r}_{2,}&-\bar\textbf{R}^T\textbf{t}\\ 0&0&0&1 \end{matrix}\right).\qquad(4.19)
X−1=(r0,0r1,0r2,0−RˉTt1).(4.19)
示例:调整相机方向。图形学中一个常见的工作是调整相机的方向从而使其看向某一个特定的位置。这里我们略微介绍一下
gluLookAt()
\text{gluLookAt()}
gluLookAt()(来自于OpenGL Utility Library,简写GLU)。即便现在对这个函数并不怎么调用了,但其实现的工作内容依旧是很常见的。假设相机位于
c
\textbf{c}
c,而我们希望相机看向目标
l
\textbf{l}
l,同时给定相机的up方向(译者注:即相机的“头顶”方向,相机的关键元素即是up方向,forward方向,以及位置)是
u
′
\textbf{u}'
u′,如图4.5所示。我们要计算一个由三个向量
{
r
,
u
,
v
}
\{r,u,v\}
{r,u,v}组成的基。视图向量
v
\textbf{v}
v的计算为
v
=
(
c
−
l
)
/
∥
c
−
l
∥
\textbf{v}=(\textbf{c}-\textbf{l})/\|\textbf{c}-\textbf{l}\|
v=(c−l)/∥c−l∥,即从目标到相机的单位向量(向量归一化)。看向“右方”的向量可以由
r
=
−
(
v
×
u
′
)
/
∥
v
×
u
′
∥
\textbf{r}=-(\textbf{v}\times\textbf{u}')/\|\textbf{v}\times\textbf{u}'\|
r=−(v×u′)/∥v×u′∥计算得到。
u
′
\textbf{u}'
u′通常无法保证刚刚好指向物体up方向,因而最终的up向量是另一个叉乘的结果,即
u
=
v
×
r
\textbf{u}=\textbf{v}\times\textbf{r}
u=v×r,它可以确定是归一化的,因为
v
\textbf{v}
v和
r
\textbf{r}
r都是归一化过的,且相互垂直。在我们要构建的相机变换矩阵
M
\textbf{M}
M中,核心思想是要将所有东西连带相机一起移动直到相机和原点位置
(
0
,
0
,
0
)
(0,0,0)
(0,0,0)重合,然后变换基本轴使得
r
\textbf{r}
r与
(
1
,
0
,
0
)
(1,0,0)
(1,0,0)对齐,
u
\textbf{u}
u与
(
0
,
1
,
0
)
(0,1,0)
(0,1,0)对齐,
v
\textbf{v}
v与
(
0
,
0
,
1
)
(0,0,1)
(0,0,1)对齐。如下:
M
=
(
r
x
r
y
r
z
0
u
x
u
y
u
z
0
v
x
v
y
v
z
0
0
0
0
1
)
(
1
0
0
−
t
x
0
1
0
−
t
y
0
0
1
−
t
z
0
0
0
1
)
=
(
r
x
r
y
r
z
−
t
⋅
r
u
x
u
y
u
z
−
t
⋅
u
v
x
v
y
v
z
−
t
⋅
v
0
0
0
1
)
.
(
4.20
)
\textbf{M}=\left(\begin{matrix} r_x&r_y&r_z&0\\ u_x&u_y&u_z&0\\ v_x&v_y&v_z&0\\ 0&0&0&1 \end{matrix}\right) \left(\begin{matrix} 1&0&0&-t_x\\ 0&1&0&-t_y\\ 0&0&1&-t_z\\ 0&0&0&1 \end{matrix}\right)= \left(\begin{matrix} r_x&r_y&r_z&-\textbf{t}·\textbf{r}\\ u_x&u_y&u_z&-\textbf{t}·\textbf{u}\\ v_x&v_y&v_z&-\textbf{t}·\textbf{v}\\ 0&0&0&1 \end{matrix}\right) .\qquad(4.20)
M=⎝⎜⎜⎛rxuxvx0ryuyvy0rzuzvz00001⎠⎟⎟⎞⎝⎜⎜⎛100001000010−tx−ty−tz1⎠⎟⎟⎞=⎝⎜⎜⎛rxuxvx0ryuyvy0rzuzvz0−t⋅r−t⋅u−t⋅v1⎠⎟⎟⎞.(4.20)
图4.5. 计算位于
c
\textbf{c}
c的相机(up向量
u
′
\textbf{u}'
u′)看向
l
\textbf{l}
l所需要的变换。出于这个目的,我们需要去计算
r
\textbf{r}
r、
u
\textbf{u}
u和
v
\textbf{v}
v。
注意当级联平移矩阵和基本轴的变换矩阵时,平移 − t -\textbf{t} −t需要放在最右方,因为它是需要首先被计算进去的。可以用下面的方式去记忆应该将 r \textbf{r} r、 u \textbf{u} u和 v \textbf{v} v中的各个元素放在哪里。我们想要将 r \textbf{r} r变成 ( 1 , 0 , 0 ) (1,0,0) (1,0,0),故而当将基本轴矩阵与 ( 1 , 0 , 0 ) (1,0,0) (1,0,0)相乘时,我们可以知道矩阵中的第一行必须是 r \textbf{r} r中的元素,因为 r ⋅ r = 1 \textbf{r}·\textbf{r}=1 r⋅r=1。进一步,第二行和第三行必须包含垂直于 r \textbf{r} r的向量,即 r ⋅ x = 0 \textbf{r}·\textbf{x}=0 r⋅x=0。同理,对 u \textbf{u} u和 v \textbf{v} v的处理也是一样的。
4.1.7 Normal Transform(法线变换)
一个单独的矩阵可以用来去变换点、线、三角形和其他几何。同样的矩阵也可以用来变换沿着这些线和三角形表面的切向量。然而,然而,这个矩阵总是没有办法被用来去变换一个几何属性,即表面法线(和顶点光照法线)。图4.6中揭示了如果应用这个矩阵的话会发生什么。
图4.6. 左侧的是原始的未发生变换的几何,一个三角形和其法线。中间的图展示了如果我们将模型和其法线应用同一个缩放矩阵(沿着
x
x
x轴缩放0.5)。右侧图中则是正确的效果。
正确的方法应该是使用伴随矩阵的转置,而不是直接乘以矩阵本身。在我们的在线线性代数附录中,有关于如何计算伴随矩阵的内容。伴随矩阵总是存在。法线在变换之后无法被保证依然是单位长度,所以通常需要归一化。
当我们说到法线的变换,一个经典的答案是计算逆矩阵的转置。这个方法通常是管用的。我们不需要完全的逆,然而,它有时也无法创建出来。逆就是伴随矩阵除以原始矩阵的秩。如果这个秩为0的话,则称这个矩阵为奇异矩阵,而其逆也不存在。
计算一个 4 ∗ 4 4*4 4∗4的矩阵的伴随矩阵亦需要很昂贵的代价,通常是没有必要的。因为法线是一个向量,平移不会对其产生影响。进一步,大部分的模型变换是仿射变换。他们不改变齐次坐标中的 w w w元素,即他们不会执行投影。在这些情况下,对于法线变换来说需要做的即是计算左上角 3 ∗ 3 3*3 3∗3部分的伴随矩阵。
通常这个伴随矩阵的计算也不是必须的。我们现在已经知道变换矩阵完全由平移、旋转、缩放操作矩阵的级联构成。平移矩阵不会影响法线。统一缩放因子只会简单得改变法线的长度。剩下的是一系列的旋转,总是产生某种形式的旋转,仅此而已。逆矩阵的转置可以用来变换法线。旋转矩阵的本质是其转置就是其逆。代入得到法线变换,两次转置(或两次求逆)得到原始旋转矩阵。将他们合并,原始的变换本身在这些条件下就也可以直接用来变换法线。
最后,理解法线的计算不是必须的。如果只是平移和缩放级联到一起,在由矩阵变换时法线的长度也不会发生改变,所以并不需要重新变换法线方向。如果统一缩放变换也级联进来,总的缩放因子(如果已知、或者可以由某处进行提取——4.2.4小节)可以直接用来归一化生成的法线。例如,如果我们一致有一系列的缩放比那换应用到物体上使其变大5.2倍,那么由这个矩阵变换后的法线可以让他们除以5.2得到。可选地,为了创建一个可以产生归一化的结果的法线变换矩阵,原始矩阵的左上角 3 ∗ 3 3*3 3∗3部分可以除以一次这个缩放因子。
注意,在变换之后,曲面法线是由三角形中得出(例如,使用三角形边的叉积),在这样的系统中,法线变换并不成为一个问题。切向量与自然法线不同,通常由原始矩阵直接进行变换。
4.1.8 Computation of Inverse(逆矩阵的计算)
在很多情况下我们都需要矩阵的逆,例如,在坐标系之间来回更改时。下面的三种计算矩阵的逆的方法可以用到(具体使用哪一种依据变具体的变换的情况):
- 如果矩阵是一个单独的变换或者是一系列给定参数的简单变换,那么计算过程即是简单得“倒置参数”和矩阵的顺序。例如,如果 M = T ( t ) R ( ϕ ) \textbf{M}=\textbf{T}(\textbf{t})\textbf{R}(\phi) M=T(t)R(ϕ),那么 M − 1 = R ( − ϕ ) T ( − t ) \textbf{M}^{-1}=\textbf{R}(-\phi)\textbf{T}(-\textbf{t}) M−1=R(−ϕ)T(−t)。这既简单同时也保留了变换的准确性,这在渲染一个庞大的世界时非常重要。
- 如果矩阵是正交的,那么 M − 1 = M T \textbf{M}^{-1}=\textbf{M}^T M−1=MT,即转置即是逆。任何序列的旋转依旧是旋转,进而可判断是正交的。
- 如果什么都不知道的情况下,那么可以使用伴随矩阵的方法、克莱默法则(Cramer’s rule),LU分解、或者高斯消除(Gaussian elimination)来计算逆。克莱默法则和伴随矩阵法通常是受欢迎的,因为他们含有较少的分支操作;在现代架构中,“if”测试越少越好,要尽量避免。参考4.1.7小节关于如果应用伴随矩阵来转换法线变换。
在优化问题上,也可以考虑逆计算。例如,如果逆被用来去变换向量,那么只有只有 3 ∗ 3 3*3 3∗3的左上角的部分通常需要倒置(参考前面的小节)。
4.2 Special Matrix Transform and Operations(空间举矩阵变换和操作)
在本节中将会介绍多种在实时图形学发挥重要作用的矩阵变换和操作。首先,我们介绍了欧拉变换(以及它对于参数的提取),这也是一种直观的描述朝向的方式。然后我们讨论从一个单独的矩阵中恢复一组基本变换。最后,我们展示一钟将实体绕任意轴旋转的的方法。
4.2.1 The Euler Transform(欧拉变换)
这个变换是以一个直观的方式来构建矩阵从而以一个特定的方向旋转你自己(即相机)或者其他任意实体。它的名字来自于瑞士数学家Leonhard Euler(1707-1783)。
首先,需要确立默认的视图方向。我们经常定义为它看向
−
z
-z
−z轴的方向,并且头绕着
y
y
y轴旋,参考图4.7。欧拉变换即是三个矩阵的乘积,即图中所示的旋转。通常,这个变换记作
E
\textbf{E}
E,公式为:
E
(
h
,
p
,
r
)
=
R
z
(
r
)
R
x
(
p
)
R
y
(
h
)
.
(
4.21
)
\textbf{E}(h,p,r)=\textbf{R}_z(r)\textbf{R}_x(p)\textbf{R}_y(h).\qquad(4.21)
E(h,p,r)=Rz(r)Rx(p)Ry(h).(4.21)
图4.7. 欧拉变换,以及他和你改变head,pitch和roll角度的方式的关联。图中所示即是默认的视图方向,看向
−
z
-z
−z轴,up方向沿着
y
y
y轴。
矩阵的顺序可以以24种不同的方式排列;我们展示的这种是比较常用的一种。因为 E \textbf{E} E是旋转矩阵的级联,所以很明确它就是正交的。故而它的逆可以表示为 E − 1 = E T = ( R z R x R y ) T = ( R y T R x T R z T ) \textbf{E}^{-1}=\textbf{E}^T=(\textbf{R}_z\textbf{R}_x\textbf{R}_y)^T=(\textbf{R}^T_y\textbf{R}^T_x\textbf{R}^T_z) E−1=ET=(RzRxRy)T=(RyTRxTRzT),当然,直接使用 E \textbf{E} E的转置也很简单。
欧拉角h,p和r分别代表着head、pitch和roll需要绕其自身轴所旋转的角度的大小。有些时候这些角度也会被称为是roll,例如,head可以称为是y-roll,pitch可以称为是x-roll。另外,head还有一个别称是yaw,尤其是在飞行模拟中。
这个变换非常直观且容易以外行能听懂的语言去交流。例如,改变head角度使得视者晃动他们的脑袋,改变pitch角度即意味着让他们点头,而改变roll则会让他们将脑袋向两侧倾斜。在这里,我们使用“改变head,pitch和roll”这种说法,而不是“绕着x、y和z轴的旋转”这种说法。注意这个变换不仅能够变换相机,同时也能够变换任何物体对象或是实体。这些变换可以使用世界空间内容全局坐标轴,或者是使用局部参考框架。
需要着重注意的是,一些欧拉角的表示中会让z轴作为初始的up方向。不同之处就只是单单符号上的不同,当然潜在地也会造成一定的混淆。在计算机图形学中依据观察世界的方式进而如何去构建世界中的内容划分出两种方式:y-up和z-up。大多数制造业流程包括3D打印,是在世界空间中将z方向作为up方向;航空和海洋载具会将-z作为up方向。建筑和GIS通常使用z-up,因为一个建筑图纸是二维的(x和y)。媒体相关的建模系统经常在世界坐标系内将y方向作为up方向,符合我们在计算机图形学中对相机屏幕up方向的描述。这两种up向量的不同选择不仅仅是一个90度旋转的区别,在实际中如果不知道是采用的哪个标准则会产生很多问题。除非有特殊说明,在本书中我们使用y-up。
我们也想要指出相机在其观察空间中的up方向和世界的up方向没有任何特殊的关系。晃动你的脑袋(roll),视图也随之倾斜,而它世界空间的uo方向就与世界的up方向不同了。正如另外一个例子,说世界使用y-up而我们的相机直直地看向地面,即以一个鸟的视角。这个朝向意味着相机需要正向改变pitch90度,从而使它的世界空间中的up方向为 ( 0 , 0 , − 1 ) (0,0,-1) (0,0,−1)。在这个方向上,相机没有y组件,而在世界空间中使-z成为了up,但是y-up在它的观察空间中依然成立。
虽然欧拉角在一些小角度改变上很有用处,但是它还是存在一些严重的限制。在将两组欧拉角组合使用时会非常困难。例如,在两组之间进行插值不是简单得对灭个角进行插值。实际上,两组不同的欧拉角可以给出同样的朝向。这就是使用替代的方向表示(例如四元数,本章的后面一段会讨论到)的其中一些原因。欧拉角的另一个问题就是万向锁(gimbal lock),4.2.2小节中将会详细解释。
4.2.2 Extracting Parameters from the Euler Transform(从欧拉变换中提取参数)
在一些情况下,将欧拉变换中的参数(h,p和r)从正交矩阵中提取出来是一个很有用的操作。如公式4.22中所示:
E
(
h
,
p
,
r
)
=
(
e
00
e
01
e
02
e
10
e
11
e
12
e
20
e
21
e
22
)
=
R
z
(
r
)
R
x
(
p
)
R
y
(
h
)
.
(
4.22
)
\textbf{E}(h,p,r)=\left(\begin{matrix} e_{00}&e_{01}&e_{02}\\ e_{10}&e_{11}&e_{12}\\ e_{20}&e_{21}&e_{22}\\ \end{matrix}\right) = \textbf{R}_z(r)\textbf{R}_x(p)\textbf{R}_y(h). \qquad(4.22)
E(h,p,r)=⎝⎛e00e10e20e01e11e21e02e12e22⎠⎞=Rz(r)Rx(p)Ry(h).(4.22)
这里我们放弃使用了 4 ∗ 4 4*4 4∗4的矩阵而采用了 3 ∗ 3 3*3 3∗3的矩阵,因为后者提供了旋转所需的所有必要信息。即同等的 4 ∗ 4 4*4 4∗4矩阵只是多了最后一行(最有边是1,其余都是0)。
将三个旋转矩阵进行级联后可得到:
E
(
h
,
p
,
r
)
=
(
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
)
.
(
4.23
)
\textbf{E}(h,p,r)=\left(\begin{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} \end{matrix}\right).\qquad(4.23)
E(h,p,r)=⎝⎛cosrcosh−sinrsinpsinhsinrcosh−cosrsinpsinh−cospsinh−sinrcospcosrcospsinpcosrsinh+sinrsinpcoshsinrsinh−cosrsinpcoshcospcosh⎠⎞.(4.23)
从中可以看出,pitch可以由
sin
p
=
e
21
\sin{p} = e_{21}
sinp=e21得出。另外,另外,将
e
01
e_{01}
e01除以
e
11
e_{11}
e11,将
e
20
e_{20}
e20除以
e
22
e_{22}
e22可以得到下面的公式,从而从中分离出head和roll参数:
e
01
e
11
=
−
sin
r
cos
r
=
−
tan
r
a
n
d
e
20
e
22
=
−
sin
h
cos
h
=
−
tan
h
(
4.24
)
\frac{e_{01}}{e_{11}}=\frac{-\sin{r}}{\cos{r}}=-\tan{r}\quad and\quad \frac{e_{20}}{e_{22}}=\frac{-\sin{h}}{\cos{h}}=-\tan{h}\qquad(4.24)
e11e01=cosr−sinr=−tanrande22e20=cosh−sinh=−tanh(4.24)
因此,通过使用
atan2(y,x)
\text{atan2(y,x)}
atan2(y,x)函数(参考第一章)就可以把欧拉参数h(head),p(pitch)和r(roll)从矩阵
E
\textbf{E}
E中提取出来,公式如下:
h
=
atan2
(
−
e
20
,
e
22
)
,
p
=
arcsin
(
e
21
)
,
r
=
atan2
(
−
e
01
,
e
11
)
.
(
4.25
)
h=\text{atan2}(-e_{20},e_{22}),\\ p=\text{arcsin}(e_{21}),\\ r=\text{atan2}(-e_{01},e_{11}).\qquad(4.25)
h=atan2(−e20,e22),p=arcsin(e21),r=atan2(−e01,e11).(4.25)
但是注意,还有一种特殊情况需要我们特殊处理。如果
cos
p
=
0
\cos{p}=0
cosp=0,我们就会遇到万向锁(4.2.2小节中提到的),此时旋转角r和h将会绕着同一个轴旋转(尽管可能是以相反方向,取决于p旋转角是
−
π
/
2
-\pi/2
−π/2还是
π
/
2
\pi/2
π/2),因此只需要获取一个角度就可以了。如果我们随意地设置
h
=
0
h=0
h=0,我们可以得到:
E
(
h
,
p
,
r
)
=
(
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
cos
h
)
.
(
4.26
)
\textbf{E}(h,p,r)=\left(\begin{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}\cos{h} \end{matrix}\right).\qquad(4.26)
E(h,p,r)=⎝⎛cosrsinr0−sinrcospcosrcospsinpsinrsinp−cosrsinpcospcosh⎠⎞.(4.26)
(译者注:原书中第一行第二列没有负号,但是根据4.23公式推导而来的话需要负号,故进行了补充修改)
因为 p p p不会影响第一列的值,当 cos p = 0 \cos{p}=0 cosp=0时我们可以使用 sin r / cos r = tan r = e 10 / e 00 \sin{r}/\cos{r}=\tan{r}=e_{10}/e_{00} sinr/cosr=tanr=e10/e00,其中 r = atan2 ( e 10 , e 00 ) r=\text{atan2}(e_{10},e_{00}) r=atan2(e10,e00)。
注意arcsin的定义,即有 − π / 2 ≤ p ≤ π / 2 -\pi/2\leq p\leq \pi/2 −π/2≤p≤π/2,这意味如果 E \textbf{E} E中的 p p p不在这个范围之内的话,参数的提取就会出现问题。 h h h, p p p和 r r r不是唯一的意味着多组不同的欧拉参数可以返回相同的变换结果。关于欧拉角变换的更多内容可以参考Shoemake在1994年的文章。上述的简单的方法可能会导致数值不稳定性问题,以一定的速度为代价的话是可以避免这个问题的。
当你使用欧拉变换,会产生万向锁(gimbal lock)的问题。这是在旋转特定角度时丢失掉一个自由度。例如,假设变换顺序为 x / y / z x/y/z x/y/z。想象一下先只绕着 y y y轴旋转 π / 2 \pi/2 π/2,然后再执行第二个旋转。这样做会使得本地 z z z轴与原先的 x x x轴重合,从而使最后绕着 z z z的旋转变得多余。
数学上,我们在公式4.26中依旧见过了万向锁,其中我们假设 cos p = 0 \cos{p}=0 cosp=0,即 p = ± π / 2 + 2 π k p=\pm\pi/2+2\pi k p=±π/2+2πk,其中 k k k为整数。在这个情况下,我们拾取了一个自由度因为矩阵仅仅依赖于一个角度, r + h r+h r+h或 r − h r-h r−h(两者其中之一)。
虽然欧拉角通常在模型系统中是以 x / y / z x/y/z x/y/z的顺序(每个轴一个旋转)表示,其他的顺序也是可行的。例如, z / x / y z/x/y z/x/y使用在动画中, z / x / z z/x/z z/x/z使用在动画和物理中。所有的方式都可以用来区分三个单独的旋转。最后一种顺序,z/x/z,在某些应用中可能更为优越,因为只有在绕 x x x旋转 π \pi π 时万向锁才会发生。并没有完全能够避免万向锁的情况。尽管如此,欧拉角仍然是常用的,因为动画师更喜欢使用曲线编辑器来指定角度如何随时间变化。
示例:约束变换。试想你拿着一个扳手在拧螺栓。为了将螺栓拧进去,你需要将扳手绕着 x x x轴旋转。此时,假设你的输入设备(鼠标,VR手套,等等)给你一个旋转矩阵,即一个特定的旋转,以控制扳手的移动。问题是应用这个变换到扳手上可能是不太合适的,因为它书需要绕 x x x轴进行旋转。为了限制输入矩阵, P \textbf{P} P,令其仅仅绕着 x x x轴旋转,使用本节中描述的方法将欧拉角( h h h, p p p和 r r r)简单地提取出来,然后创建一个新的矩阵 R x ( p ) \textbf{R}_x(p) Rx(p)。这就得到了所寻求的变换,它将绕 x x x轴旋转扳手(如果 P \textbf{P} P现在包含这样的移动的话)。
4.2.3 Matrix Decomposition(矩阵分解)
到现在为止我们一直假设我们知道我们使用的变换矩阵的初始状态和整个变换过程。但通常来说这并不是我们需要经常处理的情况。例如,变换物体只关联着一个级联的矩阵。从一个级联矩阵中获得多个变换的工作就称为了矩阵分解(matrix decomposition)。
去获取这组变换其实有很多原因,包括:
- 从一个对象中提取出缩放因子。
- 为一个特殊的系统寻找到需要的变换。(例如,一些系统可能并不允许任意 4 ∗ 4 4*4 4∗4矩阵的使用)
- 判断模型是否只经过刚体变换。
- 在动画的关键帧间进行插值,其中只有物体的矩阵是已知的。
- 从一个旋转矩阵中移除掉剪切。
我们已经展示了两种分解,即从平移和旋转变换推到刚体变换(4.1.6小节),以及从正交变换推演欧拉角(4.2.2小节)。
正如我们所见,去获取平移矩阵无价值的,因为我们只是简单得需要 4 ∗ 4 4*4 4∗4矩阵里的最后一列的元素。我们也可以依据矩阵的秩是否是负值判断是否有反射出现。将旋转,缩放和剪切分离出来则需要更多的努力。
幸运的是,关于这个课题有一些相关文章,以及网上的一些代码。Thomas和Goldman分别介绍了一些不同的针对各类变换的方法。Shoemake针对仿射矩阵提示了相关技术,因为他的算法相对于参考帧是独立的并且尝试去分解矩阵以获取到刚体变换。
4.2.4 Rotation about an Arbitrary Axis(绕任意轴的旋转)
有时,拥有一个能够使实体绕任意轴旋转一定角度的流程是非常方便的。假设旋转轴, r \textbf{r} r,已经归一化过,并且假设需要创建一个绕 r \textbf{r} r旋转 α \alpha α的变换。
为了做到这个,我们首先要变换到一个空间,这个空间里我们想要绕其旋转的轴为
x
x
x轴。这可以通过一个旋转矩阵
M
\textbf{M}
M来实现。然后执行真正的旋转变换,最后再应用
M
−
1
\textbf{M}^{-1}
M−1变换回去。这个流程可以参照图4.8。
图4.8 绕任意轴
r
\textbf{r}
r的旋转是通过找到由
r
\textbf{r}
r,
s
\textbf{s}
s和
t
构
建
的
基
本
正
交
坐
标
系
。
然
后
我
们
将
这
些
基
轴
和
标
准
基
轴
对
其
,
从
而
使
\textbf{t}构建的基本正交坐标系。然后我们将这些基轴和标准基轴对其,从而使
t构建的基本正交坐标系。然后我们将这些基轴和标准基轴对其,从而使
r
\textbf{r}
r和
x
x
x轴重合。然后绕
x
x
x轴进行旋转,最后再变换回去即可。
(实际上,任何一个非零分量都可以被取反)。数学上,这表示为:
s
ˉ
=
{
(
0
,
−
r
z
,
r
y
)
,
if
∣
r
x
∣
≤
∣
r
y
∣
a
n
d
∣
r
x
∣
≤
∣
r
z
∣
,
(
−
r
z
,
0
,
r
x
)
,
if
∣
r
y
∣
≤
∣
r
x
∣
a
n
d
∣
r
y
∣
≤
∣
r
z
∣
,
(
−
r
y
,
r
x
,
0
)
,
if
∣
r
z
∣
≤
∣
r
x
∣
a
n
d
∣
r
z
∣
≤
∣
r
y
∣
,
s
=
s
ˉ
/
∣
∣
s
ˉ
∣
∣
,
t
=
r
×
s
.
(
4.27
)
\bar{\textbf{s}}=\left\{ \begin{aligned} (0,-r_z,r_y),\text{if}\quad |r_x| \leq |r_y| \quad and\quad |r_x|\leq |r_z|, \\ (-r_z,0,r_x),\text{if}\quad |r_y| \leq |r_x| \quad and\quad |r_y|\leq |r_z|,\\ (-r_y,r_x,0),\text{if}\quad |r_z| \leq |r_x| \quad and\quad |r_z|\leq |r_y|, \end{aligned} \right.\\ \textbf{s} = \bar{\textbf{s}}/||\bar{\textbf{s}}||,\\ \textbf{t}=\textbf{r}\times\textbf{s}.\qquad(4.27)
sˉ=⎩⎪⎨⎪⎧(0,−rz,ry),if∣rx∣≤∣ry∣and∣rx∣≤∣rz∣,(−rz,0,rx),if∣ry∣≤∣rx∣and∣ry∣≤∣rz∣,(−ry,rx,0),if∣rz∣≤∣rx∣and∣rz∣≤∣ry∣,s=sˉ/∣∣sˉ∣∣,t=r×s.(4.27)
这保障了
s
ˉ
\bar{\textbf{s}}
sˉ对于
r
\textbf{r}
r是正交的(垂直的),并且
r
,
s
,
t
\textbf{r},\textbf{s},\textbf{t}
r,s,t是正交基轴。Frisvad在代码种展示了一种没有用到分支的方法,这种方法更加快速但是丢失了一定的精度。Max个Duff及其同事们在Frisvad的方法基础上提升了精度。不论是应用哪一种技术,总是用这三个向量来构建旋转矩阵:
M
=
(
r
T
s
T
t
T
)
.
(
4.28
)
\textbf{M}=\left(\begin{matrix} \textbf{r}^T\\ \textbf{s}^T\\ \textbf{t}^T \end{matrix}\right).\qquad(4.28)
M=⎝⎛rTsTtT⎠⎞.(4.28)
这个矩阵将向量
r
\textbf{r}
r变换到
x
x
x轴,向量
s
\textbf{s}
s变换到
y
y
y轴,向量
t
\textbf{t}
t变换到
z
z
z轴。因此,最终的绕
r
\textbf{r}
r旋转
α
\alpha
α的变换为:
X
=
M
T
R
x
(
α
)
M
.
(
4.29
)
\textbf{X}=\textbf{M}^T\textbf{R}_x(\alpha)\textbf{M}.\qquad(4.29)
X=MTRx(α)M.(4.29)
简单来说,这意味着首先我们使用 M \textbf{M} M进行变换从而令 r \textbf{r} r变成 x x x轴,然后我们使用 R x ( α ) \textbf{R}_x(\alpha) Rx(α)绕 x x x轴旋转 α \alpha α,然后我们使用 M \textbf{M} M的逆变换回去,即 M T \textbf{M}^T MT,因为 M \textbf{M} M为正交矩阵。
另一个绕任意轴旋转的方法由Goldman引入。这里,我们简单得展示一下他的变换:
R
=
(
cos
ϕ
+
(
1
−
cos
ϕ
)
r
x
2
(
1
−
cos
ϕ
)
r
x
r
y
−
r
z
sin
ϕ
(
1
−
cos
ϕ
)
r
x
r
z
+
r
y
sin
ϕ
(
1
−
cos
ϕ
)
r
x
r
y
+
r
z
sin
ϕ
cos
ϕ
+
(
1
−
cos
ϕ
)
r
y
2
(
1
−
cos
ϕ
)
r
y
r
z
−
r
x
sin
ϕ
(
1
−
cos
ϕ
)
r
x
r
z
−
r
y
sin
ϕ
(
1
−
cos
ϕ
)
r
y
r
z
+
r
x
sin
ϕ
cos
ϕ
+
(
1
−
cos
ϕ
)
r
z
2
)
.
(
4.30
)
\textbf{R}=\left(\begin{matrix} \cos{\phi}+(1-\cos{\phi})r_x^2 & (1-\cos{\phi})r_xr_y-r_z\sin{\phi} & (1-\cos{\phi})r_xr_z + r_y\sin{\phi} \\ (1-\cos{\phi})r_xr_y + r_z\sin{\phi} & \cos{\phi}+(1-\cos{\phi})r_y^2 & (1-\cos{\phi})r_yr_z-r_x\sin{\phi} \\ (1-\cos{\phi})r_xr_z - r_y\sin{\phi} & (1-\cos{\phi})r_yr_z+r_x\sin{\phi} & \cos{\phi}+(1-\cos{\phi})r_z^2 \end{matrix}\right).\qquad(4.30)
R=⎝⎛cosϕ+(1−cosϕ)rx2(1−cosϕ)rxry+rzsinϕ(1−cosϕ)rxrz−rysinϕ(1−cosϕ)rxry−rzsinϕcosϕ+(1−cosϕ)ry2(1−cosϕ)ryrz+rxsinϕ(1−cosϕ)rxrz+rysinϕ(1−cosϕ)ryrz−rxsinϕcosϕ+(1−cosϕ)rz2⎠⎞.(4.30)
在4.3.2小节中,我们也展示了另一种解决这个问题的方法(应用四元数)。同一,那一小节中还展示了很多更加高效的针对类似问题的算法,如从一个向量旋转到另一个向量。
4.3 Quaternions(四元数)
尽管四元数早在1843年就由Willian Roman Hamilton作为复数的扩展而发明出来,但是直到1985年才由Shoemake将他们引入到计算机图形学领域中。四元数被用来表示旋转和朝向。它在一些方面比欧拉角和矩阵都要更加优越。任何三维的方向都可以表示为一个单独的绕某个特定轴的旋转。给定这个轴和角度的表示,向四元数转换或从四元数转换是很简单的,而向任一方向做欧拉角转换可能是很有挑战性的。四元数可以用作方向的平滑且稳定的插值,而使用欧拉角则很难完美做到。
复数拥有实部和虚部。他们由两个实数构成,且第二个实数需要乘以 − 1 \sqrt{-1} −1。简单来说,四元数有四个部分。前三个部分非常近似于旋转轴,同时旋转角影响着全部四个部分(更多请参见4.3.2小节)。每个四元数由四个实数表示,每一个都和不同的部分相关联。由于四元数有一个分量,我们选择将它表示为向量,但是为了区分,我们在他们上面加了个帽子: q ^ \hat{\textbf{q}} q^。下面,我们首先介绍一些四元数的相关数学背景,然后应用他们来构建各种各样有用的变换。
4.3.1 Mathematical Background(数学背景)
我们首先开始四元数的定义的介绍。
定义。四元数
q
^
\hat{\textbf{q}}
q^可以以一下的方式进行定义,他们全部是等价的。
q
^
=
(
q
v
,
q
w
)
=
i
q
x
+
j
q
y
+
k
q
z
+
q
w
=
q
v
+
q
w
,
q
v
=
i
q
x
+
j
q
y
+
l
q
z
=
(
q
x
,
q
y
,
q
z
)
,
i
2
=
j
2
=
k
2
=
−
1
,
j
k
=
−
k
j
=
i
,
k
i
=
−
i
k
=
j
,
i
j
=
−
j
i
=
k
.
(
4.31
)
\hat{\textbf{q}}=(\textbf{q}_v,q_w)=iq_x+jq_y+kq_z+q_w=\textbf{q}_v+q_w,\\ \textbf{q}_v=iq_x+jq_y+lq_z=(q_x,q_y,q_z),\\ i^2=j^2=k^2=-1,jk=-kj=i,ki=-ik=j,ij=-ji=k. \qquad(4.31)
q^=(qv,qw)=iqx+jqy+kqz+qw=qv+qw,qv=iqx+jqy+lqz=(qx,qy,qz),i2=j2=k2=−1,jk=−kj=i,ki=−ik=j,ij=−ji=k.(4.31)
变量 q w q_w qw是四元数 q ^ \hat{\textbf{q}} q^的实部。虚部是 q v \textbf{q}_v qv,而 i i i、 j j j和 k k k被成为虚数单位。
对于虚部
q
v
\textbf{q}_v
qv来说,我们可以使用所有的法向量操作,例如加法、缩放、点乘、叉乘以及其他。应用四元数的定义,两个四元数的乘法操作可以表示如下。注意虚数单位的乘法是不满足交换律的。
乘法:
q
^
r
^
=
(
i
q
x
+
j
q
y
+
k
q
z
+
q
w
)
(
i
r
x
+
j
r
y
+
k
r
z
+
r
w
)
=
i
(
q
y
r
z
−
q
z
r
y
+
r
w
q
x
+
q
w
r
x
)
+
j
(
q
z
r
x
−
q
x
r
z
+
r
w
q
y
+
q
w
r
y
)
+
k
(
q
x
r
y
−
q
y
r
x
+
r
w
q
z
+
q
w
r
z
)
+
q
w
r
w
−
q
x
r
x
−
q
y
r
y
−
q
z
r
z
=
(
q
v
×
r
v
+
r
w
q
v
+
q
w
r
v
,
q
w
r
w
−
q
v
⋅
r
v
)
。
(
4.32
)
\begin{aligned}\hat{\textbf{q}}\hat{\textbf{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&\\ &=(\textbf{q}_v\times\textbf{r}_v+r_w\textbf{q}_v+q_w\textbf{r}_v,q_wr_w-\textbf{q}_v·\textbf{r}_v)。 \end{aligned}\qquad(4.32)
q^r^=(iqx+jqy+kqz+qw)(irx+jry+krz+rw)=i(qyrz−qzry+rwqx+qwrx)+j(qzrx−qxrz+rwqy+qwry)+k(qxry−qyrx+rwqz+qwrz)+qwrw−qxrx−qyry−qzrz=(qv×rv+rwqv+qwrv,qwrw−qv⋅rv)。(4.32)
从上面公式中可以看到,我们同时使用了点乘和叉乘来完成四元数的乘法运算。
随着四元数的定义,随之也需要定义四元数的加法、共轭、模以及单位四元数:
加法Addition:
q
^
+
r
^
=
(
q
v
,
q
w
)
+
(
r
v
,
r
w
)
=
(
q
v
+
r
v
,
q
w
+
r
w
)
.
\hat{\textbf{q}}+\hat{\textbf{r}}=(\textbf{q}_v,q_w)+(\textbf{r}_v,\textbf{r}_w)=(\textbf{q}_v+\textbf{r}_v,q_w+r_w).
q^+r^=(qv,qw)+(rv,rw)=(qv+rv,qw+rw).
共轭Conjugate:
q
^
∗
=
(
q
v
,
q
w
)
∗
=
(
−
q
v
,
q
w
)
.
\hat{\textbf{q}}^*=(\textbf{q}_v,q_w)^*=(-\textbf{q}_v,q_w).
q^∗=(qv,qw)∗=(−qv,qw).
模Norm:
n
(
q
^
)
=
q
^
q
^
∗
=
q
^
∗
q
^
=
q
v
∗
q
v
+
q
w
2
=
q
x
2
+
q
y
2
+
q
z
2
+
q
w
2
.
(
4.33
)
n(\hat{\textbf{q}})=\sqrt{\hat{\textbf{q}}\hat{\textbf{q}}^*}=\sqrt{\hat{\textbf{q}}*\hat{\textbf{q}}}=\sqrt{\textbf{q}_v*\textbf{q}_v+q_w^2}=\sqrt{q_x^2+q_y^2+q_z^2+q_w^2}.\qquad(4.33)
n(q^)=q^q^∗=q^∗q^=qv∗qv+qw2=qx2+qy2+qz2+qw2.(4.33)
单位四元数Identity:
i
^
=
(
0
,
1
)
.
\hat{\textbf{i}}=(\textbf{0},1).
i^=(0,1).
当
n
(
q
^
)
=
q
^
q
^
∗
n(\hat{\textbf{q}})=\sqrt{\hat{\textbf{q}}\hat{\textbf{q}}^*}
n(q^)=q^q^∗化简之后(即上面的结果),虚部被消掉而实部保留了下来。模通常表示为
∣
∣
q
^
=
n
(
q
^
)
∣
∣
||\hat{\textbf{q}}=n(\hat{\textbf{q}})||
∣∣q^=n(q^)∣∣。可以推导出来,上述的结果是一个倒数,表示为
q
^
−
1
\hat{\textbf{q}}^{-1}
q^−1。逆需要满足
q
^
−
1
q
^
=
q
^
q
^
−
1
=
1
\hat{\textbf{q}}^{-1}\hat{\textbf{q}}=\hat{\textbf{q}}\hat{\textbf{q}}^{-1}=1
q^−1q^=q^q^−1=1(是倒数的基本性质)。我们从模的定义可以推导出:
n
(
q
^
)
2
=
q
^
q
^
∗
⟺
q
^
q
^
∗
n
(
q
^
)
2
=
1.
(
4.34
)
n(\hat{\textbf{q}})^2=\hat{\textbf{q}}\hat{\textbf{q}}^*\Longleftrightarrow\frac{\hat{\textbf{q}}\hat{\textbf{q}}^*}{n(\hat{\textbf{q}})^2} =1.\qquad(4.34)
n(q^)2=q^q^∗⟺n(q^)2q^q^∗=1.(4.34)
从而可以从中推导出下面的倒数:
逆Inverse:
q
^
−
1
=
1
n
(
q
^
)
2
=
1.
(
4.35
)
\qquad\hat{\textbf{q}}^{-1}=\frac{1}{n(\hat\textbf{q})^2}=1.\qquad(4.35)
q^−1=n(q^)21=1.(4.35)
求逆公式使用到了向量的标量乘法,它是从公式4.3.1推导出来的:
s
q
^
=
(
0
,
s
)
(
q
v
,
q
w
)
=
(
s
q
v
,
s
q
w
)
s\hat{\textbf{q}}=(\textbf{0},s)(\textbf{q}_v,q_w)=(s\textbf{q}_v,sq_w)
sq^=(0,s)(qv,qw)=(sqv,sqw),
q
^
s
=
(
q
v
,
q
w
)
(
0
,
s
)
=
(
s
q
v
,
s
q
w
)
\hat{\textbf{q}}s=(\textbf{q}_v,q_w)(\textbf{0},s)=(s\textbf{q}_v,sq_w)
q^s=(qv,qw)(0,s)=(sqv,sqw),这也意味着标量乘法是满足交换律的:
s
q
^
=
q
^
s
=
(
s
q
v
,
s
q
w
)
s\hat{\textbf{q}}=\hat{\textbf{q}}s=(s\textbf{q}_v,sq_w)
sq^=q^s=(sqv,sqw)。
下面的公式是对定义的简单推导:
共轭法则Conjugate rules:
(
q
^
∗
)
∗
=
q
^
,
(
q
^
+
r
^
)
∗
=
q
^
∗
+
r
^
∗
,
(
4.36
)
(
q
^
r
^
)
∗
=
r
^
∗
q
^
∗
.
(\hat{\textbf{q}}^*)^*=\hat{\textbf{q}},\\ (\hat{\textbf{q}}+\hat{\textbf{r}})^*=\hat{\textbf{q}}^*+\hat{\textbf{r}}^*,\qquad(4.36)\\ (\hat{\textbf{q}}\hat{\textbf{r}})^*=\hat{\textbf{r}}^*\hat{\textbf{q}}^*.
(q^∗)∗=q^,(q^+r^)∗=q^∗+r^∗,(4.36)(q^r^)∗=r^∗q^∗.
模法则Norm rules:
n
(
q
^
∗
)
=
n
(
q
^
)
n
(
q
^
r
^
)
=
n
(
q
^
)
n
(
r
^
)
.
(
4.37
)
n(\hat{\textbf{q}}^*)=n(\hat{\textbf{q}})\\ n(\hat{\textbf{q}}\hat{\textbf{r}})=n(\hat{\textbf{q}})n(\hat{\textbf{r}}).\qquad(4.37)
n(q^∗)=n(q^)n(q^r^)=n(q^)n(r^).(4.37)
乘法Laws of Multiplication:
线性Linearity:
p
^
(
s
q
^
+
t
r
^
)
=
s
p
^
q
^
+
t
p
^
r
^
,
(
s
p
^
+
t
q
^
)
r
^
=
s
p
^
r
^
+
t
q
^
r
^
.
(
4.38
)
\hat{\textbf{p}}(s\hat{\textbf{q}}+t\hat{\textbf{r}})=s\hat{\textbf{p}}\hat{\textbf{q}}+t\hat{\textbf{p}}\hat{\textbf{r}},\\ (s\hat{\textbf{p}}+t\hat{\textbf{q}})\hat{\textbf{r}}=s\hat{\textbf{p}}\hat{\textbf{r}}+t\hat{\textbf{q}}\hat{\textbf{r}}.\qquad(4.38)
p^(sq^+tr^)=sp^q^+tp^r^,(sp^+tq^)r^=sp^r^+tq^r^.(4.38)
结合律Associativity:
p
^
(
q
^
r
^
)
=
(
p
^
q
^
)
r
^
.
\hat{\textbf{p}}(\hat{\textbf{q}}\hat{\textbf{r}})=(\hat{\textbf{p}}\hat{\textbf{q}})\hat{\textbf{r}}.
p^(q^r^)=(p^q^)r^.
对单位四元数,
q
^
=
(
q
v
,
q
w
)
\hat{\textbf{q}}=(\textbf{q}_v,q_w)
q^=(qv,qw),有
n
(
q
^
)
=
1
n(\hat{\textbf{q}})=1
n(q^)=1。进而
q
^
\hat{\textbf{q}}
q^可以写作
q
^
=
(
sin
ϕ
u
q
,
cos
ϕ
)
=
sin
ϕ
u
q
+
cos
ϕ
,
(
4.39
)
\hat{\textbf{q}}=(\sin{\phi\textbf{u}_q,\cos{\phi}})=\sin{\phi\textbf{u}_q+\cos{\phi}},\qquad(4.39)
q^=(sinϕuq,cosϕ)=sinϕuq+cosϕ,(4.39)
对三维向量
u
q
\textbf{u}_q
uq,有
∣
∣
u
q
∣
∣
=
1
||\textbf{u}_q||=1
∣∣uq∣∣=1,因为当且仅当
u
q
⋅
u
q
=
1
=
∣
∣
u
q
∣
∣
2
\textbf{u}_q·\textbf{u}_q=1=||\textbf{u}_q||^2
uq⋅uq=1=∣∣uq∣∣2
n
(
q
^
)
=
n
(
sin
ϕ
u
q
,
cos
ϕ
)
=
sin
2
ϕ
(
u
q
⋅
u
q
)
+
cos
2
ϕ
=
sin
2
ϕ
+
cos
2
ϕ
=
1
(
4.40
)
n(\hat{\textbf{q}})=n(\sin{\phi\textbf{u}_q,\cos{\phi}})=\sqrt{\sin^2{\phi(\textbf{u}_q·\textbf{u}_q)+\cos^2{\phi}}}=\sqrt{\sin^2{\phi+\cos^2{\phi}}}=1\qquad(4.40)
n(q^)=n(sinϕuq,cosϕ)=sin2ϕ(uq⋅uq)+cos2ϕ=sin2ϕ+cos2ϕ=1(4.40)
在下一节当中我们将会看到单位四元数对于创建旋转和方向是及其高效的。但是在那之前,还需要介绍一些额外的单位四元数的操作。
对于复数,一个二维的单位向量可以写作
cos
ϕ
+
i
sin
ϕ
=
e
i
ϕ
\cos{\phi}+i\sin{\phi}=e^{i\phi}
cosϕ+isinϕ=eiϕ。对应在四元数中就是
q
^
=
sin
ϕ
u
q
+
cos
ϕ
=
e
ϕ
u
q
(
4.41
)
\hat{\textbf{q}}=\sin{\phi}\textbf{u}_q+\cos{\phi}=e^{\phi\textbf{u}_q}\qquad(4.41)
q^=sinϕuq+cosϕ=eϕuq(4.41)
四元数的对数和幂运算如下公式:
对数运算Logarithm:
log
(
q
^
)
=
log
(
e
ϕ
u
q
)
=
ϕ
u
q
,
\log({\hat{\textbf{q}})}=\log(e^{\phi\textbf{u}_q})=\phi\textbf{u}_q,
log(q^)=log(eϕuq)=ϕuq,
幂运算Power:
q
^
t
=
(
sin
ϕ
u
q
+
cos
ϕ
)
t
=
e
ϕ
t
u
q
=
sin
(
ϕ
t
)
u
q
+
cos
(
ϕ
t
)
.
(
4.42
)
\hat{\textbf{q}}^t=(\sin{\phi\textbf{u}_q+\cos{\phi}})^t=e^{\phi t\textbf{u}_q}=\sin{(\phi t)\textbf{u}_q+\cos{(\phi t)}}. \qquad(4.42)
q^t=(sinϕuq+cosϕ)t=eϕtuq=sin(ϕt)uq+cos(ϕt).(4.42)
4.3.2 Quaternion Transform(四元数变换)
我们现在要探讨的是四元数的一个子类,即单位长度的四元数,称为单位四元数(unit quaternion)。单位四元数的一个最重要的特性是他们可以表示三维旋转,同时这种表示方法也是及其准确和简单的。
现在我们要解释是什么使得单位四元数在旋转和朝向上是如此得有用。首先,将一个点的四个坐标或者是向量
p
=
(
p
x
p
y
p
z
p
w
)
T
\textbf{p}=(p_x\ p_y\ p_z\ p_w)^T
p=(px py pz pw)T传入到四元数
p
^
\hat{\textbf{p}}
p^的分量中,并且假设我们有一个单位四元数
q
^
=
(
sin
ϕ
u
q
,
cos
ϕ
)
\hat{\textbf{q}}=(\sin{\phi\textbf{u}_q},\cos{\phi})
q^=(sinϕuq,cosϕ)。可以知道
q
^
p
^
q
^
−
1
(
4.43
)
\hat{\textbf{q}}\hat{\textbf{p}}\hat{\textbf{q}}^{-1}\qquad (4.43)
q^p^q^−1(4.43)
将
p
^
\hat{\textbf{p}}
p^(点
p
\textbf{p}
p)绕着轴
u
q
\textbf{u}_q
uq旋转了
2
ϕ
2\phi
2ϕ。注意因为
q
^
\hat{\textbf{q}}
q^是单位四元数,进而有
q
^
−
1
=
q
^
∗
\hat{\textbf{q}}^{-1}=\hat{\textbf{q}}^*
q^−1=q^∗。参考图4.9。
图4.9 由单位四元数(
q
^
=
(
sin
ϕ
u
q
,
cos
ϕ
)
\hat{\textbf{q}}=(\sin{\phi\textbf{u}_q},\cos{\phi})
q^=(sinϕuq,cosϕ))表示的旋转变换。变换是绕着轴
u
q
\textbf{u}_q
uq旋转了
2
ϕ
2\phi
2ϕ。
任意
q
^
\hat{\textbf{q}}
q^的非零倍数也表示相同的变换,这意味着
q
^
\hat{\textbf{q}}
q^和
−
q
^
-\hat{\textbf{q}}
−q^表示相同的旋转。其本质是,对轴
u
q
\textbf{u}_q
uq和实部
q
w
q_w
qw进行求反,可以创建一个与原始四元数实现相同旋转的四元数。这意味着从矩阵中提取四元数可以返回或者
q
^
\hat{\textbf{q}}
q^或者
−
q
^
-\hat{\textbf{q}}
−q^。
给定两个单位四元数,
q
^
\hat{\textbf{q}}
q^和
r
^
\hat{\textbf{r}}
r^,将两者按照顺序级联到
p
^
\hat{\textbf{p}}
p^(可以表示为一点
p
\textbf{p}
p),参考下面公式4.44:
r
^
(
q
^
p
^
q
^
∗
)
r
^
∗
=
(
r
^
q
^
)
p
^
(
r
^
q
^
)
∗
=
c
^
p
^
c
^
∗
.
(
4.44
)
\hat{\textbf{r}}(\hat{\textbf{q}}\hat{\textbf{p}}\hat{\textbf{q}}^*)\hat{\textbf{r}}^*=(\hat{\textbf{r}}\hat{\textbf{q}})\hat{\textbf{p}}(\hat{\textbf{r}}\hat{\textbf{q}})^*=\hat{\textbf{c}}\hat{\textbf{p}}\hat{\textbf{c}}^*.\qquad(4.44)
r^(q^p^q^∗)r^∗=(r^q^)p^(r^q^)∗=c^p^c^∗.(4.44)
此处, c ^ = r ^ q ^ \hat{\textbf{c}}=\hat{\textbf{r}}\hat{\textbf{q}} c^=r^q^是表示单位四元数 q ^ \hat{\textbf{q}} q^和 r ^ \hat{\textbf{r}} r^的级联的单位四元数。
矩阵变换
因为经常需要将多个不同的变换组合到一起,其中大部分是以矩阵形式存在,所以需要一个可以将公式4.43转换为矩阵的方法。四元数 q ^ \hat{\textbf{q}} q^可以被转换到矩阵 M q \textbf{M}^q Mq中,参考下面公式4.45:
M q = ( 1 − s ( q y 2 + q z 2 ) s ( q x q y − q w q z ) s ( q x q z + q w q y ) 0 s ( q x q y + q w q z ) 1 − s ( q x 2 + q z 2 ) s ( q y q z − q w q x ) 0 s ( q x q z − q w q y ) s ( q y q z + q w q x ) 1 − s ( q x 2 + q y 2 ) 0 0 0 0 1 ) . ( 4.45 ) \mathbf{M}^q=\left(\begin{matrix} 1-s(q_y^2+q_z^2) & s(q_x q_y-q_w q_z) & s(q_x q_z+q_w q_y) & 0 \\ s(q_x q_y+q_w q_z) & 1-s(q_x^2+q_z^2) & s(q_y q_z-q_w q_x) & 0 \\ s(q_x q_z-q_w q_y) & s(q_y q_z+q_w q_x) & 1-s(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right).\qquad(4.45) Mq=⎝⎜⎜⎛1−s(qy2+qz2)s(qxqy+qwqz)s(qxqz−qwqy)0s(qxqy−qwqz)1−s(qx2+qz2)s(qyqz+qwqx)0s(qxqz+qwqy)s(qyqz−qwqx)1−s(qx2+qy2)00001⎠⎟⎟⎞.(4.45)
此处,缩放是 s = 2 / ( n ( q ^ ) ) 2 s=2/(n(\hat{\mathbf{q}}))^2 s=2/(n(q^))2。对于单位四元数,这可以简化为:
M q = ( 1 − 2 ( q y 2 + q z 2 ) 2 ( q x q y − q w q z ) 2 ( q x q z + q w q y ) 0 2 ( q x q y + q w q z ) 1 − 2 ( q x 2 + q z 2 ) 2 ( q y q z − q w q x ) 0 2 ( q x q z − q w q y ) 2 ( q y q z + q w q x ) 1 − 2 ( q x 2 + q y 2 ) 0 0 0 0 1 ) . ( 4.46 ) \mathbf{M}^q=\left(\begin{matrix} 1-2(q_y^2+q_z^2) & 2(q_x q_y-q_w q_z) & 2(q_x q_z+q_w q_y) & 0 \\ 2(q_x q_y+q_w q_z) & 1-2(q_x^2+q_z^2) & 2(q_y q_z-q_w q_x) & 0 \\ 2(q_x q_z-q_w q_y) & 2(q_y q_z+q_w q_x) & 1-2(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 \end{matrix}\right).\qquad(4.46) Mq=⎝⎜⎜⎛1−2(qy2+qz2)2(qxqy+qwqz)2(qxqz−qwqy)02(qxqy−qwqz)1−2(qx2+qz2)2(qyqz+qwqx)02(qxqz+qwqy)2(qyqz−qwqx)1−2(qx2+qy2)00001⎠⎟⎟⎞.(4.46)
一旦构建起了四元数,就无需计算任何的三角函数,因而在这个过程中转换过程十分高效。
反向的转换,从正交矩阵 M q \mathbf{M}^q Mq,到单位四元数 q ^ \hat{\mathbf{q}} q^,则会稍微复杂一些。这一过程的关键在于以下方程4.46中矩阵的差异:
m 21 q − m 12 q = 4 q w q x , m 02 q − m 20 q = 4 q w q y , ( 4.47 ) m 10 q − m 01 q = 4 q w q z , m_{21}^q-m_{12}^q = 4q_w q_x,\\ m_{02}^q-m_{20}^q=4q_w q_y, \quad(4.47)\\ m_{10}^q-m_{01}^q=4q_w q_z, m21q−m12q=4qwqx,m02q−m20q=4qwqy,(4.47)m10q−m01q=4qwqz,
这些方程的含义是,如果 q w q_w qw已知,则可以计算向量 v q \mathbf{v}_q vq的值,从而推导出 q ^ \hat{\mathbf{q}} q^。矩阵 M q \mathbf{M}^q Mq的迹可以如下计算:
tr ( M q ) = 4 − 2 s ( q x 2 + q y 2 + q z 2 ) = 4 ( 1 − q x 2 + q y 2 + q z 2 q x 2 + q y 2 + q z 2 + q w 2 ) = 4 q w 2 q x 2 + q y 2 + q z 2 + q w 2 = 4 q w 2 ( n ( q ^ ) ) 2 . ( 4.48 ) \text{tr}(\mathbf{M}^q) = 4-2s(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}.\qquad(4.48) tr(Mq)=4−2s(qx2+qy2+qz2)=4(1−qx2+qy2+qz2+qw2qx2+qy2+qz2)=qx2+qy2+qz2+qw24qw2=(n(q^))24qw2.(4.48)
这个结果可以得到下面的四元数转换:
q w = 1 2 tr ( M q ) , q x = m 21 q − m 12 q 4 q w , q y = m 02 q − m 20 q 4 q w q z = m 10 q − m 01 q 4 q w . ( 4.49 ) q_w = \frac{1}{2} \sqrt{\text{tr}(\mathbf{M}^q)},\quad q_x = \frac{m_{21}^q-m_{12}^q}{4q_w},\\ q_y = \frac{m_{02}^q-m_{20}^q}{4q_w} \quad q_z = \frac{m_{10}^q-m_{01}^q}{4q_w}.\qquad(4.49) qw=21tr(Mq),qx=4qwm21q−m12q,qy=4qwm02q−m20qqz=4qwm10q−m01q.(4.49)
为了有一个数值稳定的程序,应该避免用小数字除法。因此,首先设置 t = q w 2 − q x 2 − q y 2 − q z 2 t=q_w^2-q_x^2-q_y^2-q_z^2 t=qw2−qx2−qy2−qz2,由此得出
m
00
=
t
+
2
q
x
2
,
m
11
=
t
+
2
q
y
2
,
m
22
=
t
+
2
q
z
2
,
u
=
m
00
+
m
11
+
m
22
=
t
+
2
q
w
2
,
(
4.50
)
m_{00} = t + 2q_x^2,\\ m_{11} = t + 2q_y^2,\\ m_{22} = t + 2q_z^2,\\ u = m_{00} + m_{11}+m_{22} = t + 2q_w^2, \qquad(4.50)
m00=t+2qx2,m11=t+2qy2,m22=t+2qz2,u=m00+m11+m22=t+2qw2,(4.50)
这又意味着
m
0
0
m_00
m00、
m
1
1
m_11
m11、
m
2
2
m_22
m22和
u
u
u中的最大值决定了
q
x
q_x
qx、
q
y
q_y
qy、
q
z
q_z
qz和
q
w
q_w
qw中哪个最大。如果
q
w
q_w
qw最大的话,那么公式4.49就会被用来做四元数的推导。否则,我们采用以下的方式:
4 q x 2 = + m 00 − m 11 − m 22 + m 33 , 4 q y 2 = − m 00 + m 11 − m 22 + m 33 , 4 q z 2 = − m 00 − m 11 + m 22 + m 33 , 4 q w 2 = tr ( M q ) . ( 4.51 ) 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 = \text{tr}(\mathbf{M}^q). \qquad(4.51) 4qx2=+m00−m11−m22+m33,4qy2=−m00+m11−m22+m33,4qz2=−m00−m11+m22+m33,4qw2=tr(Mq).(4.51)
然后会用上述方程中适当的去计算 q x q_x qx、 q y q_y qy、 q z q_z qz中最大的,在利用公式4.47计算 q ^ \hat{\mathbf{q}} q^中剩下的分量。Schuler提出了一种不一样的方法(变体),它没有分支,但是使用了四个平方根来代替。
球面线性插值
球面线性插值操作就是给定两个四元数 q ^ \hat{\mathbf{q}} q^和 r ^ \hat{\mathbf{r}} r^,以及一个参数 t ∈ [ 0 , 1 ] t\in[0,1] t∈[0,1],来计算一个两者的插值(结果返回一个四元数)。这在播放物体动画时是很有用处的。在插值相机方向时就没那么有用了,因为相机的“up”向量在插值过程中可能会倾斜,这通常是会干扰到我们的操作。
这个操作的线性代数的形式是由复合四元数 s ^ \hat{\mathbf{s}} s^来表示的,如下:
s ^ ( q ^ , r ^ , t ) = ( r ^ q ^ − 1 ) t q ^ . ( 4.45 ) \hat{\mathbf{s}}(\hat{\mathbf{q}},\hat{\mathbf{r}},t)=(\hat{\mathbf{r}}\hat{\mathbf{q}}^{-1})^t\hat{\mathbf{q}}. \qquad(4.45) s^(q^,r^,t)=(r^q^−1)tq^.(4.45)
然而,在软件实现方面,下面的这个形式则更加合适,其中 s l e r p slerp slerp代表球面线性插值:
s ^ ( q ^ , r ^ , t ) = slerp ( q ^ , r ^ , t ) = sin ( ϕ ( 1 − t ) ) sin ϕ q ^ + sin ( ϕ t ) sin ϕ r ^ . ( 4.53 ) \hat{\mathbf{s}}(\hat{\mathbf{q}},\hat{\mathbf{r}},t)=\text{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}}.\qquad (4.53) s^(q^,r^,t)=slerp(q^,r^,t)=sinϕsin(ϕ(1−t))q^+sinϕsin(ϕt)r^.(4.53)
为了计算公式中需要的
ϕ
\phi
ϕ值,可以使用下面的公式:
cos
ϕ
=
q
x
r
x
+
q
y
r
y
+
q
z
r
z
+
q
w
r
w
\cos{\phi}=q_xr_x+q_yr_y+q_zr_z+q_wr_w
cosϕ=qxrx+qyry+qzrz+qwrw。对于
t
∈
[
0
,
1
]
t\in[0,1]
t∈[0,1]来说,slerp函数计算(唯一的)插值四元数,这些四元数共同构成一个四维单位球体上的最短弧,从
q
^
\hat{\mathbf{q}}
q^(
t
=
0
t=0
t=0)到
r
^
\hat{\mathbf{r}}
r^(
t
=
1
t=1
t=1)。弧线位于由
q
^
\hat{\mathbf{q}}
q^和
r
^
\hat{\mathbf{r}}
r^给定的平面以及四维单位球体相交形成的圆上。参考图4.10。计算的旋转四元数绕一个固定轴以恒定的速度旋转。有恒定速度(0加速度)的曲线,就称为了测地曲线(geodisic curve)。一个球体上的大圆(great cycle)是由穿过球心的平面和球体求交,这部分圆就称为了大弧(great arc)。
图4.10. 单位四元数表示为单位球体上的点。slerp函数被用来在四元数之间进行插值,并且插值的路径就是球体上的大弧。注意从
q
^
1
\hat{\mathbf{q}}_1
q^1到
q
^
2
\hat{\mathbf{q}}_2
q^2的插值和从
q
^
1
\hat{\mathbf{q}}_1
q^1到
q
^
3
\hat{\mathbf{q}}_3
q^3再到
q
^
2
\hat{\mathbf{q}}_2
q^2的插值是不一样的,即便他们的朝向是一致的
slerp函数非常适合对两个方向进行插值,它表现得非常的好(固定轴,恒定速度)。而这在使用欧拉角进行插值时是做不到的。实际上,直接计算slerp是一个高消耗的操作,涉及到调用三角函数。Malyshau讨论了将四元数融合进渲染管线的课题。他指出,当不使用slerp,而只是在像素着色器中规范化四元数时,90度角的三角形方向的误差最大为4度。这个误差率在光栅化三角形时是可以接受的。Li提供了更快的增量方法来计算slerp,且不会牺牲任何精度。Eberly提出了一种只使用加法和乘法计算slerps的快速技术。
当有多个方向( q ^ 0 \hat{\mathbf{q}}_0 q^0, q ^ 1 \hat{\mathbf{q}}_1 q^1,…, q ^ n − 1 \hat{\mathbf{q}}_{n-1} q^n−1),并且我们希望做 q ^ 0 \hat{\mathbf{q}}_0 q^0到 q ^ 1 \hat{\mathbf{q}}_1 q^1再到 q ^ 2 \hat{\mathbf{q}}_2 q^2的插值,一直做到 q ^ n − 1 \hat{\mathbf{q}}_{n-1} q^n−1,slerp函数都可以直接使用。更进一步,我们可以用 q ^ i − 1 \hat{\mathbf{q}}_{i-1} q^i−1和 q ^ i \hat{\mathbf{q}}_i q^i来进行slerp运算。经过 q ^ i \hat{\mathbf{q}}_i q^i之后,我们可以使用 q ^ i \hat{\mathbf{q}}_i q^i和 q ^ i + 1 \hat{\mathbf{q}}_{i+1} q^i+1继续进行。这可能会在方向插值上出现突然的抖动,参考图4.10。这和点在线性插值发生的情况类似;参考图17.3的右上角(17章)。一些读者可能会在17章读到样条线的部分后希望再重新读下面的段落。
一个更好的插值方法就是使用某种样条线。我们在
q
^
i
\hat{\mathbf{q}}_{i}
q^i和
q
^
i
+
1
\hat{\mathbf{q}}_{i+1}
q^i+1中间引入
a
^
i
\hat{\mathbf{a}}_{i}
a^i和
a
^
i
+
1
\hat{\mathbf{a}}_{i+1}
a^i+1。在这个四元数组(
q
^
i
\hat{\mathbf{q}}_{i}
q^i,
q
^
i
+
1
\hat{\mathbf{q}}_{i+1}
q^i+1,
a
^
i
\hat{\mathbf{a}}_{i}
a^i和
a
^
i
+
1
\hat{\mathbf{a}}_{i+1}
a^i+1)下可以定义球面立方插值(Spherical cubic interpolation)。这两个额外的四元数可以以一下方式进行计算:
a
^
i
=
q
^
i
exp
[
−
log
(
q
^
i
−
1
q
^
i
−
1
)
+
log
(
q
^
i
−
1
q
^
i
+
1
)
4
]
.
(
4.54
)
\hat{\mathbf{a}}_{i}=\hat{\mathbf{q}}_{i}\exp[-\frac{\log(\hat{\mathbf{q}}_{i}^{-1}\hat{\mathbf{q}}_{i-1})+\log(\hat{\mathbf{q}}_{i}^{-1}\hat{\mathbf{q}}_{i+1})}{4}].\qquad(4.54)
a^i=q^iexp[−4log(q^i−1q^i−1)+log(q^i−1q^i+1)].(4.54)
q
^
i
\hat{\mathbf{q}}_{i}
q^i和
a
^
i
\hat{\mathbf{a}}_{i}
a^i可以使用一种平滑的立方样条线来进行球面插值,参考公式4.55:
squad
(
q
^
i
,
q
^
i
+
1
,
a
^
i
,
a
^
i
+
1
,
t
)
=
slerp
(
slerp
(
q
^
i
,
q
^
i
+
1
,
t
)
,
slerp
(
a
^
i
,
a
^
i
+
1
,
t
)
,
2
t
(
1
−
t
)
)
.
(
4.55
)
\text{squad}(\hat{\mathbf{q}}_{i},\hat{\mathbf{q}}_{i+1},\hat{\mathbf{a}}_{i},\hat{\mathbf{a}}_{i+1},t)=\text{slerp}(\text{slerp}(\hat{\mathbf{q}}_{i},\hat{\mathbf{q}}_{i+1},t),\text{slerp}(\hat{\mathbf{a}}_{i},\hat{\mathbf{a}}_{i+1},t),2t(1-t)).\qquad(4.55)
squad(q^i,q^i+1,a^i,a^i+1,t)=slerp(slerp(q^i,q^i+1,t),slerp(a^i,a^i+1,t),2t(1−t)).(4.55)
从上面内容可见,squad函数是通过重复的球面插值(slerp)构建出来的(参考17.1.1小节中点的重复线性插值)。插值会经过最初的方向 q ^ i , i ∈ [ 0 , . . . , n − 1 ] \hat{\mathbf{q}}_{i},i\in[0,...,n-1] q^i,i∈[0,...,n−1],但是不会经过 a ^ i \hat{\mathbf{a}}_{i} a^i,因为他们是用来指示初始朝向的切线方向(译者注:即样条线的控制点)。
从一个向量旋转到另外一个
一个常用的操作就是从一个方向
s
\mathbf{s}
s通过最短的可能路径变换到另一个方向
t
\mathbf{t}
t。四元数的数学过程大大简化了这一操作,并显示了四元数与这种表示的密切关系。首先,归一化
s
\mathbf{s}
s和
t
\mathbf{t}
t。然后计算单位旋转轴,称为
u
\mathbf{u}
u,由
u
=
(
s
×
t
)
/
∣
∣
s
×
t
∣
∣
\mathbf{u}=(\mathbf{s}\times\mathbf{t})/||\mathbf{s}\times\mathbf{t}||
u=(s×t)/∣∣s×t∣∣。然后,
e
=
s
⋅
t
=
cos
(
2
ϕ
)
e=\mathbf{s}·\mathbf{t}=\cos{(2\phi)}
e=s⋅t=cos(2ϕ)以及
∣
∣
s
×
t
∣
∣
=
sin
(
2
ϕ
)
||\mathbf{s}\times\mathbf{t}||=\sin{(2\phi)}
∣∣s×t∣∣=sin(2ϕ),其中
2
ϕ
2\phi
2ϕ就是
s
\mathbf{s}
s和
t
\mathbf{t}
t之间的夹角。表示二者之间的旋转的四元数即为
q
^
=
(
sin
ϕ
u
,
cos
ϕ
)
\hat{\mathbf{q}}=(\sin{\phi\mathbf{u},\cos{\phi}})
q^=(sinϕu,cosϕ)。实际上,应用半角关系和三角函数变换,简化
q
^
=
(
sin
ϕ
sin
2
ϕ
(
s
×
t
)
,
cos
ϕ
)
\hat{\mathbf{q}}=(\frac{\sin{\phi}}{\sin{2\phi}}(\mathbf{s}\times\mathbf{t}),\cos{\phi})
q^=(sin2ϕsinϕ(s×t),cosϕ)可以得到:
$
q
^
=
(
q
v
,
q
w
)
=
(
1
2
(
1
+
e
)
(
s
×
t
)
,
2
(
1
+
e
)
2
)
.
(
4.56
)
\hat{\mathbf{q}}=(\mathbf{q}_v,q_w)=(\frac{1}{\sqrt{2(1+e)}}(\mathbf{s}\times\mathbf{t}),\frac{\sqrt{2(1+e)}}{2}).\qquad(4.56)
q^=(qv,qw)=(2(1+e)1(s×t),22(1+e)).(4.56)
以这种方式直接生成四元数(相比于归一化叉积 s × t \mathbf{s}\times\mathbf{t} s×t)可以避免数值不稳定, s \mathbf{s} s和 t \mathbf{t} t几乎指向同一方向时。当两个方向指向相反方向时,两种方法都会有稳定性问题,因为会出现0做分母的情况。当检测到这种情况时,可以使用任意垂直于 s \mathbf{s} s的旋转轴来旋转到 t \mathbf{t} t。
有些时候我们需要从 s \mathbf{s} s到 t \mathbf{t} t的旋转矩阵。在经过对公式4.46进行线性代数和三角函数方面的简化后,可以得到旋转矩阵:
R ( s , t ) = ( e + h v x 2 h v x v y − v z h v x v z + v y 0 h v x v y + v z e + h v y 2 h v y v z − v x 0 h v x v z − v y h v y v z + v x e + h v z 2 0 0 0 0 1 ) . ( 4.57 ) \mathbf{R}(\mathbf{s},\mathbf{t})=\left(\begin{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 \end{matrix}\right).\qquad(4.57) R(s,t)=⎝⎜⎜⎛e+hvx2hvxvy+vzhvxvz−vy0hvxvy−vze+hvy2hvyvz+vx0hvxvz+vyhvyvz−vxe+hvz200001⎠⎟⎟⎞.(4.57)
在这个公式中,我们使用到了下面的中间计算过程:
v = s × t , e = cos ( 2 ϕ ) = s ⋅ t , h = 1 − cos 2 ϕ sin 2 ( 2 ϕ ) = 1 − e v ⋅ v = 1 1 + e . ( 4.58 ) \mathbf{v} = \mathbf{s}\times\mathbf{t},\\ e=\cos{(2\phi)}=\mathbf{s}·\mathbf{t},\\ h=\frac{1-\cos{2\phi}}{\sin^2{(2\phi)}}=\frac{1-e}{\mathbf{v}·\mathbf{v}}=\frac{1}{1+e}. \qquad(4.58) v=s×t,e=cos(2ϕ)=s⋅t,h=sin2(2ϕ)1−cos2ϕ=v⋅v1−e=1+e1.(4.58)
正如所见,简化过程简化掉了所有的平方根和三角函数,所以这是一个高效的创建矩阵的方法。注意到公式4.57的结构类似于公式4.30,而后者并不需要三角函数。注意当 s \mathbf{s} s到 t \mathbf{t} t平行或者快要平行的时候需要小心,因为 ∣ ∣ s × t ∣ ∣ ≈ 0 ||\mathbf{s}\times\mathbf{t}||\approx0 ∣∣s×t∣∣≈0。如果 ϕ ≈ 0 \phi\approx0 ϕ≈0,那么我们可以返回到单位矩阵。然而,如果 2 ϕ ≈ π 2\phi\approx\pi 2ϕ≈π,那么我们可以绕着任意轴旋转 π \pi π。这个轴可以是 s \mathbf{s} s和任意不平行于它的向量的叉积(4.2.4小节)。Moller和Hughes使用Householder矩阵以一种不同的方式来解决这种特殊情况。
4.4 Vertex Blending(顶点混合)
试想,参照图4.11那样,使用两部分——前臂和上臂,来制作数字角色的胳膊动画。该模型可以使用刚体变换设置动画(4.1.6小节)。然而,这两部分之间的关节并不会像一个真正的肘部那样。这是因为使用了两个相互独立的对象,并且进一步,关节是由这两个单独对象的重叠部分组成的。显然,在这里使用仅一个单独的物体会更好。然而,静态模型零件不能解决使关节灵活的问题。
顶点混合(vertex blending)是对这个问题的最广泛使用的解决方法。这项技术也有着一些其他的名字,如线性混合蒙皮(linear-blend skinning)、包络(envoloping)或骨架子空间变形(skeleton-subspace deformation)。虽然这里介绍的算法的确切起源还不清楚,但定义骨骼和让皮肤对变化作出反应是计算机动画中的一个古老概念。在它最简化的形式里,前臂和上臂还是和之前那样去单独分别设置动画,但是在关节处,两部分通过一个弹性“皮肤”连接起来。因此,这部分弹性组织会拥有一组由前臂矩阵所负责变换的顶点以及另一组由上臂变换的顶点。这会导致三角形的顶点可以通过不同的矩阵进行变换,而不是每个三角形内的顶点使用同一个矩阵。参考图4.11。
图 4.11. 左侧的胳膊包含两部分——前臂和上臂,现在使用两个单独的对象的刚体变换来制作胳膊的动画,使其摆动。这样得到的肘部并不自然。右侧,在一个单独的对象上应用了顶点混合。最右侧手臂旁边的手臂说明了当一个简单的皮肤将两部分直接连接起来覆盖肘部时会发生什么。最右边的手臂说明了使用顶点混合时发生的情况,有些顶点使用不同的权重——(2/3,1/3),进行混合,它表示顶点对来自上臂的变换权重为2/3,对来自前臂的变换权重为1/3。 此图还显示了最右侧插图中顶点混合的缺点。在这里,可以看到肘部内部的折叠。使用更多的骨骼并更仔细选择的权重可以获得更好的结果。
进一步说,我们可以让一个单独的顶点经由多个不同的矩阵去变换,将生成的位置加权并混合在一起。这是通过为待定制动画的物体赋予骨骼,其中每个骨骼的变换都可能影响着用户定义的每个顶点的权重。由于整个手臂可能是“弹性的”,即所有的顶点可能会受到不止一个矩阵的影响,整个网格通常会称为是蒙皮(skin)(在骨骼之上)。参考图4.12.。许多的商业建模系统都有这个相同的骨架建模特性。且不论他们的名字,骨骼不一定是刚性的。例如,Mohr和Gleicher提出添加额外关节的想法,以实现诸如肌肉膨胀之类的效果。James和Twigg讨论了使用可以挤压和拉伸的骨骼创建动画蒙皮。
=图 4.12. 一个顶点混合的实际案例。左上角的图片是手臂的两块骨骼,处于伸展位置。右上角显示了网格,其中颜色表示骨骼拥有的每个顶点。底部:位置稍稍不同的手臂的着色网格。(图片由Jeff Lander提供)
数学上,这可以表示为公式4.59,其中
p
\mathbf{p}
p是原始顶点,
u
(
t
)
\mathbf{u}(t)
u(t)是变换过后的顶点,其位置取决于时间
t
t
t:
u
(
t
)
=
∑
i
=
0
n
−
1
w
i
B
i
(
t
)
M
i
−
1
p
,
其
中
∑
i
=
0
n
−
1
w
i
=
1
,
w
i
>
0.
(
4.59
)
\mathbf{u}(t) = \sum_{i=0}^{n-1}w_i\mathbf{B}_i(t)\mathbf{M}_i^{-1}\mathbf{p},其中 \sum_{i=0}^{n-1}w_i=1,w_i>0.\qquad (4.59)
u(t)=i=0∑n−1wiBi(t)Mi−1p,其中i=0∑n−1wi=1,wi>0.(4.59)
在世界坐标系下,有 n n n块骨头影响着 p \mathbf{p} p的位置。值 w i w_i wi即是骨头 i i i对顶点KaTeX parse error: Undefined control sequence: \mahtbf at position 1: \̲m̲a̲h̲t̲b̲f̲{p}的权重。矩阵 M i \mathbf{M}_i Mi是从初始的骨骼坐标系转换到了世界坐标系。通常一块骨骼的控制关节在其坐标系统的原点。例如,前臂骨骼移动它的肘关节到原点,通过动画旋转矩阵围绕关节移动手臂的这一部分。 B i ( t ) \mathbf{B}_i(t) Bi(t)矩阵是第 i i i个骨骼的世界变换,它随着时间的变换而让物体动起来,通常是多个矩阵的级联,例如先前骨骼变换的层次结构和局部动画矩阵。
Woodland深入讨论了一种维持和更新 B i ( t ) \mathbf{B}_i(t) Bi(t)矩阵动画函数的方法。每个骨骼将一个顶点变换到一个相对于其自身参照系的位置,并从计算点集内插出最终的位置。矩阵 M i \mathbf{M}_i Mi在一些关于蒙皮的讨论中没有明确显示,它被认为是 B i ( t ) \mathbf{B}_i(t) Bi(t)的一部分。我们将它在这里展示出来,是因为它是一个极为有用的矩阵,它总是矩阵级联过程的一部分。
实际上,矩阵 B i ( t ) \mathbf{B}_i(t) Bi(t)和 M i − 1 \mathbf{M}_i^{-1} Mi−1在每一帧动画上对每个骨骼都级联在一起,并且每个得出的矩阵都去用来变换顶点。顶点 p \mathbf{p} p由不同骨骼的级联矩阵来进行变换,然后使用权重 w i w_i wi进行混合——这也是它名称顶点混合(vertex blendiing)的由来。权重是非负的,并且和为一,所以这里所进行的操作是,顶点被变换到几个位置,然后在它们之间进行插值。这样,变换过后的点 u \mathbf{u} u将会位于点集 B i ( t ) M i − 1 p \mathbf{B}_i(t)\mathbf{M}_i^{-1}\mathbf{p} Bi(t)Mi−1p( t t t取定值的情况下,对所有的 i = 0... n − 1 i=0...n-1 i=0...n−1)的凸包中。通常发现也可以使用公式4.59进行变换。依据所使用的变换(例如,如果骨骼被大幅挤压或者拉伸),可能需要使用 B i ( t ) M i − 1 \mathbf{B}_i(t)\mathbf{M}_i^{-1} Bi(t)Mi−1的逆的转置,参考4.1.7小节的讨论。
顶点混合在GPU上适配良好。网格上的顶点组可以被存在一个静态缓冲区中,它被发到GPU然后重用。在每帧中,只有骨骼矩阵改变,而顶点着色器去计算他们在存着的这块网格体上的影响。在这种方式下,要处理的从CPU传来的数据量就被最小化,这让GPU可以更加高效得去渲染网格。如果可以一起使用模型的整个网格矩阵组,这是最简单的情况;否则,模型必须进行拆分,而一些骨骼需要重复。可选地,骨骼变换可以存储在顶点可以访问得到的纹理中,这可以避免寄存器存储的限制。每个变换在使用四元数表示变换的情况下,仅仅用两个纹理就足够储存。如果可用的话,无序访问视图存储允许重用蒙皮结果。
可以指定超出范围[0,1]或总和不等于1的权重集。然而,只有在使用一些其他的混合算法的情况下才有意义,例如变形目标(morph target)算法(4.5小节)。
基本顶点混合的一个缺点是可能会发生一些折叠、扭曲和自相交。参考图4.13.。一个更好的解决方法是使用对偶四元数(dual quaternion)。这项实现蒙皮的技术帮助保留了原始变换的刚性,从而避免了在四肢上出现“糖纸包装”这样的扭曲。计算消耗低于1.5倍的线性蒙皮混合,并且结果很不错,这也使得这项技术得到快速的采用。但是,对偶四元数蒙皮可能会导致鼓胀的效果。Le和Hodgins展示了一种更好的方案,即旋转中心蒙皮。他们依赖于这样的假设:局部变换应该是刚体,并且具有相似权重
w
i
w_i
wi的顶点应该具有相似的变换。预先计算每个顶点的旋转中心,同时施加正交(刚体)约束以防止肘部塌陷和糖果包装扭曲的缺陷。在运行时,该算法类似于线性混合蒙皮,因为GPU实现在旋转中心执行线性混合蒙皮,然后再执行四元数的混合步骤。
图 4.13. 左侧展示了使用线性混合蒙皮时关节处存在的问题。右侧,使用对偶四元数的混合则改善了表现。(图片由Ladislav Kavan和其同事提供,模型由Paul Steed提供。)
4.5 Morphing(变形)
在制作动画时从一个三维模型变形为另一个可能会比较有用。试想,在时间
t
0
t_0
t0播放一个模型,然后再时间
t
1
t_1
t1我们希望它变为另一个模型。对于
t
0
t_0
t0和
t
1
t_1
t1之间的其余全部时间,通过某种插值,会得到一个连续的“混合的”模型。图4.14中是一个变形的例子。
图 4.14. 顶点变形。每个顶点定义了两个位置和两个法线。在每一帧里,中间态的位置和法线由顶点着色器线性插值得到。(图片由NVIDIA公司提供。)
变形涉及到两个主要问题,即,顶点对应(vertex correspondence)问题和插值(interpolation)问题。给定任意两个模型,他们可能有着不同的拓扑。不同的顶点数,以及不同的网格连接性,人们通常得从设置这些顶点得对应上开始做起。这是一个困难的问题,在相应的领域也有比较多的研究。我们推荐感兴趣的读者去参考Alexa的研究。
然而,如果已经在两个模型间存在一个一一对应的顶点对应关系,那么可以逐顶点进行插值。即,对于第一个模型中的每一个顶点,都在第二个模型中存在唯一的一个顶点,反之亦然。这使得插值变得简单。例如,可以在顶点上直接使用线性插值(17.1小节中有一些其他的插值方法)。要在时间
t
∈
[
t
0
,
t
1
]
t\in[t_0,t_1]
t∈[t0,t1]上计算一个变形顶点,我们首先计算
s
=
(
t
−
t
0
)
/
(
t
1
−
t
0
)
s=(t-t_0)/(t_1-t_0)
s=(t−t0)/(t1−t0),然后进行线性顶点混合,
m
=
(
1
−
s
)
p
0
+
s
p
1
,
(
4.60
)
\mathbf{m} = (1-s)\mathbf{p}_0 + s\mathbf{p}_1,\qquad(4.60)
m=(1−s)p0+sp1,(4.60)
其中
p
0
\mathbf{p}_0
p0和
p
1
\mathbf{p}_1
p1即是对应着两个时间点
t
0
t_0
t0和
t
1
t_1
t1上同一个顶点的两个位置。
变形还有一种变体,其中用户可以更直观地控制,这种变体名称是变形目标或混合形状(morph target或者blend shape)。基本思想可以用图4.15来解释。
图 4.15. 给定两种嘴部姿势,以及一组不同的向量用于计算去控制插值,或者甚至是外插值。在变形目标中,不同的向量被用来“加”一些移动到中性脸(neurtal face)上。在施加正的权重到不同向量上,我们获得了一个微笑的嘴脸,而负的权重可以给出一个相反的效果。
我们从一个中性模型开始着手,在这里就是这个中性脸(译者注:这里说的中性是指介于笑和哭之间两种模型状态下的中性)。然我们用 N \mathcal{N} N表示这个模型。此外,我们还有一组不同的脸的姿态。在样例(4.15)中,仅仅只有一种姿态,就是笑脸。通常,我们可以有 k ≥ 1 k\ge1 k≥1种不同的姿态,可以表示为 P i , i ∈ [ 1 , . . . , k ] \mathcal{P}_i,i\in[1,...,k] Pi,i∈[1,...,k]。作为预处理,“不同的脸”可以这样计算: D i = P i − N \mathcal{D}_i = \mathcal{P}_i -\mathcal{N} Di=Pi−N,即由每个不同的姿态减去中性模型。
此时,我们有一个中性模型,
N
\mathcal{N}
N,一组不同的姿态,
D
i
\mathcal{D}_i
Di。那么变形后的模型
M
\mathcal{M}
M可以使用下面的公式得到:
M
=
N
+
∑
i
=
1
k
w
i
D
i
.
(
4.61
)
\mathcal{M} = \mathcal{N} + \sum_{i=1}^k w_i\mathcal{D}_i.\qquad(4.61)
M=N+i=1∑kwiDi.(4.61)
这就是中性模型,然后在此之上我们根据意愿使用权重 w i w_i wi增加了不同姿态的特性。对于图4.15来说,其实是通过设置 w 1 = 1 w_1=1 w1=1获得了完全的笑脸,使用 w 1 = 0.5 w_1 =0.5 w1=0.5的话可以获得一个半笑不笑的脸。我们也可以使用负的权重以及大于1的一些权重值。
对于这个简单的脸部模型,我们可以增加另一个有着“悲伤”的眉毛的脸。再使用一个负的权重值可以得到“开心”的眉毛。因为位移是可以叠加的,这个眉毛的姿态可以和笑脸的姿势一起使用。
变形目标是一项强大的技术,它可以提供给动画制作者极大的掌控力,因为一个模型的不同特性可以与其他的相对独立得去操作。Lewis及其同事介绍了姿态-空间变形(pose-space deformation),这将定点混合和变形目标结合了起来。Senior使用了与计算的顶点纹理来存储和获取目标姿态间的位移。硬件支持的流输出和每个顶点的ID允许在单个模型中使用更多的目标,并且只在GPU上计算效果。使用一个低解析度的网格然后通过曲面细分和位移映射生成一个高解析度的网格,从而避免了在一个高度细致模型上蒙皮的消耗。
图 4.16. 在声名狼藉:次子(inFAMOUS Second Son)种Delsin角色的脸,是使用混合形状(blend shape)来制作得动画。所有这些快照都使用相同的静止姿态的脸,然后修改不同的权重以使该面看起来不同。(图片由淘气狗和互动娱乐公司提供。声名狼藉次子是索尼互动娱乐的商标,由Sucker Punch开发。)
图4.16中的是使用蒙皮和变形的一个真实的例子。Weronko和Andreason在教团:1886中(The Order: 1886)使用蒙皮和变形。
4.6 Geometry Cache Playback(几何缓存回放)
在剪接镜头中,会希望使用极高质量的动画,例如,哪些不能用上述任何方法表示的运动。一个笨办法是将所有帧的顶点都存储起来,从硬盘上读他们然后更新网格体的位置。然而,这会在一个简单的30000个顶点的模型上在一个简短的动画上花费50MB/s。Gneiting演示了几种方法来减少内存消耗,大约能减少10%左右。
首先,。例如,位置和纹理坐标可以使用16位的整形来存储每个坐标。因为在执行压缩后无法恢复原始数据,所以此步骤是有损的。为了进一步减少数据量,进行了空间和时间预测,并对差异进行了编码。对于空间压缩,可以使用平行四边形预测。对于一个三角形带,下一个顶点的预测位置只是当前三角形在当前三角形边周围的三角形平面上的反射,它形成了一个平行四边形。然后对与这个新位置的差异进行编码。有了良好的预测,大多数值将接近零,这是许多常用的压缩方案的理想选择。类似于MPEG压缩,预测也是在时间维度上进行的。即每n帧,执行空间压缩。其间,在时间维度上执行预测,即如果某个顶点从帧n-1到帧n移动了delta向量,那么它可能向着n+1也移动相似的程度。这些技术大大减少了存储空间,使得该系统可以用于实时流式传输数据。
4.7 Projections(投影)
在正真渲染一个场景之前,所有场景中的相关物体对象必须投影到某种平面上或者是某种类型的简单体积内。在那之后,才执行裁剪和渲染(2.3小节)。
这章到现在为止的变换还剩下第四个坐标, w w w-component,没有受到影响。即在变换之后,点和向量都依然保持他们之前原有的类型。另外, 4 × 4 4\times4 4×4矩阵的最底下那行一直都是 ( 0 0 0 1 ) (0\ 0 \ 0 \ 1) (0 0 0 1)。透视投影矩阵(Perspective Projection matrices)是这两个属性的例外:最下面一行包含向量和点操作数,并且通常需要进行均匀化处理。即, w w w经常不为1,所以需要做一个除法(除以 w w w)来得到非齐次的点。透视矩阵(Orthographic Projection)是一个常用的简化版的投影。它不会影响 w w w分量。
在本节中,假设观察者是面向相机的负 z z z轴的方向, y y y轴指向头顶而 x x x轴指向右方。这是一个右手坐标系。一些文本和软件,例如,DirectX,使用的是左手系,其中的观察者是沿着相机的正 z z z轴方向观察的。两个系统从本质上讲没有差别,即在最后获得的效果是相同的。
4.7.1 Orthographic Projection(正交投影)
正交投影的一个特点就是原本就平行的线在投影过后依旧保持平行。当使用正交投影观察场景时,物体的大小会保持不变,无论到相机的距离怎样变换。矩阵
P
0
\mathbf{P}_0
P0,如下所示,是一个简单的正交投影矩阵,其中点的
x
x
x和
y
y
y分量并没有发生改变,而
z
z
z分量置零,即它正交投影到了矩阵
z
=
0
z=0
z=0上:
P
0
=
(
1
0
0
0
0
1
0
0
0
0
0
0
0
0
0
1
)
.
(
4.62
)
\mathbf{P}_0 =\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix}. \qquad(4.62)
P0=⎝⎜⎜⎛1000010000000001⎠⎟⎟⎞.(4.62)
这个投影的效果可以参考图4.17.。显然, P 0 \mathbf{P}_0 P0是不可逆的,因为它的秩 ∣ P 0 ∣ = 0 |\mathbf{P}_0| = 0 ∣P0∣=0。换句话说,变换是从三维跌落至二维,而这是没有办法去取回已经舍弃掉的那个维度的。使用这种正交投影的问题是它将正 z z z的点和负 z z z的点都投到了投影平面上。此时,去限制 z z z值(以及 x x x和 y y y值)到一个区间内(从 n n n近面到 f f f远面)是很有用的。这就是下一个变换的目的。
图 4.17. 公式4.62生成的简单正交投影的三个不同的观察角度。这个投影可以看作是观察者沿着负
z
z
z轴进行观察,这意味着投影是跳过了(置零)
z
z
z坐标而保留了
x
x
x和
y
y
y坐标。注意
z
=
0
z=0
z=0两边的物体都被投影在了投影平面上。
一个更加通用的执行正交投影的矩阵是由一个6元数组 ( l , r , b , t , n , f ) (l,r,b,t,n,f) (l,r,b,t,n,f)表示,表示左、右、下、上、近和远平面。这个矩阵通过缩放和平移将由这些平面构成轴对齐包围盒(axis-aligned bounding box,即AABB盒,参考22.2小节中的定义)变换到中心位于原点的轴对齐立方体。AABB盒最小的角即是 ( l , b , n ) (l,b,n) (l,b,n),最大角是 ( r . t . f ) (r.t.f) (r.t.f)。需要着重强调 n > f n>f n>f,因为我们是在朝着 z z z轴负方向看这片体积空间。我们的通常的感觉告诉我们近处的值要比远处的值更加小,因此,可以让用户这样认为,然后在内部否定它们。(译者注:没必要,这种东西,理解了记住了就好了)
在OpenGL里,轴对齐立方体的最小角 ( − 1 , − 1 , − 1 ) (-1,-1,-1) (−1,−1,−1)和最大角 ( 1 , 1 , 1 ) (1,1,1) (1,1,1);在DirectX中边界是 ( − 1 , − 1 , 0 ) (-1,-1,0) (−1,−1,0)和 ( 1 , 1 , 1 ) (1,1,1) (1,1,1)。这个立方体被称为标准视景体(canonical view volume)而这个体积内的坐标则称为了标准设备坐标。变换过程可以参考图4.18.。变换到标准视景体的原因是,这样有利于后面的裁剪工作。
图 4.18. 变换轴对齐盒子到标准视景体。首先平移盒子,使它的中心与原点重合。然后将它缩放到与标准视景体的大小相同,如图右侧所示。
在变换到标准视景体后,待渲染的几何顶点依照这个立方体进行裁剪。不在立方体外部的几何体最终通过将剩余的单位正方形映射到屏幕来渲染。此正交变换如下所示:
P
0
=
S
(
s
)
T
(
t
)
=
(
2
r
−
l
0
0
0
0
2
t
−
b
0
0
0
0
2
f
−
n
0
0
0
0
1
)
(
1
0
0
−
l
+
r
2
0
1
0
−
t
+
b
2
0
0
1
−
f
+
n
2
0
0
0
1
)
=
(
2
r
−
l
0
0
−
l
+
r
r
−
l
0
2
t
−
b
0
−
t
+
b
t
−
b
0
0
2
f
−
n
−
f
+
n
f
−
n
0
0
0
1
)
.
(
4.63
)
\mathbf{P}_0 = \mathbf{S}(\textbf{s})\mathbf{T}(\textbf{t}) = \begin{pmatrix} \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 \\ \end{pmatrix} \begin{pmatrix} 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 \\ \end{pmatrix} = \begin{pmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{l+r}{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 \\ \end{pmatrix}.\qquad(4.63)
P0=S(s)T(t)=⎝⎜⎜⎛r−l20000t−b20000f−n200001⎠⎟⎟⎞⎝⎜⎜⎛100001000010−2l+r−2t+b−2f+n1⎠⎟⎟⎞=⎝⎜⎜⎛r−l20000t−b20000f−n20−r−ll+r−t−bt+b−f−nf+n1⎠⎟⎟⎞.(4.63)
从公式中可以看出, P 0 \mathbf{P}_0 P0可以看作是一个平移矩阵 T ( t ) \mathbf{T}(\mathbf{t}) T(t)和一个缩放矩阵 S ( s ) \mathbf{S}(\mathbf{s}) S(s)的级联,其中 s = ( 2 / ( r − l ) , 2 / ( t − b ) , 2 / ( f − n ) ) \mathbf{s} = (2/(r-l),2/(t-b),2/(f-n)) s=(2/(r−l),2/(t−b),2/(f−n)), t = ( − ( r + l ) / 2 , − ( t + b ) / 2 , − ( f + n ) / 2 ) \mathbf{t} = (-(r+l)/2,-(t+b)/2,-(f+n)/2) t=(−(r+l)/2,−(t+b)/2,−(f+n)/2)。这个矩阵是可逆的,即 P 0 − 1 = T ( − t ) S ( ( r − l ) / 2 , ( t − b ) / 2 , ( f − n ) / 2 ) \mathbf{P}_0^{-1} = \mathbf{T}(-\mathbf{t}) \mathbf{S}((r-l)/2,(t-b)/2,(f-n)/2) P0−1=T(−t)S((r−l)/2,(t−b)/2,(f−n)/2)。
在计算机图形学中,在投影后最常使用的就是左手坐标系统——即在视口中, x x x轴指向右, y y y轴指向上, z z z轴垂直屏幕向里。因为原平面的 z z z值要比近平面的 z z z值要小(出于我们定义AABB的方式),正交变换总是包括一个镜像变换。这里为了方便,假设原始AABB盒的大小和目标标准视景体的大小一致。那么AABB盒的坐标: ( − 1 , − 1 , 1 ) (-1,-1,1) (−1,−1,1)对应 ( l , b , n ) (l,b,n) (l,b,n), ( 1 , 1 , − 1 ) (1,1,-1) (1,1,−1)对应 ( r , t , f ) (r,t,f) (r,t,f)。代入公式4.61可以得到:
P 0 = ( 1 0 0 0 0 1 0 0 0 0 − 1 0 0 0 0 1 ) . ( 4.64 ) \mathbf{P}_0 = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix}.\qquad(4.64) P0=⎝⎜⎜⎛1000010000−100001⎠⎟⎟⎞.(4.64)
即是镜像矩阵。这个镜像即是从右手观察坐标系(看向负 z z z轴)转到左手标准化设备坐标。
DirectX将 z z z深度映射到了 [ 0 , 1 ] [0,1] [0,1]范围内,而OpenGL则是 [ − 1 , 1 ] [-1,1] [−1,1]。这是在正交矩阵后面再应用一个缩放和平移矩阵,即,
M s t = ( 1 0 0 0 0 1 0 0 0 0 0.5 0.5 0 0 0 1 ) . ( 4.65 ) \mathbf{M}_st = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0.5 & 0.5 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix}.\qquad(4.65) Mst=⎝⎜⎜⎛10000100000.50000.51⎠⎟⎟⎞.(4.65)
所以,DirectX中使用的正交矩阵是
P
0
[
0
,
1
]
=
(
2
r
−
l
0
0
−
r
+
l
r
−
l
0
2
t
−
b
0
−
t
+
b
t
−
b
0
0
1
f
−
n
−
n
f
−
n
0
0
0
1
)
.
(
4.66
)
\mathbf{P}_{0[0,1]} = \begin{pmatrix} \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 \\ \end{pmatrix}.\qquad(4.66)
P0[0,1]=⎝⎜⎜⎛r−l20000t−b20000f−n10−r−lr+l−t−bt+b−f−nn1⎠⎟⎟⎞.(4.66)
它通常是以转置的形式来展现,因为DirectX采用的是以竖列为主的形式来书写矩阵。
4.7.2 Perspective Projection(透视投影)
透视投影要比正交投影更加得复杂,它在大部分得计算机图形学应用中都有使用到。这里,原本平行的线在投影后通常就不再平行;他们可能会在极远处相交到一个点。透视更接近于我们感知世界的方式,即物体会产生近大远小的效果。
首先,我们会介绍一个启发性的对透视投影矩阵(投影到平面 z = − d , d > 0 z=-d,d>0 z=−d,d>0)的推导。我们首先从世界空间开始,以简化对世界空间到观察空间的转化过程的理解。这个推导之后会附上OpenGL中使用的传统的矩阵。
假设相机(视点)位于坐标原点,并且我们希望投影一个点 p \mathbf{p} p到平面 z = − d , d > 0 z=-d,d>0 z=−d,d>0上,得到一个新点 q = ( q x , q y , d ) \mathbf{q} = (q_x, q_y,d) q=(qx,qy,d)。可以参考图4.19.。从图中的相似三角形可以得到关于 q \mathbf{q} q的 x x x分量的推导:
q x p x = − d p z ⟺ q x = − d p x p z ( 4.67 ) \frac{q_x}{p_x} = \frac{-d}{p_z} \quad\Longleftrightarrow \quad q_x= -d\frac{p_x}{p_z}\qquad(4.67) pxqx=pz−d⟺qx=−dpzpx(4.67)
图 4.19. 推导透视投影矩阵所用到的符号表示。点
p
\mathbf{p}
p投影到平面
z
=
−
d
,
d
>
0
z=-d,d>0
z=−d,d>0,得到投影平面上一点
q
\mathbf{q}
q。投影是以相机位置的视角进行的,这里就是原点。右图是推导中用到的
x
x
x分量的相似三角形。
q \mathbf{q} q的其余分量的表达式是 q y = − d p y / p z q_y = -dp_y/p_z qy=−dpy/pz(和 q x q_x qx的推导方式类似),和 q z = − d q_z = -d qz=−d。结合上述公式,我们可以得到透视投影矩阵 P p \mathbf{P}_p Pp
P p = ( 1 0 0 0 0 1 0 0 0 0 1 0 0 0 − 1 / d 0 ) . ( 4.68 ) \mathbf{P}_p = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 \\ \end{pmatrix}.\qquad(4.68) Pp=⎝⎜⎜⎛10000100001−1/d0000⎠⎟⎟⎞.(4.68)
这个矩阵产生的正确的透视投影为
q = P p p = ( 1 0 0 0 0 1 0 0 0 0 1 0 0 0 − 1 / d 0 ) ( p x p y p z 1 ) = ( p x p y p z − p z / d ) ⇒ ( − d p x / p z − d p y / p z − d p z / p z 1 ) . ( 4.69 ) \mathbf{q} = \mathbf{P}_p\mathbf{p} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 \\ \end{pmatrix} \begin{pmatrix} p_x\\ p_y\\ p_z\\ 1\\ \end{pmatrix} =\begin{pmatrix} p_x\\ p_y\\ p_z\\ -p_z/d\\ \end{pmatrix} \Rightarrow \begin{pmatrix} -dp_x/p_z\\ -dp_y/p_z\\ -dp_z/p_z\\ 1\\ \end{pmatrix}.\qquad(4.69) q=Ppp=⎝⎜⎜⎛10000100001−1/d0000⎠⎟⎟⎞⎝⎜⎜⎛pxpypz1⎠⎟⎟⎞=⎝⎜⎜⎛pxpypz−pz/d⎠⎟⎟⎞⇒⎝⎜⎜⎛−dpx/pz−dpy/pz−dpz/pz1⎠⎟⎟⎞.(4.69)
最后一步需要对整个向量除以 w w w分量(这里就是 − p z / d -p_z/d −pz/d)来使得最后一位为1。得到的 z z z值总是 − d -d −d因为我们就是投影到这个平面上的。
直觉上,很容易理解为什么齐次坐标可以进行投影操作。对齐次化过程的一种几何解释是,它将点 ( p x , p y , p z ) (p_x,p_y,p_z) (px,py,pz)投影到平面 w = 1 w=1 w=1上。
就像正交变换那样,还有一种透视变换,它不是实际投影到平面上(不可逆的),而是将视锥体变换为前面描述的标准视景体。这里假设视锥是从 z = n z=n z=n到 z = f z=f z=f,其中 0 > n > f 0>n>f 0>n>f。在 z = n z=n z=n处形成的矩形的最小角在 ( l , b , n ) (l,b,n) (l,b,n),最大角 ( r , t , n ) (r,t,n) (r,t,n)。参考图4.20.。
图 4.20. 矩阵
P
p
\mathbf{P}_p
Pp将视锥变换到单位立方体,也被称为是视景体。
参数 ( l , r , b , t , n , f ) (l,r,b,t,n,f) (l,r,b,t,n,f)决定了相机的视锥。视野的水平区域是由视锥左右平面(由 l l l和 r r r决定)的夹角决定的。同样,视野的竖直区域是由上下平面(由t和b决定)的夹角决定的。视野区域越大,相机能够“看到”的就越多。也可以通过使 r ≠ − l r\neq-l r=−l和 t ≠ − b t\neq-b t=−b来获得不对称的锥体。不对称锥体的使用场景有立体视图和虚拟现实等(21.2.3小节)。
视域在建立场景的感觉时是一个重要的影响因素。相较于计算机屏幕,眼睛本身有一个自己的实际的视域。这个关系可以表达为:
ϕ
=
2
arctan(w/(2d))
.
(
4.70
)
\phi = 2 \text{arctan(w/(2d))}.\qquad(4.70)
ϕ=2arctan(w/(2d)).(4.70)
其中, ϕ \phi ϕ即表示视域, w w w是物体垂直于视线的宽度, d d d是到物体的距离。例如,一个25英尺的大概只能有22英尺宽。在12英尺远的地方,水平视域是85度;在20英尺处,是58度;在30英尺处,是40度。在转换相机镜头到视域时也可以用相似的公式,例如,一个标准的带有50mm的镜头的35mm的相机(其帧宽为36mm)可以给出 ϕ = 2 arctan(36/(50)) = 39.6 \phi = 2 \text{arctan(36/(50))} = 39.6 ϕ=2arctan(36/(50))=39.6度。
使用比实际更加窄的视域将会减少透视的效果,因为观察者会被放大。设置一个更宽的视域会令物体变得扭曲(类似于使用一个广角镜头),特别是在屏幕的边缘附近,也会增加附近物体的缩放。但是,更加宽的视域会带给观察者一种物体更大更加震撼的感觉,并且可以带给使用者更多的关于环境的信息。
将视锥变换到单位立方体的透视投影矩阵由公式4.71给出:
P
p
=
(
2
n
r
−
l
0
−
r
+
l
r
−
l
0
0
2
n
t
−
b
−
t
+
b
t
−
b
0
0
0
f
+
n
f
−
n
−
2
f
n
f
−
n
0
0
1
0
)
.
(
4.71
)
\mathbf{P}_{p} = \begin{pmatrix} \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{pmatrix}.\qquad(4.71)
Pp=⎝⎜⎜⎛r−l2n0000t−b2n00−r−lr+l−t−bt+bf−nf+n100−f−n2fn0⎠⎟⎟⎞.(4.71)
在将这个变换应用到一点上时,我们可以获得另一个点
q
=
(
q
x
,
q
y
,
q
z
,
q
w
)
T
\mathbf{q} = (q_x,q_y,q_z,q_w)^T
q=(qx,qy,qz,qw)T。点的
w
w
w分量
q
w
q_w
qw总是不为0且不等于1。为了得到投影点
p
\mathbf{p}
p,我们需要除以
q
w
q_w
qw,即
p
=
(
q
x
/
q
w
,
q
y
/
q
w
,
q
z
/
q
w
,
1
)
.
(
4.72
)
\mathbf{p} = (q_x/q_w,q_y/q_w,q_z/q_w,1). \qquad(4.72)
p=(qx/qw,qy/qw,qz/qw,1).(4.72)
矩阵 P p \mathbf{P}_p Pp总是认为 z = f z=f z=f映射到+1, z = n z=n z=n映射到−1。
在远平面以外的物体将会被裁剪掉并且不会出现在场景中。透视投影可以用一个无限远的平面,应用在公式4.71上就是:
P p = ( 2 n r − l 0 − r + l r − l 0 0 2 n t − b − t + b t − b 0 0 0 1 − 2 n 0 0 1 0 ) . ( 4.73 ) \mathbf{P}_{p} = \begin{pmatrix} \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{pmatrix}.\qquad(4.73) Pp=⎝⎜⎜⎛r−l2n0000t−b2n00−r−lr+l−t−bt+b1100−2n0⎠⎟⎟⎞.(4.73)
综上所述,应用透视变换(以任何形式) P p \mathbf{P}_p Pp,然后进行剪裁和齐次化(除以 w w w),从而得到标准设备坐标。
为了获得使用于OpenGL里的透视变换,首先要乘以 S ( 1 , 1 , − 1 , 1 ) \mathbf{S}(1,1,-1,1) S(1,1,−1,1),出于与正交变换相同的原因。这只是简单得将公式4.71第三竖列求反。在应用了镜像矩阵后,近和远的值就都是正的了,即 0 < n ′ < f ′ 0<n'<f' 0<n′<f′,就像传统上呈现给用户一样。但是,他们依旧表示的是沿着世界负 z z z轴方向(观察的方向)的距离。出于参考的目的,这里给出OpenGL的公式:
P OpenGL = ( 2 n ′ r − l 0 r + l r − l 0 0 2 n ′ t − b t + b t − b 0 0 0 − f ′ + n ′ f ′ − n ′ − 2 f ′ n ′ f ′ − n ′ 0 0 − 1 0 ) . ( 4.74 ) \mathbf{P}_{\text{OpenGL}} = \begin{pmatrix} \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 \\ \end{pmatrix}.\qquad(4.74) POpenGL=⎝⎜⎜⎜⎛r−l2n′0000t−b2n′00r−lr+lt−bt+b−f′−n′f′+n′−100−f′−n′2f′n′0⎠⎟⎟⎟⎞.(4.74)
一个更简单的设置是只提供竖直视域, ϕ \phi ϕ,宽高比 a = w / h a=w/h a=w/h(其中 w × h w\times h w×h是屏幕分辨率), n ′ n′ n′,和 f ′ f′ f′。到可以导出:
P OpenGL = ( c / a 0 0 0 0 c 0 0 0 0 − f ′ + n ′ f ′ − n ′ − 2 f ′ n ′ f ′ − n ′ 0 0 − 1 0 ) . ( 4.75 ) \mathbf{P}_{\text{OpenGL}} = \begin{pmatrix} 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 \\ \end{pmatrix}.\qquad(4.75) POpenGL=⎝⎜⎜⎛c/a0000c0000−f′−n′f′+n′−100−f′−n′2f′n′0⎠⎟⎟⎞.(4.75)
其中 c = 1.0 / tan ( ϕ / 2 ) c = 1.0/\text{tan}(\phi/2) c=1.0/tan(ϕ/2)。这个矩阵和以前的gluPerspective()实现的是一样的(这是OpenGL Utility Library(GLU)的一部分)。
一些API(例如,DirectX)将近平面映射到 z = 0 z=0 z=0(而不是 z = − 1 z=-1 z=−1),将远平面映射到 z = 1 z=1 z=1。此外,DirectX使用左手坐标系来定义它的投影矩阵。这意味着DirectX是沿着正 z z z轴方向观察,并且其近和远两个值都是正数。这里给出DirectX的公式:
P p [ 0 , 1 ] = ( 2 n ′ r − l 0 − r + l r − l 0 0 2 n ′ t − b − t + b t − b 0 0 0 f ′ f ′ − n ′ − f ′ n ′ f ′ − n ′ 0 0 1 0 ) . ( 4.76 ) \mathbf{P}_{p[0,1]} = \begin{pmatrix} \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 \\ \end{pmatrix}.\qquad(4.76) Pp[0,1]=⎝⎜⎜⎜⎛r−l2n′0000t−b2n′00−r−lr+l−t−bt+bf′−n′f′100−f′−n′f′n′0⎠⎟⎟⎟⎞.(4.76)
DirectX在其说明文档中使用以行为主的形式,因此这个矩阵通常是以转置的形式表示的。
使用透视变换的一个效果是,计算的深度值不随输入 p z p_z pz的值线性变化。使用公式4.74到4.76中的任意一个乘以点 p \mathbf{p} p,我们可以得到
v = P p = ( . . . . . . d p z + e ± p z ) , ( 4.77 ) \mathbf{v} = \mathbf{P}\mathbf{p} = \begin{pmatrix} ... \\ ... \\dp_z+e\\ \pm p_z\\ \end{pmatrix}, \qquad(4.77) v=Pp=⎝⎜⎜⎛......dpz+e±pz⎠⎟⎟⎞,(4.77)
其中省略了
v
x
v_x
vx和
v
y
v_y
vy的细节,常数
d
d
d和
f
f
f取决于选择的矩阵。例如如果我们使用公式4.74,那么
d
=
−
(
f
′
+
n
′
)
/
(
f
′
−
n
′
)
d = -(f'+n')/(f'-n')
d=−(f′+n′)/(f′−n′),
e
=
−
2
f
′
n
′
/
(
f
′
−
n
′
)
e = -2f'n'/(f'-n')
e=−2f′n′/(f′−n′)以及
v
x
=
−
p
z
v_x = - p_z
vx=−pz。为了获得标准设备坐标系(NDC)中的深度,我们需要除以
w
w
w分量,得到
z
N
D
C
=
d
p
z
+
e
−
p
z
=
d
−
e
p
z
,
(
4.78
)
z_{NDC} = \frac{dp_z+e}{-p_z} = d - \frac{e}{p_z},\qquad(4.78)
zNDC=−pzdpz+e=d−pze,(4.78)
其中以OpenGL投影来说 z N D C ∈ [ − 1 , + 1 ] z_{NDC}\in [-1,+1] zNDC∈[−1,+1]。正如我们所看到的,输出深度 z N D C 与 输 入 深 度 z_{NDC}与输入深度 zNDC与输入深度p_z$成反比。
例如,如果 n ′ = 10 n' = 10 n′=10, f ′ = 110 f' = 110 f′=110(应用OpenGL术语),当 p z p_z pz时沿着 z z z轴负方向的60个单位长度(即中途点),标准坐标深度值是0.833,而不是0。图4.21显示了改变近平面与原点距离的效果。近与远平面的放置影响着 z z z-buffer的精度。23.7小节中会进一步讨论该效果。
图 4.21. 改变近平面与原点的距离的效果。距离
f
′
−
n
′
f'-n'
f′−n′保持恒定为100。随着近平面距离原点越来越近,靠近远平面的点使用一个更小的标准设备坐标(NDC)深度空间阈值。这会使
z
−
z-
z−buffer在距离更大时的精度更小。
有多种方式去提高深度精度。一个通用做法是以浮点数或者整形存储 1.0 − z N D C 1.0-z_{NDC} 1.0−zNDC。图4.22中显示了两者的比较。Reed用仿真展示了使用具有反转 z z z的浮点缓冲区可以提供最佳精度,这也是整型深度缓冲区(通常每个深度有24位)的首选方法。Upchurch和Desbrun指出,对于标准映射(即,非反转 z z z)来讲,在变换中分离投影矩阵可以降低出错概率。例如,使用 P ( M p ) \mathbf{P}(\mathbf{M}\mathbf{p}) P(Mp)会比 T p \mathbf{T}\mathbf{p} Tp更加合适,其中 T = P M \mathbf{T}=\mathbf{P}\mathbf{M} T=PM。另外,在范围 [ 0.5 , 1.0 ] [0.5,1.0] [0.5,1.0]中,fp32和int24在精度上非常相似,因为fp32有一个23位尾数。 z N D C z_{NDC} zNDC与 1 / p z 1/p_z 1/pz成比例的原因是它使硬件更简单,使得对深度的压缩更成功,这将在第23.7节中详细讨论。
图 4.22. 在DirectX变换中不同的深度缓冲设置方法,即
z
N
D
C
∈
[
0
,
+
1
]
z_{NDC} \in [0,+1]
zNDC∈[0,+1]。左上:标准整型深度缓冲,这里展现的是4位精度(因此y轴上有16个标记)。右上:原平面设置到无穷远处,两个轴上的小位移表明这样做不会损失很多精度。左下:浮点深度为3个指数位和3个尾数位。注意y轴上的分布是非线性的,这使得x轴上的情况更糟。右下:反浮点数深度,即
1
−
z
N
D
C
1− z_{NDC}
1−zNDC,因此具有更好的分布。(插图由Nathan Reed提供。)
Lloyd建议使用深度值的对数来提高阴影贴图的精度。Lauritzen及其同事使用前一帧的 z z z-buffer来决定最大的近平面和最小的远平面。对于屏幕空间深度,Kemen建议使用下面的逐顶点映射:
z
=
w
(
l
o
g
2
(
max
(
1
0
−
6
,
1
+
w
)
)
f
c
−
1
)
,
[
OpenGL
]
z
=
w
l
o
g
2
(
max
(
1
0
−
6
,
1
+
w
)
)
f
c
/
2
,
[
DirectX
]
(
4.79
)
z=w(log_2(\max(10^{-6},1+w))f_c-1), [\text{OpenGL}]\\ z=wlog_2(\max(10^{-6},1+w))f_c/2, [\text{DirectX}]\qquad(4.79)
z=w(log2(max(10−6,1+w))fc−1),[OpenGL]z=wlog2(max(10−6,1+w))fc/2,[DirectX](4.79)
其中
w
w
w是经过投影矩阵的顶点的
w
w
w值,而
z
z
z是顶点着色器输出的
z
z
z。常量
f
c
f_c
fc是
f
c
=
2
/
log
2
(
f
+
1
)
f_c = 2/\log_2(f+1)
fc=2/log2(f+1),其中
f
f
f是远平面。当此变换仅应用于顶点着色器时,深度仍将由GPU在顶点处的非线性变换的深度之间在三角形上进行线性插值(公式4.79)。因为对数函数是单调的,只要分段线性插值和精确的非线性变换深度值之间的差异很小的话,遮挡剔除硬件和深度压缩技术则仍然有效。这在大多数有几何细分的情况下都是可以的。但是,也可以逐片元得去应用变换。这是通过输出一个逐顶点的值
e
=
1
+
w
e=1+w
e=1+w实现的,这个值后来会被GPU在整个三角形上进行插值。然后像素着色器将片元深度修正为
log
2
(
e
i
)
f
c
/
2
\log_2(e_i)f_c/2
log2(ei)fc/2,其中
e
i
e_i
ei是
e
e
e的插值。这是一个很好的可选项,尤其是在GPU上没有浮点深度时并且使用大距离深度渲染时。
Cozzi建议使用多视锥,这可以提高将准确率提示到任意想要的值。在深度方向上,视锥被分为多个互相不重叠的更小的子视锥(他们组合起来就是原先的视锥)。子视锥体以从后到前的顺序来进行渲染。首先,清除颜色缓冲区和深度缓冲区,将要渲染的所有对象排序到它们重叠的每个子视锥体中。对于每个子视锥体,设置其投影矩阵,清除深度缓冲区,然后渲染与子视锥体重叠的对象。
Futher Reading and Resources(深入阅读学习资源)
immersive linear algebra网站提供了一本关于这个主题的交互性的书籍,通过然你直接操作图片来帮助建立直观印象。在realtimerendering.com上还链接有一些其他的交互性的学习工具和变换代码库。
“无痛”建立关于矩阵的直观认识的最好的书籍之一就是Farin和Hansford的The Geometry Toolbox。此外,还有Lengyel的Mathmatics for 3D Game Programming and Computer Graphics。从一个不同的视角看的话,许多计算机图形文本,例如Hearn和Baker,Marschner和Shirley,以及Hughes及其同事也涉及到一些矩阵基础。Ochiai及其同事的课程介绍了在计算机图形学应用中的矩阵基础以及矩阵的指数和对数运算。Graphics Gems系列介绍了多种变换相关的算法并且网上有相关的代码。Golub和Van Loan的Matrix Computations通常是开始矩阵技术的严谨研究的地方。在Lewis及其同事的SIGGRAPH研究中可以找到更多关于骨架子空间变形/顶点混合和形状插值的内容。
Hart及其同事和Hanson提供了四元数的可视化。Pletinckx和Schlag展示了在一组四元数间进行平滑插值的不同的方法。Vlachos和Isidoro推导了四元数的 C 2 C^2 C2插值公式。与四元数插值相关的是沿曲线计算一致坐标系的问题。这是由Dougan处理的。
Alexa和Lazarus和Verroust介绍了关于需要不同变形技术的研究。Parent的书是一个关于计算机动画技术的优质来源。