draw.cpp

C++ software renderer

src/draw.cpp

7.15 KB
#include "draw.h"

#include "globals.h"
#include "mesh.h"
#include "texture.h"
#include "util_math.h"

static inline u32 vec3_to_u32RGBA(vec3f &c) {
    u8 r = (u8)(c.x * 255.0f);
    u8 g = (u8)(c.y * 255.0f);
    u8 b = (u8)(c.z * 255.0f);
    u8 a = 255;

    u32 final_color = (r << 24) | (g << 16) | (b << 8) | a;
    return final_color;
};

void clear_color_buffer(u32 color, Window *window) {
    for (int i = 0; i < window->width * window->height; i++) {
        window->back_buffer[i] = color;
    };
};

// (0,0) bottom left, screen space origin
// Y- in NDC is X+ in screen space
// Z+ in NDC is Y+ in screen space
static inline void draw_pixel(int x, int y, u32 color, Window *window) {
    if (x >= 0 && x < window->width && y >= 0 && y < window->height) {
        int flipped_y                                        = (window->height - 1) - y;
        window->back_buffer[(window->width * flipped_y) + x] = color;
    };
};

void draw_rect(int x, int y, int width, int height, u32 color, Window *window) {
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int offset_x = x + j;
            int offset_y = y - i;
            draw_pixel(offset_x, offset_y, color, window);
        };
    };
};

void draw_line_grid(u32 color, Window *window, u8 spacing = 10) {
    for (int y = 0; y < window->height; y++) {
        for (int x = 0; x < window->width; x++) {
            if (x % spacing == 0 || y % spacing == 0) {
                draw_pixel(x, y, color, window);
            };
        };
    };
};

// Bresenham's Line Algorithm
// https://wikipedia.org/wiki/Bresenham's_line_algorithm
// TODO: Wu's whenever I add pure primitives toggling
void draw_line(int x0, int y0, int x1, int y1, u32 color, Window *window) {
    // differences
    int delta_x = (x1 > x0) ? (x1 - x0) : (x0 - x1);
    int delta_y = (y1 > y0) ? (y1 - y0) : (y0 - y1);
    // direction
    int step_x = (x0 < x1) ? 1 : -1;
    int step_y = (y0 < y1) ? 1 : -1;
    // error term, to adjust direction
    int error_calib = delta_x - delta_y;

    while (1) {
        draw_pixel(x0, y0, color, window);
        if (x0 == x1 && y0 == y1) {
            break;
        }
        int error_double = 2 * error_calib;
        if (error_double > -delta_y) {
            error_calib -= delta_y;
            x0 += step_x;
        }
        if (error_double < delta_x) {
            error_calib += delta_x;
            y0 += step_y;
        };
    };
};

static inline void process_fragment(int buff_idx, float pixel_depth, u32 pixel_color, Window *window) {
    float current_depth_at_pixel = window->depth_buffer[buff_idx];
    if (pixel_depth < current_depth_at_pixel) {
        window->depth_buffer[buff_idx] = pixel_depth;
        window->back_buffer[buff_idx]  = pixel_color;
    }
}

void rasterize_triangle(ScreenTriangle *tri, Window *window) {
    vec3f v0 = tri->points[0];
    vec3f v1 = tri->points[1];
    vec3f v2 = tri->points[2];

    float inv_w0 = tri->inv_w[0];
    float inv_w1 = tri->inv_w[1];
    float inv_w2 = tri->inv_w[2];

    vec3f c0 = tri->color_over_w[0];
    vec3f c1 = tri->color_over_w[1];
    vec3f c2 = tri->color_over_w[2];

    float ndc_depth0 = tri->ndc_depth[0];
    float ndc_depth1 = tri->ndc_depth[1];
    float ndc_depth2 = tri->ndc_depth[2];

    vec2f uv_over_w0 = tri->uv_over_w[0];
    vec2f uv_over_w1 = tri->uv_over_w[1];
    vec2f uv_over_w2 = tri->uv_over_w[2];

    Material mat = materials[tri->mat_idx];

    // bounding box (expensive fmin, fmax here I think)
    float minX_f = fmin(v0.x, fmin(v1.x, v2.x));
    float minY_f = fmin(v0.y, fmin(v1.y, v2.y));
    float maxX_f = fmax(v0.x, fmax(v1.x, v2.x));
    float maxY_f = fmax(v0.y, fmax(v1.y, v2.y));

    minX_f = fmax(0.0f, minX_f);
    minY_f = fmax(0.0f, minY_f);
    maxX_f = fmin(static_cast<float>(window->width - 1), maxX_f);
    maxY_f = fmin(static_cast<float>(window->height - 1), maxY_f);

    int minX = static_cast<int>(floor(minX_f));
    int minY = static_cast<int>(floor(minY_f));
    int maxX = static_cast<int>(ceil(maxX_f));
    int maxY = static_cast<int>(ceil(maxY_f));

    // edge coefficients (dY, -dX) pineda-style rasterization
    float dX01 = v0.x - v1.x;  // delta X (v0 to v1)
    float dY01 = v0.y - v1.y;  // delta Y (v0 to v1)
    float dX12 = v1.x - v2.x;  // delta X (v1 to v2)
    float dY12 = v1.y - v2.y;  // delta Y (v1 to v2)
    float dX20 = v2.x - v0.x;  // delta X (v2 to v0)
    float dY20 = v2.y - v0.y;  // delta Y (v2 to v0)

    float triangleArea = (v1.y - v2.y) * (v0.x - v2.x) + (v2.x - v1.x) * (v0.y - v2.y);
    if (fabs(triangleArea) < 1e-6f) {  // degen triangle
        return;
    }
    float invArea = 1.0f / triangleArea;

    // center of the top-left pixel in the bounding box
    vec2f pixel_start = {(float)minX + 0.5f, (float)minY + 0.5f};

    // initial edge function (A0, B0, G0) for pixel_start
    float E0 = (v1.y - v2.y) * (pixel_start.x - v2.x) + (v2.x - v1.x) * (pixel_start.y - v2.y);
    float E1 = (v2.y - v0.y) * (pixel_start.x - v0.x) + (v0.x - v2.x) * (pixel_start.y - v0.y);
    float E2 = (v0.y - v1.y) * (pixel_start.x - v1.x) + (v1.x - v0.x) * (pixel_start.y - v1.y);

    // x delta step (move right one pixel)
    float stepX0 = dY12;
    float stepX1 = dY20;
    float stepX2 = dY01;

    // y delta step (move down one row)
    float stepY0 = dX12;
    float stepY1 = dX20;
    float stepY2 = dX01;

    float E0_row = E0;
    float E1_row = E1;
    float E2_row = E2;

    for (int y = minY; y <= maxY; ++y) {
        // reset
        E0 = E0_row;
        E1 = E1_row;
        E2 = E2_row;

        int flipped_y = (window->height - 1) - y;  // draw_pixel origin convention
        int row_start = flipped_y * window->width;

        // inside tri
        for (int x = minX; x <= maxX; ++x) {
            if (E0 >= 0.0f && E1 >= 0.0f && E2 >= 0.0f) {
                int buffer_idx = row_start + x;

                // edge based barycentric coords eval
                float alpha = E0 * invArea;
                float beta  = E1 * invArea;
                float gamma = E2 * invArea;

                // lerp the attribute accross the triangle's barycentric coords
                float lerp_inv_w = alpha * inv_w0 + beta * inv_w1 + gamma * inv_w2;
                float lerp_depth = alpha * ndc_depth0 + beta * ndc_depth1 + gamma * ndc_depth2;
                vec2f lerp_uv    = uv_over_w0 * alpha + uv_over_w1 * beta + uv_over_w2 * gamma;

                // vec3f lerped_color = alpha * c0 + beta * c1 + gamma * c2;
                // vec3f color     = lerped_color / lerp_inv_w;
                // u32   u32_color = vec3_to_u32RGBA(color);

                vec2f uv       = lerp_uv / lerp_inv_w;
                u32   tex_rgba = sample_texture(mat.texture_img_idx, uv);

                process_fragment(buffer_idx, lerp_depth, tex_rgba, window);
            }

            // increment edge functions
            // advance right one pixel: x -> x + 1
            E0 += stepX0;
            E1 += stepX1;
            E2 += stepX2;
        }

        // increment edge functions
        // advance one row: y -> y + 1
        // NOTE: Edge stepping for Y is subtraction since we start a triangle at top left (0, 0)
        E0_row -= stepY0;
        E1_row -= stepY1;
        E2_row -= stepY2;
    }
}