creator中关于旋转所使用的欧拉角和四元数

概念

四元数,欧拉角,变换矩阵,这三者是可以相互转化的,但并不是说变换矩阵等于欧拉角或者变换矩阵等于四元数,欧拉角或者说四元数他们只是旋转数据,而一个矩阵是一个空间坐标系,旋转的数据只是构造的它的一部分,他还有缩放和平移。
直观上理解,我们将使用欧拉角,因为欧拉角只针对坐标轴以及这些坐标轴旋转的角度,所以欧拉角就是一个存放三条坐标轴旋转的角度的一个数组vec3,所以我们就使用欧拉角来记录我们对节点进行的旋转角度变化的数据
解释
在这里插入图片描述

* 欧拉角中关于万向锁产生的根本原因是:
 * 旋转是按照特定顺序进行的,模型无论进行多少次怎么样的旋转,任何一次的旋转一定是从(000==* (rotateX,rotateY,rotateZ)
 * 当有一个轴旋转(90*n)度的时候,就会出现模型坐标系的一个轴与世界坐标系的一个轴重合
 * 
 * 3维坐标系有三个轴,我们把一次旋转分到各个轴上可以看成旋转了3* 假定我们的旋转顺序是Y X Z
 * 在没有旋转之前,模型坐标系是与世界坐标系完全重合的,
 * 假定第一次旋转是绕Y轴(模型坐标系或者世界坐标系)旋转30* 第二次旋转是绕X轴(模型坐标系)旋转90度,此时模型坐标系的z轴与世界坐标系的Y轴平行了
 * 第三次旋转继续绕z轴(模型坐标系)旋转20度,很明显,这次旋转也是在绕着世界坐标系的y轴旋转,
 * 所以第一次和第三次的旋转轴是一样的,即相当于失去了一个旋转轴,出现了万向锁
 * 
 * Y轴的旋转,一定是在世界坐标系下的,Y轴的旋转平面是X轴和Z轴组建的,所以第一次无论Y轴如何旋转,都只是
 * 在XZ轴这个平面上
 * 当第二次绕着X轴旋转,只要旋转了n*90这个度数,一定能让世界坐标系的Y轴和模型坐标系的Z轴重合,
 * 模型旋转都是在模型坐标系下进行的,第一次由于和世界坐标系重合,所以绕着模型坐标系下的Y轴旋转,就等价
 * 于绕着世界坐标系下的Y轴进行
 * 当我们在第二次旋转,不小心将绕着模型坐标系的x轴的旋转度数设置成90*n度数时,就会造成模型坐标系的Z轴
 * 和世界坐标系的Y轴重合,
 * 当我们第三次再继续绕Z轴的旋转的时候,就好像在绕着世界坐标系的Y轴旋转,第一次和第三次都是绕着同一个轴
 * 

正文

看下面这段代码:
当节点接收到外界旋转变化时,将变化的数据赋值给了欧拉角_eulerAngles,
然后还进行了一个更新Trs.fromAngleZ(this._trs, value),更新了this._trs这个数组里的值

/**
   * !#en
   * Angle of node, the positive value is anti-clockwise direction.
   * !#zh
   * 该节点的旋转角度,正值为逆时针方向。
   * @property angle
   * @type {Number}
   */
  angle: {
      get () {
          return this._eulerAngles.z;
      },
      set (value) {
          Vec3.set(this._eulerAngles, 0, 0, value);   
          Trs.fromAngleZ(this._trs, value);
          this.setLocalDirty(LocalDirtyFlag.ALL_ROTATION);

          if (this._eventMask & ROTATION_ON) {
              this.emit(EventType.ROTATION_CHANGED);
          }
      }
  },
  /**
   * !#en Rotation on x axis.
   * !#zh 该节点 X 轴旋转角度。
   * @property rotationX
   * @type {Number}
   * @deprecated since v2.1
   * @example
   * node.is3DNode = true;
   * node.eulerAngles = cc.v3(45, 0, 0);
   * cc.log("Node eulerAngles X: " + node.eulerAngles.x);
   */
  rotationX: {
      get () {
          if (CC_DEBUG) {
              cc.warn("`cc.Node.rotationX` is deprecated since v2.1.0, please use `eulerAngles.x` instead. (`this.node.rotationX` -> `this.node.eulerAngles.x`)");
          }
          return this._eulerAngles.x;
      },
      set (value) {
          if (CC_DEBUG) {
              cc.warn("`cc.Node.rotationX` is deprecated since v2.1.0, please set `eulerAngles` instead. (`this.node.rotationX = x` -> `this.node.is3DNode = true; this.node.eulerAngles = cc.v3(x, 0, 0)`");
          }
          if (this._eulerAngles.x !== value) {
              this._eulerAngles.x = value;
              // Update quaternion from rotation
              if (this._eulerAngles.x === this._eulerAngles.y) {
                  Trs.fromAngleZ(this._trs, -value);
              }
              else {
                  Trs.fromEulerNumber(this._trs, value, this._eulerAngles.y, 0);
              }
              this.setLocalDirty(LocalDirtyFlag.ALL_ROTATION);

              if (this._eventMask & ROTATION_ON) {
                  this.emit(EventType.ROTATION_CHANGED);
              }
          }
      },
  },
  ....

this._trs里都放了啥?

/*
trs[0] = 0; // position.x
        trs[1] = 0; // position.y
        trs[2] = 0; // position.z
        trs[3] = 0; // rotation.x
        trs[4] = 0; // rotation.y
        trs[5] = 0; // rotation.z
        trs[6] = 1; // rotation.w
        trs[7] = 1; // scale.x
        trs[8] = 1; // scale.y
        trs[9] = 1; // scale.z
*/

每一次修改旋转值都会触发四元数相关运算来修改this._trs这个数组里的值:
下面举一个绕x轴旋转的例子
1:Trs.fromAngleZ(this._trs, -value);

static fromAngleZ (out: FloatArray, a: number): FloatArray {
        Quat.fromAngleZ(tmp_quat, a);
        Trs.fromRotation(out, tmp_quat);
        return out;
    }

2:Quat.fromAngleZ

  /**
     * Set a quaternion from the given euler angle 0, 0, z.
     *
     * @param {Quat} out - Quaternion to store result.
     * @param {number} z - Angle to rotate around Z axis in degrees.
     * @returns {Quat}
     * @function
     */
    static fromAngleZ (out: Quat, z: number): Quat {
        z *= halfToRad;
        out.x = out.y = 0;
        out.z = Math.sin(z);
        out.w = Math.cos(z);
        return out;
    }

我们最后需要把这个this._trs里的值转为一个矩阵来生成一个空间坐标系

// 2D/3D matrix functions
function updateLocalMatrix3D () {
    if (this._localMatDirty & LocalDirtyFlag.TRSS) {
        // Update transform
        let t = this._matrix;
        let tm = t.m;
        Trs.toMat4(t, this._trs);

        // skew
        if (this._skewX || this._skewY) {
            let a = tm[0], b = tm[1], c = tm[4], d = tm[5];
            let skx = Math.tan(this._skewX * ONE_DEGREE);
            let sky = Math.tan(this._skewY * ONE_DEGREE);
            if (skx === Infinity)
                skx = 99999999;
            if (sky === Infinity)
                sky = 99999999;
            tm[0] = a + c * sky;
            tm[1] = b + d * sky;
            tm[4] = c + a * skx;
            tm[5] = d + b * skx;
        }
        this._localMatDirty &= ~LocalDirtyFlag.TRSS;
        // Register dirty status of world matrix so that it can be recalculated
        this._worldMatDirty = true;
    }
}

Trs.toMat4代码如下

static toMat4 (out: Mat4, trs: FloatArray): Mat4 {
        let x = trs[3], y = trs[4], z = trs[5], w = trs[6];
        let x2 = x + x;
        let y2 = y + y;
        let z2 = z + z;

        let xx = x * x2;
        let xy = x * y2;
        let xz = x * z2;
        let yy = y * y2;
        let yz = y * z2;
        let zz = z * z2;
        let wx = w * x2;
        let wy = w * y2;
        let wz = w * z2;
        let sx = trs[7];
        let sy = trs[8];
        let sz = trs[9];

        let m = out.m;
        m[0] = (1 - (yy + zz)) * sx;
        m[1] = (xy + wz) * sx;
        m[2] = (xz - wy) * sx;
        m[3] = 0;
        m[4] = (xy - wz) * sy;
        m[5] = (1 - (xx + zz)) * sy;
        m[6] = (yz + wx) * sy;
        m[7] = 0;
        m[8] = (xz + wy) * sz;
        m[9] = (yz - wx) * sz;
        m[10] = (1 - (xx + yy)) * sz;
        m[11] = 0;
        m[12] = trs[0];
        m[13] = trs[1];
        m[14] = trs[2];
        m[15] = 1;

        return out;
    }

所以最后节点的旋转数据模式是:
外界旋转----------》使用欧拉角来记录,其实就是一个长度为3的数组,里面记录了xyz三条坐标轴的旋转角度
内部更新----------》节点内部使用一个trs这个长度为10的FloatArray32数组,记录缩放旋转平移的数据,当有旋转数据修改时,就会更新这个数组,这个数组采用了四元数机制来更新
最后使用--------》就是用this._trs这个数组的数据来生成一个空间矩阵
下面这个数组负责管理this._trs,欧拉角和四元数互转的逻辑运算


import { Quat } from './quat';
import Vec3 from './vec3';
import { Mat4 } from './Mat4';

let tmp_quat = new Quat();

/*
trs[0] = 0; // position.x
trs[1] = 0; // position.y
trs[2] = 0; // position.z
trs[3] = 0; // rotation.x
trs[4] = 0; // rotation.y
trs[5] = 0; // rotation.z
trs[6] = 1; // rotation.w
trs[7] = 1; // scale.x
trs[8] = 1; // scale.y
trs[9] = 1; // scale.z
下面所说的数组就是值的这个数组
这个数组很特别,特别之处在其存储了四元数的值
位置四元数缩放:那简称一下就是 "位4缩"
*/
export default class Trs {
    /**
     * 从位4缩数组中取出四元数数据赋值给四元数
     * @param out 
     * @param a 位4缩
     */
    static toRotation(out: Quat, a: FloatArray): Quat {
        out.x = a[3];
        out.y = a[4];
        out.z = a[5];
        out.w = a[6];
        return out;
    }
    
    /**
     * 将一个四元数中的数据赋值给我们的“位4缩”中的四元数
     * @param out 
     * @param a 位4缩
     */
    static fromRotation(out: FloatArray, a: Quat): FloatArray {
        out[3] = a.x;
        out[4] = a.y;
        out[5] = a.z;
        out[6] = a.w;
        return out;
    }
    /**
     * 从位4缩数组中找出对应的值赋给欧拉角
     * 这个角度就是我们理解让xxx轴旋转xxx度
     * @param out 
     * @param a 位4缩
     */
    static toEuler(out: Vec3, a: FloatArray): Vec3 {
        //找出四元数数据
        Trs.toRotation(tmp_quat, a);
        //将四元数转为欧拉角
        Quat.toEuler(out, tmp_quat);
        return out;
    }
    
    /**
     * 使用欧拉角来更新我们的“位4缩”
     * @param out 位4缩
     * @param a 欧拉角
     */
    static fromEuler(out: FloatArray, a: Vec3): FloatArray {
        //从给定的欧拉角来生成我们的四元数数据
        Quat.fromEuler(tmp_quat, a.x, a.y, a.z);
        //使用四元数数据来更新我们的“位4缩”数组
        Trs.fromRotation(out, tmp_quat);
        return out;
    }
    /**
     * 下面这个函数和fromEuler是一样的功能,只是参数不一样
     * @param out 位4缩
     * @param x 
     * @param y 
     * @param z 
     */
    static fromEulerNumber(out: FloatArray, x: number, y: number, z: number): FloatArray {
        Quat.fromEuler(tmp_quat, x, y, z);
        Trs.fromRotation(out, tmp_quat);
        return out;
    }
    
    /**
     * 
     * @param out 
     * @param a 位4缩
     */
    static toScale(out: Vec3, a: FloatArray): Vec3 {
        out.x = a[7];
        out.y = a[8];
        out.z = a[9];
        return out;
    }
    
    /**
     * 
     * @param out 位4缩
     * @param a 
     */
    static fromScale(out: FloatArray, a: Vec3): FloatArray {
        out[7] = a.x;
        out[8] = a.y;
        out[9] = a.z;
        return out;
    }
    
    /**
     * 
     * @param out 
     * @param a 位4缩
     */
    static toPosition(out: Vec3, a: FloatArray): Vec3 {
        out.x = a[0];
        out.y = a[1];
        out.z = a[2];
        return out;
    }
    
    /**
     * 
     * @param out 位4缩
     * @param a 
     */
    static fromPosition(out: FloatArray, a: Vec3): FloatArray {
        out[0] = a.x;
        out[1] = a.y;
        out[2] = a.z;
        return out;
    }
    
    /**
     * 
     * @param out 位4缩
     * @param a 
     */
    static fromAngleZ(out: FloatArray, a: number): FloatArray {
        Quat.fromAngleZ(tmp_quat, a);
        Trs.fromRotation(out, tmp_quat);
        return out;
    }
    
    /**
     * 传入一个“位四缩”的对象,使用它的数据来赋值给一个矩阵
     * 这就是我们最终想要的,只有它才可以完成空间变换
     * @param out 
     * @param trs 位4缩
     */
    static toMat4(out: Mat4, trs: FloatArray): Mat4 {
        let x = trs[3], y = trs[4], z = trs[5], w = trs[6];
        let x2 = x + x;
        let y2 = y + y;
        let z2 = z + z;

        let xx = x * x2;
        let xy = x * y2;
        let xz = x * z2;
        let yy = y * y2;
        let yz = y * z2;
        let zz = z * z2;
        let wx = w * x2;
        let wy = w * y2;
        let wz = w * z2;
        let sx = trs[7];
        let sy = trs[8];
        let sz = trs[9];

        let m = out.m;
        m[0] = (1 - (yy + zz)) * sx;
        m[1] = (xy + wz) * sx;
        m[2] = (xz - wy) * sx;
        m[3] = 0;
        m[4] = (xy - wz) * sy;
        m[5] = (1 - (xx + zz)) * sy;
        m[6] = (yz + wx) * sy;
        m[7] = 0;
        m[8] = (xz + wy) * sz;
        m[9] = (yz - wx) * sz;
        m[10] = (1 - (xx + yy)) * sz;
        m[11] = 0;
        m[12] = trs[0];
        m[13] = trs[1];
        m[14] = trs[2];
        m[15] = 1;

        return out;
    }
}

欧拉角之万向节锁

什么是万向节锁?
解释1
欧拉角的旋转的顺序是有规定的,是先x还是先也还是z,比如xyz这样的一组顺序,正是由于这样规定的顺序才造成了万向节锁,万向节锁就是一个模型在采用欧拉角旋转后,在某些情况下,旋转出来的结果和我们在世界坐标系下的预期不一样,举一个例子,假设你在开飞机,飞机的三个动作就是绕着坐标轴进行的,绕着Y轴是左右偏航,绕着z轴是桶滚,绕着x轴做上下俯仰,在某一时刻,突然机头俯仰90度,就是绕着x轴向上旋转90度,这个时候飞机自身的坐标轴z轴和世界坐标系的y轴重合,按照原先世界坐标系的预期情况,我们飞机只要绕着世界坐标系的y轴旋转就可以是左右偏航,但是现在如果绕着世界坐标系的y轴旋转的话,就是执行自身翻滚,显然这不符合我们的预期情况
解释2:盗了下面几张图
图1:物体的初始朝向
在3维中常用的欧拉角坐标定向系统是用绕三个轴旋转的角度来表示物体的朝向(Rx,Ry,Rz)(注意三个轴是针对物体坐标系的)。如图1,物体处于世界坐标系(Xw,Yw,Zw)原点,此时物体坐标系(Xl,Yl,Zl)和世界坐标系重合(这里我使用右手坐标系。你也可以使用左手坐标系,无所谓,一样)。此时,规定物体的朝向为(0,0,0)
在这里插入图片描述
图2:物体绕物体坐标系x轴(Xl)旋转30度
现在开始旋转物体,先绕物体坐标系x轴(Xl)旋转30度(这里我规定沿着轴向轴的负方向看去,顺时针旋转为正。你也可以自己规定,无所谓,遵守规定即可),注意,此时的物体坐标系已经发生变化,见图2
在这里插入图片描述
图3:物体绕物体坐标系y轴(Yl)旋转90度
然后再绕Yl轴旋转90度,此时,你会发现Zl轴已经和了世界坐标系X轴共轴。见图3
在这里插入图片描述
图4:物体绕物体坐标系z轴(Zl)旋转-40度
此时使用欧拉角来表示当前物体的方向的话,其坐标应该是(30,90,0),对应旋转顺序是Xl->Yl->Zl。然而,有意思的是如果再继续旋转,现在按照Zl旋转-40度,发现什么了?咦,怎么感觉已经绕过这个轴旋转过一次了,虽然轴向相反?_,anyway,最后的坐标应该是(30,90,-40),见图4
在这里插入图片描述
好了,回到刚才的疑惑上,既然感觉两次旋转是绕同一轴,如果我一开始考虑全部绕该轴的旋转呢?即先绕Xl旋转30-(-40)=70度,然后再绕Yl旋转90度。_怎么样,已经到达和上次旋转的效果了吧。这说明什么?欧拉角坐标(30,90,-40)和(30-(-40),90,0)等同。甚至坐标(Rx1,90,Rz1)和(Rx2,90,Rz2)相同,只需满足Rx1-Rz1=Rx2-Rz2。当Rx1-Rz1=Rx2时,Rz2==0,即在这种情况下任何再绕Zl轴的旋转,都可以使用先绕Xl轴来做到。或者从另一个角度来说,物体现在本质上只能绕两个轴的旋转!即少了一个旋转自由度!这就是3维中的万向节死锁现象。

概括起来可以这么说,绕着物体坐标系中某一个轴,比如y轴的+(-)90度的某次旋转,使得这次旋转的前一次绕物体坐标系x轴的旋转和这次旋转的后一次绕物体坐标系z轴的旋转的两个旋转轴是一样(一样的意思是指在世界坐标系中,两次旋转轴是共轴的但方向相反),从而造成一个旋转自由度丢失。

总结
关于第二次解释,这里特别说一下,他说到了第一波旋转以后,导致模型的z轴和世界坐标系的x轴重合,那么现在继续让模型绕着自身的z轴旋转的结果,我们都可以先绕x轴来做到,这说明了啥,不就相当于这两个轴有一个轴失效了吗,不就是说丢失了一个自由的维度吗?
欧拉角旋转只是让物体旋转到某一个期望的位置,不管你怎么转,只要转到指定方位就好了
现在我们发现:欧拉角坐标(30,90,-40)和(30-(-40),90,0)等同,这是z轴和世界坐标系x轴重合的情况,就可以达到转到指定方位了,
当z轴和y轴重合时,满足下面的公式
坐标(Rx1,90,Rz1)和(Rx2,90,Rz2)相同,只需满足Rx1-Rz1=Rx2-Rz2,当Rx1-Rz1=Rx2时,Rz2==0,在这种情况下,是不是有一万种可能,
就按照上面的顺序,先X轴再y轴最后z轴,
分两波旋转对比:
第一波:
先绕x轴旋转Rx1,再绕y轴旋转90,最后绕z轴旋转Rz1,这会旋转到某一个方位
第二波操作:
先绕x轴旋转Rx2,再绕y轴旋转90,最后绕z轴旋转Rz2
就是单纯的向旋转到和第一波操作一样,,只要满足Rx1-Rz1=Rx2时,Rz2等于0,这个公式就可以,这样看来,确实第一波操作有两个变量,第二波操作有一个变量,满足这个等式成立真的有数不清的可能,还有个发现,这样两波旋转,发现第二波使用一个轴就可以达到第二波的两个轴旋转效果,那不就相当于丢失了一个维度吗?

数学模型
欧拉旋转的数学实现就是使用矩阵。而最常见的表示方法就是3*3的矩阵。在Wiki里我们可以找到这种矩阵的表示形式,以下以按XYZ的旋转顺序为例,三个矩阵分别表示了
在这里插入图片描述
在计算时,我们将原来的旋转矩阵右乘(这里使用的是列向量)上面的矩阵。从这里我们也可以证明上面所说的两种坐标系选择是一样的结果,它们之间的不同从这里来看其实就是矩阵相乘时的顺序不同。第一种坐标系情况,指的是在计算时,先从左到右直接计算R中3个矩阵的结果矩阵,最后再和原旋转矩阵相乘,因此顺序是XYZ;而第二种坐标系情况,指的是在计算时,从右往左依次相乘,因此顺序是反过来的,ZYX。你可以验证R左乘和右乘的结果表达式,就可以相信这个结论了!

数学解释
我们从最简单的矩阵来理解。还是使用XYZ的旋转顺序。当Y轴的旋转角度为90°时,我们会得到下面的旋转矩阵
在这里插入图片描述
我们对上述矩阵进行左乘可以得到下面的结果
在这里插入图片描述
可以发现,此时当我们改变第一次和第三次的旋转角度时,是同样的效果,而不会改变第一行和第三列的任何数值,从而缺失了一个维度

unity自己尝试
设置x轴的旋转角度为90度,这个时候你再动手去旋转y轴或者z轴,会发现这两次旋转在一个平面上,如果此时x轴的旋转角度不为90度,那么分别绕着y轴旋转和z轴旋转,又会发现在不同的旋转面上,所以针对第一种情况,丧失了一个自由度,这就是万向节锁
特别注意:如果后面任何一次设置了x轴的旋转不为90度,那么万象节锁立刻消失,万象节锁只存在某一个坐标轴和世界坐标系的轴重合了,并且今后的一段时间内旋转再也不动这个轴了,那么这段时间将会处于万象节锁这个状态中,按照常理,我们的欧拉角面对的xyz三个轴,虽然是按照顺序旋转的,但只要不出现某一个坐标轴与世界坐标系轴重合,就一定不会出现万向节锁

欧拉角和四元数组合使用

一个节点:Node,它含有缩放scale,旋转rotate,平移position
scale:x,y,z
rotate:x,y,z
position:x,y,z
给节点添加一个四元数属性quaternion
给节点添加一个本地矩阵matrix,该矩阵有两个方法,非常重要
方法1:this.matrix.compose( this.position, this.quaternion, this.scale );构造一个矩阵
方法2:this.matrix.decompose( this.position, this.quaternion, this.scale );从矩阵中分解出来位置,四元数,缩放信息,
对于旋转,我们在外界调用欧拉角进行旋转,这个便于我们理解,但是内部实现,却是使用四元数
也就是说,我们虽然使用欧拉接口设置绕着x轴旋转60,但其实这个接口内部就用这个数据去更新四元数了
关于这个内部实现,节点还需要添加一个四元数,因为四元数旋转,是相乘实现的,如下:
Object3D.js

//绕着某一个坐标轴旋转
rotateOnAxis: function ( axis, angle ) {
	// rotate object on axis in object space
	// axis is assumed to be normalized
	//通过坐标轴和一个角度生成一个新的四元数
	_q1.setFromAxisAngle( axis, angle );
	//用这个刚刚新生成的四元数和当前节点的四元数进行相乘来生成新的四元数
	this.quaternion.multiply( _q1 );
	return this;
},
//绕着x轴旋转
rotateX: function ( angle ) {
	return this.rotateOnAxis( _xAxis, angle );
},

Quaternion.ts

	/**
	 * 传入一个坐标轴和一个角度来设置当前的这个四元数
	 * @param axis  坐标轴
	 * @param angle 角度
	 * (u*sin(angle/2),cos(angle/2))
	 * 上面的公式中u:代表坐标轴的向量,angle代表旋转的角度,注意,外界要旋转的angle角度,则传入的值一定是angle/2
	 * 这也是下面为啥要除以2的原因
	 */
	setFromAxisAngle(axis: Vector3, angle: number) {
		// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
		// assumes axis is normalized
		const halfAngle = angle / 2, s = Math.sin(halfAngle);
		this._x = axis.x * s;
		this._y = axis.y * s;
		this._z = axis.z * s;
		this._w = Math.cos(halfAngle);
		this._onChangeCallback();
		return this;
	}
	/**
	 * 两个四元数相乘将结果赋给当前这个四元数
	 * @param a 
	 * @param b 
	 */
	private multiplyQuaternions(a, b) {
		// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
		const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
		const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;
		this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
		this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
		this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
		this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
		this._onChangeCallback();
		return this;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值