From 7b9162ea7f4c78aa56a4a73187b22593b5d54913 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Thu, 1 Jan 2026 09:20:49 -0800 Subject: Fix perspective texture mapping --- src/swgfx.c | 124 ++++++++++++++++++++++++++++++++++++------------------------ 1 file 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) { return max2i(lower, min2i(upper, p)); } -static inline R BarycentricInterp1(sgVec3 bar, R a, R b, R c) { +static inline R BarycentricInterp(sgVec3 bar, R a, R b, R c) { return bar.x*a + bar.y*b + bar.z*c; } static inline sgVec2 BarycentricInterp2(sgVec3 bar, sgVec2 a, sgVec2 b, sgVec2 c) { return add2(add2(scale2(a, bar.x), scale2(b, bar.y)), scale2(c, bar.z)); } -static inline sgVec2 PerspectiveInterp2(sgVec3 bar, sgVec3 depths, R z, sgVec2 a, sgVec2 b, sgVec2 c) { - return scale2(BarycentricInterp2(div3(bar, depths), a, b, c), z); -} static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { 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) { return (sgVec3){a,b,c}; } -// Input triangle in screen space. Retains Z for depth testing and vertex -// attribute interpolation. -static void DrawTriangle2(swgfx* gfx, const sgTri3* const tri) { +static void DrawTriangle2(swgfx* gfx, const sgTri2* const tri) { assert(gfx); assert(tri); 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) { // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. // In the case c <= 0, then point is outside the triangle. if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { - const R z = BarycentricInterp1(bar, tri->p0.pos.z, tri->p1.pos.z, tri->p2.pos.z); - R* depth = Depth(gfx, x, y); - if ((0.f <= z) && (z <= 1.f) && (z <= *depth)) { - *depth = z; - const sgVec3 depths = (sgVec3){tri->p0.pos.z, tri->p1.pos.z, tri->p2.pos.z}; - const sgVec2 uv = PerspectiveInterp2(bar, depths, z, tri->p0.uv, tri->p1.uv, tri->p2.uv); - const sgPixel colour = Sample(gfx->texture, uv); - //const sgPixel colour = (sgPixel){255, 0, 255, 255}; - // TODO: When doing lighting, need to tone-map here. - /*const int r = (int)(uv.x * 255.f); - const int g = (int)(uv.y * 255.f); - const sgPixel colour = (sgPixel){r, g, 255, 255};*/ - const sgVec2i pix = (sgVec2i){x,y}; - SetPixel(gfx, pix, colour); - } + assert((bar.x + bar.y + bar.z - 1e7) <= 1.f); + const sgVec2 uv = BarycentricInterp2(bar, tri->p0.uv, tri->p1.uv, tri->p2.uv); + const sgPixel colour = Sample(gfx->texture, uv); + SetPixel(gfx, (sgVec2i){x,y}, colour); } } } } -static inline sgVec3 PerspDivide(sgVec4 v) { - return (sgVec3){v.x / v.w, v.y / v.w, v.z / v.w}; +static inline sgVec4 PerspDivide(sgVec4 v) { + return (sgVec4){v.x / v.w, v.y / v.w, v.z / v.w, v.w}; } // TODO: Compute a viewport matrix in sgViewport() instead. -static inline sgVec3 ViewportTransform(sgViewport_t vp, sgVec3 ndc) { - return (sgVec3){ +static inline sgVec4 ViewportTransform(sgViewport_t vp, sgVec4 ndc) { + return (sgVec4){ .x = (ndc.x+1.f) * ((R)vp.width/2.f) + (R)vp.x0, .y = (ndc.y+1.f) * ((R)vp.height/2.f) + (R)vp.y0, - .z = ndc.z}; + .z = ndc.z*0.5f + 0.5f, + .w = ndc.w}; } -static inline sgVec3 ViewportToWindow(sgViewport_t vp, sgVec3 p) { - return (sgVec3){p.x, (R)vp.height - p.y, p.z}; +static inline sgVec4 ViewportToWindow(sgViewport_t vp, sgVec4 p) { + return (sgVec4){p.x, (R)vp.height - p.y, p.z, p.w}; } -static inline sgVec3 TransformPosition(const swgfx* gfx, sgVec3 p) { +static inline sgVec4 TransformPosition(const swgfx* gfx, sgVec3 p) { assert(gfx); // Model to clip space. const sgVec4 p_clip = Mat4MulVec4(gfx->mvp, Vec4FromVec3(p, 1)); // TODO: Backface culling. // Perspective divide. - const sgVec3 p_ndc = PerspDivide(p_clip); + const sgVec4 p_ndc = PerspDivide(p_clip); // TODO: Clip. - const sgVec3 p_vp = ViewportTransform(gfx->viewport, p_ndc); + const sgVec4 p_vp = ViewportTransform(gfx->viewport, p_ndc); return ViewportToWindow(gfx->viewport, p_vp); } static void DrawTriangle3(swgfx* gfx, const sgTri3* const tri) { assert(gfx); assert(tri); - const sgVec3 p0 = TransformPosition(gfx, tri->p0.pos); - const sgVec3 p1 = TransformPosition(gfx, tri->p1.pos); - const sgVec3 p2 = TransformPosition(gfx, tri->p2.pos); - const sgVec2 uv0 = tri->p0.uv; - const sgVec2 uv1 = tri->p1.uv; - const sgVec2 uv2 = tri->p2.uv; - const sgTri3 tri_screen = (sgTri3){ - (sgVert3){p0, uv0}, - (sgVert3){p1, uv1}, - (sgVert3){p2, uv2}}; - DrawTriangle2(gfx, &tri_screen); + // TODO: Inline the transform here and interleave its operations to perform + // backface culling and clipping as early as possible. + const sgVec4 p0 = TransformPosition(gfx, tri->p0.pos); + const sgVec4 p1 = TransformPosition(gfx, tri->p1.pos); + const sgVec4 p2 = TransformPosition(gfx, tri->p2.pos); + const sgVec2 p0_2d = (sgVec2){p0.x, p0.y}; + const sgVec2 p1_2d = (sgVec2){p1.x, p1.y}; + const sgVec2 p2_2d = (sgVec2){p2.x, p2.y}; + const sgAABB2 bbox = TriangleAabb2(p0_2d, p1_2d, p2_2d); + // We consider (x,y) to be the pixel center. + // Draw all pixels touched by the bounding box. TODO: Multi-sampling. + sgVec2i pmin = (sgVec2i){(int)bbox.pmin.x, (int)bbox.pmin.y}; + sgVec2i pmax = (sgVec2i){(int)(bbox.pmax.x + 0.5f), (int)(bbox.pmax.y + 0.5f)}; + // Clip to screen space. + pmin = Clip(gfx, pmin); + pmax = Clip(gfx, pmax); + // Setup for perspective texture mapping. + // 'w' is view-space z. + const sgVec3 depths = (sgVec3){p0.z, p1.z, p2.z}; + const sgVec3 one_over_zs = (sgVec3){1.f / p0.w, 1.f / p1.w, 1.f/ p2.w}; + const sgVec3 u_over_zs = (sgVec3){tri->p0.uv.x / p0.w, tri->p1.uv.x / p1.w, tri->p2.uv.x / p2.w}; + const sgVec3 v_over_zs = (sgVec3){tri->p0.uv.y / p0.w, tri->p1.uv.y / p1.w, tri->p2.uv.y / p2.w}; + // Draw. + for (int y = pmin.y; y <= pmax.y; ++y) { + for (int x = pmin.x; x <= pmax.x; ++x) { + const sgVec2 p = (sgVec2){(R)x, (R)y}; + // TODO: there is an incremental optimization to computing barycentric coordinates; + // read more about it. + const sgVec3 bar = Barycentric(p0_2d, p1_2d, p2_2d, p); + // We need to check the third coordinate. + // a + b + c = 1 + // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. + // In the case c <= 0, then point is outside the triangle. + if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { + assert((bar.x + bar.y + bar.z - 1e7) <= 1.f); + const R p_one_over_z = dot3(bar, one_over_zs); + const R p_u_over_z = dot3(bar, u_over_zs); + const R p_v_over_z = dot3(bar, v_over_zs); + const R p_depth = dot3(bar, depths); + const R z = 1.f / p_one_over_z; + const sgVec2 uv = (sgVec2){p_u_over_z * z, p_v_over_z * z}; + R* depth = Depth(gfx, x, y); + if ((0.f <= p_depth) && (p_depth <= 1.f) && (p_depth <= *depth)) { + *depth = p_depth; + const sgPixel colour = Sample(gfx->texture, uv); + // TODO: When doing lighting, need to tone-map here. + /*const int d = (int)(z*255.f); + const sgPixel colour = (sgPixel){d,d,d,255};*/ + //const sgPixel colour = (sgPixel){255, 0, 255, 255}; + /*const int r = (int)(uv.x * 255.f); + const int g = (int)(uv.y * 255.f); + const sgPixel colour = (sgPixel){r, g, 255, 255};*/ + SetPixel(gfx, (sgVec2i){x,y}, colour); + } + } + } + } } #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 void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) { assert(gfx); for (size_t i = 0; i < count; ++i) { - const sgTri3 tri3 = (sgTri3) { - .p0 = (sgVert3){.pos = (sgVec3){tris[i].p0.pos.x, tris[i].p0.pos.y, 0}, .uv = tris[i].p0.uv}, - .p1 = (sgVert3){.pos = (sgVec3){tris[i].p1.pos.x, tris[i].p1.pos.y, 0}, .uv = tris[i].p1.uv}, - .p2 = (sgVert3){.pos = (sgVec3){tris[i].p2.pos.x, tris[i].p2.pos.y, 0}, .uv = tris[i].p2.uv}, - }; - DrawTriangle2(gfx, &tri3); + DrawTriangle2(gfx, &tris[i]); } } -- cgit v1.2.3