1 /// NDC agnostic projection matrices.
2 /// Each projection can be parameterized with a NDC configuration.
3 /// NDC defines how the clip space will translate to screen coordinates.
4 /// Note that after transformation by a projection matrix, X, Y and Z vertex
5 /// coordinates must be divided by W to obtain coordinates in final NDC space.
6 /// NDC has two components (XYClip and ZClip) that will affect how the
7 /// coordinates are transformed in the final normalized clipping space.
8 /// XYClip affects only X and Y (X always to the right, Y either upwards or downwards
9 /// for leftHanded and rightHanded respectively), and ZClip affects Z depth range.
10 module gfx.math.proj;
11 
12 import gfx.math.mat;
13 import gfx.math.vec;
14 import std.traits : isFloatingPoint;
15 
16 pure @safe @nogc nothrow:
17 
18 /// Determines whether the default projection matrices will project to a clip space
19 /// where Y points upwards (left hand NDC) or downwards (right hand NDC).
20 /// Default is right hand NDC, but can be changed by setting version(GfxMathLeftHandNDC)
21 /// X-Y clip space spans from [-1 .. 1] in both cases.
22 enum XYClip
23 {
24     /// right handed NDC (Y points downwards)
25     rightHand       = 0,
26     /// left handed NDC (Y points upwards)
27     leftHand        = 2,
28 }
29 
30 /// Determines whether the default projection matrices will project to a clip space
31 /// whose depth range is [0 .. 1] or [-1 .. 1].
32 /// Default is [0 .. 1] but can be changed by setting version(GfxMathDepthMinusOneToOne)
33 /// Z points into the screen in both cases.
34 enum ZClip
35 {
36     /// Depth range is [0 .. 1]
37     zeroToOne       = 0,
38     /// Depth range is [-1 .. 1]
39     minusOneToOne   = 1,
40 }
41 
42 /// NDC aggregates both XYClip and ZClip
43 enum NDC
44 {
45     /// XYClip.rightHand and ZClip.zeroToOne
46     RH_01   = 0,
47     /// XYClip.rightHand and ZClip.minusOneToOne
48     RH_M11  = 1,
49     /// XYClip.leftHand and ZClip.zeroToOne
50     LH_01   = 2,
51     /// XYClip.leftHand and ZClip.minusOneToOne
52     LH_M11  = 3,
53 }
54 
55 /// Build NDC from XYClip and ZClip
56 NDC ndc(in XYClip xy, in ZClip z)
57 {
58     return cast(NDC)(cast(uint)xy | cast(uint)z);
59 }
60 
61 /// Get XYClip from NDC
62 @property XYClip xyClip(in NDC ndc)
63 {
64     return cast(XYClip)(cast(uint)ndc & 0x02);
65 }
66 
67 /// Get ZClip from NDC
68 @property ZClip zClip(in NDC ndc)
69 {
70     return cast(ZClip)(cast(uint)ndc & 0x01);
71 }
72 
73 /// Build an orthographic projection matrix with right-hand NDC and [0 .. 1] depth clipping
74 /// Params:
75 ///     l =     X position of the left plane
76 ///     r =     X position of the right plane
77 ///     b =     Y position of the bottom plane
78 ///     t =     Y position of the top plane
79 ///     n =     distance from origin to near plane (in Z-)
80 ///     f =     distance from origin to far plane (in Z-)
81 /// Returns: an affine matrix that maps from eye coordinates to NDC.
82 Mat4!T ortho_RH_01(T) (in T l, in T r, in T b, in T t, in T n, in T f)
83 {
84     const rl = r-l;
85     const bt = b-t;
86     const fn = f-n;
87     return Mat4!T(
88         T(2)/rl,        0,              0,          -(r+l)/rl,
89         0,              T(2)/bt,        0,          -(b+t)/bt,
90         0,              0,              T(-1)/fn,   -n/fn,
91         0,              0,              0,          1,
92     );
93 }
94 
95 ///
96 unittest {
97     import gfx.math.approx : approxUlp;
98     const m = ortho_RH_01(3f, 5f, -2f, 7f, 1f, 10f);
99     const vl = vec(3f, -2f, -1f, 1f);
100     const vh = vec(5f, 7f, -10f, 1f);
101     const vc = vec(4f, 2.5f, -5.5f, 1f);
102 
103     assert(approxUlp( m * vl, vec(-1f, 1f, 0f, 1f) ));
104     assert(approxUlp( m * vh, vec(1f, -1f, 1f, 1f) ));
105     assert(approxUlp( m * vc, vec(0f, 0f, 0.5f, 1f) ));
106 }
107 
108 
109 /// Build an orthographic projection matrix with right-hand NDC and [-1 .. 1] depth clipping
110 /// Params:
111 ///     l =     X position of the left plane
112 ///     r =     X position of the right plane
113 ///     b =     Y position of the bottom plane
114 ///     t =     Y position of the top plane
115 ///     n =     distance from origin to near plane (in Z-)
116 ///     f =     distance from origin to far plane (in Z-)
117 /// Returns: an affine matrix that maps from eye coordinates to NDC.
118 Mat4!T ortho_RH_M11(T) (in T l, in T r, in T b, in T t, in T n, in T f)
119 {
120     const rl = r-l;
121     const bt = b-t;
122     const fn = f-n;
123     return Mat4!T(
124         T(2)/rl,        0,              0,          -(r+l)/rl,
125         0,              T(2)/bt,        0,          -(b+t)/bt,
126         0,              0,              T(-2)/fn,   -(f+n)/fn,
127         0,              0,              0,          1,
128     );
129 }
130 
131 ///
132 unittest {
133     import gfx.math.approx : approxUlp;
134     const m = ortho_RH_M11(3f, 5f, -2f, 7f, 1f, 10f);
135     const v1 = vec(3f, -2f, -1f, 1f);
136     const v2 = vec(5f, 7f, -10f, 1f);
137     const v0 = vec(4f, 2.5f, -5.5f, 1f);
138 
139     assert(approxUlp( m * v1, vec(-1f, 1f, -1f, 1f) ));
140     assert(approxUlp( m * v2, vec(1f, -1f, 1f, 1f) ));
141     assert(approxUlp( m * v0, vec(0f, 0f, 0f, 1f) ));
142 }
143 
144 
145 /// Build an orthographic projection matrix with left-hand NDC and [0 .. 1] depth clipping
146 /// Params:
147 ///     l =     X position of the left plane
148 ///     r =     X position of the right plane
149 ///     b =     Y position of the bottom plane
150 ///     t =     Y position of the top plane
151 ///     n =     distance from origin to near plane (in Z-)
152 ///     f =     distance from origin to far plane (in Z-)
153 /// Returns: an affine matrix that maps from eye coordinates to NDC.
154 Mat4!T ortho_LH_01(T) (in T l, in T r, in T b, in T t, in T n, in T f)
155 {
156     const rl = r-l;
157     const tb = t-b;
158     const fn = f-n;
159     return Mat4!T(
160         T(2)/rl,        0,              0,          -(r+l)/rl,
161         0,              T(2)/tb,        0,          -(t+b)/tb,
162         0,              0,              T(-1)/fn,   -n/fn,
163         0,              0,              0,          1,
164     );
165 }
166 
167 
168 ///
169 unittest {
170     import gfx.math.approx : approxUlp;
171     const m = ortho_LH_01(3f, 5f, -2f, 7f, 1f, 10f);
172     const v1 = vec(3f, -2f, -1f, 1f);
173     const v2 = vec(5f, 7f, -10f, 1f);
174     const v0 = vec(4f, 2.5f, -5.5f, 1f);
175 
176     assert(approxUlp( m * v1, vec(-1f, -1f, 0f, 1f) ));
177     assert(approxUlp( m * v2, vec(1f, 1f, 1f, 1f) ));
178     assert(approxUlp( m * v0, vec(0f, 0f, 0.5f, 1f) ));
179 }
180 
181 /// Build an orthographic projection matrix with left-hand NDC and [-1 .. 1] depth clipping
182 /// Params:
183 ///     l =     X position of the left plane
184 ///     r =     X position of the right plane
185 ///     b =     Y position of the bottom plane
186 ///     t =     Y position of the top plane
187 ///     n =     distance from origin to near plane (in Z-)
188 ///     f =     distance from origin to far plane (in Z-)
189 /// Returns: an affine matrix that maps from eye coordinates to NDC.
190 Mat4!T ortho_LH_M11(T) (in T l, in T r, in T b, in T t, in T n, in T f)
191 {
192     const rl = r-l;
193     const tb = t-b;
194     const fn = f-n;
195     return Mat4!T(
196         T(2)/rl,        0,              0,          -(r+l)/rl,
197         0,              T(2)/tb,        0,          -(t+b)/tb,
198         0,              0,              T(-2)/fn,   -(f+n)/fn,
199         0,              0,              0,          1,
200     );
201 }
202 
203 ///
204 unittest {
205     import gfx.math.approx : approxUlp;
206     const m = ortho_LH_M11(3f, 5f, -2f, 7f, 1f, 10f);
207     const v1 = vec(3f, -2f, -1f, 1f);
208     const v2 = vec(5f, 7f, -10f, 1f);
209     const v0 = vec(4f, 2.5f, -5.5f, 1f);
210 
211     assert(approxUlp( m * v1, vec(-1f, -1f, -1f, 1f) ));
212     assert(approxUlp( m * v2, vec(1f, 1f, 1f, 1f) ));
213     assert(approxUlp( m * v0, vec(0f, 0f, 0f, 1f) ));
214 }
215 
216 /// Build an orthographic projection matrix with NDC set at compile-time.
217 template orthoCT(NDC ndc)
218 {
219     static if (ndc == NDC.RH_01) {
220         alias orthoCT = ortho_RH_01;
221     }
222     else static if (ndc == NDC.RH_M11) {
223         alias orthoCT = ortho_RH_M11;
224     }
225     else static if (ndc == NDC.LH_01) {
226         alias orthoCT = ortho_LH_01;
227     }
228     else static if (ndc == NDC.LH_M11) {
229         alias orthoCT = ortho_LH_M11;
230     }
231     else {
232         static assert(false);
233     }
234 }
235 
236 /// Build an orthographic projection matrix with default NDC
237 /// Params:
238 ///     l =     X position of the left plane
239 ///     r =     X position of the right plane
240 ///     b =     Y position of the bottom plane
241 ///     t =     Y position of the top plane
242 ///     n =     distance from origin to near plane (in Z-)
243 ///     f =     distance from origin to far plane (in Z-)
244 /// Returns: an affine matrix that maps from eye coordinates to NDC.
245 alias defOrtho = orthoCT!(defNdc);
246 
247 /// Build an orthographic projection matrix with NDC determined at runtime
248 /// Params:
249 ///     ndc =   the target NDC
250 ///     l =     X position of the left plane
251 ///     r =     X position of the right plane
252 ///     b =     Y position of the bottom plane
253 ///     t =     Y position of the top plane
254 ///     n =     distance from origin to near plane (in Z-)
255 ///     f =     distance from origin to far plane (in Z-)
256 /// Returns: an affine matrix that maps from eye coordinates to NDC.
257 Mat4!T ortho(T)(NDC ndc, in T l, in T r, in T b, in T t, in T n, in T f)
258 {
259     final switch (ndc)
260     {
261     case NDC.RH_01:
262         return ortho_RH_01(l, r, b, t, n, f);
263     case NDC.RH_M11:
264         return ortho_RH_M11(l, r, b, t, n, f);
265     case NDC.LH_01:
266         return ortho_LH_01(l, r, b, t, n, f);
267     case NDC.LH_M11:
268         return ortho_LH_M11(l, r, b, t, n, f);
269     }
270 }
271 
272 /// Build a perspective projection matrix with right-hand NDC and [0 .. 1] depth clipping
273 /// Params:
274 ///     l =     X position of the left edge at the near plane
275 ///     r =     X position of the right edge at the near plane
276 ///     b =     Y position of the bottom edge at the near plane
277 ///     t =     Y position of the top edge at the near plane
278 ///     n =     distance from origin to near plane (in Z-)
279 ///     f =     distance from origin to far plane (in Z-)
280 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
281 Mat4!T frustum_RH_01(T)(in T l, in T r, in T b, in T t, in T n, in T f)
282 {
283     const rl = r-l;
284     const bt = b-t;
285     const fn = f-n;
286 
287     return Mat4!T (
288         2*n/rl, 0,          (r+l)/rl,   0,
289         0,      2*n/bt,     (b+t)/bt,   0,
290         0,      0,          -f/fn,      -f*n/fn,
291         0,      0,          -1,         0,
292     );
293 }
294 
295 ///
296 unittest {
297     const m = frustum_RH_01!float(-2, 2, -4, 4, 2, 4);
298     const vl = fvec(-2, -4, -2);
299     const vh = fvec(4, 8, -4);
300     const vc = fvec(0, 0, -3);
301 
302     auto toNdc(in FVec3 v) {
303         const clip = m * fvec(v, 1);
304         return (clip / clip.w).xyz;
305     }
306 
307     import gfx.math.approx : approxUlp;
308     assert(approxUlp( toNdc(vl), fvec(-1, 1, 0) ));
309     assert(approxUlp( toNdc(vh), fvec(1, -1, 1) ));
310     assert(approxUlp( toNdc(vc), fvec(0, 0, 2f/3f) ));
311 }
312 
313 
314 /// Build a perspective projection matrix with right-hand NDC and [0 .. 1] depth clipping
315 /// Params:
316 ///     l =     X position of the left edge at the near plane
317 ///     r =     X position of the right edge at the near plane
318 ///     b =     Y position of the bottom edge at the near plane
319 ///     t =     Y position of the top edge at the near plane
320 ///     n =     distance from origin to near plane (in Z-)
321 ///     f =     distance from origin to far plane (in Z-)
322 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
323 Mat4!T frustum_RH_M11(T)(in T l, in T r, in T b, in T t, in T n, in T f)
324 {
325     const rl = r-l;
326     const bt = b-t;
327     const fn = f-n;
328 
329     return Mat4!T (
330         2*n/rl, 0,          (r+l)/rl,   0,
331         0,      2*n/bt,     (b+t)/bt,   0,
332         0,      0,          -(f+n)/fn,  -2*f*n/fn,
333         0,      0,          -1,         0,
334     );
335 }
336 
337 ///
338 unittest {
339     const m = frustum_RH_M11!float(-2, 2, -4, 4, 2, 4);
340     const vl = fvec(-2, -4, -2);
341     const vh = fvec(4, 8, -4);
342     const vc = fvec(0, 0, -3);
343 
344     auto toNdc(in FVec3 v) {
345         const clip = m * fvec(v, 1);
346         return (clip / clip.w).xyz;
347     }
348 
349     import gfx.math.approx : approxUlp;
350     assert(approxUlp( toNdc(vl), fvec(-1, 1, -1) ));
351     assert(approxUlp( toNdc(vh), fvec(1, -1, 1) ));
352     assert(approxUlp( toNdc(vc), fvec(0, 0, 1f/3f) ));
353 }
354 
355 /// Build a perspective projection matrix with left-hand NDC and [0 .. 1] depth clipping
356 /// Params:
357 ///     l =     X position of the left edge at the near plane
358 ///     r =     X position of the right edge at the near plane
359 ///     b =     Y position of the bottom edge at the near plane
360 ///     t =     Y position of the top edge at the near plane
361 ///     n =     distance from origin to near plane (in Z-)
362 ///     f =     distance from origin to far plane (in Z-)
363 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
364 Mat4!T frustum_LH_01(T)(in T l, in T r, in T b, in T t, in T n, in T f)
365 {
366     const rl = r-l;
367     const tb = t-b;
368     const fn = f-n;
369 
370     return Mat4!T (
371         2*n/rl, 0,          (r+l)/rl,   0,
372         0,      2*n/tb,     (t+b)/tb,   0,
373         0,      0,          -f/fn,      -f*n/fn,
374         0,      0,          -1,         0,
375     );
376 }
377 
378 /// Build a perspective projection matrix with left-hand NDC and [-1 .. 1] depth clipping
379 /// Params:
380 ///     l =     X position of the left edge at the near plane
381 ///     r =     X position of the right edge at the near plane
382 ///     b =     Y position of the bottom edge at the near plane
383 ///     t =     Y position of the top edge at the near plane
384 ///     n =     distance from origin to near plane (in Z-)
385 ///     f =     distance from origin to far plane (in Z-)
386 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
387 Mat4!T frustum_LH_M11(T)(in T l, in T r, in T b, in T t, in T n, in T f)
388 {
389     const rl = r-l;
390     const tb = t-b;
391     const fn = f-n;
392 
393     return Mat4!T (
394         2*n/rl, 0,          (r+l)/rl,   0,
395         0,      2*n/tb,     (t+b)/tb,   0,
396         0,      0,          -(f+n)/fn,  -2*f*n/fn,
397         0,      0,          -1,         0,
398     );
399 }
400 
401 ///
402 unittest {
403     const m = frustum_LH_01!float(-2, 2, -4, 4, 2, 4);
404     const vl = fvec(-2, -4, -2);
405     const vh = fvec(4, 8, -4);
406     const vc = fvec(0, 0, -3);
407 
408     auto toNdc(in FVec3 v) {
409         const clip = m * fvec(v, 1);
410         return (clip / clip.w).xyz;
411     }
412 
413     import gfx.math.approx : approxUlp;
414     assert(approxUlp( toNdc(vl), fvec(-1, -1, 0) ));
415     assert(approxUlp( toNdc(vh), fvec(1, 1, 1) ));
416     assert(approxUlp( toNdc(vc), fvec(0, 0, 2f/3f) ));
417 }
418 
419 
420 /// Build an frustum perspective projection matrix with NDC set at compile-time.
421 template frustumCT(NDC ndc)
422 {
423     static if (ndc == NDC.RH_01) {
424         alias frustumCT = frustum_RH_01;
425     }
426     else static if (ndc == NDC.RH_M11) {
427         alias frustumCT = frustum_RH_M11;
428     }
429     else static if (ndc == NDC.LH_01) {
430         alias frustumCT = frustum_LH_01;
431     }
432     else static if (ndc == NDC.LH_M11) {
433         alias frustumCT = frustum_LH_M11;
434     }
435     else {
436         static assert(false);
437     }
438 }
439 
440 /// Build an frustum perspective projection matrix with default NDC and DepthClip
441 /// Params:
442 ///     l =     X position of the left edge at the near plane
443 ///     r =     X position of the right edge at the near plane
444 ///     b =     Y position of the bottom edge at the near plane
445 ///     t =     Y position of the top edge at the near plane
446 ///     n =     distance from origin to near plane (in Z-)
447 ///     f =     distance from origin to far plane (in Z-)
448 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
449 alias defFrustum = frustumCT!(defNdc);
450 
451 /// Build an frustum perspective projection matrix with NDC and DepthClip selected at runtime
452 /// Params:
453 ///     ndc =   the target NDC
454 ///     l =     X position of the left edge at the near plane
455 ///     r =     X position of the right edge at the near plane
456 ///     b =     Y position of the bottom edge at the near plane
457 ///     t =     Y position of the top edge at the near plane
458 ///     n =     distance from origin to near plane (in Z-)
459 ///     f =     distance from origin to far plane (in Z-)
460 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
461 Mat4!T frustum(T)(NDC ndc, in T l, in T r, in T b, in T t, in T n, in T f)
462 {
463     final switch (ndc)
464     {
465     case NDC.RH_01:
466         return frustum_RH_01(l, r, b, t, n, f);
467     case NDC.RH_M11:
468         return frustum_RH_M11(l, r, b, t, n, f);
469     case NDC.LH_01:
470         return frustum_LH_01(l, r, b, t, n, f);
471     case NDC.LH_M11:
472         return frustum_LH_M11(l, r, b, t, n, f);
473     }
474 }
475 
476 /// Build a perspective projection matrix with right-hand NDC and [0 .. 1] depth clipping
477 /// Params:
478 ///     fovx =      horizontal field of view in degrees
479 ///     aspect =    aspect ratio (width / height)
480 ///     near =      position of the near plane
481 ///     far =       position of the far plane
482 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
483 Mat4!T perspective_RH_01(T)(in T fovx, in T aspect, in T near, in T far)
484 if (isFloatingPoint!T)
485 {
486     import std.math : PI, tan;
487     const r = cast(T)(near * tan(fovx * PI / T(360)));
488     const t = r / aspect;
489     return frustum_RH_01(-r, r, -t, t, near, far);
490 }
491 
492 ///
493 unittest {
494     const m = perspective_RH_01!float(90, 2, 2, 4);
495     const vl = fvec(-2, -1, -2);
496     const vh = fvec(4, 2, -4);
497     const vc = fvec(0, 0, -3);
498 
499     auto toNdc(in FVec3 v) {
500         const clip = m * fvec(v, 1);
501         return (clip / clip.w).xyz;
502     }
503 
504     import gfx.math.approx : approxUlp;
505     assert(approxUlp( toNdc(vl), fvec(-1, 1, 0) ));
506     assert(approxUlp( toNdc(vh), fvec(1, -1, 1) ));
507     assert(approxUlp( toNdc(vc), fvec(0, 0, 2f/3f) ));
508 }
509 
510 
511 /// Build a perspective projection matrix with right-hand NDC and [-1 .. 1] depth clipping
512 /// Params:
513 ///     fovx =      horizontal field of view in degrees
514 ///     aspect =    aspect ratio (width / height)
515 ///     near =      position of the near plane
516 ///     far =       position of the far plane
517 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
518 Mat4!T perspective_RH_M11(T)(in T fovx, in T aspect, in T near, in T far)
519 if (isFloatingPoint!T)
520 {
521     import std.math : PI, tan;
522     const r = cast(T)(near * tan(fovx * PI / T(360)));
523     const t = r / aspect;
524     return frustum_RH_M11(-r, r, -t, t, near, far);
525 }
526 
527 ///
528 unittest {
529     const m = perspective_RH_M11!float(90, 2, 2, 4);
530     const vl = fvec(-2, -1, -2);
531     const vh = fvec(4, 2, -4);
532     const vc = fvec(0, 0, -3);
533 
534     auto toNdc(in FVec3 v) {
535         const clip = m * fvec(v, 1);
536         return (clip / clip.w).xyz;
537     }
538 
539     import gfx.math.approx : approxUlp;
540     assert(approxUlp( toNdc(vl), fvec(-1, 1, -1) ));
541     assert(approxUlp( toNdc(vh), fvec(1, -1, 1) ));
542     assert(approxUlp( toNdc(vc), fvec(0, 0, 1f/3f) ));
543 }
544 
545 
546 /// Build a perspective projection matrix with left-hand NDC and [0 .. 1] depth clipping
547 /// Params:
548 ///     fovx =      horizontal field of view in degrees
549 ///     aspect =    aspect ratio (width / height)
550 ///     near =      position of the near plane
551 ///     far =       position of the far plane
552 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
553 Mat4!T perspective_LH_01(T)(in T fovx, in T aspect, in T near, in T far)
554 if (isFloatingPoint!T)
555 {
556     import std.math : PI, tan;
557     const r = cast(T)(near * tan(fovx * PI / T(360)));
558     const t = r / aspect;
559     return frustum_LH_01(-r, r, -t, t, near, far);
560 }
561 
562 ///
563 unittest {
564     const m = perspective_LH_01!float(90, 2, 2, 4);
565     const vl = fvec(-2, -1, -2);
566     const vh = fvec(4, 2, -4);
567     const vc = fvec(0, 0, -3);
568 
569     auto toNdc(in FVec3 v) {
570         const clip = m * fvec(v, 1);
571         return (clip / clip.w).xyz;
572     }
573 
574     import gfx.math.approx : approxUlp;
575     assert(approxUlp( toNdc(vl), fvec(-1, -1, 0) ));
576     assert(approxUlp( toNdc(vh), fvec(1, 1, 1) ));
577     assert(approxUlp( toNdc(vc), fvec(0, 0, 2f/3f) ));
578 }
579 
580 
581 /// Build a perspective projection matrix with left-hand NDC and [-1 .. 1] depth clipping
582 /// Params:
583 ///     fovx =      horizontal field of view in degrees
584 ///     aspect =    aspect ratio (width / height)
585 ///     near =      position of the near plane
586 ///     far =       position of the far plane
587 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
588 Mat4!T perspective_LH_M11(T)(in T fovx, in T aspect, in T near, in T far)
589 if (isFloatingPoint!T)
590 {
591     import std.math : PI, tan;
592     const r = cast(T)(near * tan(fovx * PI / T(360)));
593     const t = r / aspect;
594     return frustum_LH_M11(-r, r, -t, t, near, far);
595 }
596 
597 ///
598 unittest {
599     const m = perspective_LH_M11!float(90, 2, 2, 4);
600     const vl = fvec(-2, -1, -2);
601     const vh = fvec(4, 2, -4);
602     const vc = fvec(0, 0, -3);
603 
604     auto toNdc(in FVec3 v) {
605         const clip = m * fvec(v, 1);
606         return (clip / clip.w).xyz;
607     }
608 
609     import gfx.math.approx : approxUlp;
610     assert(approxUlp( toNdc(vl), fvec(-1, -1, -1) ));
611     assert(approxUlp( toNdc(vh), fvec(1, 1, 1) ));
612     assert(approxUlp( toNdc(vc), fvec(0, 0, 1f/3f) ));
613 }
614 
615 
616 /// Build an perspective projection matrix with NDC set at compile-time.
617 template perspectiveCT(NDC ndc)
618 {
619     static if (ndc == NDC.RH_01) {
620         alias perspectiveCT = perspective_RH_01;
621     }
622     else static if (ndc == NDC.RH_M11) {
623         alias perspectiveCT = perspective_RH_M11;
624     }
625     else static if (ndc == NDC.LH_01) {
626         alias perspectiveCT = perspective_LH_01;
627     }
628     else static if (ndc == NDC.LH_M11) {
629         alias perspectiveCT = perspective_LH_M11;
630     }
631     else {
632         static assert(false);
633     }
634 }
635 
636 /// Build a perspective projection matrix with default NDC and DepthClip
637 /// Params:
638 ///     fovx =      horizontal field of view in degrees
639 ///     aspect =    aspect ratio (width / height)
640 ///     near =      position of the near plane
641 ///     far =       position of the far plane
642 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
643 alias defPerspective = perspectiveCT!(defNdc);
644 
645 /// Build a perspective projection matrix with NDC selected at run-time.
646 /// Params:
647 ///     ndc =       the target NDC
648 ///     fovx =      horizontal field of view in degrees
649 ///     aspect =    aspect ratio (width / height)
650 ///     near =      position of the near plane
651 ///     far =       position of the far plane
652 /// Returns: a matrix that maps from eye space to clip space. To obtain NDC, the vector must be divided by w.
653 Mat4!T perspective(T)(NDC ndc, in T fovx, in T aspect, in T near, in T far)
654 {
655     final switch (ndc)
656     {
657     case NDC.RH_01:
658         return perspective_RH_01(fovx, aspect, near, far);
659     case NDC.RH_M11:
660         return perspective_RH_M11(fovx, aspect, near, far);
661     case NDC.LH_01:
662         return perspective_LH_01(fovx, aspect, near, far);
663     case NDC.LH_M11:
664         return perspective_LH_M11(fovx, aspect, near, far);
665     }
666 }
667 
668 
669 private:
670 
671 version(GfxMathDepthMinusOneToOne) {
672     enum defZClip = ZClip.minusOneToOne;
673 }
674 else {
675     enum defZClip = ZClip.zeroToOne;
676 }
677 
678 version(GfxMathLeftHandNDC) {
679     enum defXYClip = XYClip.leftHand;
680 }
681 else {
682     enum defXYClip = XYClip.rightHand;
683 }
684 
685 enum defNdc = ndc(defXYClip, defZClip);