diff --git a/compile b/compile
index 1a38627..706ff02 100755
--- a/compile
+++ b/compile
@@ -12,6 +12,7 @@ RAYTRACER_SRC="src/window/*.c \
 	src/scene/*.c \
 	src/raytracer/*.c \
 	src/math/*.c \
+	src/camera/*.c \
 "
 
 RASTERISER_SRC="src/window/*.c \
@@ -20,6 +21,7 @@ RASTERISER_SRC="src/window/*.c \
 	src/list/*.c \
 	src/rasteriser/*.c \
 	src/math/*.c \
+	src/camera/*.c \
 	$WAPP_SRC \
 "
 
diff --git a/include/camera/camera.h b/include/camera/camera.h
new file mode 100644
index 0000000..c13a57c
--- /dev/null
+++ b/include/camera/camera.h
@@ -0,0 +1,15 @@
+#ifndef CAMERA_H
+#define CAMERA_H
+
+#include "vector/vec.h"
+#include "window/window.h"
+
+typedef struct {
+  vec3f_t position;
+  vec3f_t rotation;
+} camera_t;
+
+vec2i_t project_point(vec3f_t point, const window_t *wnd,
+                      const camera_t *camera, vec3f_t viewport);
+
+#endif // !CAMERA_H
diff --git a/include/window/window.h b/include/window/window.h
index 1ac1d71..ba9f134 100644
--- a/include/window/window.h
+++ b/include/window/window.h
@@ -38,7 +38,8 @@ void clear_window(window_t *wnd, colour_t colour);
 void set_pixel(window_t *wnd, i32 x, i32 y, colour_t colour);
 void swap_buffers(window_t *wnd);
 
-vec3f_t window_to_viewport(window_t *wnd, i32 x, i32 y, vec3f_t viewport);
+vec3f_t window_to_viewport(const window_t *wnd, i32 x, i32 y, vec3f_t viewport);
+vec2i_t viewport_to_window(const window_t *wnd, f32 x, f32 y, vec3f_t viewport);
 
 colour_t colour_add_colour(colour_t a, colour_t b);
 colour_t colour_mul(colour_t colour, f32 scalar);
diff --git a/src/camera/camera.c b/src/camera/camera.c
new file mode 100644
index 0000000..180987e
--- /dev/null
+++ b/src/camera/camera.c
@@ -0,0 +1,15 @@
+#include "camera/camera.h"
+#include "vector/vec.h"
+#include "window/window.h"
+
+vec2i_t project_point(vec3f_t point, const window_t *wnd,
+                      const camera_t *camera, vec3f_t viewport) {
+  if (point.z == 0.0f) {
+    return (vec2i_t){.x = 0, .y = 0};
+  }
+
+  f32 x = point.x * viewport.x / point.z;
+  f32 y = point.y * viewport.y / point.z;
+
+  return viewport_to_window(wnd, x, y, viewport);
+}
diff --git a/src/rasteriser/main.c b/src/rasteriser/main.c
index 899dd07..6c90563 100644
--- a/src/rasteriser/main.c
+++ b/src/rasteriser/main.c
@@ -1,3 +1,4 @@
+#include "camera/camera.h"
 #include "mem_arena.h"
 #include "mem_utils.h"
 #include "misc/misc_utils.h"
@@ -11,6 +12,12 @@
 int main(void) {
   colour_t bg =
       (colour_t){.rgba.r = 27, .rgba.g = 38, .rgba.b = 79, .rgba.a = 255};
+  camera_t camera = {
+      .position = {.x = 0.0f, .y = 0.0f, .z = 0.0f},
+      .rotation = {.x = 0.0f, .y = 0.0f, .z = 0.0f},
+  };
+  vec3f_t viewport = {.x = 1.0f, .y = 1.0f, .z = 1.0f};
+
   window_t window = {0};
 
   if (!init_window(&window, 800, 800, "CG From Scratch Rasteriser")) {
@@ -25,14 +32,47 @@ int main(void) {
   // i32 h_min = ((i32)window.half_height) * -1;
   // i32 h_max = (i32)window.half_height;
 
-  triangle_t triangle = {
-      .p0 = {-200, -250},
-      .p1 = {200, 50},
-      .p2 = {20, 250},
-      .h0 = 0,
-      .h1 = 0,
-      .h2 = 1,
-  };
+  // The four "front" vertices
+  vec3f_t vAf = {-2, -0.5, 5};
+  vec3f_t vBf = {-2, 0.5, 5};
+  vec3f_t vCf = {-1, 0.5, 5};
+  vec3f_t vDf = {-1, -0.5, 5};
+
+  // The four "back" vertices
+  vec3f_t vAb = {-2, -0.5, 6};
+  vec3f_t vBb = {-2, 0.5, 6};
+  vec3f_t vCb = {-1, 0.5, 6};
+  vec3f_t vDb = {-1, -0.5, 6};
+
+  // The front face
+  line_t lABf = {project_point(vAf, &window, &camera, viewport),
+                 project_point(vBf, &window, &camera, viewport)};
+  line_t lBCf = {project_point(vBf, &window, &camera, viewport),
+                 project_point(vCf, &window, &camera, viewport)};
+  line_t lCDf = {project_point(vCf, &window, &camera, viewport),
+                 project_point(vDf, &window, &camera, viewport)};
+  line_t lDAf = {project_point(vDf, &window, &camera, viewport),
+                 project_point(vAf, &window, &camera, viewport)};
+
+  // The back face
+  line_t lABb = {project_point(vAb, &window, &camera, viewport),
+                 project_point(vBb, &window, &camera, viewport)};
+  line_t lBCb = {project_point(vBb, &window, &camera, viewport),
+                 project_point(vCb, &window, &camera, viewport)};
+  line_t lCDb = {project_point(vCb, &window, &camera, viewport),
+                 project_point(vDb, &window, &camera, viewport)};
+  line_t lDAb = {project_point(vDb, &window, &camera, viewport),
+                 project_point(vAb, &window, &camera, viewport)};
+
+  // The front-to-back edges
+  line_t lABfb = {project_point(vAf, &window, &camera, viewport),
+                  project_point(vAb, &window, &camera, viewport)};
+  line_t lBCfb = {project_point(vBf, &window, &camera, viewport),
+                  project_point(vBb, &window, &camera, viewport)};
+  line_t lCDfb = {project_point(vCf, &window, &camera, viewport),
+                  project_point(vCb, &window, &camera, viewport)};
+  line_t lDAfb = {project_point(vDf, &window, &camera, viewport),
+                  project_point(vDb, &window, &camera, viewport)};
 
   Arena *arena = NULL;
   u64 capacity = 10ull * 1024ull * 1024ull * 1024ull;
@@ -51,10 +91,20 @@ int main(void) {
 
     clear_window(&window, bg);
 
-    draw_shaded_triangle(&window, arena, triangle,
-                         (colour_t){.colour = 0x00ff00ff});
-    draw_wireframe_triangle(&window, arena, triangle,
-                            (colour_t){.colour = 0xff008aff});
+    draw_line(&window, arena, lABf, (colour_t){.colour = 0xffaa00ff});
+    draw_line(&window, arena, lBCf, (colour_t){.colour = 0xffaa00ff});
+    draw_line(&window, arena, lCDf, (colour_t){.colour = 0xffaa00ff});
+    draw_line(&window, arena, lDAf, (colour_t){.colour = 0xffaa00ff});
+
+    draw_line(&window, arena, lABb, (colour_t){.colour = 0x00ffaaff});
+    draw_line(&window, arena, lBCb, (colour_t){.colour = 0x00ffaaff});
+    draw_line(&window, arena, lCDb, (colour_t){.colour = 0x00ffaaff});
+    draw_line(&window, arena, lDAb, (colour_t){.colour = 0x00ffaaff});
+
+    draw_line(&window, arena, lABfb, (colour_t){.colour = 0xee0099ff});
+    draw_line(&window, arena, lBCfb, (colour_t){.colour = 0xee0099ff});
+    draw_line(&window, arena, lCDfb, (colour_t){.colour = 0xee0099ff});
+    draw_line(&window, arena, lDAfb, (colour_t){.colour = 0xee0099ff});
 
     swap_buffers(&window);
 
diff --git a/src/raytracer/main.c b/src/raytracer/main.c
index 554853c..53ac937 100644
--- a/src/raytracer/main.c
+++ b/src/raytracer/main.c
@@ -1,4 +1,5 @@
 #include "aliases.h"
+#include "camera/camera.h"
 #include "raytracer/raytracer.h"
 #include "scene/scene.h"
 #include "vector/vec.h"
@@ -11,11 +12,6 @@
 
 #define RECURSION_DEPTH 3
 
-typedef struct {
-  vec3f_t position;
-  vec3f_t rotation;
-} camera_t;
-
 i32 main(i32 argc, char *argv[]) {
   colour_t bg =
       (colour_t){.rgba.r = 27, .rgba.g = 38, .rgba.b = 79, .rgba.a = 255};
diff --git a/src/window/window.c b/src/window/window.c
index 7fd7d8f..559949d 100644
--- a/src/window/window.c
+++ b/src/window/window.c
@@ -159,7 +159,8 @@ void set_screen_pixel(window_t *wnd, u32 x, u32 y, colour_t colour) {
   pixels[index] = colour.colour;
 }
 
-vec3f_t window_to_viewport(window_t *wnd, i32 x, i32 y, vec3f_t viewport) {
+vec3f_t window_to_viewport(const window_t *wnd, i32 x, i32 y,
+                           vec3f_t viewport) {
   return (vec3f_t){
       .x = x * viewport.x / wnd->width,
       .y = y * viewport.y / wnd->height,
@@ -167,6 +168,14 @@ vec3f_t window_to_viewport(window_t *wnd, i32 x, i32 y, vec3f_t viewport) {
   };
 }
 
+vec2i_t viewport_to_window(const window_t *wnd, f32 x, f32 y,
+                           vec3f_t viewport) {
+  return (vec2i_t){
+      .x = x / viewport.x * wnd->width,
+      .y = y / viewport.y * wnd->height,
+  };
+}
+
 colour_t colour_add_colour(colour_t a, colour_t b) {
   f32 alpha = clamp((f32)(a.rgba.a + b.rgba.a), 0.0f, (f32)UINT8_MAX);