1 /// Affine transforms module 2 module gfx.math.transform; 3 4 import gfx.math.mat; 5 import gfx.math.vec; 6 import std.meta : allSatisfy; 7 import std.traits : CommonType, isFloatingPoint, isNumeric; 8 9 @safe pure: 10 11 /// Build a translation matrix. 12 auto translation(X, Y)(in X x, in Y y) 13 { 14 alias ResMat = Mat3x3!(CommonType!(X, Y)); 15 return ResMat( 16 1, 0, x, 17 0, 1, y, 18 0, 0, 1, 19 ); 20 } 21 22 /// ditto 23 auto translation(V)(in V v) if (isVec2!V) 24 { 25 return Mat3x3!(V.Component) ( 26 1, 0, v.x, 27 0, 1, v.y, 28 0, 0, 1, 29 ); 30 } 31 32 /// ditto 33 auto translation(X, Y, Z)(in X x, in Y y, in Z z) 34 { 35 alias ResMat = Mat4x4!(CommonType!(X, Y, Z)); 36 return ResMat( 37 1, 0, 0, x, 38 0, 1, 0, y, 39 0, 0, 1, z, 40 0, 0, 0, 1, 41 ); 42 } 43 44 /// ditto 45 auto translation(V)(in V v) if (isVec3!V) 46 { 47 return Mat4x4!(V.Component) ( 48 1, 0, 0, v.x, 49 0, 1, 0, v.y, 50 0, 0, 1, v.z, 51 0, 0, 0, 1, 52 ); 53 } 54 55 unittest 56 { 57 import gfx.math.approx : approxUlp; 58 59 immutable v2 = dvec(4, 6); 60 assert( approxUlp(translation(2, 7) * dvec(v2, 1), dvec(6, 13, 1)) ); 61 62 immutable v3 = dvec(5, 6, 7); 63 assert( approxUlp(translation(7, 4, 1) * dvec(v3, 1), dvec(12, 10, 8, 1)) ); 64 } 65 66 /// Append a translation transform inferred from arguments to the matrix m. 67 /// This is equivalent to the expression $(D_CODE translation(...) * m) 68 /// but actually save computation by knowing 69 /// where the ones and zeros are in a pure translation matrix. 70 M translate(M, X, Y)(in M m, in X x, in Y y) 71 if (isMat!(3, 3, M) && allSatisfy!(isNumeric, X, Y)) 72 { 73 return M ( 74 // row 1 75 m[0, 0] + m[2, 0] * x, 76 m[0, 1] + m[2, 1] * x, 77 m[0, 2] + m[2, 2] * x, 78 // row 2 79 m[1, 0] + m[2, 0] * y, 80 m[1, 1] + m[2, 1] * y, 81 m[1, 2] + m[2, 2] * y, 82 // row 3 83 m[2, 0], m[2, 1], m[2, 2] 84 ); 85 } 86 87 /// ditto 88 M translate(M, X, Y)(in M m, in X x, in Y y) 89 if (isMat!(2, 3, M) && allSatisfy!(isNumeric, X, Y)) 90 { 91 return M ( 92 // row 1 93 m[0, 0], 94 m[0, 1], 95 m[0, 2] + x, 96 // row 2 97 m[1, 0], 98 m[1, 1], 99 m[1, 2] + y, 100 ); 101 } 102 103 /// ditto 104 M translate(M, V)(in M m, in V v) 105 if ((isMat!(2, 3, M) || isMat!(3, 3, M)) && isVec!(2, V)) 106 { 107 return translate (m, v[0] ,v[1]); 108 } 109 110 /// ditto 111 M translate (M, X, Y, Z)(in M m, in X x, in Y y, in Z z) 112 if (isMat!(4, 4, M) && allSatisfy!(isNumeric, X, Y, Z)) 113 { 114 return M ( 115 // row 1 116 m[0, 0] + m[3, 0] * x, 117 m[0, 1] + m[3, 1] * x, 118 m[0, 2] + m[3, 2] * x, 119 m[0, 3] + m[3, 3] * x, 120 // row 2 121 m[1, 0] + m[3, 0] * y, 122 m[1, 1] + m[3, 1] * y, 123 m[1, 2] + m[3, 2] * y, 124 m[1, 3] + m[3, 3] * y, 125 // row 3 126 m[2, 0] + m[3, 0] * z, 127 m[2, 1] + m[3, 1] * z, 128 m[2, 2] + m[3, 2] * z, 129 m[2, 3] + m[3, 3] * z, 130 // row 4 131 m[3, 0], m[3, 1], m[3, 2], m[3, 3] 132 ); 133 } 134 135 /// ditto 136 M translate (M, X, Y, Z)(in M m, in X x, in Y y, in Z z) 137 if (isMat!(3, 4, M) && allSatisfy!(isNumeric, X, Y, Z)) 138 { 139 return M ( 140 // row 1 141 m[0, 0] + m[3, 0] * x, 142 m[0, 1] + m[3, 1] * x, 143 m[0, 2] + m[3, 2] * x, 144 m[0, 3] + m[3, 3] * x, 145 // row 2 146 m[1, 0] + m[3, 0] * y, 147 m[1, 1] + m[3, 1] * y, 148 m[1, 2] + m[3, 2] * y, 149 m[1, 3] + m[3, 3] * y, 150 // row 3 151 m[2, 0] + m[3, 0] * z, 152 m[2, 1] + m[3, 1] * z, 153 m[2, 2] + m[3, 2] * z, 154 m[2, 3] + m[3, 3] * z, 155 ); 156 } 157 158 /// ditto 159 M translate(M, V)(in M m, in V v) 160 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isVec!(3, V)) 161 { 162 return translate (m, v[0] ,v[1], v[2]); 163 } 164 165 /// 166 unittest 167 { 168 import gfx.math.approx : approxUlp; 169 170 immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 ); 171 172 immutable expected = translation(7, 12) * m; // full multiplication 173 immutable result = translate(m, 7, 12); // simplified multiplication 174 175 assert (approxUlp(expected, result)); 176 } 177 /// 178 unittest 179 { 180 import gfx.math.approx : approxUlp; 181 182 immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ); 183 184 immutable expected = translation(7, 12, 18) * m; // full multiplication 185 immutable result = translate(m, 7, 12, 18); // simplified multiplication 186 187 assert (approxUlp(expected, result)); 188 } 189 190 191 /// Build a pure 3d rotation matrix with angle in radians 192 auto rotationPure(T, V) (in T angle, in V axis) 193 if (isFloatingPoint!T && isVec!(3, V)) 194 { 195 static assert ( 196 isFloatingPoint!(V.Component), 197 "rotationPure must be passed a floating point axis" 198 ); 199 import std.math : cos, sin; 200 const u = normalize(axis); 201 const c = cos(angle); 202 const s = sin(angle); 203 const c1 = 1 - c; 204 return Mat3x3!(V.Component) ( 205 // row 1 206 c1 * u.x * u.x + c, 207 c1 * u.x * u.y - s * u.z, 208 c1 * u.x * u.z + s * u.y, 209 // row 2 210 c1 * u.y * u.x + s * u.z, 211 c1 * u.y * u.y + c, 212 c1 * u.y * u.z - s * u.x, 213 // row 3 214 c1 * u.z * u.x - s * u.y, 215 c1 * u.z * u.y + s * u.x, 216 c1 * u.z * u.z + c, 217 ); 218 } 219 220 /// Build a rotation matrix. 221 /// angle in radians. 222 Mat3x3!T rotation(T) (in T angle) if (isFloatingPoint!T) 223 { 224 import std.math : cos, sin; 225 const c = cast(T) cos(angle); 226 const s = cast(T) sin(angle); 227 return Mat3x3!T ( 228 c, -s, 0, 229 s, c, 0, 230 0, 0, 1 231 ); 232 } 233 234 /// ditto 235 auto rotation(T, V) (in T angle, in V axis) 236 if (isVec!(3, V) && isFloatingPoint!T) 237 { 238 static assert ( 239 isFloatingPoint!(V.Component), 240 "rotation must be passed a floating point axis" 241 ); 242 const m = rotationPure(angle, axis); 243 return mat( 244 vec(m[0], 0), 245 vec(m[1], 0), 246 vec(m[2], 0), 247 vec(0, 0, 0, 1) 248 ); 249 } 250 251 /// ditto 252 auto rotation(T) (in T angle, in T x, in T y, in T z) 253 if (isFloatingPoint!T) 254 { 255 return rotation(angle, vec(x, y, z)); 256 } 257 258 /// Append a rotation transform inferred from arguments to the matrix m. 259 /// This is equivalent to the expression $(D_CODE rotation(...) * m) 260 /// but actually save computation by knowing 261 /// where the ones and zeros are in a pure rotation matrix. 262 M rotate (M, T) (in M m, in T angle) 263 if (isMat!(3, 3, M) && isFloatingPoint!T) 264 { 265 import std.math : cos, sin; 266 immutable c = cos(angle); 267 immutable s = sin(angle); 268 return M ( 269 // row 1 270 c * m[0, 0] - s * m[1, 0], 271 c * m[0, 1] - s * m[1, 1], 272 c * m[0, 2] - s * m[1, 2], 273 // row 2 274 s * m[0, 0] + c * m[1, 0], 275 s * m[0, 1] + c * m[1, 1], 276 s * m[0, 2] + c * m[1, 2], 277 // row 3 278 m[2, 0], m[2, 1], m[2, 2] 279 ); 280 } 281 282 /// ditto 283 M rotate (M, T) (in M m, in T angle) 284 if (isMat!(2, 3, M) && isFloatingPoint!T) 285 { 286 import std.math : cos, sin; 287 immutable c = cos(angle); 288 immutable s = sin(angle); 289 return M ( 290 // row 1 291 c * m[0, 0] - s * m[1, 0], 292 c * m[0, 1] - s * m[1, 1], 293 c * m[0, 2] - s * m[1, 2], 294 // row 2 295 s * m[0, 0] + c * m[1, 0], 296 s * m[0, 1] + c * m[1, 1], 297 s * m[0, 2] + c * m[1, 2] 298 ); 299 } 300 301 /// ditto 302 M rotate (M, T, V) (in M m, in T angle, in V axis) 303 if (isMat!(4, 4, M) && isFloatingPoint!T && isVec!(3, V)) 304 { 305 static assert ( 306 isFloatingPoint!(V.Component), 307 "rotate must be passed a floating point axis" 308 ); 309 immutable r = rotationPure(angle, axis); 310 return M ( 311 // row 1 312 r[0, 0]*m[0, 0] + r[0, 1]*m[1, 0] + r[0, 2]*m[2, 0], 313 r[0, 0]*m[0, 1] + r[0, 1]*m[1, 1] + r[0, 2]*m[2, 1], 314 r[0, 0]*m[0, 2] + r[0, 1]*m[1, 2] + r[0, 2]*m[2, 2], 315 r[0, 0]*m[0, 3] + r[0, 1]*m[1, 3] + r[0, 2]*m[2, 3], 316 // row 2 317 r[1, 0]*m[0, 0] + r[1, 1]*m[1, 0] + r[1, 2]*m[2, 0], 318 r[1, 0]*m[0, 1] + r[1, 1]*m[1, 1] + r[1, 2]*m[2, 1], 319 r[1, 0]*m[0, 2] + r[1, 1]*m[1, 2] + r[1, 2]*m[2, 2], 320 r[1, 0]*m[0, 3] + r[1, 1]*m[1, 3] + r[1, 2]*m[2, 3], 321 // row 3 322 r[2, 0]*m[0, 0] + r[2, 1]*m[1, 0] + r[2, 2]*m[2, 0], 323 r[2, 0]*m[0, 1] + r[2, 1]*m[1, 1] + r[2, 2]*m[2, 1], 324 r[2, 0]*m[0, 2] + r[2, 1]*m[1, 2] + r[2, 2]*m[2, 2], 325 r[2, 0]*m[0, 3] + r[2, 1]*m[1, 3] + r[2, 2]*m[2, 3], 326 // row 4 327 m[3, 0], m[3, 1], m[3, 2], m[3, 3] 328 ); 329 } 330 331 /// ditto 332 M rotate (M, T, V) (in M m, in T angle, in V axis) 333 if (isMat!(3, 4, M) && isVec!(3, V) && isFloatingPoint!T) 334 { 335 static assert ( 336 isFloatingPoint!(V.Component), 337 "rotate must be passed a floating point axis" 338 ); 339 const r = rotationPure(angle, axis); 340 return M ( 341 // row 1 342 r[0, 0]*m[0, 0] + r[0, 1]*m[1, 0] + r[0, 2]*m[2, 0], 343 r[0, 0]*m[0, 1] + r[0, 1]*m[1, 1] + r[0, 2]*m[2, 1], 344 r[0, 0]*m[0, 2] + r[0, 1]*m[1, 2] + r[0, 2]*m[2, 2], 345 r[0, 0]*m[0, 3] + r[0, 1]*m[1, 3] + r[0, 2]*m[2, 3], 346 // row 2 347 r[1, 0]*m[0, 0] + r[1, 1]*m[1, 0] + r[1, 2]*m[2, 0], 348 r[1, 0]*m[0, 1] + r[1, 1]*m[1, 1] + r[1, 2]*m[2, 1], 349 r[1, 0]*m[0, 2] + r[1, 1]*m[1, 2] + r[1, 2]*m[2, 2], 350 r[1, 0]*m[0, 3] + r[1, 1]*m[1, 3] + r[1, 2]*m[2, 3], 351 // row 3 352 r[2, 0]*m[0, 0] + r[2, 1]*m[1, 0] + r[2, 2]*m[2, 0], 353 r[2, 0]*m[0, 1] + r[2, 1]*m[1, 1] + r[2, 2]*m[2, 1], 354 r[2, 0]*m[0, 2] + r[2, 1]*m[1, 2] + r[2, 2]*m[2, 2], 355 r[2, 0]*m[0, 3] + r[2, 1]*m[1, 3] + r[2, 2]*m[2, 3], 356 ); 357 } 358 359 /// ditto 360 M rotate (M, T) (in M m, in T angle, in T x, in T y, in T z) 361 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isFloatingPoint!T) 362 { 363 return rotate(m, angle, vec(x, y, z)); 364 } 365 366 /// 367 unittest 368 { 369 import gfx.math.approx : approxUlp; 370 import std.math : PI; 371 372 immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 ); 373 374 immutable expected = rotation!double(PI) * m; // full multiplication 375 immutable result = rotate(m, PI); // simplified multiplication 376 377 assert (approxUlp(expected, result)); 378 } 379 /// 380 unittest 381 { 382 import gfx.math.approx : approxUlp; 383 import std.math : PI; 384 385 immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ); 386 immutable angle = PI; 387 immutable v = fvec(3, 4, 5); 388 389 immutable expected = rotation(angle, v) * m; // full multiplication 390 immutable result = rotate(m, angle, v); // simplified multiplication 391 392 assert (approxUlp(expected, result)); 393 } 394 395 396 /// Build a scale matrix. 397 Mat3!(CommonType!(X, Y)) scale(X, Y) (in X x, in Y y) 398 if (allSatisfy!(isNumeric, X, Y)) 399 { 400 return Mat3!(CommonType!(X, Y))( 401 x, 0, 0, 402 0, y, 0, 403 0, 0, 1, 404 ); 405 } 406 407 /// ditto 408 auto scale(V) (in V v) if (isVec2!V) 409 { 410 return Mat3!(V.Component) ( 411 v.x, 0, 0, 412 0, v.y, 0, 413 0, 0, 1, 414 ); 415 } 416 417 /// ditto 418 Mat4!(CommonType!(X, Y, Z)) scale (X, Y, Z) (in X x, in Y y, in Z z) 419 if (allSatisfy!(isNumeric, X, Y, Z)) 420 { 421 return Mat4!(CommonType!(X, Y, Z))( 422 x, 0, 0, 0, 423 0, y, 0, 0, 424 0, 0, z, 0, 425 0, 0, 0, 1, 426 ); 427 } 428 429 /// ditto 430 auto scale(V) (in V v) if (isVec3!V) 431 { 432 return Mat4!(V.Component) ( 433 v.x, 0, 0, 0, 434 0, v.y, 0, 0, 435 0, 0, v.z, 0, 436 0, 0, 0, 1, 437 ); 438 } 439 440 /// Append a scale transform inferred from arguments to the matrix m. 441 /// This is equivalent to the expression $(D_CODE scale(...) * m) 442 /// but actually save computation by knowing 443 /// where the ones and zeros are in a pure scale matrix. 444 M scale (M, X, Y)(in M m, in X x, in Y y) 445 if (isMat!(3, 3, M) && allSatisfy!(isNumeric, X, Y)) 446 { 447 return M ( 448 // row 1 449 m[0, 0] * x, 450 m[0, 1] * x, 451 m[0, 2] * x, 452 // row 2 453 m[1, 0] * y, 454 m[1, 1] * y, 455 m[1, 2] * y, 456 // row 3 457 m[2, 0], m[2, 1], m[2, 2] 458 ); 459 } 460 461 /// ditto 462 M scale (M, X, Y)(in M m, in X x, in Y y) 463 if (isMat!(2, 3, M) && allSatisfy!(isNumeric, X, Y)) 464 { 465 return M ( 466 // row 1 467 m[0, 0] * x, 468 m[0, 1] * x, 469 m[0, 2] * x, 470 // row 2 471 m[1, 0] * y, 472 m[1, 1] * y, 473 m[1, 2] * y, 474 ); 475 } 476 477 /// ditto 478 M scale (M, V)(in M m, in V v) 479 if ((isMat!(2, 3, M) || isMat!(3, 3, M)) && isVec!(2, V)) 480 { 481 return scale(m, v[0], v[1]); 482 } 483 484 /// ditto 485 M scale (M, X, Y, Z)(in M m, in X x, in Y y, in Z z) 486 if (isMat!(4, 4, M) && allSatisfy!(isNumeric, X, Y, Z)) 487 { 488 return M ( 489 // row 1 490 m[0, 0] * x, 491 m[0, 1] * x, 492 m[0, 2] * x, 493 m[0, 3] * x, 494 // row 2 495 m[1, 0] * y, 496 m[1, 1] * y, 497 m[1, 2] * y, 498 m[1, 3] * y, 499 // row 3 500 m[2, 0] * z, 501 m[2, 1] * z, 502 m[2, 2] * z, 503 m[2, 3] * z, 504 // row 4 505 m[3, 0], m[3, 1], m[3, 2], m[3, 3] 506 ); 507 } 508 509 /// ditto 510 M scale (M, X, Y, Z)(in M m, in X x, in Y y, in Z z) 511 if (isMat!(3, 4, M) && allSatisfy!(isNumeric, X, Y, Z)) 512 { 513 return M ( 514 // row 1 515 m[0, 0] * x, 516 m[0, 1] * x, 517 m[0, 2] * x, 518 m[0, 3] * x, 519 // row 2 520 m[1, 0] * y, 521 m[1, 1] * y, 522 m[1, 2] * y, 523 m[1, 3] * y, 524 // row 3 525 m[2, 0] * z, 526 m[2, 1] * z, 527 m[2, 2] * z, 528 m[2, 3] * z, 529 ); 530 } 531 532 /// ditto 533 M scale (M, V)(in M m, in V v) 534 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isVec!(3, V)) 535 { 536 return scale(m, v[0], v[1], v[2]); 537 } 538 539 540 /// 541 unittest 542 { 543 import gfx.math.approx : approxUlp; 544 545 immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 ); 546 547 immutable expected = scale(4, 5) * m; // full multiplication 548 immutable result = scale(m, 4, 5); // simplified multiplication 549 550 assert (approxUlp(expected, result)); 551 } 552 /// 553 unittest 554 { 555 import gfx.math.approx : approxUlp; 556 557 immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ); 558 559 immutable expected = scale(4, 5, 6) * m; // full multiplication 560 immutable result = scale(m, 4, 5, 6); // simplified multiplication 561 562 assert (approxUlp(expected, result)); 563 } 564 565 /// Affine matrix multiplication. 566 /// 567 /// Perform a multiplication of two 2x3 or two 3x4 matrices as if their last row 568 /// were [ 0, 0, 1] or [ 0, 0, 0, 1 ]. 569 /// Allows manipulation of smaller matrices when only affine transformation are 570 /// required. 571 /// Note: translation, rotation, scaling, shearing and any combination of those 572 /// are affine transforms. Projection is not affine. 573 /// I.e. for 2D, an affine transform is held in 2x3 matrix, 2x2 for rotation and 574 /// scaling and an additional column for translation. 575 /// The same applies with 3D and 3x4 matrices. 576 /// 577 /// This is not implemented as an operator as it is not a mathematical 578 /// operation (ml.columnLength != mr.rowLength). 579 auto affineMult(ML, MR)(in ML ml, in MR mr) 580 if (areMat!(2, 3, ML, MR) || areMat!(3, 4, ML, MR)) 581 { 582 alias Comp = CommonType!(ML.Component, MR.Component); 583 enum rowLength = ML.rowLength; 584 enum columnLength = ML.columnLength; 585 alias ResMat = Mat!(Comp, rowLength, columnLength); 586 587 ResMat res = void; 588 static foreach(r; 0 .. rowLength) 589 { 590 static foreach (c; 0 .. columnLength) 591 {{ 592 Comp resComp = 0; 593 static foreach (rc; 0 .. rowLength) // that is columnCount-1 594 { 595 resComp += ml[r, rc] * mr[rc, c]; 596 } 597 static if (c == columnLength-1) 598 { 599 resComp += ml[r, c]; // that is the last one in the last row 600 } 601 res[r, c] = resComp; 602 }} 603 } 604 return res; 605 } 606 607 /// 608 unittest 609 { 610 import gfx.math.approx : approxUlp; 611 612 /// full matrices 613 immutable fm1 = FMat3x3( 614 1, 2, 3, 615 4, 5, 6, 616 0, 0, 1 617 ); 618 immutable fm2 = DMat3x3( 619 7, 8, 9, 620 10, 11, 12, 621 0, 0, 1 622 ); 623 624 /// affine matrices 625 immutable am1 = FMat2x3( 626 1, 2, 3, 627 4, 5, 6, 628 ); 629 immutable am2 = DMat2x3( 630 7, 8, 9, 631 10, 11, 12, 632 ); 633 634 immutable expected = (fm1 * fm2).slice!(0, 2, 0, 3); 635 immutable result = affineMult(am1, am2); 636 assert( approxUlp(expected, result) ); 637 } 638 639 640 /// Transform a vector by a matrix in homogenous coordinates. 641 auto transform(V, M)(in V v, in M m) 642 if (isVec!(2, V) && isMat!(3, 3, M)) 643 { 644 return (m * vec(v, 1)).xy; 645 } 646 /// ditto 647 auto transform(V, M)(in V v, in M m) 648 if (isVec!(2, V) && isMat!(2, 3, M)) 649 { 650 return m * vec(v, 1); 651 } 652 /// ditto 653 auto transform(V, M)(in V v, in M m) 654 if (isVec!(3, V) && isMat!(4, 4, M)) 655 { 656 return (m * vec(v, 1)).xyz; 657 } 658 /// ditto 659 auto transform(V, M)(in V v, in M m) 660 if (isVec!(3, V) && isMat!(3, 4, M)) 661 { 662 return m * vec(v, 1); 663 } 664 /// ditto 665 auto transform(V, M)(in V v, in M m) 666 if (isVec!(4, V) && isMat!(4, 4, M)) 667 { 668 return m * v; 669 } 670 671 unittest 672 { 673 import gfx.math.approx : approxUlp; 674 675 // 2x3 matrix can hold affine 2D transforms 676 immutable transl = DMat2x3( 677 1, 0, 3, 678 0, 1, 2, 679 ); 680 assert( approxUlp(transform(dvec(3, 5), transl), dvec(6, 7)) ); 681 } 682 683 /// 684 unittest 685 { 686 import gfx.math.approx : approxUlp, approxUlpAndAbs; 687 import std.math : PI; 688 689 immutable v = dvec(2, 0); 690 auto m = DMat2x3.identity; 691 692 m = m.rotate(PI/2); 693 assert ( approxUlpAndAbs(transform(v, m), dvec(0, 2)) ); 694 695 m = m.translate(2, 2); 696 assert ( approxUlp(transform(v, m), dvec(2, 4)) ); 697 698 m = m.scale(2, 2); 699 assert ( approxUlp(transform(v, m), dvec(4, 8)) ); 700 } 701 702 /// 703 unittest 704 { 705 import gfx.math.approx : approxUlp; 706 707 auto st = scale!float(2, 2).translate(3, 1); 708 assert( approxUlp(transform(fvec(0, 0), st), fvec(3, 1)) ); 709 assert( approxUlp(transform(fvec(1, 1), st), fvec(5, 3)) ); 710 711 auto ts = translation!float(3, 1).scale(2, 2); 712 assert( approxUlp(transform(fvec(0, 0), ts), fvec(6, 2)) ); 713 assert( approxUlp(transform(fvec(1, 1), ts), fvec(8, 4)) ); 714 }