rasterizer

c++ software renderer

renderer.cpp

17 kB
  1#include "renderer.hpp"
  2
  3#include <stdio.h>
  4
  5#include <cmath>
  6#include <vector>
  7
  8#include "SDL.h"
  9#include "camera.hpp"
 10#include "clip.hpp"
 11#include "draw.hpp"
 12#include "math.hpp"
 13#include "mesh.hpp"
 14
 15using std::vector;
 16
 17static_global Window window = {
 18    .sdl_window   = nullptr,
 19    .renderer     = nullptr,
 20    .front_buffer = nullptr,
 21    .back_buffer  = nullptr,
 22    .width        = 1280,
 23    .height       = 800,
 24};
 25
 26static_global Camera camera = {
 27    .position = {-5.0, 0.0, 1.25},
 28    .forward  = {1.0, 0.0, 0.0},
 29    .right    = {0.0, -1.0, 0.0},
 30    .up       = {0.0, 0.0, 1.0},
 31    .pitch    = 0.0,
 32    .yaw      = 0.0,
 33};
 34
 35static_global InputState input_state = {0};
 36static_global bool       IS_RUNNING  = false;
 37
 38static_global float aspect       = float(window.width) / float(window.height);
 39static_global float fov_y        = 59.0f * M_PI / 180.0f;
 40static_global float grid_spacing = (fmin(window.width, window.height) / 2) / 10;
 41
 42// NOTE: should be like a module type thingy along with the entire pipeline?
 43static_global vector<Mesh> scene;                   // vertex stage input
 44static_global vector<ScreenTriangle> raster_queue;  // raster stage input
 45
 46void initialize_scene() {
 47    Mesh plane = parse_obj("../assets/Plane.obj");
 48    scene.emplace_back(plane);
 49    scene.back().transform.position = {0.0, 0.0, 0.0};
 50    scene.back().transform.scale    = {10.0, 10.0, 10.0};
 51    scene.back().color              = {0.1, 0.1, 0.1};
 52
 53    Mesh tri = parse_obj("../assets/Tri.obj");
 54    scene.emplace_back(tri);
 55    scene.back().transform.position = {3.0, 3.0, 1.5};
 56    scene.back().transform.scale    = {2.0, 2.0, 2.0};
 57    scene.back().color              = {0.0, 0.0, 0.5};
 58
 59    Mesh cube = parse_obj("../assets/Cube.obj");
 60    scene.emplace_back(cube);
 61    scene.back().transform.position = {3.0, -3.0, 1.0};
 62    scene.back().color              = {0.5, 0.0, 0.0};
 63
 64    Mesh duck = parse_obj("../assets/Duck.obj");
 65    scene.emplace_back(duck);
 66    scene.back().transform.position = {0.0, 3.0, 0.65};
 67    scene.back().color              = {0.5, 0.5, 0.0};
 68
 69    Mesh teapot = parse_obj("../assets/Teapot.obj");
 70    scene.emplace_back(teapot);
 71    scene.back().transform.position = {0.0, -3.0, 0.65};
 72    scene.back().color              = {0.0, 0.5, 0.0};
 73}
 74
 75void update_scene() {
 76    scene[1].transform.rotation.x += 0.01f;
 77    scene[2].transform.rotation.y += 0.01f;
 78    scene[3].transform.rotation.z += 0.01f;
 79    scene[4].transform.rotation.z += 0.01f;
 80}
 81
 82void process_input(void) {
 83    // reset mouse delta
 84    input_state.mouse_dx = 0;
 85    input_state.mouse_dy = 0;
 86
 87    // mouse input
 88    int x, y;
 89    SDL_GetRelativeMouseState(&x, &y);
 90    input_state.mouse_dx = x;
 91    input_state.mouse_dy = y;
 92
 93    SDL_Event event;
 94    while (SDL_PollEvent(&event)) {
 95        switch (event.type) {
 96            case SDL_QUIT:
 97                IS_RUNNING = false;
 98                break;
 99            case SDL_KEYDOWN:
100            case SDL_KEYUP: {
101                bool is_down = (event.type == SDL_KEYDOWN);
102                switch (event.key.keysym.sym) {
103                    case SDLK_ESCAPE:
104                        IS_RUNNING = false;
105                        break;
106                    case SDLK_w:
107                        input_state.move_forward = is_down;
108                        break;
109                    case SDLK_s:
110                        input_state.move_backward = is_down;
111                        break;
112                    case SDLK_a:
113                        input_state.move_left = is_down;
114                        break;
115                    case SDLK_d:
116                        input_state.move_right = is_down;
117                        break;
118                    case SDLK_e:
119                        input_state.move_up = is_down;
120                        break;
121                    case SDLK_q:
122                        input_state.move_down = is_down;
123                        break;
124                }
125            } break;
126        }
127    }
128}
129
130// same convention as rot functions in math.hpp
131// increasing angle == CCW rot, source
132void update_camera(float dt) {
133    float move_speed        = 10.0f;
134    float mouse_sensitivity = 0.0025f;
135    float velocity          = move_speed * dt;
136
137    camera.yaw -= input_state.mouse_dx * mouse_sensitivity;
138    camera.pitch -= input_state.mouse_dy * mouse_sensitivity;
139    camera.pitch = fmaxf(-1.5f, fminf(1.5f, camera.pitch));
140
141    // not the biggest fan of trig here,
142    // rather this than the matrices juggling
143    // perfect place to do quaternions later maybe
144    camera.forward = {
145        cosf(camera.pitch) * cosf(camera.yaw),
146        cosf(camera.pitch) * sinf(camera.yaw),
147        sinf(camera.pitch)
148    };
149
150    camera.forward = camera.forward.normalized();
151    camera.right   = camera.forward.cross(vec3f(0.0, 0.0, 1.0)).normalized();
152    camera.up      = camera.right.cross(camera.forward).normalized();
153
154    if (input_state.move_forward) {
155        camera.position = camera.position + camera.forward * velocity;
156    }
157    if (input_state.move_backward) {
158        camera.position = camera.position - camera.forward * velocity;
159    }
160    if (input_state.move_left) {
161        camera.position = camera.position - camera.right * velocity;
162    }
163    if (input_state.move_right) {
164        camera.position = camera.position + camera.right * velocity;
165    }
166    if (input_state.move_up) {
167        camera.position.z += velocity;
168    }
169    if (input_state.move_down) {
170        camera.position.z -= velocity;
171}
172}
173
174inline vec3f project_to_screen(
175    const vec4f &clip_pos, const float &inv_w, int win_width, int win_height
176) {
177    // NDC construction, should remap to []
178    float ndc_x = clip_pos.x * inv_w;  // NDC depth coordinate
179    float ndc_y = clip_pos.y * inv_w;  // NDC screen X coordinate
180    float ndc_z = clip_pos.z * inv_w;  // NDC screen Y coordinate
181
182    // screen space mapping (y is horizontal and z is vertical)
183    float screen_x = (ndc_y + 1.0f) * 0.5f * win_width;
184    float screen_y = (ndc_z + 1.0f) * 0.5f * win_height;
185
186    return {screen_x, screen_y, ndc_x};
187}
188
189void vertex_stage(
190    Mesh &m, Camera &camera, float fov_y, float aspect, vector<vec3f> &world_verts, 
191    vector<vec3f> &view_verts
192) {
193    world_verts.resize(m.vertices.size());
194    view_verts.resize(m.vertices.size());
195
196    mat4 world_mat = m.transform.model_matrix();
197    mat4 view_mat  = lookAt(&camera);
198
199    for (int i = 0; i < m.vertices.size(); i++) {
200        vec4f obj_v = vec4f(m.vertices[i], 1.0f);
201
202        // world space
203        vec4f world_v  = obj_v * world_mat;
204        world_verts[i] = vec3f(world_v.x, world_v.y, world_v.z);
205
206        // view space
207        vec4f view_v  = world_v * view_mat;
208        view_verts[i] = vec3f(view_v.x, view_v.y, view_v.z);
209    }
210}
211
212// just bool wrappers over the same planes defined for clipping
213bool inside_x_min(const vec4f &v) {
214    return plane_x_min(v) >= 0.0f;
215}
216bool inside_x_max(const vec4f &v) {
217    return plane_x_max(v) >= 0.0f;
218}
219bool inside_y_min(const vec4f &v) {
220    return plane_y_min(v) >= 0.0f;
221}
222bool inside_y_max(const vec4f &v) {
223    return plane_y_max(v) >= 0.0f;
224}
225bool inside_z_min(const vec4f &v) {
226    return plane_z_min(v) >= 0.0f;
227}
228bool inside_z_max(const vec4f &v) {
229    return plane_z_max(v) >= 0.0f;
230}
231
232enum class FrustumStatus { Outside,
233                           Inside,
234                           Straddle };
235FrustumStatus triangle_frustum_status(const vec4f verts[3]) {
236    bool any_straddle = false;
237
238    bool (*planes[6])(const vec4f &) = {
239        inside_x_min, inside_x_max, inside_y_min, inside_y_max, inside_z_min, inside_z_max
240    };
241
242    for (int p = 0; p < 6; p++) {
243        int inside = 0, outside = 0;
244
245        for (int i = 0; i < 3; i++) {
246            if (planes[p](verts[i])) {
247                inside++;
248            } else {
249                outside++;
250            }
251        }
252
253        if (outside == 3) {
254            return FrustumStatus::Outside;
255        }
256
257        if (inside > 0 && outside > 0) {
258            any_straddle = true;
259        }
260    }
261
262    return any_straddle ? FrustumStatus::Straddle : FrustumStatus::Inside;
263}
264
265static inline vec2f get_uv(const vector<vec2f> &texcoords, int idx) {
266    if (idx >= 0 && idx < (int)texcoords.size()) {
267        return texcoords[idx];
268    }
269    return vec2f(0.0f, 0.0f);
270}
271
272void run_pipeline(void) {
273    raster_queue.clear();
274
275    mat4 projection_matrix = perspective(fov_y, aspect, 0.1f, 5000.0f);
276
277    // NOTE: not 100% sold on the static here, look into this
278    static_local vector<vec3f> world_verts;
279    static_local vector<vec3f> view_verts;
280    static_local vector<ClipVertex> clip_verts;
281
282    for (int mesh_idx = 0; mesh_idx < scene.size(); mesh_idx++) {
283        Mesh &m = scene[mesh_idx];
284
285        world_verts.resize(m.vertices.size());
286        view_verts.resize(m.vertices.size());
287        clip_verts.resize(m.vertices.size());
288
289        vertex_stage(m, camera, fov_y, aspect, world_verts, view_verts);
290
291        for (int i = 0; i < m.faces.size(); i += 3) {
292            Face f0 = m.faces[i + 0];
293            Face f1 = m.faces[i + 1];
294            Face f2 = m.faces[i + 2];
295
296            // populated by vertex_stage()
297            vec3f v0_view = view_verts[f0.vertex_idx];
298            vec3f v1_view = view_verts[f1.vertex_idx];
299            vec3f v2_view = view_verts[f2.vertex_idx];
300
301            vec3f v0_world = world_verts[f0.vertex_idx];
302            vec3f v1_world = world_verts[f1.vertex_idx];
303            vec3f v2_world = world_verts[f2.vertex_idx];
304
305            // TODO: use the Mesh vertex normal
306            vec3f world_edge1 = v1_world - v0_world;
307            vec3f world_edge2 = v2_world - v0_world;
308            vec3f face_normal = world_edge2.cross(world_edge1).normalized();
309
310            // backface culling (view space)
311            vec3f tri_edge1  = v1_view - v0_view;
312            vec3f tri_edge2  = v2_view - v0_view;
313            vec3f tri_normal = tri_edge2.cross(tri_edge1).normalized();
314            if (tri_normal.dot(v0_view) >= 0.0f) {
315                continue;
316            }
317
318            // early and cheap near plane rejection (view space)
319            const float near_plane_dist   = 0.1f;  // same as proj_mat
320            int         behind_vert_count = 0;
321            if (v0_view.x < near_plane_dist) {
322                behind_vert_count++;
323            }
324            if (v1_view.x < near_plane_dist) {
325                behind_vert_count++;
326            }
327            if (v2_view.x < near_plane_dist) {
328                behind_vert_count++;
329            }
330            if (behind_vert_count == 3) {
331                continue;
332            }
333
334            // only the worthy go further (clip space)
335            vec4f v0_clip = vec4f(v0_view, 1.0f) * projection_matrix;
336            vec4f v1_clip = vec4f(v1_view, 1.0f) * projection_matrix;
337            vec4f v2_clip = vec4f(v2_view, 1.0f) * projection_matrix;
338
339            vec4f tri_clip[3] = {v0_clip, v1_clip, v2_clip};  // clip space tri
340            vec2f tri_uv[3]   = {
341                get_uv(m.texcoords, f0.vertex_texture_idx),
342                get_uv(m.texcoords, f1.vertex_texture_idx),
343                get_uv(m.texcoords, f2.vertex_texture_idx)
344            };
345
346            FrustumStatus status = triangle_frustum_status(tri_clip);
347            if (status == FrustumStatus::Outside) {
348                continue;  // reject
349            }
350
351            clip_verts.clear();
352            if (status == FrustumStatus::Inside) {
353                clip_verts.reserve(3);
354                for (int vi = 0; vi < 3; vi++) {
355                    ClipVertex cv;
356                    cv.pos    = tri_clip[vi];
357                    cv.color  = m.color;
358                    cv.normal = tri_normal;
359                    cv.uv     = tri_uv[vi];
360                    clip_verts.push_back(cv);
361                }
362            } else {
363                // straddle case
364                // expernsive 6 plane clipping so get here if all else fails
365                clip_verts = clip_polygon_with_attrs(tri_clip, tri_uv, m.color, face_normal);
366                if (clip_verts.size() < 3) {
367                    continue;
368                }
369            }
370
371            for (int k = 1; k < clip_verts.size() - 1; ++k) {
372                // NOTE: rudimentary triangulation, not sure if somethin more robust is used or
373                // needed tbh. Look into it
374                const ClipVertex &cv0  = clip_verts[0];
375                const ClipVertex &cvk  = clip_verts[k];
376                const ClipVertex &cvk1 = clip_verts[k + 1];
377
378                ScreenTriangle tri;
379
380                const ClipVertex cvs[3] = {cv0, cvk, cvk1};
381                for (int i = 0; i < 3; ++i) {
382                    const vec4f &p     = cvs[i].pos;
383                    float        inv_w = 1.0f / p.w;
384                    vec3f        ndc   = project_to_screen(p, inv_w, window.width, window.height);
385
386                    tri.points[i]       = ndc;
387                    tri.inv_w[i]        = 1.0f / cvs[i].pos.w;
388                    tri.uv_over_w[i]    = cvs[i].uv * inv_w;
389                    tri.color_over_w[i] = cvs[i].color * inv_w;
390                    tri.ndc_depth[i]    = ndc.z;
391                }
392                raster_queue.push_back(tri);
393            }
394        }
395    }
396}
397
398void rasterize(void) {
399    clear_color_buffer(0x00000000, &window);
400
401    //  clear depth buffer
402    for (int i = 0; i < window.width * window.height; ++i) {
403        // NOTE: look into log depth
404        window.depth_buffer[i] = 1.0f;
405    }
406
407    draw_line_grid(0x252525FF, &window, grid_spacing);
408    draw_line(window.width / 2, window.height, window.width / 2, 0, 0x656565FF, &window);
409    draw_line(0, window.height / 2, window.width, window.height / 2, 0x656565FF, &window);
410
411    for (int i = 0; i < raster_queue.size(); i++) {
412        ScreenTriangle &triangle = raster_queue[i];
413        // TODO: toggle primitives
414
415        // triangle primitives
416        rasterize_triangle(&triangle, &window);
417
418        // line primitives
419        // const u32 line_color = 0x000000FF;
420        // draw_line(
421        //     triangle.points[0].x,
422        //     triangle.points[0].y,
423        //     triangle.points[1].x,
424        //     triangle.points[1].y,
425        //     line_color,
426        //     &window
427        // );
428        // draw_line(
429        //     triangle.points[1].x,
430        //     triangle.points[1].y,
431        //     triangle.points[2].x,
432        //     triangle.points[2].y,
433        //     line_color,
434        //     &window
435        // );
436        // draw_line(
437        //     triangle.points[2].x,
438        //     triangle.points[2].y,
439        //     triangle.points[0].x,
440        //     triangle.points[0].y,
441        //     line_color,
442        //     &window
443        // );
444
445        // points primitives
446        // draw_rect(triangle.points[0].x, triangle.points[0].y, 5, 5, line_color, &window);
447        // draw_rect(triangle.points[1].x, triangle.points[1].y, 5, 5, line_color, &window);
448        // draw_rect(triangle.points[2].x, triangle.points[2].y, 5, 5, line_color, &window);
449    }
450
451    // blit the color buffer onto a texture
452    // basically the present frame stage in a pipeline
453    SDL_RenderPresent(window.renderer);
454    SDL_UpdateTexture(window.front_buffer, nullptr, window.back_buffer, (int)(window.width * sizeof(u32)));
455    SDL_RenderCopy(window.renderer, window.front_buffer, nullptr, nullptr);
456}
457
458int main(int argc, char *argv[]) {
459    // init sdl
460    if (SDL_Init(SDL_INIT_VIDEO != 0 || SDL_INIT_EVENTS) != 0) {
461        printf("SDL_Init failed: %s\n", SDL_GetError());
462        return -1;
463    }
464    window.sdl_window = SDL_CreateWindow(
465        "cpp-rasterizer",
466        SDL_WINDOWPOS_CENTERED,
467        SDL_WINDOWPOS_CENTERED,
468        window.width,
469        window.height,
470        SDL_WINDOW_SHOWN
471    );
472    if (!window.sdl_window) {
473        printf("SDL_CreateWindow failed: %s\n", SDL_GetError());
474        return -1;
475    }
476    window.renderer = SDL_CreateRenderer(window.sdl_window, -1, 0);
477    if (!window.renderer) {
478        printf("SDL_CreateRenderer failed: %s\n", SDL_GetError());
479        return -1;
480    }
481    window.front_buffer = SDL_CreateTexture(
482        window.renderer,
483        SDL_PIXELFORMAT_RGBA8888,
484        SDL_TEXTUREACCESS_STREAMING,
485        window.width,
486        window.height
487    );
488    if (!window.front_buffer) {
489        printf("Error creating SDL texture: %s\n", SDL_GetError());
490    }
491    SDL_SetRelativeMouseMode(SDL_TRUE);
492
493    // init globals
494    window.back_buffer  = (u32 *)malloc(sizeof(u32) * window.height * window.width);
495    window.depth_buffer = (float *)malloc(sizeof(float) * window.height * window.width);
496    initialize_scene();
497    IS_RUNNING = true;
498
499    u32   last_frame_time = 0;
500    float delta_time      = 0.0f;
501    while (IS_RUNNING) {
502        u32 current_time = SDL_GetTicks();
503        delta_time       = (current_time - last_frame_time) / 1000.0f;
504        last_frame_time  = current_time;
505        update_scene();
506        process_input();
507        update_camera(delta_time);
508        run_pipeline();
509        rasterize();
510    }
511
512    free(window.depth_buffer);
513    free(window.back_buffer);
514    SDL_DestroyRenderer(window.renderer);
515    SDL_DestroyWindow(window.sdl_window);
516    SDL_Quit();
517
518    return 0;
519}