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