[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}} 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
旋转变换与缩放变换是线性变换,在三维空间中变换矩阵可以表示成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] R x ( θ ) = ⎣ ⎢ ⎢ ⎡ 1 0 0 0 0 cos θ sin θ 0 0 − sin θ cos θ 0 0 0 0 1 ⎦ ⎥ ⎥ ⎤
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] R y ( θ ) = ⎣ ⎢ ⎢ ⎡ cos θ 0 sin θ 0 0 1 0 0 − sin θ 0 cos θ 0 0 0 0 1 ⎦ ⎥ ⎥ ⎤
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] R z ( θ ) = ⎣ ⎢ ⎢ ⎡ cos θ sin θ 0 0 − sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ⎦ ⎥ ⎥ ⎤
给定欧拉角的变换
如果旋转的需求是一个欧拉角,那么我们要怎么处理呢?事实上对于一个给定欧拉角的旋转,我们需要确定一个旋转顺序,可以想象以不同的顺序绕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}} 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
座标系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}} M r o t a t i o n = M Y M X M Z
第一步旋转变换M Z {{M}_{Z}} M Z ,因为还没有发生座标系变换M Z = M r o t a t i o n Z {{M}_{Z}}={{M}_{rotationZ}} M Z = M r o t a t i o n Z ,初始座标系的基为I I I ,经过M Z {{M}_{Z}} M Z 变换,新座标的基为M Z T {{M}_{Z}}^{T} M Z T (转置是因为使用列向量表示点,所以M r o t a t i o n Z {{M}_{rotationZ}} M r o t a t i o n Z 变换矩阵的行向量表示基,求解过渡矩阵需要的是列向量形式的基),基I I I 到基M Z T {{M}_{Z}}^{T} M Z T 的过渡矩阵P有I × P = M Z T I\times P={{M}_{Z}}^{T} I × P = M Z T , P = M r o t a t i o n Z T P={{M}_{rotationZ}}^{T} P = M r o t a t i o n Z T ,旋转矩阵正交 P = M r o t a t i o n Z − 1 P={{M}_{rotationZ}}^{-1} P = M r o t a t i o n Z − 1 。
第二步旋转变换M X {{M}_{X}} M X ,是M r o t a t i o n X {{M}_{rotationX}} M r o t a t i o n X 在基I I I 中的表达,而矩阵连续变化则已经更换了基,线性变换M r o t a t i o n X {{M}_{rotationX}} M r o t a t i o n X 在新基M Z T {{M}_{Z}}^{T} M Z T 中表示为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} 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 ,由I I I 到第二步变换后基( M X M Z ) T ({{M}_{X}}{{M}_{Z}})^{T} ( M X M Z ) 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 = M r o t a t i o n Z M r o t a t i o n X 。
第三步旋转变换M Y {{M}_{Y}} M Y ,同理是M r o t a t i o n Y {{M}_{rotationY}} M r o t a t i o n Y 在基I I I 中的表达,线性变换M r o t a t i o n Y {{M}_{rotationY}} M r o t a t i o n Y 在新基( M X M Z ) T ({{M}_{X}}{{M}_{Z}})^{T} ( M X M Z ) 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} 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 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}}
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
实际上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}} 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
欧拉角表示旋转的缺点
我们可以看出,用欧拉角表示旋转是顺序相关的,会导致结果不够直观,所谓的万向节死锁 就是这种不直观造成的现象。具体现象是第二个旋转轴(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] 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 cosh + cos r sin p sinh − cos p sinh − sin r cos p cos r cos p sin p cos r sinh + sin r sin p cosh sin r sinh − cos r sin p cosh cos p cosh ⎦ ⎤
p = ± 90 ° p=±90° p = ± 9 0 ° 时
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] \\
M r o t a t i o n = ⎣ ⎡ cos r cosh ∓ sin r sinh sin r cosh ± cos r sinh 0 0 0 ± 1 cos r sinh ± sin r cosh sin r sinh ∓ cos r cosh 0 ⎦ ⎤ = + 9 0 ∘ ⎣ ⎡ cos ( r + h ) sin ( r + h ) 0 0 0 1 sin ( r + h ) − cos ( r + h ) 0 ⎦ ⎤ = − 9 0 ∘ ⎣ ⎡ cos ( r − h ) sin ( r − h ) 0 0 0 − 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 x a 表示空间A的基x x x 在空间B中的值:
P B = M A → B P A
{{P}_{B}}={{M}_{A\to B}}{{P}_{A}}
P B = M A → B P A P A = M B → A P B
{{P}_{A}}={{M}_{B\to A}}{{P}_{B}}
P A = M B → A P B P B T = P A T M B → A
{{P}_{B}}^T={{P}_{A}}^T{{M}_{B\to A}}
P B T = P A T M B → A P A T = P B T M A → B
{{P}_{A}}^T={{P}_{B}}^T{{M}_{A\to B}}
P A T = P B T M A → 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
M A → B = ⎣ ⎡ ∣ x a ∣ ∣ y a ∣ ∣ z a ∣ ⎦ ⎤ = ⎣ ⎡ − − − x b y b z b − − − ⎦ ⎤ = M B → A T 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
M B → A = ⎣ ⎡ ∣ x b ∣ ∣ y b ∣ ∣ z b ∣ ⎦ ⎤ = ⎣ ⎡ − − − x a y a z a − − − ⎦ ⎤ = M A → B T
例如把法线贴图(N T a n g e n t N_{Tangent} N T a n g e n t )从切线空间变换到世界空间(N W o r l d N_{World} N W o r l d ),已知由法线、切线、副法线,构成的矩阵:
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]
M W o r l d → T a n g e n t = ⎣ ⎡ − − − n t b − − − ⎦ ⎤ 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}}
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
使用行向量在左,矩阵在右的乘法。
法线变换
如上图所示,在存在缩放时,法线的变换矩阵与模型顶点的变换矩阵不同。切线可以用模型上两个点的差值表示,所以模型顶点的变换公式适用于切线。当使用3X3性变换矩阵M A → B {M}_{A\to B} M A → B 表示座标空间A到座标空间B的顶点变换时,切线变换可以表示为
T B = M A → B T A
{T_{B}} = {{M}_{A\to B}}{T_{A}}
T B = M A → B T A
此时法线变换可以表示为
N B = ( M A → B T ) − 1 N A
{N_{B}} = ({{M}_{A\to B}}^T)^{-1}{N_{A}}
N B = ( M A → B T ) − 1 N A
注意,这个变换不保证法线的长度是归一化的,需要再归一化操作一次。
证明:
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
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 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}
M N = ( M A → B − 1 ) T = ( M A → B T ) − 1
在不存在缩放时M A → B {{{M}_{A\to B}}} M A → B 是正交矩阵M N = ( M A → B − 1 ) T = M A → B {M_N} =({{{M}_{A\to B}}}^{-1})^{T}={{{M}_{A\to B}}} M N = ( M A → B − 1 ) T = M A → B ;系数为k k k 的统一缩放变换时,M N = 1 k M A → B {M_N} =\frac{1}{k}{{{M}_{A\to B}}} M N = k 1 M A → B 。实际操作过程中,会把不存在缩放与统一缩放合并处理,最后都进行归一化操作。