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