1 module sbylib.math.quaternion; 2 3 import sbylib.math.vector; 4 import sbylib.math.matrix; 5 import sbylib.math.angle; 6 import std.traits; 7 import std.conv : to; 8 import std.format : format; 9 import std.range : isInputRange, RangeElementType = ElementType; 10 11 alias quat = Quaternion!float; 12 13 /** 14 Quaternion type 15 */ 16 struct Quaternion(T) 17 if (__traits(isArithmetic, T)) 18 { 19 /** 20 Components of quaternion. 21 22 q = ix + jy + kz + w 23 */ 24 T x, y, z, w; 25 26 /** 27 Constructor from scalar components 28 */ 29 this(T x, T y, T z, T w) { 30 this.x = x; 31 this.y = y; 32 this.z = z; 33 this.w = w; 34 } 35 36 /** 37 Constructor from imaginary vector and real part 38 */ 39 this(Vector!(T,3) v, T w) { 40 this(v[0],v[1],v[2],w); 41 } 42 43 /** 44 Constructor by InputRange. 45 This quaternion's all elements are filled by given range. 46 47 Params: 48 elements = values which fills the quaternion 49 */ 50 this(Range)(Range r) 51 if (isInputRange!(Range) && is(RangeElementType!(Range) : T)) 52 { 53 import std.range : front, popFront, empty; 54 55 static foreach (i; 0..4) { 56 this[i] = r.front; 57 r.popFront; 58 } 59 assert(r.empty); 60 } 61 62 /** 63 Get the x-axis basis vector of the transformation of this quaternion 64 65 Returns: x basis vector 66 */ 67 Vector!(T,3) baseX() const { 68 return Vector!(T,3) 69 (1-2*(y*y+z*z), 70 2*(x*y+w*z), 71 2*(x*z-w*y)); 72 } 73 74 /** 75 Get the y-axis basis vector of the transformation of this quaternion 76 77 Returns: y basis vector 78 */ 79 Vector!(T,3) baseY() const { 80 return Vector!(T,3) 81 (2*(x*y-z*w), 82 1-2*(x*x+z*z), 83 2*(y*z+w*x)); 84 } 85 86 /** 87 Get the z-axis basis vector of the transformation of this quaternion 88 89 Returns: z basis vector 90 */ 91 Vector!(T,3) baseZ() const { 92 return Vector!(T,3) 93 (2*(x*z+w*y), 94 2*(y*z-w*x), 95 1-2*(x*x+y*y)); 96 } 97 98 unittest { 99 import std.algorithm : map; 100 import std.range : iota; 101 import std.random : uniform; 102 import sbylib.math.vector : approxEqual; 103 foreach (i; 0..100) { 104 const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))).normalize; 105 assert(approxEqual(q.rotate(vec3(1,0,0)), q.baseX)); 106 assert(approxEqual(q.rotate(vec3(0,1,0)), q.baseY)); 107 assert(approxEqual(q.rotate(vec3(0,0,1)), q.baseZ)); 108 } 109 } 110 111 /** 112 Unary operation for "+". 113 114 this is identity function 115 116 Returns: a quaternionly which is equal to this quaternion 117 */ 118 Quaternion!T opUnary(string op)() const 119 if (op == "+") 120 { 121 return this; 122 } 123 124 unittest { 125 import std.algorithm : map; 126 import std.range : iota; 127 import std.random : uniform; 128 129 foreach (i; 0..100) { 130 const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))); 131 assert(approxEqual(q * +1, +q)); 132 } 133 } 134 135 /** 136 Unary operation for "-". 137 138 Returns: a quaternion whose sign is inverted 139 */ 140 Quaternion!T opUnary(string op)() const 141 if (op == "-") 142 { 143 Quaternion!T result; 144 static foreach (i; 0..4) { 145 result[i] = -this[i]; 146 } 147 return result; 148 } 149 150 unittest { 151 import std.algorithm : map; 152 import std.range : iota; 153 import std.random : uniform; 154 155 foreach (i; 0..100) { 156 const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))); 157 assert(approxEqual(q * -1, -q)); 158 } 159 } 160 161 /** 162 Unary operation for "~". 163 This means conjugation. 164 165 Returns: a quaternion conjugate with this quaternion 166 */ 167 Quaternion!T opUnary(string op)() const 168 if (op == "~") 169 { 170 return Quaternion!T(-x,-y,-z,+w); 171 } 172 173 unittest { 174 import std.algorithm : map; 175 import std.range : iota; 176 import std.random : uniform; 177 178 foreach (i; 0..100) { 179 const q = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))); 180 181 assert(approxEqual(q * ~q, quat(vec3(0), lengthSq(q)))); 182 } 183 } 184 185 /** 186 Binary operation between quaternion for "+" or "-" 187 188 Params: 189 q = target quaternion 190 191 Returns: calculated quaternion 192 */ 193 Quaternion!(CommonType!(T,S)) opBinary(string op,S)(Quaternion!S q) const 194 if (op == "+" || op == "-") 195 { 196 Quaternion!(CommonType!(T,S)) result; 197 static foreach (i; 0..4) { 198 result[i] = mixin(format!q{ 199 this[i] %s q[i] 200 }(op)); 201 } 202 return result; 203 } 204 205 unittest { 206 import std.algorithm : map; 207 import std.range : iota; 208 import std.random : uniform; 209 import std.math : approxEqual; 210 211 foreach (k; 0..100) { 212 const a = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))); 213 const b = quat(iota(4).map!(_ => uniform(-1.0f, +1.0f))); 214 215 const c = a + b; 216 const d = a - b; 217 218 static foreach (i; 0..4) { 219 assert(approxEqual(a[i] + b[i], c[i])); 220 assert(approxEqual(a[i] - b[i], d[i])); 221 } 222 } 223 } 224 225 /** 226 Binary operation between quaternion for "*" 227 228 Params: 229 q = target quaternion 230 231 Returns: calculated quaternion 232 */ 233 Quaternion!(CommonType!(T,S)) opBinary(string op,S)(Quaternion!S q) const 234 if (op == "*") 235 { 236 Quaternion!(CommonType!(T,S)) result; 237 result.w = w * q.w - x * q.x - y * q.y - z * q.z; 238 result.x = w * q.x + x * q.w + y * q.z - z * q.y; 239 result.y = w * q.y - x * q.z + y * q.w + z * q.x; 240 result.z = w * q.z + x * q.y - y * q.x + z * q.w; 241 return result; 242 } 243 244 unittest { 245 import std.algorithm : map; 246 import std.range : iota; 247 import std.random : uniform; 248 249 foreach (i; 0..100) { 250 const a = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 251 const b = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 252 const c = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 253 assert(approxEqual((a * b) * c, a * (b * c))); 254 } 255 } 256 257 /** 258 Binary operation between scalar type for "*" or "/". 259 260 Params: 261 e = target scalar value 262 263 Returns: calculated quaternion 264 */ 265 Quaternion!(CommonType!(T,S)) opBinary(string op, S)(S e) const 266 if (__traits(isArithmetic, S) && (op == "*" || op == "/")) 267 { 268 Quaternion!(CommonType!(T,S)) result; 269 static foreach (i; 0..4) { 270 result[i] = mixin(format!q{ 271 this[i] %s e 272 }(op)); 273 } 274 return result; 275 } 276 277 unittest { 278 import std.algorithm : map; 279 import std.range : iota; 280 import std.random : uniform; 281 import std.math : approxEqual; 282 283 foreach (k; 0..100) { 284 const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 285 const s = uniform(-1.0f, +1.0f); 286 const q2 = q * s; 287 const q3 = q / s; 288 static foreach (i; 0..4) { 289 assert(approxEqual(q[i] * s, q2[i])); 290 assert(approxEqual(q[i] / s, q3[i])); 291 } 292 } 293 } 294 295 /** 296 Binary operation between scalar type for "*". 297 298 Params: 299 e = target scalar value 300 301 Returns: calculated quaternion 302 */ 303 Quaternion!(CommonType!(T,S)) opBinaryRight(string op, S)(S e) const 304 if (__traits(isArithmetic, S) && (op == "*")) 305 { 306 Quaternion!(CommonType!(T,S)) result; 307 static foreach (i; 0..4) { 308 result[i] = mixin(format!q{ 309 this[i] %s e 310 }(op)); 311 } 312 return result; 313 } 314 315 unittest { 316 import std.algorithm : map; 317 import std.range : iota; 318 import std.random : uniform; 319 import std.math : approxEqual; 320 321 foreach (k; 0..100) { 322 const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 323 const s = uniform(-1.0f, +1.0f); 324 const q2 = s * q; 325 static foreach (i; 0..4) { 326 assert(approxEqual(s * q[i], q2[i])); 327 } 328 } 329 } 330 331 /** 332 Operator assign for quaternion. 333 This function supports "+" or "-" operation. 334 335 Params: 336 q = target quaternion 337 */ 338 Quaternion opOpAssign(string op,S)(Quaternion!S q) 339 if (op == "+" || op == "-") 340 { 341 static foreach (i; 0..4) { 342 mixin(format!q{ 343 this[i] %s= q[i]; 344 }(op)); 345 } 346 return this; 347 } 348 349 unittest { 350 import std.algorithm : map; 351 import std.range : iota; 352 import std.random : uniform; 353 354 foreach (i; 0..100) { 355 auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 356 const q2 = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 357 const q3 = q + q2; 358 q += q2; 359 360 assert(approxEqual(q, q3)); 361 } 362 } 363 364 /** 365 Operator assign for quaternion. 366 This function supports "*" operation. 367 368 Params: 369 q = target quaternion 370 */ 371 Quaternion opOpAssign(string op,S)(Quaternion!(S) q) 372 if (op == "*") 373 { 374 Quaternion result = this * q; 375 static foreach (i; 0..4) { 376 this[i] = result[i]; 377 } 378 return this; 379 } 380 381 unittest { 382 import std.algorithm : map; 383 import std.range : iota; 384 import std.random : uniform; 385 foreach (i; 0..100) { 386 auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 387 const s = uniform(-1.0f, +1.0f); 388 const q2 = q * s; 389 q *= s; 390 391 assert(approxEqual(q, q2)); 392 } 393 } 394 395 /** 396 Operator assign for scalar type. 397 This function supports "*" or "/" operation. 398 399 Params: 400 q = target quaternion 401 */ 402 Quaternion opOpAssign(string op, S)(S e) 403 if (__traits(isArithmetic, S) && (op == "*" || op == "/")) 404 { 405 static foreach (i; 0..4) { 406 mixin(format!q{ 407 this[i] %s= e; 408 }(op)); 409 } 410 return this; 411 } 412 413 unittest { 414 import std.algorithm : map; 415 import std.range : iota; 416 import std.random : uniform; 417 418 foreach (i; 0..100) { 419 auto q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 420 const s = uniform(-1.0f, +1.0f); 421 const q2 = q * s; 422 q *= s; 423 424 assert(approxEqual(q, q2)); 425 } 426 } 427 428 /** 429 Indexing operation. 430 431 Returns: i-th element of this quaternion 432 */ 433 T opIndex(size_t i) const 434 in (i < 4) 435 { 436 final switch (i) { 437 case 0: return x; 438 case 1: return y; 439 case 2: return z; 440 case 3: return w; 441 } 442 } 443 444 unittest { 445 import std.algorithm : map; 446 import std.range : iota; 447 import std.random : uniform; 448 import std.array : array; 449 450 foreach (k; 0..100) { 451 float[4] v; 452 static foreach (i; 0..4) { 453 v[i] = uniform(-1.0f, +1.0f); 454 } 455 const q = quat(v.array); 456 457 static foreach (i; 0..4) { 458 assert(v[i] == q[i]); 459 } 460 } 461 } 462 463 /** 464 Indexing operation. 465 466 Returns: i-th element of this quaternion 467 */ 468 ref T opIndex(size_t i) 469 in (i < 4) 470 { 471 final switch (i) { 472 case 0: return x; 473 case 1: return y; 474 case 2: return z; 475 case 3: return w; 476 } 477 } 478 479 unittest { 480 import std.algorithm : map; 481 import std.range : iota; 482 import std.random : uniform; 483 484 foreach (k; 0..100) { 485 quat q; 486 487 static foreach (i; 0..4) {{ 488 const v = uniform(-1.0f, +1.0f); 489 q[i] = v; 490 assert(q[i] == v); 491 }} 492 } 493 } 494 495 string toString() const { 496 return format!"q(%s, %s, %s, %s)"(x, y, z, w); 497 } 498 499 /** 500 Get raw array data. 501 502 Returns: raw array data 503 */ 504 T[4] array() const { 505 return [x,y,z,w]; 506 } 507 508 unittest { 509 import std.range : iota; 510 const q = quat(iota(4)); 511 assert(q.array == [0,1,2,3]); 512 } 513 514 Vector!(T,3) axis() @property const { 515 return Vector!(T,3)(x,y,z); 516 } 517 518 unittest { 519 const q = quat(1,2,3,4); 520 assert(q.axis == vec3(1,2,3)); 521 } 522 523 void axis(const Vector!(T,3) axis) @property { 524 this.x = axis[0]; 525 this.y = axis[1]; 526 this.z = axis[2]; 527 } 528 529 unittest { 530 quat q; 531 q.axis = vec3(1,2,3); 532 assert(q.axis == vec3(1,2,3)); 533 } 534 535 /** 536 Returns rotation quaternion decided by its rotation axis and angle. 537 538 Params: 539 axis = rotation axis vector, which must be normalized 540 angle = rotation angle 541 542 Returns: rotation quaternion 543 */ 544 static Quaternion axisAngle(const Vector!(T,3) axis, const Angle rad) { 545 return Quaternion(axis * sin(rad/2), cos(rad/2)); 546 } 547 548 unittest { 549 import std.algorithm : map; 550 import std.random : uniform; 551 import std.range : iota; 552 import std.math : abs, approxEqual; 553 import sbylib.math.vector : dot, normalize, ortho, length; 554 555 foreach (i; 0..100) { 556 const point = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))); 557 const axis = point.ortho.normalize; 558 const angle = uniform(0,180.0f).deg; 559 const r = mat3.axisAngle(axis, angle); 560 const point2 = r * point; 561 assert(approxEqual(dot(point, point2), length(point) * length(point2) * cos(angle))); 562 } 563 } 564 565 /** 566 Returns rotation quaternion which converts a vector to another vector. 567 568 Params: 569 from = before vector, which must be normalized 570 to = after vector, which must be normalized 571 572 Returns: rotation quaternion 573 */ 574 static Quaternion!T rotFromTo(const Vector!(T,3) from, const Vector!(T,3) to) { 575 import sbylib.math.vector : vnormalize = normalize; 576 import std.math : sgn; 577 578 const c = dot(from, to); 579 const v = vnormalize(cross(from, to)); 580 if (v.hasNaN) { 581 return Quaternion!T(0,0,0,sgn(dot(from,to))); 582 } 583 return axisAngle(v, c.acos); 584 } 585 586 unittest { 587 import std.algorithm : map; 588 import std.random : uniform; 589 import std.range : iota; 590 import sbylib.math.vector : approxEqual, normalize; 591 592 foreach (i; 0..100) { 593 const a = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 594 const b = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 595 const c = rotFromTo(a,b).rotate(a); 596 assert(approxEqual(b,c)); 597 } 598 } 599 } 600 601 /** 602 Returns length of a quaternion. 603 604 Params: 605 q = target quaternion 606 607 Returns: length of the target quaternion 608 */ 609 T lengthSq(T)(const Quaternion!T q) { 610 T sum = 0; 611 static foreach (i; 0..4) { 612 sum += q[i] * q[i]; 613 } 614 return sum; 615 } 616 617 /** 618 Returns length of a quaternion. 619 620 Params: 621 q = target quaternion 622 623 Returns: length of the target quaternion 624 */ 625 T length(T)(const Quaternion!T q) 626 if (__traits(isFloating, T)) 627 { 628 import std.math : sqrt; 629 return sqrt(lengthSq(q)); 630 } 631 632 /** 633 Returns a quaternion whose length 1 from target quaternion. 634 635 Params: 636 q = target quaternion 637 638 Returns: normalized target quaternion 639 */ 640 Quaternion!T normalize(T)(const Quaternion!T q) 641 if (__traits(isFloating, T)) 642 { 643 return q / length(q); 644 } 645 646 /** 647 Returns a quaternion whose length 1 from target vector. 648 When the length is 0, returns zero quaternion. 649 650 Params: 651 v = target quaternion 652 653 Returns: normalized target quaternion if the length is greater than 0, otherwise zero vector 654 */ 655 Quaternion!(T) safeNormalize(T)(const Quaternion!(T) q) 656 if (__traits(isFloating, T)) 657 { 658 const l = length(q); 659 if (l == 0) return q; 660 return q / l; 661 } 662 663 unittest { 664 import std.algorithm : map; 665 import std.random : uniform; 666 import std.range : iota; 667 import std.math : approxEqual; 668 669 foreach (i; 0..100) { 670 const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))); 671 assert(q == quat(0,0,0,0) || safeNormalize(q).length.approxEqual(1)); 672 } 673 } 674 675 /** 676 Rotate a vector by a quaternion. 677 678 Params: 679 vec = rotation target vector 680 q = rotation quaternion 681 682 Returns: rotated vector 683 */ 684 Vector!(T,3) rotate(T)(const Quaternion!(T) q, Vector!(T,3) vec) { 685 return (q * Quaternion!T(vec, 0) * ~q).axis; 686 } 687 688 unittest { 689 import std.algorithm : map; 690 import std.random : uniform; 691 import std.range : iota; 692 import sbylib.math.vector : normalize, length; 693 import std.math : approxEqual; 694 695 foreach (i; 0..100) { 696 const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))); 697 const axis = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 698 const angle = uniform(0,180.0f).deg; 699 assert(approxEqual(p.length, quat.axisAngle(axis, angle).rotate(p).length)); 700 } 701 } 702 703 /** 704 Interpolate two quaternions spherically. 705 706 Params: 707 q1 = target quaternion 708 q2 = target quaternion 709 t = interpolation coefficient (0 <= t <= 1) 710 711 Returns: Interpolated quaternion 712 */ 713 Quaternion!(T) slerp(T)(const Quaternion!(T) q1, const Quaternion!(T) q2, T t) { 714 import std.math : approxEqual; 715 716 const c = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; 717 if (c < 0) return slerp(q1, -q2, t); 718 if (c.approxEqual(1, float.epsilon)) return q1; 719 const angle = acos(c); 720 const s = sin(angle); 721 722 return sin((1-t)*angle) / s * q1 + sin(t*angle) / s * q2; 723 } 724 725 unittest { 726 import std.algorithm : map; 727 import std.random : uniform; 728 import std.range : iota; 729 import sbylib.math.vector : normalize, length; 730 import std.math : approxEqual; 731 732 foreach (i; 0..100) { 733 const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))); 734 735 const axis1 = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 736 const angle1 = uniform(0,180.0f).deg; 737 const q1 = quat.axisAngle(axis1, angle1); 738 739 const axis2 = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 740 const angle2 = uniform(0,180.0f).deg; 741 const q2 = quat.axisAngle(axis2, angle2); 742 743 foreach (j; 0..100) { 744 const t = j * 0.01; 745 const q = slerp(q1, q2, t); 746 assert(approxEqual(p.length, q.rotate(p).length)); 747 } 748 749 } 750 } 751 752 /** 753 Converts a quaternion into matrix 3x3. 754 755 Params: 756 q = target quaternion 757 758 Returns: converted matrix 759 */ 760 Matrix!(T,3,3) toMatrix3(T)(const Quaternion!T q) { 761 with (q) { 762 return Matrix!(T,3,3)( 763 1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+w*y), 764 2*(x*y+w*z), 1-2*(x*x+z*z), 2*(y*z-w*x), 765 2*(x*z-w*y), 2*(y*z+w*x), 1-2*(x*x+y*y)); 766 } 767 } 768 769 unittest { 770 import std.algorithm : map; 771 import std.random : uniform; 772 import std.range : iota; 773 import sbylib.math.vector : approxEqual; 774 775 foreach (i; 0..100) { 776 const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))); 777 const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 778 assert(approxEqual(q.toMatrix3() * p, q.rotate(p))); 779 } 780 } 781 782 /** 783 Converts a quaternion into matrix 4x4. 784 785 Params: 786 q = target quaternion 787 788 Returns: converted matrix 789 */ 790 Matrix!(T,4,4) toMatrix4(T)(const Quaternion!T q) { 791 import sbylib.math.matrix : toMatrix4; 792 return q.toMatrix3().toMatrix4(); 793 } 794 795 unittest { 796 import std.algorithm : map; 797 import std.random : uniform; 798 import std.range : iota; 799 import sbylib.math.vector : approxEqual; 800 801 foreach (i; 0..100) { 802 const p = vec3(3.iota.map!(_ => uniform(-1.0f, +1.0f))); 803 const q = quat(4.iota.map!(_ => uniform(-1.0f, +1.0f))).normalize; 804 assert(approxEqual((q.toMatrix4() * vec4(p,1)).xyz, q.rotate(p))); 805 } 806 } 807 808 /** 809 Returns true if the distance between the given two point is less than eps. 810 811 Params: 812 a = target point 813 b = target point 814 eps = criteria of approximation 815 816 Returns: true if 2 vectors are approximately equal. 817 */ 818 bool approxEqual(T)(const Quaternion!(T) a, const Quaternion!(T) b, T eps = 1e-5) { 819 return length(a-b) < eps; 820 }