1 module sbylib.math.quaternion;
2 
3 import sbylib.math.vector;
4 import sbylib.math.matrix;
5 import sbylib.math.angle;
6 import std.traits;
7 import std.conv : to;
8 import std.format : format;
9 import std.range : isInputRange, RangeElementType = ElementType;
10 
11 alias quat = Quaternion!float;
12 
13 /**
14 Quaternion type
15 */
16 struct Quaternion(T)
17 if (__traits(isArithmetic, T)) 
18 {
19     /**
20     Components of quaternion.
21 
22     q = ix + jy + kz + w
23     */
24     T x, y, z, w;
25 
26     /**
27     Constructor from scalar components
28     */
29     this(T x, T y, T z, T w) {
30         this.x = x;
31         this.y = y;
32         this.z = z;
33         this.w = w;
34     }
35 
36     /**
37     Constructor from imaginary vector and real part
38     */
39     this(Vector!(T,3) v, T w) {
40         this(v[0],v[1],v[2],w);
41     }
42 
43     /**
44     Constructor by InputRange.
45     This quaternion's all elements are filled by given range.
46 
47     Params:
48         elements = values which fills the quaternion
49     */
50     this(Range)(Range r)
51     if (isInputRange!(Range) && is(RangeElementType!(Range) : T))
52     {
53         import std.range : front, popFront, empty;
54 
55         static foreach (i; 0..4) {
56             this[i] = r.front;
57             r.popFront;
58         }
59         assert(r.empty);
60     }
61 
62     /**
63     Get the x-axis basis vector of the transformation of this quaternion
64 
65     Returns: x basis vector
66     */
67     Vector!(T,3) baseX() const {
68         return Vector!(T,3)
69             (1-2*(y*y+z*z),
70              2*(x*y+w*z),
71              2*(x*z-w*y));
72     }
73 
74     /**
75     Get the y-axis basis vector of the transformation of this quaternion
76 
77     Returns: y basis vector
78     */
79     Vector!(T,3) baseY() const {
80         return Vector!(T,3)
81             (2*(x*y-z*w),
82              1-2*(x*x+z*z),
83              2*(y*z+w*x));
84     }
85 
86     /**
87     Get the z-axis basis vector of the transformation of this quaternion
88 
89     Returns: z basis vector
90     */
91     Vector!(T,3) baseZ() const {
92         return Vector!(T,3)
93             (2*(x*z+w*y),
94              2*(y*z-w*x),
95              1-2*(x*x+y*y));
96     }
97 
98     unittest {
99         import std.algorithm : map;
100         import std.range : iota;
101         import std.random : uniform;
102         import sbylib.math.vector : approxEqual;
103         foreach (i; 0..100) {
104             const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))).normalize;
105             assert(approxEqual(q.rotate(vec3(1,0,0)), q.baseX));
106             assert(approxEqual(q.rotate(vec3(0,1,0)), q.baseY));
107             assert(approxEqual(q.rotate(vec3(0,0,1)), q.baseZ));
108         }
109     }
110 
111     /**
112     Unary operation for "+".
113 
114     this is identity function
115 
116     Returns: a quaternionly which is equal to this quaternion
117     */
118     Quaternion!T opUnary(string op)() const 
119     if (op == "+")
120     {
121         return this;
122     }
123 
124     unittest {
125         import std.algorithm : map;
126         import std.range : iota;
127         import std.random : uniform;
128 
129         foreach (i; 0..100) {
130             const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f)));
131             assert(approxEqual(q * +1, +q));
132         }
133     }
134 
135     /**
136     Unary operation for "-".
137 
138     Returns: a quaternion whose sign is inverted
139     */
140     Quaternion!T opUnary(string op)() const 
141     if (op == "-")
142     {
143         Quaternion!T result;
144         static foreach (i; 0..4) {
145             result[i] = -this[i];
146         }
147         return result;
148     }
149 
150     unittest {
151         import std.algorithm : map;
152         import std.range : iota;
153         import std.random : uniform;
154 
155         foreach (i; 0..100) {
156             const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f)));
157             assert(approxEqual(q * -1, -q));
158         }
159     }
160 
161     /**
162     Unary operation for "~".
163     This means conjugation.
164 
165     Returns: a quaternion conjugate with this quaternion
166     */
167     Quaternion!T opUnary(string op)() const
168     if (op == "~")
169     {
170         return Quaternion!T(-x,-y,-z,+w);
171     }
172 
173     unittest {
174         import std.algorithm : map;
175         import std.range : iota;
176         import std.random : uniform;
177 
178         foreach (i; 0..100) {
179             const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f)));
180 
181             assert(approxEqual(q * ~q, quat(vec3(0), lengthSq(q))));
182         }
183     }
184 
185     /**
186     Binary operation between quaternion for "+" or "-"
187 
188     Params:
189         q = target quaternion
190 
191     Returns: calculated quaternion
192     */
193     Quaternion!(CommonType!(T,S)) opBinary(string op,S)(Quaternion!S q) const 
194     if (op == "+" || op == "-")
195     {
196         Quaternion!(CommonType!(T,S)) result;
197         static foreach (i; 0..4) {
198             result[i] = mixin(format!q{
199                 this[i] %s q[i]
200             }(op));
201         }
202         return result;
203     }
204 
205     unittest {
206         import std.algorithm : map;
207         import std.range : iota;
208         import std.random : uniform;
209         import std.math : approxEqual;
210 
211         foreach (k; 0..100) {
212             const a = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f)));
213             const b = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f)));
214 
215             const c = a + b;
216             const d = a - b;
217 
218             static foreach (i; 0..4) {
219                 assert(approxEqual(a[i] + b[i], c[i]));
220                 assert(approxEqual(a[i] - b[i], d[i]));
221             }
222         }
223     }
224 
225     /**
226     Binary operation between quaternion for "*"
227 
228     Params:
229         q = target quaternion
230 
231     Returns: calculated quaternion
232     */
233     Quaternion!(CommonType!(T,S)) opBinary(string op,S)(Quaternion!S q) const 
234     if (op == "*")
235     {
236         Quaternion!(CommonType!(T,S)) result;
237         result.w = w * q.w - x * q.x - y * q.y - z * q.z;
238         result.x = w * q.x + x * q.w + y * q.z - z * q.y;
239         result.y = w * q.y - x * q.z + y * q.w + z * q.x;
240         result.z = w * q.z + x * q.y - y * q.x + z * q.w;
241         return result;
242     }
243 
244     unittest {
245         import std.algorithm : map;
246         import std.range : iota;
247         import std.random : uniform;
248 
249         foreach (i; 0..100) {
250             const a = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
251             const b = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
252             const c = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
253             assert(approxEqual((a * b) * c, a * (b * c)));
254         }
255     }
256 
257     /**
258     Binary operation between scalar type for "*" or "/".
259 
260     Params:
261         e = target scalar value
262 
263     Returns: calculated quaternion
264     */
265     Quaternion!(CommonType!(T,S)) opBinary(string op, S)(S e) const
266     if (__traits(isArithmetic, S) && (op == "*" || op == "/"))
267     {
268         Quaternion!(CommonType!(T,S)) result;
269         static foreach (i; 0..4) {
270             result[i] = mixin(format!q{
271                 this[i] %s e
272             }(op));
273         }
274         return result;
275     }
276 
277     unittest {
278         import std.algorithm : map;
279         import std.range : iota;
280         import std.random : uniform;
281         import std.math : approxEqual;
282 
283         foreach (k; 0..100) {
284             const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
285             const s = uniform(-1.0f, +1.0f);
286             const q2 = q * s;
287             const q3 = q / s;
288             static foreach (i; 0..4) {
289                 assert(approxEqual(q[i] * s, q2[i]));
290                 assert(approxEqual(q[i] / s, q3[i]));
291             }
292         }
293     }
294 
295     /**
296     Binary operation between scalar type for "*".
297 
298     Params:
299         e = target scalar value
300 
301     Returns: calculated quaternion
302     */
303     Quaternion!(CommonType!(T,S)) opBinaryRight(string op, S)(S e) const
304     if (__traits(isArithmetic, S) && (op == "*"))
305     {
306         Quaternion!(CommonType!(T,S)) result;
307         static foreach (i; 0..4) {
308             result[i] = mixin(format!q{
309                 this[i] %s e
310             }(op));
311         }
312         return result;
313     }
314 
315     unittest {
316         import std.algorithm : map;
317         import std.range : iota;
318         import std.random : uniform;
319         import std.math : approxEqual;
320 
321         foreach (k; 0..100) {
322             const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
323             const s = uniform(-1.0f, +1.0f);
324             const q2 = s * q;
325             static foreach (i; 0..4) {
326                 assert(approxEqual(s * q[i], q2[i]));
327             }
328         }
329     }
330 
331     /**
332     Operator assign for quaternion.
333     This function supports "+" or "-" operation.
334 
335     Params:
336         q = target quaternion
337     */
338     Quaternion opOpAssign(string op,S)(Quaternion!S q) 
339     if (op == "+" || op == "-")
340     {
341         static foreach (i; 0..4) {
342             mixin(format!q{
343                 this[i] %s= q[i];
344             }(op));
345         }
346         return this;
347     }
348 
349     unittest {
350         import std.algorithm : map;
351         import std.range : iota;
352         import std.random : uniform;
353 
354         foreach (i; 0..100) {
355             auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
356             const q2 = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
357             const q3 = q + q2;
358             q += q2;
359 
360             assert(approxEqual(q, q3));
361         }
362     }
363 
364     /**
365     Operator assign for quaternion.
366     This function supports "*" operation.
367 
368     Params:
369         q = target quaternion
370     */
371     Quaternion opOpAssign(string op,S)(Quaternion!(S) q)
372     if (op == "*")
373     {
374         Quaternion result = this * q;
375         static foreach (i; 0..4) {
376             this[i] = result[i];
377         }
378         return this;
379     }
380 
381     unittest {
382         import std.algorithm : map;
383         import std.range : iota;
384         import std.random : uniform;
385         foreach (i; 0..100) {
386             auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
387             const s = uniform(-1.0f, +1.0f);
388             const q2 = q * s;
389             q *= s;
390 
391             assert(approxEqual(q, q2));
392         }
393     }
394 
395     /**
396     Operator assign for scalar type.
397     This function supports "*" or "/" operation.
398 
399     Params:
400         q = target quaternion
401     */
402     Quaternion opOpAssign(string op, S)(S e) 
403     if (__traits(isArithmetic, S) && (op == "*" || op == "/"))
404     {
405         static foreach (i; 0..4) {
406             mixin(format!q{
407                 this[i] %s= e;
408             }(op));
409         }
410         return this;
411     }
412 
413     unittest {
414         import std.algorithm : map;
415         import std.range : iota;
416         import std.random : uniform;
417 
418         foreach (i; 0..100) {
419             auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
420             const s = uniform(-1.0f, +1.0f);
421             const q2 = q * s;
422             q *= s;
423 
424             assert(approxEqual(q, q2));
425         }
426     }
427 
428     /**
429     Indexing operation.
430 
431     Returns: i-th element of this quaternion
432     */
433     T opIndex(size_t i)  const
434     in (i < 4)
435     {
436         final switch (i) {
437         case 0: return x;
438         case 1: return y;
439         case 2: return z;
440         case 3: return w;
441         }
442     }
443 
444     unittest {
445         import std.algorithm : map;
446         import std.range : iota;
447         import std.random : uniform;
448         import std.array : array;
449 
450         foreach (k; 0..100) {
451             float[4] v;
452             static foreach (i; 0..4) {
453                 v[i] = uniform(-1.0f, +1.0f);
454             }
455             const q = quat(v.array);
456 
457             static foreach (i; 0..4) {
458                 assert(v[i] == q[i]);
459             }
460         }
461     }
462 
463     /**
464     Indexing operation.
465 
466     Returns: i-th element of this quaternion
467     */
468     ref T opIndex(size_t i) 
469     in (i < 4)
470     {
471         final switch (i) {
472         case 0: return x;
473         case 1: return y;
474         case 2: return z;
475         case 3: return w;
476         }
477     }
478 
479     unittest {
480         import std.algorithm : map;
481         import std.range : iota;
482         import std.random : uniform;
483 
484         foreach (k; 0..100) {
485             quat q;
486 
487             static foreach (i; 0..4) {{
488                 const v = uniform(-1.0f, +1.0f);
489                 q[i] = v;
490                 assert(q[i] == v);
491             }}
492         }
493     }
494 
495     string toString() const {
496         return format!"q(%s, %s, %s, %s)"(x, y, z, w);
497     }
498 
499     /**
500     Get raw array data.
501 
502     Returns: raw array data
503     */
504     T[4] array() const {
505         return [x,y,z,w];
506     }
507 
508     unittest {
509         import std.range : iota;
510         const q = quat(iota(4));
511         assert(q.array == [0,1,2,3]);
512     }
513 
514     Vector!(T,3) axis() @property const {
515         return Vector!(T,3)(x,y,z);
516     }
517 
518     unittest {
519         const q = quat(1,2,3,4);
520         assert(q.axis == vec3(1,2,3));
521     }
522 
523     void axis(const Vector!(T,3) axis) @property {
524         this.x = axis[0];
525         this.y = axis[1];
526         this.z = axis[2];
527     }
528 
529     unittest {
530         quat q;
531         q.axis = vec3(1,2,3);
532         assert(q.axis == vec3(1,2,3));
533     }
534 
535     /**
536     Returns rotation quaternion decided by its rotation axis and angle.
537 
538     Params:
539         axis = rotation axis vector, which must be normalized
540         angle = rotation angle
541 
542     Returns: rotation quaternion
543     */
544     static Quaternion axisAngle(const Vector!(T,3) axis, const Angle rad) {
545         return Quaternion(axis * sin(rad/2), cos(rad/2));
546     }
547 
548     unittest {
549         import std.algorithm : map;
550         import std.random : uniform;
551         import std.range : iota;
552         import std.math : abs, approxEqual;
553         import sbylib.math.vector : dot, normalize, ortho, length;
554 
555         foreach (i; 0..100) {
556             const point = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f)));
557             const axis = point.ortho.normalize;
558             const angle = uniform(0,180.0f).deg;
559             const r = mat3.axisAngle(axis, angle);
560             const point2 = r * point;
561             assert(approxEqual(dot(point, point2), length(point) * length(point2) * cos(angle)));
562         }
563     }
564 
565     /**
566     Returns rotation quaternion which converts a vector to another vector.
567 
568     Params:
569         from = before vector, which must be normalized
570         to = after vector, which must be normalized
571 
572     Returns: rotation quaternion
573     */
574     static Quaternion!T rotFromTo(const Vector!(T,3) from, const Vector!(T,3) to) {
575         import sbylib.math.vector : vnormalize = normalize;
576         import std.math : sgn;
577 
578         const c = dot(from, to);
579         const v = vnormalize(cross(from, to));
580         if (v.hasNaN) {
581             return Quaternion!T(0,0,0,sgn(dot(from,to)));
582         }
583         return axisAngle(v, c.acos);
584     }
585 
586     unittest {
587         import std.algorithm : map;
588         import std.random : uniform;
589         import std.range : iota;
590         import sbylib.math.vector : approxEqual, normalize;
591 
592         foreach (i; 0..100) {
593             const a = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
594             const b = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
595             const c = rotFromTo(a,b).rotate(a);
596             assert(approxEqual(b,c));
597         }
598     }
599 }
600 
601 /**
602 Returns length of a quaternion.
603 
604 Params:
605     q = target quaternion
606 
607 Returns: length of the target quaternion
608 */
609 T lengthSq(T)(const Quaternion!T q) {
610     T sum = 0;
611     static foreach (i; 0..4) {
612         sum += q[i] * q[i];
613     }
614     return sum;
615 }
616 
617 /**
618 Returns length of a quaternion.
619 
620 Params:
621     q = target quaternion
622 
623 Returns: length of the target quaternion
624 */
625 T length(T)(const Quaternion!T q) 
626 if (__traits(isFloating, T))
627 {
628     import std.math : sqrt;
629     return sqrt(lengthSq(q));
630 }
631 
632 /**
633 Returns a quaternion whose length 1 from target quaternion.
634 
635 Params:
636     q = target quaternion
637 
638 Returns: normalized target quaternion
639 */
640 Quaternion!T normalize(T)(const Quaternion!T q) 
641 if (__traits(isFloating, T))
642 {
643     return q / length(q);
644 }
645 
646 /**
647 Returns a quaternion whose length 1 from target vector.
648 When the length is 0, returns zero quaternion.
649 
650 Params:
651     v = target quaternion
652 
653 Returns: normalized target quaternion if the length is greater than 0, otherwise zero vector
654 */
655 Quaternion!(T) safeNormalize(T)(const Quaternion!(T) q) 
656 if (__traits(isFloating, T))
657 {
658     const l = length(q);
659     if (l == 0) return q;
660     return q / l;
661 }
662 
663 unittest {
664     import std.algorithm : map;
665     import std.random : uniform;
666     import std.range : iota;
667     import std.math : approxEqual;
668 
669     foreach (i; 0..100) {
670         const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f)));
671         assert(q == quat(0,0,0,0) || safeNormalize(q).length.approxEqual(1));
672     }
673 }
674 
675 /**
676 Rotate a vector by a quaternion.
677 
678 Params:
679     vec = rotation target vector
680     q = rotation quaternion
681 
682 Returns: rotated vector
683 */
684 Vector!(T,3) rotate(T)(const Quaternion!(T) q, Vector!(T,3) vec) {
685     return (q * Quaternion!T(vec, 0) * ~q).axis;
686 }
687 
688 unittest {
689     import std.algorithm : map;
690     import std.random : uniform;
691     import std.range : iota;
692     import sbylib.math.vector : normalize, length;
693     import std.math : approxEqual;
694 
695     foreach (i; 0..100) {
696         const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f)));
697         const axis = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
698         const angle = uniform(0,180.0f).deg;
699         assert(approxEqual(p.length, quat.axisAngle(axis, angle).rotate(p).length));
700     }
701 }
702 
703 /**
704 Interpolate two quaternions spherically.
705 
706 Params:
707     q1 = target quaternion
708     q2 = target quaternion
709     t  = interpolation coefficient (0 <= t <= 1)
710 
711 Returns: Interpolated quaternion
712 */
713 Quaternion!(T) slerp(T)(const Quaternion!(T) q1, const Quaternion!(T) q2, T t) {
714     import std.math : approxEqual;
715 
716     const c = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
717     if (c < 0) return slerp(q1, -q2, t);
718     if (c.approxEqual(1, float.epsilon)) return q1;
719     const angle = acos(c);
720     const s = sin(angle);
721 
722     return sin((1-t)*angle) / s * q1 + sin(t*angle) / s * q2;
723 }
724 
725 unittest {
726     import std.algorithm : map;
727     import std.random : uniform;
728     import std.range : iota;
729     import sbylib.math.vector : normalize, length;
730     import std.math : approxEqual;
731 
732     foreach (i; 0..100) {
733         const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f)));
734 
735         const axis1 = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
736         const angle1 = uniform(0,180.0f).deg;
737         const q1 = quat.axisAngle(axis1, angle1);
738 
739         const axis2 = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
740         const angle2 = uniform(0,180.0f).deg;
741         const q2 = quat.axisAngle(axis2, angle2);
742 
743         foreach (j; 0..100) {
744             const t = j * 0.01;
745             const q = slerp(q1, q2, t);
746             assert(approxEqual(p.length, q.rotate(p).length));
747         }
748 
749     }
750 }
751 
752 /**
753 Converts a quaternion into matrix 3x3.
754 
755 Params:
756     q = target quaternion
757 
758 Returns: converted matrix
759 */
760 Matrix!(T,3,3) toMatrix3(T)(const Quaternion!T q) {
761     with (q) {
762         return Matrix!(T,3,3)(
763                 1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+w*y),
764                 2*(x*y+w*z), 1-2*(x*x+z*z), 2*(y*z-w*x),
765                 2*(x*z-w*y), 2*(y*z+w*x), 1-2*(x*x+y*y));
766     }
767 }
768 
769 unittest {
770     import std.algorithm : map;
771     import std.random : uniform;
772     import std.range : iota;
773     import sbylib.math.vector : approxEqual;
774 
775     foreach (i; 0..100) {
776         const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f)));
777         const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
778         assert(approxEqual(q.toMatrix3() * p, q.rotate(p)));
779     }
780 }
781 
782 /**
783 Converts a quaternion into matrix 4x4.
784 
785 Params:
786     q = target quaternion
787 
788 Returns: converted matrix
789 */
790 Matrix!(T,4,4) toMatrix4(T)(const Quaternion!T q) {
791     import sbylib.math.matrix : toMatrix4;
792     return q.toMatrix3().toMatrix4();
793 }
794 
795 unittest {
796     import std.algorithm : map;
797     import std.random : uniform;
798     import std.range : iota;
799     import sbylib.math.vector : approxEqual;
800 
801     foreach (i; 0..100) {
802         const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f)));
803         const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize;
804         assert(approxEqual((q.toMatrix4() * vec4(p,1)).xyz, q.rotate(p)));
805     }
806 }
807 
808 /**
809 Returns true if the distance between the given two point is less than eps.
810 
811 Params:
812     a = target point
813     b = target point
814     eps = criteria of approximation
815 
816 Returns: true if 2 vectors are approximately equal.
817 */
818 bool approxEqual(T)(const Quaternion!(T) a, const Quaternion!(T) b, T eps = 1e-5) {
819     return length(a-b) < eps;
820 }