renderer.cpp

C++ software renderer

src/renderer.cpp

17.12 KB
#include "renderer.h"

#include <math.h>
#include <stdio.h>

#include "SDL.h"
#include "camera.h"
#include "clip.h"
#include "draw.h"
#include "mesh.h"
#include "texture.h"
#include "util_math.h"

static_global Window window = {
    .sdl_window   = nullptr,
    .renderer     = nullptr,
    .front_buffer = nullptr,
    .back_buffer  = nullptr,
    .width        = 1280,
    .height       = 800,
};

static_global Camera camera = {
    .position = {-5.0, 0.0, 1.25},
    .forward  = {1.0, 0.0, 0.0},
    .right    = {0.0, -1.0, 0.0},
    .up       = {0.0, 0.0, 1.0},
    .pitch    = 0.0,
    .yaw      = 0.0,
};

static_global InputState input_state = {0};
static_global bool       IS_RUNNING  = false;

static_global float aspect       = float(window.width) / float(window.height);
static_global float fov_y        = 59.0f * M_PI / 180.0f;
static_global float grid_spacing = (fmin(window.width, window.height) / 2) / 10;

static_global vector<Mesh> scene;  // vertex stage input

// should be a part of a single big scene struct
vector<Material> materials;
vector<Texture>  textures;

static_global vector<ScreenTriangle> raster_queue;  // raster stage input

void initialize_scene() {
    Texture  missing_texture  = load_texture("../assets/Missing_t.png");
    Material missing_material = {
        .name            = "missing_material",
        .ambient         = vec3f(1.0f, 1.0f, 1.0f),
        .diffuse         = vec3f(1.0f, 1.0f, 1.0f),
        .specular        = vec3f(1.0f, 1.0f, 1.0f),
        .texture_img_idx = 0,
    };
    textures.push_back(missing_texture);
    materials.push_back(missing_material);

    Mesh plane = parse_obj("../assets/Plane.obj");
    scene.emplace_back(plane);
    scene.back().transform.position = {0.0, 0.0, 0.0};
    scene.back().transform.scale    = {10.0, 10.0, 10.0};
    scene.back().color              = {0.1, 0.1, 0.1};

    Mesh tri = parse_obj("../assets/Tri.obj");
    scene.emplace_back(tri);
    scene.back().transform.position = {3.0, 3.0, 1.5};
    scene.back().transform.scale    = {2.0, 2.0, 2.0};
    scene.back().color              = {0.0, 0.0, 0.5};

    Mesh cube = parse_obj("../assets/Cube.obj");
    scene.emplace_back(cube);
    scene.back().transform.position = {3.0, -3.0, 1.0};
    scene.back().color              = {0.5, 0.0, 0.0};

    Mesh duck = parse_obj("../assets/Duck.obj");
    scene.emplace_back(duck);
    scene.back().transform.position = {0.0, 3.0, 0.65};
    scene.back().color              = {0.5, 0.5, 0.0};

    Mesh teapot = parse_obj("../assets/Teapot.obj");
    scene.emplace_back(teapot);
    scene.back().transform.position = {0.0, -3.0, 0.65};
    scene.back().color              = {0.0, 0.5, 0.0};
}

void update_scene() {
    scene[1].transform.rotation.x += 0.01f;
    scene[2].transform.rotation.y += 0.01f;
    scene[3].transform.rotation.z += 0.01f;
    scene[4].transform.rotation.z += 0.01f;
}

void process_input(void) {
    // reset mouse delta
    input_state.mouse_dx = 0;
    input_state.mouse_dy = 0;

    // mouse input
    int x, y;
    SDL_GetRelativeMouseState(&x, &y);
    input_state.mouse_dx = x;
    input_state.mouse_dy = y;

    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        switch (event.type) {
            case SDL_QUIT:
                IS_RUNNING = false;
                break;
            case SDL_KEYDOWN:
            case SDL_KEYUP: {
                bool is_down = (event.type == SDL_KEYDOWN);
                switch (event.key.keysym.sym) {
                    case SDLK_ESCAPE:
                        IS_RUNNING = false;
                        break;
                    case SDLK_w:
                        input_state.move_forward = is_down;
                        break;
                    case SDLK_s:
                        input_state.move_backward = is_down;
                        break;
                    case SDLK_a:
                        input_state.move_left = is_down;
                        break;
                    case SDLK_d:
                        input_state.move_right = is_down;
                        break;
                    case SDLK_e:
                        input_state.move_down = is_down;
                        break;
                    case SDLK_q:
                        input_state.move_up = is_down;
                        break;
                }
            } break;
        }
    }
}

// same convention as rot functions in math.h
// increasing angle == CCW rot, source
void update_camera(float dt) {
    float move_speed        = 10.0f;
    float mouse_sensitivity = 0.0025f;
    float velocity          = move_speed * dt;

    camera.yaw -= input_state.mouse_dx * mouse_sensitivity;
    camera.pitch -= input_state.mouse_dy * mouse_sensitivity;
    camera.pitch = fmaxf(-1.5f, fminf(1.5f, camera.pitch));

    // not the biggest fan of trig here,
    // rather this than the matrices juggling
    // perfect place to do quaternions later maybe
    camera.forward = {
        cosf(camera.pitch) * cosf(camera.yaw),
        cosf(camera.pitch) * sinf(camera.yaw),
        sinf(camera.pitch)
    };

    camera.forward = camera.forward.normalized();
    camera.right   = camera.forward.cross(vec3f(0.0, 0.0, 1.0)).normalized();
    camera.up      = camera.right.cross(camera.forward).normalized();

    if (input_state.move_forward) {
        camera.position = camera.position + camera.forward * velocity;
    }
    if (input_state.move_backward) {
        camera.position = camera.position - camera.forward * velocity;
    }
    if (input_state.move_left) {
        camera.position = camera.position - camera.right * velocity;
    }
    if (input_state.move_right) {
        camera.position = camera.position + camera.right * velocity;
    }
    if (input_state.move_up) {
        camera.position.z += velocity;
    }
    if (input_state.move_down) {
        camera.position.z -= velocity;
    }
}

inline vec3f project_to_screen(
    const vec4f &clip_pos, const float &inv_w, int win_width, int win_height
) {
    // NDC construction, should remap to []
    float ndc_x = clip_pos.x * inv_w;  // NDC depth coordinate
    float ndc_y = clip_pos.y * inv_w;  // NDC screen X coordinate
    float ndc_z = clip_pos.z * inv_w;  // NDC screen Y coordinate

    // screen space mapping (y is horizontal and z is vertical)
    float screen_x = (ndc_y + 1.0f) * 0.5f * win_width;
    float screen_y = (ndc_z + 1.0f) * 0.5f * win_height;

    return {screen_x, screen_y, ndc_x};
}

void vertex_stage(
    Mesh &m, Camera &camera, float fov_y, float aspect, vector<vec3f> &world_verts,
    vector<vec3f> &view_verts
) {
    world_verts.resize(m.vertices.size());
    view_verts.resize(m.vertices.size());

    mat4 world_mat = m.transform.model_matrix();
    mat4 view_mat  = lookAt(&camera);

    for (int i = 0; i < m.vertices.size(); i++) {
        vec4f obj_v = vec4f(m.vertices[i], 1.0f);

        // world space
        vec4f world_v  = obj_v * world_mat;
        world_verts[i] = vec3f(world_v.x, world_v.y, world_v.z);

        // view space
        vec4f view_v  = world_v * view_mat;
        view_verts[i] = vec3f(view_v.x, view_v.y, view_v.z);
    }
}

// just bool wrappers over the same planes defined for clipping
bool inside_x_min(const vec4f &v) {
    return plane_x_min(v) >= 0.0f;
}
bool inside_x_max(const vec4f &v) {
    return plane_x_max(v) >= 0.0f;
}
bool inside_y_min(const vec4f &v) {
    return plane_y_min(v) >= 0.0f;
}
bool inside_y_max(const vec4f &v) {
    return plane_y_max(v) >= 0.0f;
}
bool inside_z_min(const vec4f &v) {
    return plane_z_min(v) >= 0.0f;
}
bool inside_z_max(const vec4f &v) {
    return plane_z_max(v) >= 0.0f;
}

enum FrustumStatus {
    Outside,
    Inside,
    Straddle
};

FrustumStatus triangle_frustum_status(const vec4f verts[3]) {
    bool any_straddle = false;

    bool (*planes[6])(const vec4f &) = {
        inside_x_min,
        inside_x_max,
        inside_y_min,
        inside_y_max,
        inside_z_min,
        inside_z_max
    };

    for (int p = 0; p < 6; p++) {
        int inside  = 0;
        int outside = 0;

        for (int i = 0; i < 3; i++) {
            if (planes[p](verts[i])) {
                inside++;
            } else {
                outside++;
            }
        }

        if (outside == 3) {
            return Outside;
        }

        if (inside > 0 && outside > 0) {
            any_straddle = true;
        }
    }

    return any_straddle ? Straddle : Inside;
}

static inline vec2f get_uv(const vector<vec2f> &texcoords, int idx) {
    if (idx >= 0 && idx < (int)texcoords.size()) {
        return texcoords[idx];
    }
    return vec2f(0.0f, 0.0f);
}

void run_pipeline(void) {
    raster_queue.clear();

    mat4 projection_matrix = perspective(fov_y, aspect, 0.1f, 5000.0f);

    // NOTE: not 100% sold on the static here, look into this
    static_local vector<vec3f> world_verts;
    static_local vector<vec3f> view_verts;
    static_local vector<ClipVertex> clip_verts;

    for (int mesh_idx = 0; mesh_idx < scene.size(); mesh_idx++) {
        Mesh &m = scene[mesh_idx];

        world_verts.resize(m.vertices.size());
        view_verts.resize(m.vertices.size());
        clip_verts.resize(m.vertices.size());

        vertex_stage(m, camera, fov_y, aspect, world_verts, view_verts);

        for (int i = 0; i < m.faces.size(); i += 3) {
            Face f0 = m.faces[i + 0];
            Face f1 = m.faces[i + 1];
            Face f2 = m.faces[i + 2];

            // populated by vertex_stage()
            vec3f v0_view = view_verts[f0.vertex_idx];
            vec3f v1_view = view_verts[f1.vertex_idx];
            vec3f v2_view = view_verts[f2.vertex_idx];

            vec3f v0_world = world_verts[f0.vertex_idx];
            vec3f v1_world = world_verts[f1.vertex_idx];
            vec3f v2_world = world_verts[f2.vertex_idx];

            // TODO: use the Mesh vertex normal
            vec3f world_edge1 = v1_world - v0_world;
            vec3f world_edge2 = v2_world - v0_world;
            vec3f face_normal = world_edge2.cross(world_edge1).normalized();

            // backface culling (view space)
            vec3f tri_edge1  = v1_view - v0_view;
            vec3f tri_edge2  = v2_view - v0_view;
            vec3f tri_normal = tri_edge2.cross(tri_edge1).normalized();
            if (tri_normal.dot(v0_view) >= 0.0f) {
                continue;
            }

            // early and cheap near plane rejection (view space)
            const float near_plane_dist   = 0.1f;  // same as proj_mat
            int         behind_vert_count = 0;
            if (v0_view.x < near_plane_dist) {
                behind_vert_count++;
            }
            if (v1_view.x < near_plane_dist) {
                behind_vert_count++;
            }
            if (v2_view.x < near_plane_dist) {
                behind_vert_count++;
            }
            if (behind_vert_count == 3) {
                continue;
            }

            // only the worthy go further (clip space)
            vec4f v0_clip = vec4f(v0_view, 1.0f) * projection_matrix;
            vec4f v1_clip = vec4f(v1_view, 1.0f) * projection_matrix;
            vec4f v2_clip = vec4f(v2_view, 1.0f) * projection_matrix;

            vec4f tri_clip[3] = {v0_clip, v1_clip, v2_clip};  // clip space tri
            vec2f tri_uv[3]   = {
                get_uv(m.texcoords, f0.vertex_texture_idx),
                get_uv(m.texcoords, f1.vertex_texture_idx),
                get_uv(m.texcoords, f2.vertex_texture_idx)
            };

            FrustumStatus status = triangle_frustum_status(tri_clip);
            if (status == Outside) {
                continue;  // reject
            }

            clip_verts.clear();
            if (status == Inside) {
                clip_verts.reserve(3);
                for (int vi = 0; vi < 3; vi++) {
                    ClipVertex cv;
                    cv.pos    = tri_clip[vi];
                    cv.color  = m.color;
                    cv.normal = tri_normal;
                    cv.uv     = tri_uv[vi];
                    clip_verts.push_back(cv);
                }
            } else {
                // straddle case
                // expernsive 6 plane clipping so get here if all else fails
                clip_verts = clip_polygon_with_attrs(tri_clip, tri_uv, m.color, face_normal);
                if (clip_verts.size() < 3) {
                    continue;
                }
            }

            for (int k = 1; k < clip_verts.size() - 1; ++k) {
                // NOTE: rudimentary triangulation, not sure if somethin more robust is used or
                // needed tbh. Look into it
                const ClipVertex &cv0  = clip_verts[0];
                const ClipVertex &cvk  = clip_verts[k];
                const ClipVertex &cvk1 = clip_verts[k + 1];

                ScreenTriangle tri;

                const ClipVertex cvs[3] = {cv0, cvk, cvk1};
                for (int i = 0; i < 3; ++i) {
                    const vec4f &p     = cvs[i].pos;
                    float        inv_w = 1.0f / p.w;
                    vec3f        ndc   = project_to_screen(p, inv_w, window.width, window.height);

                    tri.points[i]       = ndc;
                    tri.inv_w[i]        = 1.0f / cvs[i].pos.w;
                    tri.uv_over_w[i]    = cvs[i].uv * inv_w;
                    tri.color_over_w[i] = cvs[i].color * inv_w;
                    tri.ndc_depth[i]    = ndc.z;
                    tri.mat_idx         = f0.mat_idx;
                }
                raster_queue.push_back(tri);
            }
        }
    }
}

void rasterize(void) {
    clear_color_buffer(0x00000000, &window);

    //  clear depth buffer
    for (int i = 0; i < window.width * window.height; ++i) {
        // NOTE: look into log depth
        window.depth_buffer[i] = 1.0f;
    }

    draw_line_grid(0x252525FF, &window, grid_spacing);
    draw_line(window.width / 2, window.height, window.width / 2, 0, 0x656565FF, &window);
    draw_line(0, window.height / 2, window.width, window.height / 2, 0x656565FF, &window);

    for (int i = 0; i < raster_queue.size(); i++) {
        ScreenTriangle &triangle = raster_queue[i];

        // triangle primitives
        rasterize_triangle(&triangle, &window);

        // line primitives
        // const u32 line_color = 0x000000FF;
        // draw_line(
        //     triangle.points[0].x,
        //     triangle.points[0].y,
        //     triangle.points[1].x,
        //     triangle.points[1].y,
        //     line_color,
        //     &window
        // );
        // draw_line(
        //     triangle.points[1].x,
        //     triangle.points[1].y,
        //     triangle.points[2].x,
        //     triangle.points[2].y,
        //     line_color,
        //     &window
        // );
        // draw_line(
        //     triangle.points[2].x,
        //     triangle.points[2].y,
        //     triangle.points[0].x,
        //     triangle.points[0].y,
        //     line_color,
        //     &window
        // );

        // points primitives
        // draw_rect(triangle.points[0].x, triangle.points[0].y, 5, 5, line_color, &window);
        // draw_rect(triangle.points[1].x, triangle.points[1].y, 5, 5, line_color, &window);
        // draw_rect(triangle.points[2].x, triangle.points[2].y, 5, 5, line_color, &window);
    }

    // blit the color buffer onto a texture
    // basically the present frame stage in a pipeline
    SDL_RenderPresent(window.renderer);
    SDL_UpdateTexture(window.front_buffer, nullptr, window.back_buffer, (int)(window.width * sizeof(u32)));
    SDL_RenderCopy(window.renderer, window.front_buffer, nullptr, nullptr);
}

int main(int argc, char *argv[]) {
    // init sdl
    if (SDL_Init(SDL_INIT_VIDEO != 0 || SDL_INIT_EVENTS) != 0) {
        printf("SDL_Init failed: %s\n", SDL_GetError());
        return -1;
    }
    window.sdl_window = SDL_CreateWindow(
        "cpp-rasterizer",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        window.width,
        window.height,
        SDL_WINDOW_SHOWN
    );
    if (!window.sdl_window) {
        printf("SDL_CreateWindow failed: %s\n", SDL_GetError());
        return -1;
    }
    window.renderer = SDL_CreateRenderer(window.sdl_window, -1, 0);
    if (!window.renderer) {
        printf("SDL_CreateRenderer failed: %s\n", SDL_GetError());
        return -1;
    }
    window.front_buffer = SDL_CreateTexture(
        window.renderer,
        SDL_PIXELFORMAT_RGBA8888,
        SDL_TEXTUREACCESS_STREAMING,
        window.width,
        window.height
    );
    if (!window.front_buffer) {
        printf("Error creating SDL texture: %s\n", SDL_GetError());
    }
    SDL_SetRelativeMouseMode(SDL_TRUE);

    // init globals
    window.back_buffer  = (u32 *)malloc(sizeof(u32) * window.height * window.width);
    window.depth_buffer = (float *)malloc(sizeof(float) * window.height * window.width);
    initialize_scene();
    IS_RUNNING = true;

    u32   last_frame_time = 0;
    float delta_time      = 0.0f;
    while (IS_RUNNING) {
        u32 current_time = SDL_GetTicks();
        delta_time       = (current_time - last_frame_time) / 1000.0f;
        last_frame_time  = current_time;
        update_scene();
        process_input();
        update_camera(delta_time);
        run_pipeline();
        rasterize();
    }

    free(window.depth_buffer);
    free(window.back_buffer);
    SDL_DestroyRenderer(window.renderer);
    SDL_DestroyWindow(window.sdl_window);
    SDL_Quit();

    return 0;
}