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}