From 454f82f12325b07b234d06dbd526f68cf00fdd55 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sun, 4 Jan 2026 20:45:07 -0800 Subject: Bilinear texture filtering --- src/swgfx.c | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/swgfx.c b/src/swgfx.c index 7ade051..c54fdc1 100644 --- a/src/swgfx.c +++ b/src/swgfx.c @@ -46,11 +46,15 @@ typedef struct swgfx { // before rendering the model's triangles. sgMat4 viewProj; // View-projection matrix. sgMat4 mvp; // Model-view-projection matrix. - const sgTexture_t* texture;// User-specified texture. - sgTexture_t defaultTexture; // A default for when no texture is provided. + const sgImage* texture;// User-specified texture. + sgImage defaultTexture; // A default for when no texture is provided. sgPixel defaultPixel; // The single-pixel of the default texture. } swgfx; +static inline int mod(int a, int m) { return (m + (a % m)) % m; } + +static inline R frac(R a) { return a - (R)((int)a); } + static inline R rmin(R a, R b) { return (a <= b) ? a : b; } static inline R rmax(R a, R b) { return (a >= b) ? a : b; } static inline int imin(int a, int b) { return (a <= b) ? a : b; } @@ -61,6 +65,8 @@ static inline sgVec2 max2(sgVec2 a, sgVec2 b) { return (sgVec2){.x = rmax(a.x, static inline sgVec2i min2i(sgVec2i a, sgVec2i b) { return (sgVec2i){.x = imin(a.x, b.x), .y = imin(a.y, b.y) }; } static inline sgVec2i max2i(sgVec2i a, sgVec2i b) { return (sgVec2i){.x = imax(a.x, b.x), .y = imax(a.y, b.y) }; } +static inline sgVec2 frac2(sgVec2 v) { return (sgVec2){frac(v.x), frac(v.y)}; } + static inline sgVec2 add2(sgVec2 a, sgVec2 b) { return (sgVec2){a.x + b.x, a.y + b.y}; } static inline sgVec2 scale2(sgVec2 v, R s) { return (sgVec2){v.x * s, v.y * s}; } @@ -71,6 +77,14 @@ static inline R dot3(sgVec3 a, sgVec3 b) { return a.x * b.x + a.y * b.y + a.z * static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } static inline R norm3 (sgVec3 v) { return (R)sqrt(normsq3(v)); } +static inline sgVec4 lerp4(sgVec4 a, sgVec4 b, R t) { + return (sgVec4){ + .x = a.x + t * (b.x - a.x), + .y = a.y + t * (b.y - a.y), + .z = a.z + t * (b.z - a.z), + .w = a.w + t * (b.w - a.w)}; +} + static inline sgVec3 cross3(sgVec3 a, sgVec3 b) { return (sgVec3) { a.y * b.z - a.z * b.y, @@ -237,6 +251,13 @@ static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) { 0, 0, -1, 0); } +static inline sgVec4 PixelToVec4(sgPixel p) { + return (sgVec4){(R)p.r / 255.f, (R)p.g / 255.f, (R)p.b / 255.f, (R)p.a / 255.f}; +} +static inline sgPixel Vec4ToPixel(sgVec4 p) { + return (sgPixel){(int)(p.x * 255.f), (int)(p.y * 255.f), (int)(p.z * 255.f), (int)(p.w * 255.f)}; +} + #ifndef _NDEBUG static bool InBounds(int width, int height, int x, int y) { return (0 <= x) && (x < width) && @@ -258,28 +279,70 @@ static inline R* Depth(swgfx* gfx, int x, int y) { return gfx->depth + (y * gfx->dims.x) + x; } -void SetPixel(swgfx* gfx, const sgVec2i p, sgPixel colour) { +static inline void SetPixel(swgfx* gfx, const sgVec2i p, sgPixel colour) { assert(gfx); *Pixel(gfx, p.x, p.y) = colour; } -void SetDepth(swgfx* gfx, const sgVec2i p, R depth) { +static inline void SetDepth(swgfx* gfx, const sgVec2i p, R depth) { assert(gfx); *Depth(gfx, p.x, p.y) = depth; } -// TODO: Mipmapping. -sgPixel Sample(const sgTexture_t* texture, sgVec2 uv) { +static inline sgPixel ReadTexture(const sgImage* texture, sgVec2i xy) { + assert(texture); + assert(texture->pixels); + assert(InBounds(texture->width, texture->height, xy.x, xy.y)); + return texture->pixels[xy.y * texture->width + xy.x]; +} +// Output normalized to [0,1]. +static inline sgVec4 ReadTextureFloat(const sgImage* texture, sgVec2i xy) { + return PixelToVec4(ReadTexture(texture, xy)); +} + +static inline sgVec2i TextureRepeat(const sgImage* texture, sgVec2i p) { + return (sgVec2i){mod(p.x, texture->width), mod(p.y, texture->height)}; +} + +static inline sgPixel SampleNearest(const sgImage* texture, sgVec2 uv) { + assert(texture); + assert(texture->pixels); + const sgVec2i xy = { + (int)(uv.x * (R)texture->width), + (int)(uv.y * (R)texture->height)}; + const sgVec2i xy2 = TextureRepeat(texture, xy); + return ReadTexture(texture, xy2); +} + +static inline sgPixel SampleBilinear(const sgImage* texture, sgVec2 uv) { assert(texture); assert(texture->pixels); -#define INDEX(X,Y) texture->pixels[(Y) * texture->width + (X)] - // Doing a nearest sample for now. TODO: Other sampling strategies. - const int x = (int)(uv.x * (R)texture->width); - const int y = (int)(uv.y * (R)texture->height); - // Repeat for now. TODO: Clamping and other strategies. - const int xx = x % texture->width; - const int yy = y % texture->height; - return INDEX(xx,yy); +#define ADDR(x,y) TextureRepeat(texture, (sgVec2i){x,y}) + // Find the closest grid vertex, then interpolate the 4 neighbouring pixel + // centers. + const sgVec2i tl = ADDR( + (int)(uv.x * (R)(texture->width - 1)), + (int)(uv.y * (R)(texture->height - 1))); + const sgVec2i tr = ADDR(tl.x+1, tl.y); + const sgVec2i bl = ADDR(tl.x, tl.y+1); + const sgVec2i br = ADDR(tl.x+1, tl.y+1); + const sgVec2 t = frac2(uv); + const sgVec4 tl_pix = ReadTextureFloat(texture, tl); + const sgVec4 tr_pix = ReadTextureFloat(texture, tr); + const sgVec4 bl_pix = ReadTextureFloat(texture, bl); + const sgVec4 br_pix = ReadTextureFloat(texture, br); + const sgVec4 x1 = lerp4(tl_pix, tr_pix, t.x); + const sgVec4 x2 = lerp4(bl_pix, br_pix, t.x); + const sgVec4 y = lerp4(x1, x2, t.y); + return Vec4ToPixel(y); +} + +// TODO: Mipmapping. +// TODO: Clamping and other addressing strategies. +static inline sgPixel Sample(const sgImage* texture, sgVec2 uv) { + // TODO: Add a member to sgImage that determines how it should be filtered. + //return SampleNearest(texture, uv); + return SampleBilinear(texture, uv); } static inline sgAABB2 TriangleAabb2(sgVec2 p0, sgVec2 p1, sgVec2 p2) { @@ -489,7 +552,7 @@ swgfx* sgNew(int width, int height, void* mem) { gfx->colour = SG_ALLOC(&aligned, width * height, sgPixel); gfx->depth = SG_ALLOC(&aligned, width * height, R); gfx->defaultPixel = (sgPixel){255, 255, 255, 255}; - gfx->defaultTexture = (sgTexture_t){ + gfx->defaultTexture = (sgImage){ .width = 1, .height = 1, .pixels = &gfx->defaultPixel, @@ -578,7 +641,7 @@ void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { gfx->viewport = (sgViewport_t){x0, y0, width, height}; } -void sgTexture(swgfx* gfx, const sgTexture_t* texture) { +void sgTexture(swgfx* gfx, const sgImage* texture) { assert(gfx); assert(texture); gfx->texture = texture; -- cgit v1.2.3