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