[Rotation Transform] 旋转变换
这篇文章纪录一些关于旋转变换的知识,和Unity引擎中的旋转相关API。
游戏中对Transform的变换通常包含平移旋转与缩放,常用的旋转变换有旋转矩阵和四元数两种。
旋转矩阵
绕当前坐标系指定轴的变换矩阵
一个对向量进行位移旋转缩放的复合变换的矩阵可以表示成以下形式:
P
n
e
w
=
M
t
r
a
n
s
l
a
t
i
o
n
M
r
o
t
a
t
i
o
n
M
s
c
a
l
e
P
o
l
d
{P_{new}} = {M_{translation}}{M_{rotation}}{M_{scale}}{P_{old}}
Pnew=MtranslationMrotationMscalePold
旋转变换与缩放变换是线性变换,在三维空间中变换矩阵可以表示成3X3的形式,而位移变换是仿射变换,需要4X4的矩阵表示,因为本文只讨论旋转变换,定义后面的部分会直接使用3X3矩阵。且对于线性变换,我们使用列向量代表点,则3X3过渡矩阵的行向量代表坐标变换中的基(
x
、
y
、
z
x、y、z
x、y、z轴)。
绕
x
、
y
、
z
x、y、z
x、y、z轴旋转
θ
\theta
θ度的矩阵:
R
x
(
θ
)
=
[
1
0
0
0
0
cos
θ
−
sin
θ
0
0
sin
θ
cos
θ
0
0
0
0
1
]
{{R}_{x}}(\theta )=\left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & \cos \theta & -\sin \theta & 0 \\ 0 & \sin \theta & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right]
Rx(θ)=⎣⎢⎢⎡10000cosθsinθ00−sinθcosθ00001⎦⎥⎥⎤
R
y
(
θ
)
=
[
cos
θ
0
−
sin
θ
0
0
1
0
0
sin
θ
0
cos
θ
0
0
0
0
1
]
{{R}_{y}}(\theta )=\left[ \begin{matrix} \cos \theta & 0 & -\sin \theta & 0 \\ 0 & \text{1} & \text{0} & 0 \\ \sin \theta & \text{0} & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right]
Ry(θ)=⎣⎢⎢⎡cosθ0sinθ00100−sinθ0cosθ00001⎦⎥⎥⎤
R
z
(
θ
)
=
[
cos
θ
−
sin
θ
0
0
sin
θ
cos
θ
0
0
0
0
1
0
0
0
0
1
]
{{R}_{z}}(\theta )=\left[ \begin{matrix} \cos \theta & -\sin \theta & 0 & 0 \\ \sin \theta & \cos \theta & \text{0} & 0 \\ 0 & \text{0} & \text{1} & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right]
Rz(θ)=⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤
给定欧拉角的变换
如果旋转的需求是一个欧拉角,那么我们要怎么处理呢?事实上对于一个给定欧拉角的旋转,我们需要确定一个旋转顺序,可以想象以不同的顺序绕
x
、
y
、
z
x、y、z
x、y、z轴旋转得到的结果是不同的(对于四元数也是一样的)。Unity中的约定为
z
、
x
、
y
z、x、y
z、x、y: “Applies a rotation of eulerAngles.z degrees around the z-axis, eulerAngles.x degrees around the x-axis, and eulerAngles.y degrees around the y-axis (in that order)”。
还需要注意的是这个
z
、
x
、
y
z、x、y
z、x、y顺序所在的坐标系指的是旋转前的坐标系,不随旋转而改变,我们观察一个组合旋转矩阵:
M
r
o
t
a
t
i
o
n
=
M
r
o
t
a
t
i
o
n
Z
M
r
o
t
a
t
i
o
n
X
M
r
o
t
a
t
i
o
n
Y
{{M}_{rotation}}={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}}
Mrotation=MrotationZMrotationXMrotationY
坐标系Transform可以用一个矩阵来表示,而旋转矩阵实际作用就是旋转坐标系,从上面的3个旋转矩阵中可以知道,每一个旋转都是对当前坐标系的旋转。该矩阵去变换一个坐标系的的过程为,绕当前坐标系Y轴旋转
θ
\theta
θ度,得到一个新的旋转后坐标系,继续绕当前旋转后坐标系X轴旋转
θ
\theta
θ度,得到一个新的两次旋转后坐标系,继续绕当前旋转后坐标系Z轴旋转
θ
\theta
θ度。这代表的是一个在当前坐标系,按
y
、
x
、
z
y、x、z
y、x、z顺序的旋转。
上面的过程与按照旋转前坐标系的旋转顺序
z
、
x
、
y
z、x、y
z、x、y是等价的,就是说他们所代表的顺序是相反的。
证明:
按照旋转前坐标系的旋转顺序z、x、y可以用以下矩阵表示
M r o t a t i o n = M Y M X M Z {{M}_{rotation}}={{M}_{Y}}{{M}_{X}}{{M}_{Z}} Mrotation=MYMXMZ
第一步旋转变换 M Z {{M}_{Z}} MZ,因为还没有发生坐标系变换 M Z = M r o t a t i o n Z {{M}_{Z}}={{M}_{rotationZ}} MZ=MrotationZ,初始坐标系的基为 I I I,经过 M Z {{M}_{Z}} MZ变换,新坐标的基为 M Z T {{M}_{Z}}^{T} MZT(转置是因为使用列向量表示点,所以 M r o t a t i o n Z {{M}_{rotationZ}} MrotationZ变换矩阵的行向量表示基,求解过渡矩阵需要的是列向量形式的基),基 I I I到基 M Z T {{M}_{Z}}^{T} MZT的过渡矩阵P有 I × P = M Z T I\times P={{M}_{Z}}^{T} I×P=MZT, P = M r o t a t i o n Z T P={{M}_{rotationZ}}^{T} P=MrotationZT,旋转矩阵正交 P = M r o t a t i o n Z − 1 P={{M}_{rotationZ}}^{-1} P=MrotationZ−1。
第二步旋转变换 M X {{M}_{X}} MX,是 M r o t a t i o n X {{M}_{rotationX}} MrotationX在基 I I I中的表达,而矩阵连续变化则已经更换了基,线性变换 M r o t a t i o n X {{M}_{rotationX}} MrotationX在新基 M Z T {{M}_{Z}}^{T} MZT中表示为 M X = P − 1 M r o t a t i o n X P = M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Z − 1 {{M}_{X}}={{P}^{-1}}{{M}_{rotationX}}{P}={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationZ}}^{-1} MX=P−1MrotationXP=MrotationZMrotationXMrotationZ−1,由 I I I到第二步变换后基 ( M X M Z ) T ({{M}_{X}}{{M}_{Z}})^{T} (MXMZ)T的的过度矩阵为 P = M r o t a t i o n Z M r o t a t i o n X {P}={{M}_{rotationZ}}{{M}_{rotationX}} P=MrotationZMrotationX。
第三步旋转变换 M Y {{M}_{Y}} MY,同理是 M r o t a t i o n Y {{M}_{rotationY}} MrotationY在基 I I I中的表达,线性变换 M r o t a t i o n Y {{M}_{rotationY}} MrotationY在新基 ( M X M Z ) T ({{M}_{X}}{{M}_{Z}})^{T} (MXMZ)T中表示为 M Y = P − 1 M r o t a t i o n Y P = M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Y ( M r o t a t i o n Z M r o t a t i o n X ) − 1 {{M}_{Y}}={{P}^{-1}}{{M}_{rotationY}}{P}={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}}({{M}_{rotationZ}}{{M}_{rotationX}})^{-1} MY=P−1MrotationYP=MrotationZMrotationXMrotationY(MrotationZMrotationX)−1。M r o t a t i o n = M Y M X M Z = ( M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Y ( M r o t a t i o n Z M r o t a t i o n X ) − 1 ) ( M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Z ) ( M r o t a t i o n Z − 1 ) = M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Y {{M}_{rotation}}={{M}_{Y}}{{M}_{X}}{{M}_{Z}} \\ =({{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}}{{({{M}_{rotationZ}}{{M}_{rotationX}})}^{-1}})({{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationZ}})({{M}_{rotationZ}}^{-1}) \\ ={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}} Mrotation=MYMXMZ=(MrotationZMrotationXMrotationY(MrotationZMrotationX)−1)(MrotationZMrotationXMrotationZ)(MrotationZ−1)=MrotationZMrotationXMrotationY
实际上Unity中的旋转按照欧拉角的表达就是: M r o t a t i o n = M r o t a t i o n Z M r o t a t i o n X M r o t a t i o n Y {{M}_{rotation}}={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}} Mrotation=MrotationZMrotationXMrotationY
欧拉角表示旋转的缺点
我们可以看出,用欧拉角表示旋转是顺序相关的,会导致结果不够直观,所谓的万向节死锁就是这种不直观造成的现象。具体现象是第二个旋转轴(X轴)的旋转角度为±90°时,第三次旋转与第一次旋转对应相同或相反的旋转轴,也可以理解为第二次旋转将第三次旋转的旋转轴旋转到与第一次旋转的旋转轴共线的位置,把transform面板上的Rotation的x设置为±90时改变
z
、
y
z、y
z、y即可尝试。
我们看看万向节死锁的数学表达是什么,
z
、
x
、
y
z、x、y
z、x、y轴转动度数分别
r
、
p
、
h
r、p、h
r、p、h时为进一步展开旋转矩阵:
M
r
o
t
a
t
i
o
n
=
M
r
o
t
a
t
i
o
n
Z
M
r
o
t
a
t
i
o
n
X
M
r
o
t
a
t
i
o
n
Y
=
[
cos
r
cosh
−
sin
r
sin
p
sinh
−
sin
r
cos
p
cos
r
sinh
+
sin
r
sin
p
cosh
sin
r
cosh
+
cos
r
sin
p
sinh
cos
r
cos
p
sin
r
sinh
−
cos
r
sin
p
cosh
−
cos
p
sinh
sin
p
cos
p
cosh
]
{{M}_{rotation}}={{M}_{rotationZ}}{{M}_{rotationX}}{{M}_{rotationY}}\\=\left[ \begin{matrix} \cos r\cosh -\sin r\sin p\sinh & -\sin r\cos p & \cos r\sinh +\sin r\sin p\cosh \\ \sin r\cosh +\cos r\sin p\sinh & \cos r\cos p & \sin r\sinh -\cos r\sin p\cosh \\ -\cos p\sinh & \sin p & \cos p\cosh \\ \end{matrix} \right]
Mrotation=MrotationZMrotationXMrotationY=⎣⎡cosrcosh−sinrsinpsinhsinrcosh+cosrsinpsinh−cospsinh−sinrcospcosrcospsinpcosrsinh+sinrsinpcoshsinrsinh−cosrsinpcoshcospcosh⎦⎤
p
=
±
90
°
p=±90°
p=±90°时
M
r
o
t
a
t
i
o
n
=
[
cos
r
cosh
∓
sin
r
sinh
0
cos
r
sinh
±
sin
r
cosh
sin
r
cosh
±
cos
r
sinh
0
sin
r
sinh
∓
cos
r
cosh
0
±
1
0
]
=
+
90
∘
[
cos
(
r
+
h
)
0
sin
(
r
+
h
)
sin
(
r
+
h
)
0
−
cos
(
r
+
h
)
0
1
0
]
=
−
90
∘
[
cos
(
r
−
h
)
0
−
sin
(
r
−
h
)
sin
(
r
−
h
)
0
cos
(
r
−
h
)
0
−
1
0
]
{{M}_{rotation}}=\left[ \begin{matrix} \cos r\cosh \mp \sin r\sinh & 0 & \cos r\sinh \pm \sin r\cosh \\ \sin r\cosh \pm \cos r\sinh & 0 & \sin r\sinh \mp \cos r\cosh \\ 0 & \pm 1 & 0 \\ \end{matrix} \right] \\ \overset{+{{90}^{\circ }}}{\mathop{=}}\,\left[ \begin{matrix} \cos (r+h) & 0 & \sin (r+h) \\ \sin (r+h) & 0 & -\cos (r+h) \\ 0 & 1 & 0 \\ \end{matrix} \right] \\ \overset{-{{90}^{\circ }}}{\mathop{=}}\,\left[ \begin{matrix} \cos (r-h) & 0 & -\sin (r-h) \\ \sin (r-h) & 0 & \cos (r-h) \\ 0 & -1 & 0 \\ \end{matrix} \right] \\
Mrotation=⎣⎡cosrcosh∓sinrsinhsinrcosh±cosrsinh000±1cosrsinh±sinrcoshsinrsinh∓cosrcosh0⎦⎤=+90∘⎣⎡cos(r+h)sin(r+h)0001sin(r+h)−cos(r+h)0⎦⎤=−90∘⎣⎡cos(r−h)sin(r−h)000−1−sin(r−h)cos(r−h)0⎦⎤
可以看出
r
、
h
r、h
r、h出现共线的转动的原因。
四元数与旋转
对旋转而言,四元数要比欧拉角和矩阵更好用,对于给定轴与角度的旋转非常直观,易于进行球面插值,很多运算过程不需要计算三角函数。关于四元数的数学意义这里不赘述了,四元数
q
^
p
^
q
^
−
1
\hat{q}\hat{p}{{\hat{q}}^{-1}}
q^p^q^−1表示四元数
q
^
\hat{q}
q^对向量
p
^
\hat{p}
p^的变换,Unity中被封装为乘法。虽然Transform使用四元数存储计算旋转信息,但对于需要显示的图形,还是会将其转换为矩阵传入Shader。
四元数是区别于矩阵的一种旋转的表达方式,但旋转结果是相同的,所以通过欧拉角来构造四元数也需要遵行约定好顺序
z
、
x
、
y
z、x、y
z、x、y。
旋转顺序的验证
在Unity中对旋转顺序进行验证。
using UnityEngine;
public class Rotation : MonoBehaviour
{
public Vector3 angle;
public Transform[] transforms;
public void Rotate()
{
transforms[0].Rotate(angle, Space.Self);
ZXY_OriginalCoordinate(transforms[1], angle);
YXZ_FollowCoordinate(transforms[2], angle);
transforms[3].rotation *= Quaternion.Euler(angle);
QuaternionZXY_OriginalCoordinate(transforms[4], angle);
QuaternionYXZ_FollowCoordinate(transforms[5], angle);
}
public void ResetRotation()
{
for (int i = 0; i < transforms.Length; i++)
{
transforms[i].rotation = Quaternion.identity;
}
}
private void ZXY_OriginalCoordinate(Transform transform, Vector3 angle)
{
Vector3 x = transform.right;
Vector3 y = transform.up;
Vector3 z = transform.forward;
transform.Rotate(z, angle.z, Space.World);
transform.Rotate(x, angle.x, Space.World);
transform.Rotate(y, angle.y, Space.World);
}
private void YXZ_FollowCoordinate(Transform transform, Vector3 angle)
{
transform.Rotate(Vector3.up, angle.y, Space.Self);
transform.Rotate(Vector3.right, angle.x, Space.Self);
transform.Rotate(Vector3.forward, angle.z, Space.Self);
}
private void QuaternionZXY_OriginalCoordinate(Transform transform, Vector3 angle)
{
Quaternion x = Quaternion.AngleAxis(angle.x, transform.right);
Quaternion y = Quaternion.AngleAxis(angle.y, transform.up);
Quaternion z = Quaternion.AngleAxis(angle.z, transform.forward);
transform.rotation = (y * (x * (z * transform.rotation)));
}
private void QuaternionYXZ_FollowCoordinate(Transform transform, Vector3 angle)
{
transform.rotation *= Quaternion.AngleAxis(angle.y, Vector3.up);
transform.rotation *= Quaternion.AngleAxis(angle.x, Vector3.right);
transform.rotation *= Quaternion.AngleAxis(angle.z, Vector3.forward);
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
for (int i = 0; i < transforms.Length; i++)
{
UnityEditor.Handles.PositionHandle(transforms[i].position, transforms[i].rotation);
}
}
#endif
}
对于不同欧拉角,六种旋转方式的结果是相同的。
Shader中的方向变换
Shader中常用到的方向变换包括直线光方向、视线方向、统一缩放下的法线等。变换表示线性3X3变换矩阵M与三维向量p相乘,但Mp与pM的形式都有使用,前者把向量作为行向量,后者是列向量。我们具体选行向量还是列向量呢?下面是变换矩阵的总结,
x
a
x_a
xa表示空间A的基
x
x
x在空间B中的值:
P
B
=
M
A
→
B
P
A
{{P}_{B}}={{M}_{A\to B}}{{P}_{A}}
PB=MA→BPA
P
A
=
M
B
→
A
P
B
{{P}_{A}}={{M}_{B\to A}}{{P}_{B}}
PA=MB→APB
P
B
T
=
P
A
T
M
B
→
A
{{P}_{B}}^T={{P}_{A}}^T{{M}_{B\to A}}
PBT=PATMB→A
P
A
T
=
P
B
T
M
A
→
B
{{P}_{A}}^T={{P}_{B}}^T{{M}_{A\to B}}
PAT=PBTMA→B
M
A
→
B
=
[
∣
∣
∣
x
a
y
a
z
a
∣
∣
∣
]
=
[
−
x
b
−
−
y
b
−
−
z
b
−
]
=
M
B
→
A
T
{{M}_{A\to B}}=\left[ \begin{matrix} | & | & | \\ {{x}_{a}} & {{y}_{a}} & {{z}_{a}} \\ | & | & | \\ \end{matrix} \right]=\left[ \begin{matrix} -& {{x}_{b}} & - \\ -& {{y}_{b}} & - \\ -& {{z}_{b}} & - \\ \end{matrix} \right]={{M}_{B\to A}}^T
MA→B=⎣⎡∣xa∣∣ya∣∣za∣⎦⎤=⎣⎡−−−xbybzb−−−⎦⎤=MB→AT
M
B
→
A
=
[
∣
∣
∣
x
b
y
b
z
b
∣
∣
∣
]
=
[
−
x
a
−
−
y
a
−
−
z
a
−
]
=
M
A
→
B
T
{{M}_{B\to A}}=\left[ \begin{matrix} | & | & | \\ {{x}_{b}} & {{y}_{b}} & {{z}_{b}} \\ | & | & | \\ \end{matrix} \right]=\left[ \begin{matrix} -& {{x}_{a}} & - \\ -& {{y}_{a}} & - \\ -& {{z}_{a}} & - \\ \end{matrix} \right]={{M}_{A\to B}}^T
MB→A=⎣⎡∣xb∣∣yb∣∣zb∣⎦⎤=⎣⎡−−−xayaza−−−⎦⎤=MA→BT
例如把法线贴图(
N
T
a
n
g
e
n
t
N_{Tangent}
NTangent)从切线空间变换到世界空间(
N
W
o
r
l
d
N_{World}
NWorld),已知由法线、切线、副法线,构成的矩阵:
M
W
o
r
l
d
→
T
a
n
g
e
n
t
=
[
−
n
−
−
t
−
−
b
−
]
{{M}_{World\to Tangent}}=\left[ \begin{matrix} -& {n} & - \\ -& {t} & - \\ -& {b} & - \\ \end{matrix} \right]
MWorld→Tangent=⎣⎡−−−ntb−−−⎦⎤
N
W
o
r
l
d
T
=
N
T
a
n
g
e
n
t
T
M
W
o
r
l
d
→
T
a
n
g
e
n
t
{N_{World}}^T = {N_{Tangent}}^T {{M}_{World\to Tangent}}
NWorldT=NTangentTMWorld→Tangent
使用行向量在左,矩阵在右的乘法。
法线变换
如上图所示,在存在缩放时,法线的变换矩阵与模型顶点的变换矩阵不同。切线可以用模型上两个点的差值表示,所以模型顶点的变换公式适用于切线。当使用3X3性变换矩阵
M
A
→
B
{M}_{A\to B}
MA→B表示坐标空间A到坐标空间B的顶点变换时,切线变换可以表示为
T
B
=
M
A
→
B
T
A
{T_{B}} = {{M}_{A\to B}}{T_{A}}
TB=MA→BTA
此时法线变换可以表示为
N
B
=
(
M
A
→
B
T
)
−
1
N
A
{N_{B}} = ({{M}_{A\to B}}^T)^{-1}{N_{A}}
NB=(MA→BT)−1NA
注意,这个变换不保证法线的长度是归一化的,需要再归一化操作一次。
证明:
T A ⋅ N A = T A T N A = T A T ( M A → B − 1 M A → B ) T N A = T A T M A → B T ( M A → B − 1 ) T N A = ( M A → B T A ) T ( M A → B − 1 ) T N A = T B T N B = 0 {T_{A}}\cdot {N_{A}} ={T_{A}}^{T}{N_{A}} \\= {T_{A}}^{T}({{{M}_{A\to B}}}^{-1}{{{M}_{A\to B}}})^{T}{N_{A}} \\={T_{A}}^{T}{{{M}_{A\to B}}}^{T}({{{M}_{A\to B}}}^{-1})^{T}{N_{A}} \\=({{M}_{A\to B}}{T_{A}})^{T}({{{M}_{A\to B}}}^{-1})^{T}{N_{A}} \\={T_{B}}^{T}{N_B} =0 TA⋅NA=TATNA=TAT(MA→B−1MA→B)TNA=TATMA→BT(MA→B−1)TNA=(MA→BTA)T(MA→B−1)TNA=TBTNB=0 M N = ( M A → B − 1 ) T = ( M A → B T ) − 1 {M_N} =({{{M}_{A\to B}}}^{-1})^{T}=({{{M}_{A\to B}}}^{T})^{-1} MN=(MA→B−1)T=(MA→BT)−1
在不存在缩放时 M A → B {{{M}_{A\to B}}} MA→B是正交矩阵 M N = ( M A → B − 1 ) T = M A → B {M_N} =({{{M}_{A\to B}}}^{-1})^{T}={{{M}_{A\to B}}} MN=(MA→B−1)T=MA→B;系数为 k k k的统一缩放变换时, M N = 1 k M A → B {M_N} =\frac{1}{k}{{{M}_{A\to B}}} MN=k1MA→B。实际操作过程中,会把不存在缩放与统一缩放合并处理,最后都进行归一化操作。