1 module gfm.math.quaternion;
2 
3 import std.math,
4        std.string;
5 
6 import gfm.math.vector,
7        gfm.math.matrix,
8        funcs = gfm.math.funcs;
9 
10 
11 /// Quaternion implementation.
12 /// Holds a rotation + angle in a proper but wild space.
13 align(1) struct Quaternion(T)
14 {
15     align(1):
16     public
17     {
18         union
19         {
20             Vector!(T, 4u) v;
21             struct
22             {
23                 T x, y, z, w;
24             }
25         }
26 
27         /// Construct a Quaternion from a value.
28         this(U)(U x) pure nothrow if (isAssignable!U)
29         {
30             opAssign!U(x);
31         }
32 
33         /// Constructs a Quaternion from axis + angle.
34         static Quaternion fromAxis(Vector!(T, 3u) axis, T angle) pure nothrow
35         {
36             Quaternion q = void;
37             axis.normalize();
38             T cos_a = cos(angle / 2);
39             T sin_a = sin(angle / 2);
40             q.x = sin_a * axis.x;
41             q.y = sin_a * axis.y;
42             q.z = sin_a * axis.z;
43             q.w = cos_a;
44             return q; // should be normalized
45         }
46 
47         /// Assign from another Quaternion.
48         ref Quaternion opAssign(U)(U u) pure nothrow if (is(typeof(U._isQuaternion)) && is(U._T : T))
49         {
50             v = u.v;
51             return this;
52         }
53 
54         /// Assign from a vector of 4 elements.
55         ref Quaternion opAssign(U)(U u) pure nothrow if (is(U : Vector!(T, 4u)))
56         {
57             v = u;
58             return this;
59         }
60 
61         /// Converts to a pretty string.
62         string toString() const nothrow
63         {
64             try
65                 return format("%s", v);
66             catch (Exception e) 
67                 assert(false); // should not happen since format is right
68         }
69 
70         /// Normalizes a quaternion.
71         void normalize() pure nothrow
72         {
73             v.normalize();
74         }
75 
76         /// Returns: Normalized quaternion.
77         Quaternion normalized() pure const nothrow
78         {
79             Quaternion res = void;
80             res.v = v.normalized();
81             return res;
82         }
83 
84         /// Inverse a quaternion.
85         void inverse() pure nothrow
86         {
87             x = -x;
88             y = -y;
89             z = -z;
90         }
91 
92         /// Returns: Inversed quaternion.
93         Quaternion inversed() pure const nothrow
94         {
95             Quaternion res = void;
96             res.v = v;
97             res.inverse();
98             return res;
99         }
100 
101         ref Quaternion opOpAssign(string op, U)(U q) pure nothrow
102             if (is(U : Quaternion) && (op == "*"))
103         {
104             T nx = w * q.x + x * q.w + y * q.z - z * q.y,
105               ny = w * q.y + y * q.w + z * q.x - x * q.z,
106               nz = w * q.z + z * q.w + x * q.y - y * q.x,
107               nw = w * q.w - x * q.x - y * q.y - z * q.z;
108             x = nx;
109             y = ny;
110             z = nz;
111             w = nw;
112             return this;
113         }
114 
115         ref Quaternion opOpAssign(string op, U)(U operand) pure nothrow if (isConvertible!U)
116         {
117             Quaternion conv = operand;
118             return opOpAssign!op(conv);
119         }
120 
121         Quaternion opBinary(string op, U)(U operand) pure const nothrow
122             if (is(U: Quaternion) || (isConvertible!U))
123         {
124             Quaternion temp = this;
125             return temp.opOpAssign!op(operand);
126         }
127 
128         /// Compare two Quaternions.
129         bool opEquals(U)(U other) pure const if (is(U : Quaternion))
130         {
131             return v == other.v;
132         }
133 
134         /// Compare Quaternion and other types.
135         bool opEquals(U)(U other) pure const nothrow if (isConvertible!U)
136         {
137             Quaternion conv = other;
138             return opEquals(conv);
139         }
140 
141         /// Convert to a 3x3 rotation matrix.
142         /// TODO: check out why we can't do is(Unqual!U == mat3!T)
143         U opCast(U)() pure const nothrow if (is(typeof(U._isMatrix))
144                                           && is(U._T : _T)
145                                           && (U._R == 3) && (U._C == 3))
146         {
147             // do not assume that quaternion is normalized
148             T norm = x*x + y*y + z*z + w*w;
149             T s = (norm > 0) ? 2 / norm : 0;
150 
151             T xx = x * x * s, xy = x * y * s, xz = x * z * s, xw = x * w * s,
152                               yy = y * y * s, yz = y * z * s, yw = y * w * s,
153                                               zz = z * z * s, zw = z * w * s;
154             return mat3!(U._T)
155             (
156                 1 - (yy + zz)   ,   (xy - zw)    ,   (xz + yw)    ,
157                   (xy + zw)     , 1 - (xx + zz)  ,   (yz - xw)    ,
158                   (xz - yw)     ,   (yz + xw)    , 1 - (xx + yy)
159             );
160         }
161 
162         /// Converts a to a 4x4 rotation matrix.
163         /// Bugs: check why we can't do is(Unqual!U == mat4!T)
164         U opCast(U)() pure const nothrow if (is(typeof(U._isMatrix))
165                                           && is(U._T : _T)
166                                           && (U._R == 4) && (U._C == 4))
167         {
168             auto m3 = cast(mat3!(U._T))(this);
169             return cast(U)(m3);
170         }
171 
172         // Workaround Vector not being constructable through CTFE
173         static Quaternion IDENTITY() pure nothrow @property
174         {
175             Quaternion q;
176             q.x = q.y = q.z = 0;
177             q.w = 1;
178             return q;
179         }
180     }
181 
182     private
183     {
184         alias T _T;
185         enum _isQuaternion = true;
186 
187         template isAssignable(T)
188         {
189             enum bool isAssignable =
190                 is(typeof(
191                 {
192                     T x;
193                     Quaternion v = x;
194                 }()));
195         }
196 
197         // define types that can be converted to Quaternion, but are not Quaternion
198         template isConvertible(T)
199         {
200             enum bool isConvertible = (!is(T : Quaternion)) && isAssignable!T;
201         }
202     }
203 }
204 
205 alias Quaternion!float quaternionf;
206 alias Quaternion!double quaterniond;
207 
208 /// Linear interpolation, for quaternions.
209 Quaternion!T lerp(T)(Quaternion!T a, Quaternion!T b, float t) pure nothrow
210 {
211     Quaternion!T res = void;
212     res.v = funcs.lerp(a.v, b.v, t);
213     return res;
214 }
215 
216 
217 /// Returns: Nlerp of quaternions.
218 /// See_also: $(WEB keithmaggio.wordpress.com/2011/02/15/math-magician-lerp-slerp-and-nlerp/, Math Magician – Lerp, Slerp, and Nlerp)
219 Quaternion!T Nlerp(T)(Quaternion!T a, Quaternion!T b, float t) pure nothrow
220 {
221     assert(t >= 0 && t <= 1); // else probably doesn't make sense 
222     Quaternion!T res = void;
223     res.v = funcs.lerp(a.v, b.v, t);
224     res.v.normalize();
225     return res;
226 }
227 
228 /// Returns: Slerp of quaternions. Slerp is more expensive than Nlerp.
229 /// See_also: "Understanding Slerp, Then Not Using It"
230 Quaternion!T slerp(T)(Quaternion!T a, Quaternion!T b, T t) pure nothrow
231 {
232     assert(t >= 0 && t <= 1); // else probably doesn't make sense 
233 
234     Quaternion!T res = void;
235 
236     T dotProduct = dot(a.v, b.v);
237 
238     // spherical lerp always has 2 potential paths
239     // here we always take the shortest
240     if (dotProduct < 0)
241     {
242         b.v *= -1;
243         dotProduct = dot(a.v, b.v);
244     }
245 
246     immutable T threshold = 10 * T.epsilon; // idMath uses 1e-6f for 32-bits float precision
247     if ((1 - dotProduct) > threshold) // if small difference, use lerp
248         return lerp(a, b, t);
249 
250     T theta_0 = funcs.safeAcos(dotProduct); // angle between this and other
251     T theta = theta_0 * t; // angle between this and result
252 
253     vec3!T v2 = dot(b.v, a.v * dotProduct);
254     v2.normalize();
255 
256     res.v = dot(b.v, a.v * dotProduct);
257     res.v.normalize();
258 
259     res.v = a.v * cos(theta) + res.v * sin(theta);
260     return res;
261 }
262 
263 unittest
264 {
265     quaternionf a = quaternionf.fromAxis(vec3f(1, 0, 0), 1);
266     quaternionf b = quaternionf.fromAxis(vec3f(0, 1, 0), 0);
267     a = a * b;
268 
269     quaternionf c = lerp(a, b, 0.5f);
270     quaternionf d = Nlerp(a, b, 0.1f);
271     quaternionf e = slerp(a, b, 0.0f);
272 }