游戏引擎中,用四元数表示旋转

0 其他方法表示旋转

欧拉角

  1. 只有3个分量,占用空间虽然少,但是对于任意方向的旋转轴,无法插值
  2. 万向节死锁,当旋转90°时,三个主轴的两个周会完全对齐,此时就会出现死锁。例如若让物体绕x轴旋转90°,y轴便会与z轴完全对齐。那么,就不能再单独绕原来的y轴旋转了,因为绕y轴和z轴的旋转实际上已经等效。
  3. 必须规定绕轴顺序,次序不对旋转出来的结果是不一样的。单独的三个标量并不能定义一个旋转,还需要一个顺序!

3×3矩阵

  1. 占用空间多:矩阵需9个float,如果是仿射变换,那就是16个
  2. 计算复杂:矩阵乘法来旋转矢量,需3个点积,共9个乘法 + 6个加法。
  3. 不直观:一坨矩阵表,可读性差。
  4. 用矩阵表示旋转不会出现万向节死锁哦,不要跟欧拉角表示混淆

1 单位四元代表三维旋转

四元数: q = [ q x    q y    q z    q w ] \Large q = [q_x\space\space q_y\space\space q_z\space\space q_w] q=[qx  qy  qz  qw]。四元数看起来就是一个四维矢量,但是行为上是有区别的。

注意下面三维矢量,惯例加粗,四元数就不加粗了

记住:能代表三维旋转的只能是单位四元数 q x 2 + q y 2 + q z 2 + q w 2 = 1 q^2_x+q^2_y+q^2_z+q^2_w = 1 qx2+qy2+qz2+qw2=1

单位四元数 = 三维矢量 q V \large q_V qV + 标量 q S \large q_S qS

  • q V \large q_V qV:旋转的单位轴 x 旋转半角的正弦;下标V代表Vector
  • q S \large q_S qS:旋转半角的余弦;下标S代表Scale

单位四元数可写成:
q = [ q V    q S ] = [ a sin ⁡ θ 2    cos ⁡ θ 2 ] = [ a x sin ⁡ θ 2    a y sin ⁡ θ 2    a z sin ⁡ θ 2    cos ⁡ θ 2 ] \large q=[\bold{q}_V\space\space q_S]=[\bold{a}\sin{\frac{\theta}{2}} \space\space\cos{\frac{\theta}{2}}]=[a_x\sin{\frac{\theta}{2}}\space\space a_y\sin{\frac{\theta}{2}}\space\space a_z\sin{\frac{\theta}{2}}\space\space \cos{\frac{\theta}{2}}] q=[qV  qS]=[asin2θ  cos2θ]=[axsin2θ  aysin2θ  azsin2θ  cos2θ]

  • a \bold a a:旋转轴方向的单位矢量
  • θ \theta θ:旋转角度

注意,在数学定义中,四元数是由1个实部+3个虚部组成,即
q = a + b i + c j + d k ( a , b , c , d ∈ R ) \large q = a + bi+ cj + dk (a,b,c,d∈R) q=a+bi+cj+dk(a,b,c,dR)
因此,一般会写成标量在前,3个虚部为一组放在后面
q = [ q S    q V ] q=[\large q_S\space\space \bold{q}_V] q=[qS  qV]
但在游戏、图形领域,可能是出于内存布局的考虑,大部分都将实部放在最后一位,即
q = [ q V    q S ] \large q=[\bold{q}_V\space\space q_S] q=[qV  qS]

2 四元数运算

因为旋转要求是单位四元数,因此基本不会进行+ -运算

2.1 乘法

乘法四元数最重要的运算法则。给定两个单位四元数 p 和 q ,分别代表旋转 P \bold P P Q \bold Q Q,则pq则代表两个旋转的合成旋转(先 Q \bold Q Q P \bold P P),四元数乘法不止一种,但是三维旋转应用相关的乘法称为格拉斯曼积(Grassmann Product):
p q = [ ( p S q V + q s p V + p V × q V )     ( p S q S − p V ⋅ q V ) \Large pq = [(p_S\bold{q}_V + q_s\bold{p}_V+\bold{p}_V\times\bold{q}_V)\space\space\space (p_Sq_S-\bold{p}_V·\bold{q}_V) pq=[(pSqV+qspV+pV×qV)   (pSqSpVqV) 注意,这里面矢量为四元数的前3个分量(x,y,z),标量是最后1个分量(w)

2.2 四元数的共轭和取逆

四元数的模
∣ ∣ q ∣ ∣ = a 2 + b 2 + c 2 + d 2 \large ||q|| = \sqrt{a^2+b^2+c^2+d^2} ∣∣q∣∣=a2+b2+c2+d2
共轭四元数:实部不变,虚部取反
q ∗ = [ − q V q S ] \large q^* = [-\bold q_V\quad q_S] q=[qVqS]

四元数的逆:模方分之伴随,由于我们是在讨论三维旋转,四元数都是单位长度的,因此 ∣ q ∣ = 1 |q| =1 q=1
q − 1 = q ∗ ∣ q ∣ 2 = q ∗ = [ − q V q S ] \large q^{-1}=\frac{q^*}{|q|^2}=q^*=[-\bold q_V\quad q_S] q1=q2q=q=[qVqS]
这一结论是非常有价值的,因为它意味着计算逆四元数时,当知道四元数已被归一化,就
不用除以模平方了(相对费时)。同时也意味着,计算逆四元数比计算3×3逆矩阵快得
,在某些情况下,我们可以利用这一特点优化引擎。

其他的一些运算性质,跟普通向量运算一样

互逆性: q − 1 q = q q − 1 = 1 \large q^{-1}q = qq^{-1}=1 q1q=qq1=1

积的共轭: ( p q ) ∗ = q ∗ p ∗ (pq)^*=q^*p^* (pq)=qp

积的逆: ( p q ) − 1 = q − 1 p − 1 (pq)^{-1}=q^{-1}p^{-1} (pq)1=q1p1

积的转置: ( p q ) T = q T p T (pq)^{T}=q^{T}p^{T} (pq)T=qTpT

2.3 用四元数旋转矢量

首先要把矢量重写为四元数形式。把标量项 q S q_S qS设为0。给定矢量 v \bold v v,可把它写成对应的四元数 v = [ v 0 ] = [ v x v y v z 0 ] v=[\bold v\quad 0]=[v_x\quad v_y\quad v_z\quad 0] v=[v0]=[vxvyvz0]

以四元数 q q q 旋转矢量 v \bold v v
v ′ = q v q − 1 v'=qvq^{-1} v=qvq1
因为 q − 1 = q ∗ q^{-1}=q^* q1=q,因此也可以用共轭四元数代替四元数的逆
v ′ = q v q ∗ v'=qvq^{*} v=qvq
最后,提取 v ′ v' v的矢量部分就是旋转后的矢量 v ′ \bold v' v

多个旋转是可以进行拼接的,左右一直加就行了

注意,在游戏引擎中,四元数的使用场合很多,不光是传统的旋转变换,下面这种场景也用的特别多

案例: 一个游戏对象,在世界空间中以某个姿态摆放着,已知该物体当前的旋转状态为 Transform.Rotation =(roll, pitch, yaw) (游戏引擎中一般还是用欧拉角来表示朝向状态,但是内部计算都是转成四元数的),现在,我想求它的三个方向向量 Forward UpRight

  • 欧拉角表示旋转信息,不用四元数,用4x4矩阵来旋转:
    这种方式必须遵循引擎、软件的规定,固定旋转顺序 比如先绕X轴旋转(pitch),然后绕Y轴旋转(yaw),最后绕Z轴旋转(roll)的顺序,因为顺序不一样,旋转的结果会不同
    glm::vec3 = getObjectTransform().Rotation;
    
    // 创建旋转矩阵,必须按顺序
    glm::mat4 rotationMatrix(1.0f);
    rotationMatrix = glm::rotate(rotationMatrix, glm::radians(yaw), glm::vec3(1.0f, 0.0f, 0.0f));
    rotationMatrix = glm::rotate(rotationMatrix, glm::radians(pitch), glm::vec3(0.0f, 1.0f, 0.0f));
    rotationMatrix = glm::rotate(rotationMatrix, glm::radians(roll), glm::vec3(0.0f, 0.0f, 1.0f));
    
    // 应用旋转矩阵得到新的向量
    glm::vec3 forward = glm::vec3(rotationMatrix * glm::vec4(0.0f, 0.0f, 1.0f, 0.0f));
    glm::vec3 up = glm::vec3(rotationMatrix * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f));
    glm::vec3 right = glm::vec3(rotationMatrix * glm::vec4(1.0f, 0.0f, 0.0f, 0.0f));
    
  • 用四元数来旋转: 把物体的Rotation构造成四元数,它代表了物体在初始局部坐标空间到当前世界空间中的旋转变换。因此把该四元数变换应用给局部 Forward UpRight就能得到结果
    // glm::quat()会根据传入的欧拉角表示的三个值,计算半角正弦半角余弦,然后构造一个四元数
    glm::quat rotationQuat = glm::quat(getObjectTransform().Rotation);
    // 这是glm库的
    glm::vec3 forward = glm::rotate(rotationQuat , glm::vec3(0.0f, 0.0f, 1.0f));
    glm::vec3 up= glm::rotate(rotationQuat , glm::vec3(0.0f, 1.0f, 0.0f));
    glm::vec3 right = glm::rotate(rotationQuat , glm::vec3(1.0f, 0.0f, 0.0f));
    

2.4 等价的四元数和矩阵

任何三维旋转都可以从3×3矩阵表达方式 R \bold R R和四元数表达方式 q q q 之间自由转换(可以直接用glm或者其他数学库)。

当我们想要将四元数和三维旋转矩阵相互转换时,以下是数学公式:

四元数到旋转矩阵:

给定四元数 q = [ x , y , z , w ] q = [x, y, z, w] q=[x,y,z,w],对应的旋转矩阵 R \bold R R是:
R = [ 1 − 2 y 2 − 2 z 2 2 x y − 2 z w 2 x z + 2 y w 2 x y + 2 z w 1 − 2 x 2 − 2 z 2 2 y z − 2 x w 2 x z − 2 y w 2 y z + 2 x w 1 − 2 x 2 − 2 y 2 ] R = \begin{bmatrix} 1 - 2y^2 - 2z^2 & 2xy - 2zw & 2xz + 2yw \\ 2xy + 2zw & 1 - 2x^2 - 2z^2 & 2yz - 2xw \\ 2xz - 2yw & 2yz + 2xw & 1 - 2x^2 - 2y^2 \end{bmatrix} R= 12y22z22xy+2zw2xz2yw2xy2zw12x22z22yz+2xw2xz+2yw2yz2xw12x22y2

旋转矩阵到四元数:

w = 1 + tr ( R ) 2   x = R 23 − R 32 4 w   y = R 31 − R 13 4 w   z = R 12 − R 21 4 w   tr ( A ) = ∑ i = 1 n A i i w = \frac{\sqrt{1 + \text{tr}(R)}}{2} \\ \ \\ x = \frac{R_{23} - R_{32}}{4w} \\ \ \\y = \frac{R_{31} - R_{13}}{4w} \\ \ \\ z = \frac{R_{12} - R_{21}}{4w} \\ \ \\\text{tr}(A) = \sum_{i=1}^{n} A_{ii} w=21+tr(R)  x=4wR23R32 y=4wR31R13 z=4wR12R21 tr(A)=i=1nAii

为了生活,还是用第三方库吧,在glm里面可以用

// 将四元数转换为旋转矩阵
glm::mat4 rotationMatrix = glm::mat4_cast(quaternion);
// 将旋转矩阵转换为四元数
glm::quat quaternionFromMatrix = glm::quat_cast(rotationMatrix);

提一句,glm一般变换矩阵都是mat4,因为这是仿射变换,支持旋转、位移、缩放的。旋转矩阵第四行和第四列通常是(0, 0, 0, 1)。

3 旋转的线性插值

就用线性插值(LERP)来插值四元数的每个分量就行了,

q ( t ) = ( 1 − t ) ⋅ q 1 + t ⋅ q 2 q(t) = (1 - t) \cdot q_1 + t \cdot q_2 q(t)=(1t)q1+tq2
q ( t ) = n o r m a l i z e ( [ x s ( t ) = ( 1 − t ) ⋅ x 1 + t ⋅ x 2 y s ( t ) = ( 1 − t ) ⋅ y 1 + t ⋅ y 2 z s ( t ) = ( 1 − t ) ⋅ z 1 + t ⋅ z 2 w s ( t ) = ( 1 − t ) ⋅ w 1 + t ⋅ w 2 ] T ) q(t)=normalize(\begin{bmatrix} x_s(t) = (1 - t) \cdot x_1 + t \cdot x_2 \\ y_s(t) = (1 - t) \cdot y_1 + t \cdot y_2\\ z_s(t) = (1 - t) \cdot z_1 + t \cdot z_2\\ w_s(t) = (1 - t) \cdot w_1 + t \cdot w_2 \end{bmatrix}^T) q(t)=normalize( xs(t)=(1t)x1+tx2ys(t)=(1t)y1+ty2zs(t)=(1t)z1+tz2ws(t)=(1t)w1+tw2 T)

更准确的可以用SLERP(Spherical Linear Interpolation),计算量更大,效果好了一点点。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宗浩多捞

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值