Quaternion.js

单位四元数Unit quaternion)可以用于表示三维空间里的旋转[1]。它与常用的另外两种表示方式(三维正交矩阵欧拉角)是等价的,但是避免了欧拉角表示法中的万向锁问题。比起三维正交矩阵表示,四元数表示能够更方便地给出旋转的转轴与旋转角。 ——维基百科(https://zh.wikipedia.org/wiki/%E5%9B%9B%E5%85%83%E6%95%B0%E4%B8%8E%E7%A9%BA%E9%97%B4%E6%97%8B%E8%BD%AC

四元数的维基百科:https://zh.wikipedia.org/wiki/%E5%9B%9B%E5%85%83%E6%95%B8
三个虚数单位:i, j, k. Three.js 源码阅读 #2 Euler, Quaternion - 图1,张成四维空间。
算是对虚数对一种拓展,虚数是有一个虚数单位:i,Three.js 源码阅读 #2 Euler, Quaternion - 图2,张成二维空间。
表示旋转的话,分别用i,j,k表示绕x,y,z轴旋转,比较优雅。

setFromEuler( euler, update )

欧拉角转四元数,这个好像比较复杂,Three里是直接用了Matlab里的某个实现,具体的推导需要有时间看看。

setFromAxisAngle( axis, angle )

从轴和角度转化为四元数,这个还是可以看一下的,可以了解下四元数最直接的是怎么表示旋转的。

  1. var halfAngle = angle / 2, s = Math.sin( halfAngle );
  2. this._x = axis.x * s;
  3. this._y = axis.y * s;
  4. this._z = axis.z * s;
  5. this._w = Math.cos( halfAngle );

这其实算是四元数表示旋转的一种具体定义吧:
Three.js 源码阅读 #2 Euler, Quaternion - 图3
其中a是旋转的角度。
具体一些四元数的知识,可以参考Three源码中给的链接:http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/index.htm
至于为啥是sin(a/2), cos(a/2),网上查了不少知识,发现这应当是个比较复杂的数学问题,略微看不懂,就姑且先记下,毕竟,这个形式是非常优雅的,数学的确是一个奇妙的东西!

setFromRotationMatrix( m )

又是关于四种旋转方式的转变,突然发现,Three中好多关于数学的知识,都参考了http://www.euclideanspace.com/ 上的知识,这个网站值得好好看看。
其中,有总结四种旋转表示方法的相互转换:http://www.euclideanspace.com/maths/geometry/rotations/conversions/index.htm

setFromUnitVectors( vFrom, vTo )

两个法向量也可以用来表示旋转,这么看,应该算是有五种旋转的表达方式了。
这个的实现还是有点小trick:https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another

一般情况下,直接叉乘两个向量,即得到了x,y,z分量,当然源码中没有调用叉乘的函数,而是直接算的:
Three.js 源码阅读 #2 Euler, Quaternion - 图4

  1. this._x = vFrom.y * vTo.z - vFrom.z * vTo.y;
  2. this._y = vFrom.z * vTo.x - vFrom.x * vTo.z;
  3. this._z = vFrom.x * vTo.y - vFrom.y * vTo.x;
  4. this._w = r;

这里的r,本意是Three.js 源码阅读 #2 Euler, Quaternion - 图5,因为是单位向量,第一项是1。

但是,如果两个向量方向平行(相同或相反),叉乘会得到0向量,就不对了,因此需要特殊考虑。
这时候,其实两个向量平行,保证垂直于其中一个向量就OK了,代码里的实现也比较直接:

  1. if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {
  2. // Q> 不用归一化?
  3. this._x = - vFrom.y;
  4. this._y = vFrom.x;
  5. this._z = 0;
  6. this._w = r;
  7. } else {
  8. this._x = 0;
  9. this._y = - vFrom.z;
  10. this._z = vFrom.y;
  11. this._w = r;
  12. }

不过这里有个小疑问,这个四元数的x,y,z分量不用做归一化么?

angleTo(q)

计算两个四元数之间的夹角,这算是一种定义吧。这里有个是stackexchange上的回答:https://math.stackexchange.com/questions/90081/quaternion-distance

inverse() conjugate()

两个的作用都是翻转一下,反方向转。其实就是把轴反一下就可以了。

multiplyQuaternions( a, b )

四元数的乘法,并不是简单的对应相乘,更像是一种矩阵相乘,这里说了两者的相似之处:http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/geometric/orthogonal/index.htm
啊,数学真是个奇妙的东西!!!

slerp( qb, t )

做基于四元数的旋转的插值。这个讲得的确很好了:http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
噗,找到了好的资料就假装自己看了并假装自己会了呵呵呵

static method

Quaternion 中还有两个static的method,这里,其static method是这样实现的:

  1. Object.assign( Quaternion, {
  2. slerp: function ( qa, qb, qm, t ) {
  3. ...
  4. }
  5. slerpFlat: ...
  6. }

也算是一个很好的解决方案,当然,在ES6中,有 static 的关键字,就很棒了

其实就实现来看,static的method相当于可以把插值的结果直接赋值把:

  1. slerp: function ( qa, qb, qm, t ) {
  2. return qm.copy( qa ).slerp( qb, t );
  3. }

而对于 slerpFlat 的话,就是直接操作数组,没有什么特别的实现,可能会减少后续的一些实现时反复取元素的麻烦和开销吧。

Euler.js

欧拉角的介绍:https://www.matongxue.com/madocs/442.html
其实,欧拉角是相对于四元数以及旋转矩阵而言,比较直观和易于理解的一种旋转表示方式。

旋转步骤如下:

  • 物体绕全局image.svg轴旋转image.svg
  • 继续绕自己image.svg轴(也就是图中的image.svg轴)旋转image.svg
  • 最后绕自己image.svg轴旋转image.svg

关于万向轴死锁的问题,上边那篇博客里讲得很好。

接下来看看Three.js中的欧拉角实现。

RotationOrders

  1. Euler.RotationOrders = [ 'XYZ', 'YZX', 'ZXY', 'XZY', 'YXZ', 'ZYX' ];
  2. Euler.DefaultOrder = 'XYZ';

欧拉角的旋转结果与顺序相关,绕3个轴旋转有6种顺序。

setFromQuaternion( q, order, update )

  1. _matrix.makeRotationFromQuaternion( q );
  2. return this.setFromRotationMatrix( _matrix, order, update );

四元数到欧拉角的转换。其实是用旋转矩阵过渡,先生成旋转矩阵,再转换为欧拉角。
不知道这里有没有更高效的转换方式,总感觉会有点效率低。

setFromRotationMatrix( m, order, update )

首先说明这里的 update 参数,bool类型,用来设定是否调用更新的回调函数,但目前的源码中没有涉及到这个回调函数的用处,因此暂时不谈。

旋转矩阵转欧拉角并不惟一,而且如果欧拉角的方向不同,计算方式也不同。这里关于具体的计算公式找了很久的资料,也没有一个描述很清晰的资料。姑且就先记下吧,具体的推倒暂且不管了。