diff options
| author | 3gg <3gg@shellblade.net> | 2026-01-01 09:20:49 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2026-01-01 09:20:49 -0800 |
| commit | 7b9162ea7f4c78aa56a4a73187b22593b5d54913 (patch) | |
| tree | ef761e3215f939a07d22511b395a5327143fb393 | |
| parent | 1ae1b370da0ce0cf8501b9e6be8c6ac25e249e1a (diff) | |
Fix perspective texture mapping
| -rw-r--r-- | src/swgfx.c | 124 |
1 files changed, 75 insertions, 49 deletions
diff --git a/src/swgfx.c b/src/swgfx.c index 612f82d..3bec663 100644 --- a/src/swgfx.c +++ b/src/swgfx.c | |||
| @@ -297,15 +297,12 @@ static inline sgVec2i Clip(const swgfx* gfx, const sgVec2i p) { | |||
| 297 | return max2i(lower, min2i(upper, p)); | 297 | return max2i(lower, min2i(upper, p)); |
| 298 | } | 298 | } |
| 299 | 299 | ||
| 300 | static inline R BarycentricInterp1(sgVec3 bar, R a, R b, R c) { | 300 | static inline R BarycentricInterp(sgVec3 bar, R a, R b, R c) { |
| 301 | return bar.x*a + bar.y*b + bar.z*c; | 301 | return bar.x*a + bar.y*b + bar.z*c; |
| 302 | } | 302 | } |
| 303 | static inline sgVec2 BarycentricInterp2(sgVec3 bar, sgVec2 a, sgVec2 b, sgVec2 c) { | 303 | static inline sgVec2 BarycentricInterp2(sgVec3 bar, sgVec2 a, sgVec2 b, sgVec2 c) { |
| 304 | return add2(add2(scale2(a, bar.x), scale2(b, bar.y)), scale2(c, bar.z)); | 304 | return add2(add2(scale2(a, bar.x), scale2(b, bar.y)), scale2(c, bar.z)); |
| 305 | } | 305 | } |
| 306 | static inline sgVec2 PerspectiveInterp2(sgVec3 bar, sgVec3 depths, R z, sgVec2 a, sgVec2 b, sgVec2 c) { | ||
| 307 | return scale2(BarycentricInterp2(div3(bar, depths), a, b, c), z); | ||
| 308 | } | ||
| 309 | 306 | ||
| 310 | static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { | 307 | static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { |
| 311 | return (a.y - b.y)*p.x + (b.x - a.x)*p.y + a.x*b.y - b.x*a.y; | 308 | return (a.y - b.y)*p.x + (b.x - a.x)*p.y + a.x*b.y - b.x*a.y; |
| @@ -327,9 +324,7 @@ static inline sgVec3 Barycentric(sgVec2 p0, sgVec2 p1, sgVec2 p2, sgVec2 p) { | |||
| 327 | return (sgVec3){a,b,c}; | 324 | return (sgVec3){a,b,c}; |
| 328 | } | 325 | } |
| 329 | 326 | ||
| 330 | // Input triangle in screen space. Retains Z for depth testing and vertex | 327 | static void DrawTriangle2(swgfx* gfx, const sgTri2* const tri) { |
| 331 | // attribute interpolation. | ||
| 332 | static void DrawTriangle2(swgfx* gfx, const sgTri3* const tri) { | ||
| 333 | assert(gfx); | 328 | assert(gfx); |
| 334 | assert(tri); | 329 | assert(tri); |
| 335 | const sgVec2 p0 = (sgVec2){tri->p0.pos.x, tri->p0.pos.y}; | 330 | const sgVec2 p0 = (sgVec2){tri->p0.pos.x, tri->p0.pos.y}; |
| @@ -355,68 +350,104 @@ static void DrawTriangle2(swgfx* gfx, const sgTri3* const tri) { | |||
| 355 | // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. | 350 | // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. |
| 356 | // In the case c <= 0, then point is outside the triangle. | 351 | // In the case c <= 0, then point is outside the triangle. |
| 357 | if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { | 352 | if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { |
| 358 | const R z = BarycentricInterp1(bar, tri->p0.pos.z, tri->p1.pos.z, tri->p2.pos.z); | 353 | assert((bar.x + bar.y + bar.z - 1e7) <= 1.f); |
| 359 | R* depth = Depth(gfx, x, y); | 354 | const sgVec2 uv = BarycentricInterp2(bar, tri->p0.uv, tri->p1.uv, tri->p2.uv); |
| 360 | if ((0.f <= z) && (z <= 1.f) && (z <= *depth)) { | 355 | const sgPixel colour = Sample(gfx->texture, uv); |
| 361 | *depth = z; | 356 | SetPixel(gfx, (sgVec2i){x,y}, colour); |
| 362 | const sgVec3 depths = (sgVec3){tri->p0.pos.z, tri->p1.pos.z, tri->p2.pos.z}; | ||
| 363 | const sgVec2 uv = PerspectiveInterp2(bar, depths, z, tri->p0.uv, tri->p1.uv, tri->p2.uv); | ||
| 364 | const sgPixel colour = Sample(gfx->texture, uv); | ||
| 365 | //const sgPixel colour = (sgPixel){255, 0, 255, 255}; | ||
| 366 | // TODO: When doing lighting, need to tone-map here. | ||
| 367 | /*const int r = (int)(uv.x * 255.f); | ||
| 368 | const int g = (int)(uv.y * 255.f); | ||
| 369 | const sgPixel colour = (sgPixel){r, g, 255, 255};*/ | ||
| 370 | const sgVec2i pix = (sgVec2i){x,y}; | ||
| 371 | SetPixel(gfx, pix, colour); | ||
| 372 | } | ||
| 373 | } | 357 | } |
| 374 | } | 358 | } |
| 375 | } | 359 | } |
| 376 | } | 360 | } |
| 377 | 361 | ||
| 378 | static inline sgVec3 PerspDivide(sgVec4 v) { | 362 | static inline sgVec4 PerspDivide(sgVec4 v) { |
| 379 | return (sgVec3){v.x / v.w, v.y / v.w, v.z / v.w}; | 363 | return (sgVec4){v.x / v.w, v.y / v.w, v.z / v.w, v.w}; |
| 380 | } | 364 | } |
| 381 | 365 | ||
| 382 | // TODO: Compute a viewport matrix in sgViewport() instead. | 366 | // TODO: Compute a viewport matrix in sgViewport() instead. |
| 383 | static inline sgVec3 ViewportTransform(sgViewport_t vp, sgVec3 ndc) { | 367 | static inline sgVec4 ViewportTransform(sgViewport_t vp, sgVec4 ndc) { |
| 384 | return (sgVec3){ | 368 | return (sgVec4){ |
| 385 | .x = (ndc.x+1.f) * ((R)vp.width/2.f) + (R)vp.x0, | 369 | .x = (ndc.x+1.f) * ((R)vp.width/2.f) + (R)vp.x0, |
| 386 | .y = (ndc.y+1.f) * ((R)vp.height/2.f) + (R)vp.y0, | 370 | .y = (ndc.y+1.f) * ((R)vp.height/2.f) + (R)vp.y0, |
| 387 | .z = ndc.z}; | 371 | .z = ndc.z*0.5f + 0.5f, |
| 372 | .w = ndc.w}; | ||
| 388 | } | 373 | } |
| 389 | 374 | ||
| 390 | static inline sgVec3 ViewportToWindow(sgViewport_t vp, sgVec3 p) { | 375 | static inline sgVec4 ViewportToWindow(sgViewport_t vp, sgVec4 p) { |
| 391 | return (sgVec3){p.x, (R)vp.height - p.y, p.z}; | 376 | return (sgVec4){p.x, (R)vp.height - p.y, p.z, p.w}; |
| 392 | } | 377 | } |
| 393 | 378 | ||
| 394 | static inline sgVec3 TransformPosition(const swgfx* gfx, sgVec3 p) { | 379 | static inline sgVec4 TransformPosition(const swgfx* gfx, sgVec3 p) { |
| 395 | assert(gfx); | 380 | assert(gfx); |
| 396 | // Model to clip space. | 381 | // Model to clip space. |
| 397 | const sgVec4 p_clip = Mat4MulVec4(gfx->mvp, Vec4FromVec3(p, 1)); | 382 | const sgVec4 p_clip = Mat4MulVec4(gfx->mvp, Vec4FromVec3(p, 1)); |
| 398 | // TODO: Backface culling. | 383 | // TODO: Backface culling. |
| 399 | // Perspective divide. | 384 | // Perspective divide. |
| 400 | const sgVec3 p_ndc = PerspDivide(p_clip); | 385 | const sgVec4 p_ndc = PerspDivide(p_clip); |
| 401 | // TODO: Clip. | 386 | // TODO: Clip. |
| 402 | const sgVec3 p_vp = ViewportTransform(gfx->viewport, p_ndc); | 387 | const sgVec4 p_vp = ViewportTransform(gfx->viewport, p_ndc); |
| 403 | return ViewportToWindow(gfx->viewport, p_vp); | 388 | return ViewportToWindow(gfx->viewport, p_vp); |
| 404 | } | 389 | } |
| 405 | 390 | ||
| 406 | static void DrawTriangle3(swgfx* gfx, const sgTri3* const tri) { | 391 | static void DrawTriangle3(swgfx* gfx, const sgTri3* const tri) { |
| 407 | assert(gfx); | 392 | assert(gfx); |
| 408 | assert(tri); | 393 | assert(tri); |
| 409 | const sgVec3 p0 = TransformPosition(gfx, tri->p0.pos); | 394 | // TODO: Inline the transform here and interleave its operations to perform |
| 410 | const sgVec3 p1 = TransformPosition(gfx, tri->p1.pos); | 395 | // backface culling and clipping as early as possible. |
| 411 | const sgVec3 p2 = TransformPosition(gfx, tri->p2.pos); | 396 | const sgVec4 p0 = TransformPosition(gfx, tri->p0.pos); |
| 412 | const sgVec2 uv0 = tri->p0.uv; | 397 | const sgVec4 p1 = TransformPosition(gfx, tri->p1.pos); |
| 413 | const sgVec2 uv1 = tri->p1.uv; | 398 | const sgVec4 p2 = TransformPosition(gfx, tri->p2.pos); |
| 414 | const sgVec2 uv2 = tri->p2.uv; | 399 | const sgVec2 p0_2d = (sgVec2){p0.x, p0.y}; |
| 415 | const sgTri3 tri_screen = (sgTri3){ | 400 | const sgVec2 p1_2d = (sgVec2){p1.x, p1.y}; |
| 416 | (sgVert3){p0, uv0}, | 401 | const sgVec2 p2_2d = (sgVec2){p2.x, p2.y}; |
| 417 | (sgVert3){p1, uv1}, | 402 | const sgAABB2 bbox = TriangleAabb2(p0_2d, p1_2d, p2_2d); |
| 418 | (sgVert3){p2, uv2}}; | 403 | // We consider (x,y) to be the pixel center. |
| 419 | DrawTriangle2(gfx, &tri_screen); | 404 | // Draw all pixels touched by the bounding box. TODO: Multi-sampling. |
| 405 | sgVec2i pmin = (sgVec2i){(int)bbox.pmin.x, (int)bbox.pmin.y}; | ||
| 406 | sgVec2i pmax = (sgVec2i){(int)(bbox.pmax.x + 0.5f), (int)(bbox.pmax.y + 0.5f)}; | ||
| 407 | // Clip to screen space. | ||
| 408 | pmin = Clip(gfx, pmin); | ||
| 409 | pmax = Clip(gfx, pmax); | ||
| 410 | // Setup for perspective texture mapping. | ||
| 411 | // 'w' is view-space z. | ||
| 412 | const sgVec3 depths = (sgVec3){p0.z, p1.z, p2.z}; | ||
| 413 | const sgVec3 one_over_zs = (sgVec3){1.f / p0.w, 1.f / p1.w, 1.f/ p2.w}; | ||
| 414 | const sgVec3 u_over_zs = (sgVec3){tri->p0.uv.x / p0.w, tri->p1.uv.x / p1.w, tri->p2.uv.x / p2.w}; | ||
| 415 | const sgVec3 v_over_zs = (sgVec3){tri->p0.uv.y / p0.w, tri->p1.uv.y / p1.w, tri->p2.uv.y / p2.w}; | ||
| 416 | // Draw. | ||
| 417 | for (int y = pmin.y; y <= pmax.y; ++y) { | ||
| 418 | for (int x = pmin.x; x <= pmax.x; ++x) { | ||
| 419 | const sgVec2 p = (sgVec2){(R)x, (R)y}; | ||
| 420 | // TODO: there is an incremental optimization to computing barycentric coordinates; | ||
| 421 | // read more about it. | ||
| 422 | const sgVec3 bar = Barycentric(p0_2d, p1_2d, p2_2d, p); | ||
| 423 | // We need to check the third coordinate. | ||
| 424 | // a + b + c = 1 | ||
| 425 | // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. | ||
| 426 | // In the case c <= 0, then point is outside the triangle. | ||
| 427 | if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { | ||
| 428 | assert((bar.x + bar.y + bar.z - 1e7) <= 1.f); | ||
| 429 | const R p_one_over_z = dot3(bar, one_over_zs); | ||
| 430 | const R p_u_over_z = dot3(bar, u_over_zs); | ||
| 431 | const R p_v_over_z = dot3(bar, v_over_zs); | ||
| 432 | const R p_depth = dot3(bar, depths); | ||
| 433 | const R z = 1.f / p_one_over_z; | ||
| 434 | const sgVec2 uv = (sgVec2){p_u_over_z * z, p_v_over_z * z}; | ||
| 435 | R* depth = Depth(gfx, x, y); | ||
| 436 | if ((0.f <= p_depth) && (p_depth <= 1.f) && (p_depth <= *depth)) { | ||
| 437 | *depth = p_depth; | ||
| 438 | const sgPixel colour = Sample(gfx->texture, uv); | ||
| 439 | // TODO: When doing lighting, need to tone-map here. | ||
| 440 | /*const int d = (int)(z*255.f); | ||
| 441 | const sgPixel colour = (sgPixel){d,d,d,255};*/ | ||
| 442 | //const sgPixel colour = (sgPixel){255, 0, 255, 255}; | ||
| 443 | /*const int r = (int)(uv.x * 255.f); | ||
| 444 | const int g = (int)(uv.y * 255.f); | ||
| 445 | const sgPixel colour = (sgPixel){r, g, 255, 255};*/ | ||
| 446 | SetPixel(gfx, (sgVec2i){x,y}, colour); | ||
| 447 | } | ||
| 448 | } | ||
| 449 | } | ||
| 450 | } | ||
| 420 | } | 451 | } |
| 421 | 452 | ||
| 422 | #define is_pow2_or_0(X) ((X & (X - 1)) == 0) | 453 | #define is_pow2_or_0(X) ((X & (X - 1)) == 0) |
| @@ -585,12 +616,7 @@ void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour | |||
| 585 | void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) { | 616 | void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) { |
| 586 | assert(gfx); | 617 | assert(gfx); |
| 587 | for (size_t i = 0; i < count; ++i) { | 618 | for (size_t i = 0; i < count; ++i) { |
| 588 | const sgTri3 tri3 = (sgTri3) { | 619 | DrawTriangle2(gfx, &tris[i]); |
| 589 | .p0 = (sgVert3){.pos = (sgVec3){tris[i].p0.pos.x, tris[i].p0.pos.y, 0}, .uv = tris[i].p0.uv}, | ||
| 590 | .p1 = (sgVert3){.pos = (sgVec3){tris[i].p1.pos.x, tris[i].p1.pos.y, 0}, .uv = tris[i].p1.uv}, | ||
| 591 | .p2 = (sgVert3){.pos = (sgVec3){tris[i].p2.pos.x, tris[i].p2.pos.y, 0}, .uv = tris[i].p2.uv}, | ||
| 592 | }; | ||
| 593 | DrawTriangle2(gfx, &tri3); | ||
| 594 | } | 620 | } |
| 595 | } | 621 | } |
| 596 | 622 | ||
