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