1 /**
2 A Quaternion it's used to store a space orientation / rotation, and makes easy
3 to interpolate rotations, at same time that avoid grimbal locks that have using
4 Euler angles
5 */
6 module zmath.quaternion;
7 
8 import zmath.vector;
9 import zmath.matrix;
10 
11 alias Qua_f = Quaternion!float; /// Alias of a Quaternion with floats
12 alias Qua_d = Quaternion!double; /// Alias of a Quaternion with doubles
13 alias Qua_r = Quaternion!real; /// Alias of a Quaternion with reals
14 
15 /**
16 * Quaternion over a FloatPoint type,
17 */
18 public struct Quaternion(T = floatt) if (__traits(isFloating, T)) {
19 nothrow:
20   static assert(__traits(isFloating, T));
21 
22   static enum size_t dim = 4; /// Quaternion Dimension
23 
24   union {
25     private T[4] coor; /// Quaternion coords in an array
26     private Vector!(T, 4) v; /// Quaternion like a vector
27     struct {
28       T i; /// i complex component
29       T j; /// j complex component
30       T k; /// k complex component
31       T w; /// w real component
32     }
33 
34     struct {
35       T x; /// alias of i component
36       T y; /// alias of j component
37       T z; /// alias of k component
38     }
39   }
40 
41   /**
42   * Build a new Quaternion from a set of initial values
43   * Params:
44   *	i = i imaginary component
45   *	j = j imaginary component
46   *	k = k imaginary component
47   *	w = i real component
48   */
49   this(in T i, in T j, in T k, in T w) {
50     this.i = i;
51     this.j = j;
52     this.k = k;
53     this.w = w;
54   }
55 
56   /**
57   * Build a new Quaternion from a array
58   * If no there values for j, and k, will be set to 0. w is set to 1
59   * Params:
60   *	xs = Array with coords
61   */
62   this(in T[] xs) {
63     size_t l = xs.length > dim ? dim : xs.length;
64     coor[0 .. l] = xs[0 .. l].dup;
65     if (l < dim) {
66       coor[l .. $ - 1] = 0; // imaginary part
67       w = 1; // real part
68     }
69   }
70 
71   /**
72   * Build a new Quaternion from a 3D Vector
73   * w will be set to 1
74   * Params:
75   *	vec = Vector 3d
76   */
77   @nogc this(in Vector!(T, 3) vec) pure {
78     this.v = Vector!(T, 4)(vec.x, vec.y, vec.z);
79     this.w = 1;
80   }
81 
82   /**
83   * Build a new Quaternion from a 4D Vector
84   * Params:
85   *	vec = Vector 4d
86   */
87   @nogc this(in Vector!(T, 4) vec) pure {
88     this.v = vec;
89   }
90 
91   /**
92   * Creates a Quaternion from a 3d Vector and angle
93   *	Params:
94   *	v = Rotation axis
95   * angle = Rotation angle in radians
96   *	Returns:
97   *	Quaternion that represents a rotation
98   */
99   @nogc this(U)(Vector!(T, 3) v, U angle) if (is(U : real))
100   in {
101     assert(v.isOk, "Invalid vector");
102   } body {
103     import std.math : sin, cos;
104 
105     v.normalize();
106     const sin_2 = sin(angle / 2.0L);
107     i = v.x * sin_2;
108     j = v.y * sin_2;
109     k = v.z * sin_2;
110     w = cos(angle / 2.0L);
111   }
112 
113   /**
114   * Creates a new Quaternion from  Euler angles
115   * Params :
116   * roll = Rotation around X axis in radians (aka bank)
117   *	pitch = Rotation around Y axis in radians (aka attitude)
118   *	yaw = Rotation around Z axis in radians (aka heading)
119   *	Returns:
120   *	Quaternion that represents a rotation
121   */
122   @nogc this(U)(U roll, U pitch, U yaw) if (__traits(isFloating, U)) {
123     import std.math : sin, cos;
124 
125     const sinPitch = sin(pitch / 2.0);
126     const cosPitch = cos(pitch / 2.0);
127     const sinYaw = sin(yaw / 2.0);
128     const cosYaw = cos(yaw / 2.0);
129     const sinRoll = sin(roll / 2.0);
130     const cosRoll = cos(roll / 2.0);
131 
132     const cosPitchCosYaw = cosPitch * cosYaw;
133     const sinPitchSinYaw = sinPitch * sinYaw;
134 
135     this.i = sinRoll * cosPitchCosYaw - cosRoll * sinPitchSinYaw;
136     this.j = cosRoll * sinPitch * cosYaw + sinRoll * cosPitch * sinYaw;
137     this.k = cosRoll * cosPitch * sinYaw - sinRoll * sinPitch * cosYaw;
138     this.w = cosRoll * cosPitchCosYaw + sinRoll * sinPitchSinYaw;
139   }
140 
141   // Basic Properties **********************************************************
142 
143   /**
144   * Returns i coord of this vector
145   */
146   @nogc T opIndex(size_t i) pure const {
147     return coor[i];
148   }
149 
150   /**
151   * Assigns a value to a coord
152   */
153   @nogc void opIndexAssign(T c, size_t i) {
154     coor[i] = c;
155   }
156 
157   /**
158   * Returns the actual length of this quaternion
159   */
160   @property @nogc T length() pure const {
161     import std.math : sqrt;
162 
163     return sqrt(sq_length);
164   }
165 
166   /**
167   * Returns the actual squared length of this quaternion
168   */
169   @property @nogc T sq_length() pure const {
170     return x * x + y * y + z * z + w * w;
171   }
172 
173   // Operations ****************************************************************
174 
175   /**
176   * Define Equality
177   */
178   @nogc bool opEquals()(auto ref const Quaternion rhs) pure const {
179     if (x != rhs.x) {
180       return false;
181     }
182     if (y != rhs.y) {
183       return false;
184     }
185     if (z != rhs.z) {
186       return false;
187     }
188     if (w != rhs.w) {
189       return false;
190     }
191     return true;
192   }
193 
194   /**
195   * Approximated equality with controlable precision
196   */
197   @nogc bool approxEqual()(auto ref const Quaternion rhs, T maxRelDiff = 1e-02, T maxAbsDiff = 1e-05) pure const {
198     import std.math : approxEqual;
199 
200     return approxEqual(x, rhs.x, maxRelDiff, maxAbsDiff) && approxEqual(y, rhs.y, maxRelDiff,
201         maxAbsDiff) && approxEqual(z, rhs.z, maxRelDiff, maxAbsDiff)
202       && approxEqual(w, rhs.w, maxRelDiff, maxAbsDiff);
203   }
204 
205   /**
206   * Define unary operators + and -
207   */
208   @nogc Quaternion opUnary(string op)() pure const if (op == "+" || op == "-") {
209     return Quaternion(mixin(op ~ "x"), mixin(op ~ "y"), mixin(op ~ "z"), mixin(op ~ "w"));
210   }
211 
212   /**
213   * Define binary operator + and -
214   */
215   @nogc Quaternion opBinary(string op)(auto ref const Quaternion rhs) pure const
216       if (op == "+" || op == "-") {
217     return Quaternion(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y"),
218         mixin("z" ~ op ~ "rhs.z"), mixin("w" ~ op ~ "rhs.w"));
219   }
220 
221   /**
222   * Define Scalar multiplication
223   */
224   @nogc Quaternion opBinary(string op)(in T rhs) pure const if (op == "*") {
225     return Quaternion(x * rhs, y * rhs, z * rhs, w * rhs);
226   }
227 
228   /**
229   * Defines Hamilton product for Quaternions
230   * Params:
231   * rhs = Quaternion at rigth of operation
232   *	Return:
233   *	Quaternion result of hamilton product of <strong>this</strong> and rhs (this
234 * rhs)
235   */
236   @nogc Quaternion opBinary(string op)(auto ref const Quaternion rhs) pure const if (op == "*") {
237     // (a1a2 − b1b2 − c1c2 − d1d2)w
238     auto _w = w * rhs.w - x * rhs.x - y * rhs.y - z * rhs.z;
239     // (a1b2 + b1a2 + c1d2 − d1c2)i
240     auto _i = w * rhs.x + x * rhs.w + y * rhs.z - z * rhs.y;
241     // (a1c2 − b1d2 + c1a2 + d1b2)j
242     auto _j = w * rhs.y - x * rhs.z + y * rhs.w + z * rhs.x;
243     // (a1d2 + b1c2 − c1b2 + d1a2)k
244     auto _k = w * rhs.z + x * rhs.y - y * rhs.x + z * rhs.w;
245     return Quaternion(_i, _j, _k, _w);
246   }
247 
248   /**
249   * Obtain conjugate of a Quaternion
250   */
251   @nogc Quaternion conj() pure const {
252     return Quaternion(-x, -y, -z, w);
253   }
254 
255   /**
256   * It's a unitary Quaternion (length == 1)
257   */
258   @property @nogc bool isUnit() pure const {
259     import std.math : approxEqual, abs;
260 
261     return approxEqual(abs(this.sq_length - 1.0), 0);
262   }
263 
264   /**
265   * Normalize this Quaternion
266   */
267   @nogc void normalize() pure {
268     if (!isUnit()) {
269       const T l = 1 / this.length;
270       x = x * l;
271       y = y * l;
272       z = z * l;
273       w = w * l;
274     }
275   }
276 
277   /**
278   * Apply a 3d rotation Quaternion over a 3d/4d Vector
279   */
280   V rotate(V)(auto ref const V v) pure const if (isVector!V && V.dim >= 3) {
281     Quaternion resQua = Quaternion(v) * this.conj();
282     resQua = this * resQua;
283     return V(resQua.coor);
284   }
285 
286   // Misc **********************************************************************
287 
288   /**
289   * Calc the Axis and angle of rotation of this Quaternion
290   * Params:
291   *	angle = Set to angle of rotation
292   *	Returns:
293   *	Axis of rotation vector
294   */
295   @nogc Vector!(T, 3) toAxisAngle(out T angle) pure const
296   in {
297     assert(this.isUnit());
298   }
299   body {
300     // TODO return a tuple with vector and angle
301     import std.math : acos, sqrt, approxEqual;
302 
303     angle = 2 * acos(w);
304     if (approxEqual(angle, 0)) {
305       return Vector!(T, 3)(1, 0, 0);
306     }
307     auto inv_qw = 1 / sqrt(1 - w * w);
308     return Vector!(T, 3)(i * inv_qw, j * inv_qw, k * inv_qw);
309   }
310 
311   /**
312   * Returns a Vector with euler's angles
313   * Returns:
314   *	Vector with Euler angles of rotation in radians
315   *
316   * TODO: Improve using code from here to avoid requiring pre-normalize the quaternion
317   * http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
318   */
319   @nogc Vector!(T, 3) toEuler() pure const
320   in {
321     assert(this.isUnit());
322   }
323   body {
324     /*
325     roll = Rotation around X axis in radians
326     pitch = Rotation around Y axis in radians
327     yaw = Rotation around Z axis in radians
328     */
329     import std.math : asin, atan2, abs, PI_2;
330 
331     // roll (x-axis rotation)
332     auto sinr_cosp = 2 * (w * x + y * z);
333     auto cosr_cosp = 1 - 2 * (x * x + y * y);
334     auto roll = atan2(sinr_cosp, cosr_cosp);
335 
336     // pitch (y-axis rotation)
337     T pitch = void;
338     auto sinp = 2 * (w * y - z * x);
339     if (sinp >= 1) { // North pole
340       pitch = PI_2;
341     } else if (sinp <= -1) { // South pole
342       pitch = -PI_2;
343     } else {
344       pitch = asin(sinp);
345     }
346 
347      // yaw (z-axis rotation)
348     auto siny_cosp = 2 * (w * z + x * y);
349     auto cosy_cosp = 1 - 2 * (y * y + z * z);
350     auto yaw = atan2(siny_cosp, cosy_cosp);
351 
352     return Vector!(T, 3)(roll, pitch, yaw);
353   }
354 
355   /**
356   * Casting for convert between Quaternions
357   */
358   @nogc Tout opCast(Tout)() pure const if (isQuaternion!(Tout)) {
359     import std.conv : to;
360 
361     Tout newQ;
362     static if (is(typeof(newQ.x) == typeof(this.x))) {
363       foreach (i, s; coor)
364         newQ.coor[i] = s;
365     } else {
366       foreach (i, s; coor)
367         newQ.coor[i] = to!(typeof(newQ.x))(s);
368     }
369     return newQ;
370   }
371 
372   /**
373   * Casting to rotation Matrix
374   */
375   @nogc Tout opCast(Tout)() pure const if (isMatrix!(Tout) && Tout.dim >= 3)
376   in {
377     assert(this.isUnit());
378   }
379   body {
380     auto x2 = x * x;
381     auto y2 = y * y;
382     auto z2 = z * z;
383     auto xy = x * y;
384     auto xz = x * z;
385     auto yz = y * z;
386     auto wx = w * x;
387     auto wy = w * y;
388     auto wz = w * z;
389     static if (Tout.dim == 4) {
390       return Tout([
391           1.0L - 2.0L * (y2 + z2), 2.0L * (xy - wz), 2.0L * (xz + wy), 0.0L,
392           2.0L * (xy + wz), 1.0L - 2.0L * (x2 + z2), 2.0L * (yz - wx), 0.0L,
393           2.0L * (xz - wy), 2.0L * (yz + wx), 1.0L - 2.0L * (x2 + y2), 0.0L,
394           0.0L, 0.0L, 0.0L, 1.0L
395           ]);
396     } else {
397       return Tout([
398           1.0L - 2.0L * (y2 + z2), 2.0L * (xy - wz), 2.0L * (xz + wy),
399           2.0L * (xy + wz), 1.0L - 2.0L * (x2 + z2), 2.0L * (yz - wx),
400           2.0L * (xz - wy), 2.0L * (yz + wx), 1.0L - 2.0L * (x2 + y2)
401           ]);
402     }
403   }
404 
405   /**
406   * Returns a string representation of this Quaternion
407   */
408   string toString() {
409     import std.conv : to;
410     try {
411       return "[" ~ to!string(x) ~ ", " ~ to!string(y) ~ ", " ~ to!string(z) ~ ", " ~ to!string(w) ~ "]";
412     } catch (Exception ex) {
413       assert(false); // This never should happen
414     }
415   }
416 }
417 
418 /**
419 * Say if a thing it's a Quaternion
420 */
421 template isQuaternion(T) {
422   //immutable bool isQuaternion = is(T == Vector);
423   immutable bool isQuaternion = __traits(compiles, () {
424     T t;
425     static assert(T.dim == 4);
426     auto coor = t.coor;
427     auto x = t.x;
428     auto y = t.y;
429     auto z = t.z;
430     auto w = t.w;
431     T conj = t.conj();
432     // TODO : Should test for methods ?
433   });
434 }
435 
436 unittest {
437   // Ctors.
438   auto q = Qua_r(1, 2, 3, 4);
439   assert(q.coor == [1, 2, 3, 4]);
440   real[] arr = [4.0, 3.0, 2.0, 1.0, 20.0, 30.0];
441   q = Qua_r(arr);
442   arr[0] = 7.7;
443   assert(q.coor == [4, 3, 2, 1]);
444   q = Qua_r(Vec4r(1, 2, 3, 4));
445   assert(q.coor == [1, 2, 3, 4]);
446 }
447 
448 unittest {
449   auto q_AxisX = Qua_d(Vec3d.X_AXIS, 0);
450   assert(q_AxisX == Qua_d(0, 0, 0, 1)); // TODO Do more checks
451   assert(q_AxisX.isUnit());
452 }
453 
454 unittest {
455   import std.math : PI_2, PI_4;
456   import std.conv : to;
457 
458   auto qRoll = Qua_d(PI_4, 0, 0);
459   assert(qRoll.isUnit());
460   assert(qRoll.toEuler().approxEqual(Vec3d(PI_4, 0, 0)));
461 
462   auto qPitch = Qua_d(0, -PI_4, 0);
463   assert(qPitch.isUnit());
464   assert(qPitch.toEuler().approxEqual(Vec3d(0, -PI_4, 0)), qPitch.toEuler().toString());
465 
466   auto qYaw = Qua_d(0, 0, PI_2);
467   assert(qYaw.isUnit());
468   assert(qYaw.approxEqual(Qua_d(0, 0, 0.707107, 0.707107)), qYaw.toString());
469   assert(qYaw.toEuler().approxEqual(Vec3d(0, 0, PI_2)), qYaw.toEuler().toString());
470 
471   auto q_r = Qua_d(1, -PI_4, PI_4);
472   assert(q_r.isUnit());
473   assert(q_r.toEuler().approxEqual(Vec3d(1, -PI_4, PI_4)));
474 
475   // Example from http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
476   auto q = Qua_d(0.707107, 0, 0, 0.707107);
477   assert(q.isUnit());
478   assert(q.toEuler().approxEqual(Vec3d(PI_2, 0, 0)), q.toEuler().toString());
479 
480 }
481 
482 unittest {
483   auto q = Qua_f(10, 0, 0, 0);
484   assert(q.sq_length == 100);
485   q = Qua_f(1, 1, 1, 1);
486   assert(q.length == 2);
487 }
488 
489 unittest {
490   auto q1 = Qua_d(1, 2, 3, 4);
491   auto qj = Qua_d(0, 1, 0, 0);
492   assert(q1 == q1);
493   assert(q1 != qj);
494   assert(qj != q1);
495   assert(q1.approxEqual(q1) && !q1.approxEqual(qj));
496 }
497 
498 unittest {
499   // Check addition / subtraction
500   const qk = Qua_d(0, 0, 1, 0);
501   const qj = Qua_d(0, 1, 0, 0);
502   const qi = Qua_d(1, 0, 0, 0);
503   const q1 = Qua_d(1, 1, 1, 1);
504   const qki = qk + qi;
505   const q1j = q1 + qj;
506   const q1k = q1 - qk;
507   assert(qki == Qua_d(1, 0, 1, 0));
508   assert(q1j == Qua_d(1, 2, 1, 1));
509   assert(q1k == Qua_d(1, 1, 0, 1));
510 }
511 
512 unittest {
513   // Check Hamilton multiplication
514   const qi = Qua_d(1, 0, 0, 0);
515   const q1 = Qua_d(1, 1, 1, 1);
516   const q1i = q1 * qi;
517   const qi1 = qi * q1;
518   assert(q1i == Qua_d(1, 1, -1, -1)); // TODO Check this values
519   assert(qi1 == Qua_d(1, -1, 1, -1));
520 }
521 
522 unittest {
523   auto q1 = Qua_d(1, 1, 1, 1);
524   const conj_q1 = q1.conj();
525   assert(conj_q1 == Qua_d(-1, -1, -1, 1));
526   const qr = q1 * q1.conj();
527   assert(qr == Qua_d(0, 0, 0, 4));
528 }
529 
530 unittest {
531   auto q = Qua_r(10, 10, 1, 5);
532   assert(!q.isUnit);
533   q.normalize();
534   assert(q.isUnit);
535 }
536 
537 unittest {
538   import std.math : PI_2, PI_4;
539 
540   // Check rotation with quaternion
541   auto qRoll = Qua_d(PI_4, 0, 0);
542   auto qPitch = Qua_d(0, -PI_4, 0);
543   auto qYaw = Qua_d(0, 0, PI_2);
544   auto v1 = Vec3d(1, 1, 1);
545 
546   auto v4d = cast(Vec4d) v1;
547   auto v2 = qRoll.rotate(v1);
548   assert(v2.approxEqual(Vec3d(1, 0, 1.41421)), v2.toString());
549 
550   auto v3 = qPitch.rotate(v1);
551   assert(v3.approxEqual(Vec3d(0, 1, 1.41421)), v3.toString());
552 
553   auto v4 = qYaw.rotate(v1);
554   assert(v4.approxEqual(Vec3d(-1, 1, 1)), v4.toString());
555 
556   auto v5 = qPitch.rotate(v4d);
557   assert(v5.approxEqual(Vec4d(0, 1, 1.41421, 1)), v5.toString());
558 }
559 
560 unittest {
561   import std.math : approxEqual, PI_4;
562 
563   // Check conversion to Axis/angle
564   auto qRoll = Qua_d(PI_4, 0, 0);
565   double angle;
566   auto axis = qRoll.toAxisAngle(angle);
567   assert(axis.approxEqual(Vec3d(1, 0, 0)));
568   assert(approxEqual(angle, PI_4));
569 }
570 
571 unittest {
572   import std.math : PI_4;
573 
574   auto q = Qua_d(PI_4, PI_4, PI_4);
575   auto rot = q.toEuler();
576   assert(rot.approxEqual(Vec3d(PI_4, PI_4, PI_4)));
577 }
578 
579 unittest {
580   auto q = Qua_d(1, 1, 1, 1);
581   auto qf = cast(Qua_f) q;
582   auto qreal = cast(Qua_r) q;
583   auto qd = cast(Qua_d) qf;
584 
585   assert(is(typeof(qf) == Qua_f));
586   assert(is(typeof(qreal) == Qua_r));
587   assert(is(typeof(qd) == Qua_d));
588   for (auto i = 0; i < 4; i++) {
589     assert(qf[i] == 1);
590     assert(qreal[i] == 1);
591     assert(qd[i] == 1);
592   }
593 }
594 
595 unittest {
596   import std.math : approxEqual, PI_2, PI_4;
597 
598   // Generation of Rotation matrix
599   const qRoll = Qua_d(PI_4, 0, 0);
600   const qPitch = Qua_d(0, -PI_4, 0);
601   const qYaw = Qua_d(0, 0, PI_2);
602   auto m = cast(Mat4d) qRoll;
603   assert(approxEqual(m.determinant, 1));
604   // dfmt off
605   assert(m.approxEqual(Mat4d([
606           1,  0       ,  0       ,  0,
607           0,  0.707107, -0.707107,  0,
608           0,  0.707107,  0.707107,  0,
609           0,  0       ,  0       ,  1
610   ])), m.toString());
611   // dfmt on
612 
613   m = cast(Mat4d) qPitch;
614   assert(approxEqual(m.determinant, 1));
615   // dfmt off
616   assert(m.approxEqual(Mat4d([
617           0.707107, 0, -0.707107, 0,
618           0       , 1,         0, 0,
619           0.707107, 0,  0.707107, 0,
620           0       , 0,         0, 1
621   ])), m.toString());
622   // dfmt on
623 
624   m = cast(Mat4d) qYaw;
625   assert(approxEqual(m.determinant, 1));
626   // dfmt off
627   assert(m.approxEqual(Mat4d([
628           0, -1, 0, 0,
629           1,  0, 0, 0,
630           0,  0, 1, 0,
631           0,  0, 0, 1
632   ])), m.toString());
633   // dfmt on
634 }
635 
636 
637 unittest {
638   auto q = Qua_f(1, 2, 3, 4);
639   assert(!isQuaternion!(int));
640   assert(isQuaternion!(typeof(q)));
641 }