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);