1 ///
2 module gfm.math.quaternion;
3 
4 import std.math,
5        std.string;
6 
7 import gfm.math.vector,
8        gfm.math.matrix,
9        funcs = gfm.math.funcs;
10 
11 /// Quaternion implementation.
12 /// Holds a rotation + angle in a proper but wild space.
13 struct Quaternion(T)
14 {
15     public
16     {
17         union
18         {
19             Vector!(T, 4u) v;
20             struct
21             {
22                 T x, y, z, w;
23             }
24         }
25 
26         /// Construct a Quaternion from a value.
27         @nogc this(U)(U x) pure nothrow if (isAssignable!U)
28         {
29             opAssign!U(x);
30         }
31 
32         /// Constructs a Quaternion from coordinates.
33         /// Warning: order of coordinates is different from storage.
34         @nogc this(T qw, T qx, T qy, T qz) pure nothrow
35         {
36             x = qx;
37             y = qy;
38             z = qz;
39             w = qw;
40         }
41 
42         /// Constructs a Quaternion from axis + angle.
43         @nogc static Quaternion fromAxis(Vector!(T, 3) axis, T angle) pure nothrow
44         {
45             Quaternion q = void;
46             axis.normalize();
47             T cos_a = cos(angle / 2);
48             T sin_a = sin(angle / 2);
49             q.x = sin_a * axis.x;
50             q.y = sin_a * axis.y;
51             q.z = sin_a * axis.z;
52             q.w = cos_a;
53             return q; // should be normalized
54         }
55 
56         /// Constructs a Quaternion from Euler angles.
57         /// All paramers given in radians.
58         /// Roll->X axis, Pitch->Y axis, Yaw->Z axis
59         /// See_also: $(LINK https://www.cs.princeton.edu/~gewang/projects/darth/stuff/quat_faq.html)
60         @nogc static Quaternion fromEulerAngles(T roll, T pitch, T yaw) pure nothrow
61         {
62             Quaternion q = void;
63             T sinPitch = sin(pitch / 2);
64             T cosPitch = cos(pitch / 2);
65             T sinYaw = sin(yaw / 2);
66             T cosYaw = cos(yaw / 2);
67             T sinRoll = sin(roll / 2);
68             T cosRoll = cos(roll / 2);
69             T cosPitchCosYaw = cosPitch * cosYaw;
70             T sinPitchSinYaw = sinPitch * sinYaw;
71             q.x = sinRoll * cosPitchCosYaw    - cosRoll * sinPitchSinYaw;
72             q.y = cosRoll * sinPitch * cosYaw + sinRoll * cosPitch * sinYaw;
73             q.z = cosRoll * cosPitch * sinYaw - sinRoll * sinPitch * cosYaw;
74             q.w = cosRoll * cosPitchCosYaw    + sinRoll * sinPitchSinYaw;
75             return q;
76         }
77 
78         /// Converts a quaternion to Euler angles.
79         /// TODO: adds a EulerAngles type.
80         /// Returns: A vector which contains roll-pitch-yaw as x-y-z.
81         @nogc vec3!T toEulerAngles() pure const nothrow
82         {
83             mat3!T m = cast(mat3!T)(this);
84 
85             T pitch, yaw, roll;
86             T s = sqrt(m.c[0][0] * m.c[0][0] + m.c[1][0] * m.c[1][0]);
87             if (s > T.epsilon)
88             {
89                 pitch = - atan2(m.c[2][0], s);
90                 yaw = atan2(m.c[1][0], m.c[0][0]);
91                 roll = atan2(m.c[2][1], m.c[2][2]);
92             }
93             else
94             {
95                 pitch = m.c[2][0] < 0.0f ? T(PI) /2 : -T(PI) / 2;
96                 yaw = -atan2(m.c[0][1], m.c[1][1]);
97                 roll = 0.0f;
98             }
99             return vec3!T(roll, pitch, yaw);
100         }
101 
102         /// Assign from another Quaternion.
103         @nogc ref Quaternion opAssign(U)(U u) pure nothrow if (isQuaternionInstantiation!U && is(U._T : T))
104         {
105             v = u.v;
106             return this;
107         }
108 
109         /// Assign from a vector of 4 elements.
110         @nogc ref Quaternion opAssign(U)(U u) pure nothrow if (is(U : Vector!(T, 4u)))
111         {
112             v = u;
113             return this;
114         }
115 
116         /// Converts to a pretty string.
117         string toString() const nothrow
118         {
119             try
120                 return format("%s", v);
121             catch (Exception e)
122                 assert(false); // should not happen since format is right
123         }
124 
125         /// Normalizes a quaternion.
126         @nogc void normalize() pure nothrow
127         {
128             v.normalize();
129         }
130 
131         /// Returns: Normalized quaternion.
132         @nogc Quaternion normalized() pure const nothrow
133         {
134             Quaternion res = void;
135             res.v = v.normalized();
136             return res;
137         }
138 
139         /// Inverses a quaternion in-place.
140         @nogc void inverse() pure nothrow
141         {
142             x = -x;
143             y = -y;
144             z = -z;
145         }
146 
147         /// Returns: Inverse of quaternion.
148         @nogc Quaternion inversed() pure const nothrow
149         {
150             Quaternion res = void;
151             res.v = v;
152             res.inverse();
153             return res;
154         }
155 
156         @nogc ref Quaternion opOpAssign(string op, U)(U q) pure nothrow
157             if (is(U : Quaternion) && (op == "*"))
158         {
159             T nx = w * q.x + x * q.w + y * q.z - z * q.y,
160               ny = w * q.y + y * q.w + z * q.x - x * q.z,
161               nz = w * q.z + z * q.w + x * q.y - y * q.x,
162               nw = w * q.w - x * q.x - y * q.y - z * q.z;
163             x = nx;
164             y = ny;
165             z = nz;
166             w = nw;
167             return this;
168         }
169 
170         @nogc ref Quaternion opOpAssign(string op, U)(U operand) pure nothrow if (isConvertible!U)
171         {
172             Quaternion conv = operand;
173             return opOpAssign!op(conv);
174         }
175 
176         @nogc Quaternion opBinary(string op, U)(U operand) pure const nothrow
177             if (is(U: Quaternion) || (isConvertible!U))
178         {
179             Quaternion temp = this;
180             return temp.opOpAssign!op(operand);
181         }
182 
183         /// Compare two Quaternions.
184         bool opEquals(U)(U other) pure const if (is(U : Quaternion))
185         {
186             return v == other.v;
187         }
188 
189         /// Compare Quaternion and other types.
190         bool opEquals(U)(U other) pure const nothrow if (isConvertible!U)
191         {
192             Quaternion conv = other;
193             return opEquals(conv);
194         }
195 
196         /// Convert to a 3x3 rotation matrix.
197         /// TODO: check out why we can't do is(Unqual!U == mat3!T)
198         @nogc U opCast(U)() pure const nothrow if (isMatrixInstantiation!U
199                                                    && is(U._T : _T)
200                                                    && (U._R == 3) && (U._C == 3))
201         {
202             // do not assume that quaternion is normalized
203             T norm = x*x + y*y + z*z + w*w;
204             T s = (norm > 0) ? 2 / norm : 0;
205 
206             T xx = x * x * s, xy = x * y * s, xz = x * z * s, xw = x * w * s,
207                               yy = y * y * s, yz = y * z * s, yw = y * w * s,
208                                               zz = z * z * s, zw = z * w * s;
209             return mat3!(U._T)
210             (
211                 1 - (yy + zz)   ,   (xy - zw)    ,   (xz + yw)    ,
212                   (xy + zw)     , 1 - (xx + zz)  ,   (yz - xw)    ,
213                   (xz - yw)     ,   (yz + xw)    , 1 - (xx + yy)
214             );
215         }
216 
217         /// Converts a to a 4x4 rotation matrix.
218         /// Bugs: check why we can't do is(Unqual!U == mat4!T)
219         @nogc U opCast(U)() pure const nothrow if (isMatrixInstantiation!U
220                                                    && is(U._T : _T)
221                                                    && (U._R == 4) && (U._C == 4))
222         {
223             auto m3 = cast(mat3!(U._T))(this);
224             return cast(U)(m3);
225         }
226 
227         /// Workaround Vector not being constructable through CTFE
228         @nogc static Quaternion identity() pure nothrow @property
229         {
230             Quaternion q;
231             q.x = q.y = q.z = 0;
232             q.w = 1;
233             return q;
234         }
235     }
236 
237     private
238     {
239         alias T _T;
240 
241         template isAssignable(T)
242         {
243             enum bool isAssignable =
244                 is(typeof(
245                 {
246                     T x;
247                     Quaternion v = x;
248                 }()));
249         }
250 
251         // define types that can be converted to Quaternion, but are not Quaternion
252         template isConvertible(T)
253         {
254             enum bool isConvertible = (!is(T : Quaternion)) && isAssignable!T;
255         }
256     }
257 }
258 
259 template isQuaternionInstantiation(U)
260 {
261     private static void isQuaternion(T)(Quaternion!T x)
262     {
263     }
264 
265     enum bool isQuaternionInstantiation = is(typeof(isQuaternion(U.init)));
266 }
267 
268 alias Quaternion!float quatf;///
269 alias Quaternion!double quatd;///
270 
271 /// Linear interpolation, for quaternions.
272 @nogc Quaternion!T lerp(T)(Quaternion!T a, Quaternion!T b, float t) pure nothrow
273 {
274     Quaternion!T res = void;
275     res.v = funcs.lerp(a.v, b.v, t);
276     return res;
277 }
278 
279 /// Nlerp of quaternions
280 /// Returns: Nlerp of quaternions.
281 /// See_also: $(WEB keithmaggio.wordpress.com/2011/02/15/math-magician-lerp-slerp-and-nlerp/, Math Magician – Lerp, Slerp, and Nlerp)
282 @nogc Quaternion!T Nlerp(T)(Quaternion!T a, Quaternion!T b, float t) pure nothrow
283 {
284     assert(t >= 0 && t <= 1); // else probably doesn't make sense
285     Quaternion!T res = void;
286     res.v = funcs.lerp(a.v, b.v, t);
287     res.v.normalize();
288     return res;
289 }
290 
291 /// Slerp of quaternions
292 /// Returns: Slerp of quaternions. Slerp is more expensive than Nlerp.
293 /// See_also: "Understanding Slerp, Then Not Using It"
294 @nogc Quaternion!T slerp(T)(Quaternion!T a, Quaternion!T b, T t) pure nothrow
295 {
296     assert(t >= 0 && t <= 1); // else probably doesn't make sense
297 
298     Quaternion!T res = void;
299 
300     T dotProduct = dot(a.v, b.v);
301 
302     // spherical lerp always has 2 potential paths
303     // here we always take the shortest
304     if (dotProduct < 0)
305     {
306         b.v *= -1;
307         dotProduct = dot(a.v, b.v);
308     }
309 
310     immutable T threshold = 10 * T.epsilon; // idMath uses 1e-6f for 32-bits float precision
311     if ((1 - dotProduct) > threshold) // if small difference, use lerp
312         return lerp(a, b, t);
313 
314     T theta_0 = funcs.safeAcos(dotProduct); // angle between this and other
315     T theta = theta_0 * t; // angle between this and result
316 
317     vec3!T v2 = dot(b.v, a.v * dotProduct);
318     v2.normalize();
319 
320     res.v = dot(b.v, a.v * dotProduct);
321     res.v.normalize();
322 
323     res.v = a.v * cos(theta) + res.v * sin(theta);
324     return res;
325 }
326 
327 unittest
328 {
329     quatf a = quatf.fromAxis(vec3f(1, 0, 0), 1);
330     quatf b = quatf.fromAxis(vec3f(0, 1, 0), 0);
331     a = a * b;
332 
333     quatf c = lerp(a, b, 0.5f);
334     quatf d = Nlerp(a, b, 0.1f);
335     quatf e = slerp(a, b, 0.0f);
336     quatd f = quatd(1.0, 4, 5.0, 6.0);
337     quatf g = quatf.fromEulerAngles(-0.1f, 1.2f, -0.3f);
338     vec3f ga = g.toEulerAngles();
339 }