#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;
}