1 /** 2 A Quaternion it's used to store a space orientation / rotation, and makes easy 3 to interpolate rotations, at same time that avoid grimbal locks that have using 4 Euler angles 5 */ 6 module zmath.quaternion; 7 8 import zmath.vector; 9 import zmath.matrix; 10 11 alias Qua_f = Quaternion!float; /// Alias of a Quaternion with floats 12 alias Qua_d = Quaternion!double; /// Alias of a Quaternion with doubles 13 alias Qua_r = Quaternion!real; /// Alias of a Quaternion with reals 14 15 /** 16 * Quaternion over a FloatPoint type, 17 */ 18 public struct Quaternion(T = floatt) if (__traits(isFloating, T)) { 19 nothrow: 20 static assert(__traits(isFloating, T)); 21 22 static enum size_t dim = 4; /// Quaternion Dimension 23 24 union { 25 private T[4] coor; /// Quaternion coords in an array 26 private Vector!(T, 4) v; /// Quaternion like a vector 27 struct { 28 T i; /// i complex component 29 T j; /// j complex component 30 T k; /// k complex component 31 T w; /// w real component 32 } 33 34 struct { 35 T x; /// alias of i component 36 T y; /// alias of j component 37 T z; /// alias of k component 38 } 39 } 40 41 /** 42 * Build a new Quaternion from a set of initial values 43 * Params: 44 * i = i imaginary component 45 * j = j imaginary component 46 * k = k imaginary component 47 * w = i real component 48 */ 49 this(in T i, in T j, in T k, in T w) { 50 this.i = i; 51 this.j = j; 52 this.k = k; 53 this.w = w; 54 } 55 56 /** 57 * Build a new Quaternion from a array 58 * If no there values for j, and k, will be set to 0. w is set to 1 59 * Params: 60 * xs = Array with coords 61 */ 62 this(in T[] xs) { 63 size_t l = xs.length > dim ? dim : xs.length; 64 coor[0 .. l] = xs[0 .. l].dup; 65 if (l < dim) { 66 coor[l .. $ - 1] = 0; // imaginary part 67 w = 1; // real part 68 } 69 } 70 71 /** 72 * Build a new Quaternion from a 3D Vector 73 * w will be set to 1 74 * Params: 75 * vec = Vector 3d 76 */ 77 @nogc this(in Vector!(T, 3) vec) pure { 78 this.v = Vector!(T, 4)(vec.x, vec.y, vec.z); 79 this.w = 1; 80 } 81 82 /** 83 * Build a new Quaternion from a 4D Vector 84 * Params: 85 * vec = Vector 4d 86 */ 87 @nogc this(in Vector!(T, 4) vec) pure { 88 this.v = vec; 89 } 90 91 /** 92 * Creates a Quaternion from a 3d Vector and angle 93 * Params: 94 * v = Rotation axis 95 * angle = Rotation angle in radians 96 * Returns: 97 * Quaternion that represents a rotation 98 */ 99 @nogc this(U)(Vector!(T, 3) v, U angle) if (is(U : real)) 100 in { 101 assert(v.isOk, "Invalid vector"); 102 } body { 103 import std.math : sin, cos; 104 105 v.normalize(); 106 const sin_2 = sin(angle / 2.0L); 107 i = v.x * sin_2; 108 j = v.y * sin_2; 109 k = v.z * sin_2; 110 w = cos(angle / 2.0L); 111 } 112 113 /** 114 * Creates a new Quaternion from Euler angles 115 * Params : 116 * roll = Rotation around X axis in radians (aka bank) 117 * pitch = Rotation around Y axis in radians (aka attitude) 118 * yaw = Rotation around Z axis in radians (aka heading) 119 * Returns: 120 * Quaternion that represents a rotation 121 */ 122 @nogc this(U)(U roll, U pitch, U yaw) if (__traits(isFloating, U)) { 123 import std.math : sin, cos; 124 125 const sinPitch = sin(pitch / 2.0); 126 const cosPitch = cos(pitch / 2.0); 127 const sinYaw = sin(yaw / 2.0); 128 const cosYaw = cos(yaw / 2.0); 129 const sinRoll = sin(roll / 2.0); 130 const cosRoll = cos(roll / 2.0); 131 132 const cosPitchCosYaw = cosPitch * cosYaw; 133 const sinPitchSinYaw = sinPitch * sinYaw; 134 135 this.i = sinRoll * cosPitchCosYaw - cosRoll * sinPitchSinYaw; 136 this.j = cosRoll * sinPitch * cosYaw + sinRoll * cosPitch * sinYaw; 137 this.k = cosRoll * cosPitch * sinYaw - sinRoll * sinPitch * cosYaw; 138 this.w = cosRoll * cosPitchCosYaw + sinRoll * sinPitchSinYaw; 139 } 140 141 // Basic Properties ********************************************************** 142 143 /** 144 * Returns i coord of this vector 145 */ 146 @nogc T opIndex(size_t i) pure const { 147 return coor[i]; 148 } 149 150 /** 151 * Assigns a value to a coord 152 */ 153 @nogc void opIndexAssign(T c, size_t i) { 154 coor[i] = c; 155 } 156 157 /** 158 * Returns the actual length of this quaternion 159 */ 160 @property @nogc T length() pure const { 161 import std.math : sqrt; 162 163 return sqrt(sq_length); 164 } 165 166 /** 167 * Returns the actual squared length of this quaternion 168 */ 169 @property @nogc T sq_length() pure const { 170 return x * x + y * y + z * z + w * w; 171 } 172 173 // Operations **************************************************************** 174 175 /** 176 * Define Equality 177 */ 178 @nogc bool opEquals()(auto ref const Quaternion rhs) pure const { 179 if (x != rhs.x) { 180 return false; 181 } 182 if (y != rhs.y) { 183 return false; 184 } 185 if (z != rhs.z) { 186 return false; 187 } 188 if (w != rhs.w) { 189 return false; 190 } 191 return true; 192 } 193 194 /** 195 * Approximated equality with controlable precision 196 */ 197 @nogc bool approxEqual()(auto ref const Quaternion rhs, T maxRelDiff = 1e-02, T maxAbsDiff = 1e-05) pure const { 198 import std.math : approxEqual; 199 200 return approxEqual(x, rhs.x, maxRelDiff, maxAbsDiff) && approxEqual(y, rhs.y, maxRelDiff, 201 maxAbsDiff) && approxEqual(z, rhs.z, maxRelDiff, maxAbsDiff) 202 && approxEqual(w, rhs.w, maxRelDiff, maxAbsDiff); 203 } 204 205 /** 206 * Define unary operators + and - 207 */ 208 @nogc Quaternion opUnary(string op)() pure const if (op == "+" || op == "-") { 209 return Quaternion(mixin(op ~ "x"), mixin(op ~ "y"), mixin(op ~ "z"), mixin(op ~ "w")); 210 } 211 212 /** 213 * Define binary operator + and - 214 */ 215 @nogc Quaternion opBinary(string op)(auto ref const Quaternion rhs) pure const 216 if (op == "+" || op == "-") { 217 return Quaternion(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y"), 218 mixin("z" ~ op ~ "rhs.z"), mixin("w" ~ op ~ "rhs.w")); 219 } 220 221 /** 222 * Define Scalar multiplication 223 */ 224 @nogc Quaternion opBinary(string op)(in T rhs) pure const if (op == "*") { 225 return Quaternion(x * rhs, y * rhs, z * rhs, w * rhs); 226 } 227 228 /** 229 * Defines Hamilton product for Quaternions 230 * Params: 231 * rhs = Quaternion at rigth of operation 232 * Return: 233 * Quaternion result of hamilton product of <strong>this</strong> and rhs (this 234 * rhs) 235 */ 236 @nogc Quaternion opBinary(string op)(auto ref const Quaternion rhs) pure const if (op == "*") { 237 // (a1a2 − b1b2 − c1c2 − d1d2)w 238 auto _w = w * rhs.w - x * rhs.x - y * rhs.y - z * rhs.z; 239 // (a1b2 + b1a2 + c1d2 − d1c2)i 240 auto _i = w * rhs.x + x * rhs.w + y * rhs.z - z * rhs.y; 241 // (a1c2 − b1d2 + c1a2 + d1b2)j 242 auto _j = w * rhs.y - x * rhs.z + y * rhs.w + z * rhs.x; 243 // (a1d2 + b1c2 − c1b2 + d1a2)k 244 auto _k = w * rhs.z + x * rhs.y - y * rhs.x + z * rhs.w; 245 return Quaternion(_i, _j, _k, _w); 246 } 247 248 /** 249 * Obtain conjugate of a Quaternion 250 */ 251 @nogc Quaternion conj() pure const { 252 return Quaternion(-x, -y, -z, w); 253 } 254 255 /** 256 * It's a unitary Quaternion (length == 1) 257 */ 258 @property @nogc bool isUnit() pure const { 259 import std.math : approxEqual, abs; 260 261 return approxEqual(abs(this.sq_length - 1.0), 0); 262 } 263 264 /** 265 * Normalize this Quaternion 266 */ 267 @nogc void normalize() pure { 268 if (!isUnit()) { 269 const T l = 1 / this.length; 270 x = x * l; 271 y = y * l; 272 z = z * l; 273 w = w * l; 274 } 275 } 276 277 /** 278 * Apply a 3d rotation Quaternion over a 3d/4d Vector 279 */ 280 V rotate(V)(auto ref const V v) pure const if (isVector!V && V.dim >= 3) { 281 Quaternion resQua = Quaternion(v) * this.conj(); 282 resQua = this * resQua; 283 return V(resQua.coor); 284 } 285 286 // Misc ********************************************************************** 287 288 /** 289 * Calc the Axis and angle of rotation of this Quaternion 290 * Params: 291 * angle = Set to angle of rotation 292 * Returns: 293 * Axis of rotation vector 294 */ 295 @nogc Vector!(T, 3) toAxisAngle(out T angle) pure const 296 in { 297 assert(this.isUnit()); 298 } 299 body { 300 // TODO return a tuple with vector and angle 301 import std.math : acos, sqrt, approxEqual; 302 303 angle = 2 * acos(w); 304 if (approxEqual(angle, 0)) { 305 return Vector!(T, 3)(1, 0, 0); 306 } 307 auto inv_qw = 1 / sqrt(1 - w * w); 308 return Vector!(T, 3)(i * inv_qw, j * inv_qw, k * inv_qw); 309 } 310 311 /** 312 * Returns a Vector with euler's angles 313 * Returns: 314 * Vector with Euler angles of rotation in radians 315 * 316 * TODO: Improve using code from here to avoid requiring pre-normalize the quaternion 317 * http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/ 318 */ 319 @nogc Vector!(T, 3) toEuler() pure const 320 in { 321 assert(this.isUnit()); 322 } 323 body { 324 /* 325 roll = Rotation around X axis in radians 326 pitch = Rotation around Y axis in radians 327 yaw = Rotation around Z axis in radians 328 */ 329 import std.math : asin, atan2, abs, PI_2; 330 331 // roll (x-axis rotation) 332 auto sinr_cosp = 2 * (w * x + y * z); 333 auto cosr_cosp = 1 - 2 * (x * x + y * y); 334 auto roll = atan2(sinr_cosp, cosr_cosp); 335 336 // pitch (y-axis rotation) 337 T pitch = void; 338 auto sinp = 2 * (w * y - z * x); 339 if (sinp >= 1) { // North pole 340 pitch = PI_2; 341 } else if (sinp <= -1) { // South pole 342 pitch = -PI_2; 343 } else { 344 pitch = asin(sinp); 345 } 346 347 // yaw (z-axis rotation) 348 auto siny_cosp = 2 * (w * z + x * y); 349 auto cosy_cosp = 1 - 2 * (y * y + z * z); 350 auto yaw = atan2(siny_cosp, cosy_cosp); 351 352 return Vector!(T, 3)(roll, pitch, yaw); 353 } 354 355 /** 356 * Casting for convert between Quaternions 357 */ 358 @nogc Tout opCast(Tout)() pure const if (isQuaternion!(Tout)) { 359 import std.conv : to; 360 361 Tout newQ; 362 static if (is(typeof(newQ.x) == typeof(this.x))) { 363 foreach (i, s; coor) 364 newQ.coor[i] = s; 365 } else { 366 foreach (i, s; coor) 367 newQ.coor[i] = to!(typeof(newQ.x))(s); 368 } 369 return newQ; 370 } 371 372 /** 373 * Casting to rotation Matrix 374 */ 375 @nogc Tout opCast(Tout)() pure const if (isMatrix!(Tout) && Tout.dim >= 3) 376 in { 377 assert(this.isUnit()); 378 } 379 body { 380 auto x2 = x * x; 381 auto y2 = y * y; 382 auto z2 = z * z; 383 auto xy = x * y; 384 auto xz = x * z; 385 auto yz = y * z; 386 auto wx = w * x; 387 auto wy = w * y; 388 auto wz = w * z; 389 static if (Tout.dim == 4) { 390 return Tout([ 391 1.0L - 2.0L * (y2 + z2), 2.0L * (xy - wz), 2.0L * (xz + wy), 0.0L, 392 2.0L * (xy + wz), 1.0L - 2.0L * (x2 + z2), 2.0L * (yz - wx), 0.0L, 393 2.0L * (xz - wy), 2.0L * (yz + wx), 1.0L - 2.0L * (x2 + y2), 0.0L, 394 0.0L, 0.0L, 0.0L, 1.0L 395 ]); 396 } else { 397 return Tout([ 398 1.0L - 2.0L * (y2 + z2), 2.0L * (xy - wz), 2.0L * (xz + wy), 399 2.0L * (xy + wz), 1.0L - 2.0L * (x2 + z2), 2.0L * (yz - wx), 400 2.0L * (xz - wy), 2.0L * (yz + wx), 1.0L - 2.0L * (x2 + y2) 401 ]); 402 } 403 } 404 405 /** 406 * Returns a string representation of this Quaternion 407 */ 408 string toString() { 409 import std.conv : to; 410 try { 411 return "[" ~ to!string(x) ~ ", " ~ to!string(y) ~ ", " ~ to!string(z) ~ ", " ~ to!string(w) ~ "]"; 412 } catch (Exception ex) { 413 assert(false); // This never should happen 414 } 415 } 416 } 417 418 /** 419 * Say if a thing it's a Quaternion 420 */ 421 template isQuaternion(T) { 422 //immutable bool isQuaternion = is(T == Vector); 423 immutable bool isQuaternion = __traits(compiles, () { 424 T t; 425 static assert(T.dim == 4); 426 auto coor = t.coor; 427 auto x = t.x; 428 auto y = t.y; 429 auto z = t.z; 430 auto w = t.w; 431 T conj = t.conj(); 432 // TODO : Should test for methods ? 433 }); 434 } 435 436 unittest { 437 // Ctors. 438 auto q = Qua_r(1, 2, 3, 4); 439 assert(q.coor == [1, 2, 3, 4]); 440 real[] arr = [4.0, 3.0, 2.0, 1.0, 20.0, 30.0]; 441 q = Qua_r(arr); 442 arr[0] = 7.7; 443 assert(q.coor == [4, 3, 2, 1]); 444 q = Qua_r(Vec4r(1, 2, 3, 4)); 445 assert(q.coor == [1, 2, 3, 4]); 446 } 447 448 unittest { 449 auto q_AxisX = Qua_d(Vec3d.X_AXIS, 0); 450 assert(q_AxisX == Qua_d(0, 0, 0, 1)); // TODO Do more checks 451 assert(q_AxisX.isUnit()); 452 } 453 454 unittest { 455 import std.math : PI_2, PI_4; 456 import std.conv : to; 457 458 auto qRoll = Qua_d(PI_4, 0, 0); 459 assert(qRoll.isUnit()); 460 assert(qRoll.toEuler().approxEqual(Vec3d(PI_4, 0, 0))); 461 462 auto qPitch = Qua_d(0, -PI_4, 0); 463 assert(qPitch.isUnit()); 464 assert(qPitch.toEuler().approxEqual(Vec3d(0, -PI_4, 0)), qPitch.toEuler().toString()); 465 466 auto qYaw = Qua_d(0, 0, PI_2); 467 assert(qYaw.isUnit()); 468 assert(qYaw.approxEqual(Qua_d(0, 0, 0.707107, 0.707107)), qYaw.toString()); 469 assert(qYaw.toEuler().approxEqual(Vec3d(0, 0, PI_2)), qYaw.toEuler().toString()); 470 471 auto q_r = Qua_d(1, -PI_4, PI_4); 472 assert(q_r.isUnit()); 473 assert(q_r.toEuler().approxEqual(Vec3d(1, -PI_4, PI_4))); 474 475 // Example from http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/ 476 auto q = Qua_d(0.707107, 0, 0, 0.707107); 477 assert(q.isUnit()); 478 assert(q.toEuler().approxEqual(Vec3d(PI_2, 0, 0)), q.toEuler().toString()); 479 480 } 481 482 unittest { 483 auto q = Qua_f(10, 0, 0, 0); 484 assert(q.sq_length == 100); 485 q = Qua_f(1, 1, 1, 1); 486 assert(q.length == 2); 487 } 488 489 unittest { 490 auto q1 = Qua_d(1, 2, 3, 4); 491 auto qj = Qua_d(0, 1, 0, 0); 492 assert(q1 == q1); 493 assert(q1 != qj); 494 assert(qj != q1); 495 assert(q1.approxEqual(q1) && !q1.approxEqual(qj)); 496 } 497 498 unittest { 499 // Check addition / subtraction 500 const qk = Qua_d(0, 0, 1, 0); 501 const qj = Qua_d(0, 1, 0, 0); 502 const qi = Qua_d(1, 0, 0, 0); 503 const q1 = Qua_d(1, 1, 1, 1); 504 const qki = qk + qi; 505 const q1j = q1 + qj; 506 const q1k = q1 - qk; 507 assert(qki == Qua_d(1, 0, 1, 0)); 508 assert(q1j == Qua_d(1, 2, 1, 1)); 509 assert(q1k == Qua_d(1, 1, 0, 1)); 510 } 511 512 unittest { 513 // Check Hamilton multiplication 514 const qi = Qua_d(1, 0, 0, 0); 515 const q1 = Qua_d(1, 1, 1, 1); 516 const q1i = q1 * qi; 517 const qi1 = qi * q1; 518 assert(q1i == Qua_d(1, 1, -1, -1)); // TODO Check this values 519 assert(qi1 == Qua_d(1, -1, 1, -1)); 520 } 521 522 unittest { 523 auto q1 = Qua_d(1, 1, 1, 1); 524 const conj_q1 = q1.conj(); 525 assert(conj_q1 == Qua_d(-1, -1, -1, 1)); 526 const qr = q1 * q1.conj(); 527 assert(qr == Qua_d(0, 0, 0, 4)); 528 } 529 530 unittest { 531 auto q = Qua_r(10, 10, 1, 5); 532 assert(!q.isUnit); 533 q.normalize(); 534 assert(q.isUnit); 535 } 536 537 unittest { 538 import std.math : PI_2, PI_4; 539 540 // Check rotation with quaternion 541 auto qRoll = Qua_d(PI_4, 0, 0); 542 auto qPitch = Qua_d(0, -PI_4, 0); 543 auto qYaw = Qua_d(0, 0, PI_2); 544 auto v1 = Vec3d(1, 1, 1); 545 546 auto v4d = cast(Vec4d) v1; 547 auto v2 = qRoll.rotate(v1); 548 assert(v2.approxEqual(Vec3d(1, 0, 1.41421)), v2.toString()); 549 550 auto v3 = qPitch.rotate(v1); 551 assert(v3.approxEqual(Vec3d(0, 1, 1.41421)), v3.toString()); 552 553 auto v4 = qYaw.rotate(v1); 554 assert(v4.approxEqual(Vec3d(-1, 1, 1)), v4.toString()); 555 556 auto v5 = qPitch.rotate(v4d); 557 assert(v5.approxEqual(Vec4d(0, 1, 1.41421, 1)), v5.toString()); 558 } 559 560 unittest { 561 import std.math : approxEqual, PI_4; 562 563 // Check conversion to Axis/angle 564 auto qRoll = Qua_d(PI_4, 0, 0); 565 double angle; 566 auto axis = qRoll.toAxisAngle(angle); 567 assert(axis.approxEqual(Vec3d(1, 0, 0))); 568 assert(approxEqual(angle, PI_4)); 569 } 570 571 unittest { 572 import std.math : PI_4; 573 574 auto q = Qua_d(PI_4, PI_4, PI_4); 575 auto rot = q.toEuler(); 576 assert(rot.approxEqual(Vec3d(PI_4, PI_4, PI_4))); 577 } 578 579 unittest { 580 auto q = Qua_d(1, 1, 1, 1); 581 auto qf = cast(Qua_f) q; 582 auto qreal = cast(Qua_r) q; 583 auto qd = cast(Qua_d) qf; 584 585 assert(is(typeof(qf) == Qua_f)); 586 assert(is(typeof(qreal) == Qua_r)); 587 assert(is(typeof(qd) == Qua_d)); 588 for (auto i = 0; i < 4; i++) { 589 assert(qf[i] == 1); 590 assert(qreal[i] == 1); 591 assert(qd[i] == 1); 592 } 593 } 594 595 unittest { 596 import std.math : approxEqual, PI_2, PI_4; 597 598 // Generation of Rotation matrix 599 const qRoll = Qua_d(PI_4, 0, 0); 600 const qPitch = Qua_d(0, -PI_4, 0); 601 const qYaw = Qua_d(0, 0, PI_2); 602 auto m = cast(Mat4d) qRoll; 603 assert(approxEqual(m.determinant, 1)); 604 // dfmt off 605 assert(m.approxEqual(Mat4d([ 606 1, 0 , 0 , 0, 607 0, 0.707107, -0.707107, 0, 608 0, 0.707107, 0.707107, 0, 609 0, 0 , 0 , 1 610 ])), m.toString()); 611 // dfmt on 612 613 m = cast(Mat4d) qPitch; 614 assert(approxEqual(m.determinant, 1)); 615 // dfmt off 616 assert(m.approxEqual(Mat4d([ 617 0.707107, 0, -0.707107, 0, 618 0 , 1, 0, 0, 619 0.707107, 0, 0.707107, 0, 620 0 , 0, 0, 1 621 ])), m.toString()); 622 // dfmt on 623 624 m = cast(Mat4d) qYaw; 625 assert(approxEqual(m.determinant, 1)); 626 // dfmt off 627 assert(m.approxEqual(Mat4d([ 628 0, -1, 0, 0, 629 1, 0, 0, 0, 630 0, 0, 1, 0, 631 0, 0, 0, 1 632 ])), m.toString()); 633 // dfmt on 634 } 635 636 637 unittest { 638 auto q = Qua_f(1, 2, 3, 4); 639 assert(!isQuaternion!(int)); 640 assert(isQuaternion!(typeof(q))); 641 }