• 游戏引擎中为什么要用四元数表示旋转而不用欧拉角旋转?


    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));
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 用四元数来旋转: 把物体的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));
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    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 =

    [12y22z22xy2zw2xz+2yw2xy+2zw12x22z22yz2xw2xz2yw2yz+2xw12x22y2]" role="presentation" style="position: relative;">[12y22z22xy2zw2xz+2yw2xy+2zw12x22z22yz2xw2xz2yw2yz+2xw12x22y2]
    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);
    
    • 1
    • 2
    • 3
    • 4

    提一句,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(

    [xs(t)=(1t)x1+tx2ys(t)=(1t)y1+ty2zs(t)=(1t)z1+tz2ws(t)=(1t)w1+tw2]" role="presentation" style="position: relative;">[xs(t)=(1t)x1+tx2ys(t)=(1t)y1+ty2zs(t)=(1t)z1+tz2ws(t)=(1t)w1+tw2]
    ^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),计算量更大,效果好了一点点。

  • 相关阅读:
    qemu中中断model虚拟化是如何实现的?
    如何使用API数据接口给自己创造收益
    生成对抗网络(GAN)
    java时间相关的API总结
    承前启后,Java对象内存布局和对象头
    Mysql使用中的性能优化——索引对插入操作的性能影响
    虹科案例 | AR内窥镜手术应用为手术节约45分钟?
    python使用PIL模块加载图像、通过resize函数改变图像的大小、使用save函数保存处理过的图像、并自定义指定保存后的格式
    pandas入门 数据结构
    Redis 持久化
  • 原文地址:https://blog.csdn.net/Motarookie/article/details/134166285