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