A 3D software rasterizer written from scratch in C++. No OpenGL, no DirectX. SDL2 opens a window and blits a pixel buffer; every pixel of every triangle is computed by CPU code in this repo.
A GPU does three things when rendering a 3D scene:
- Vertex transform — model/view/projection matrices move geometry into screen space.
- Rasterization — determine which screen pixels each triangle covers.
- Shading — color those pixels with lighting and textures.
This project implements all three on the CPU so every step is readable code. The goal is understanding, not performance.
Mesh (positions, normals, UVs)
│
▼ Mat4: model × view × projection
│
├─ back-face cull (dot(face_normal, to_camera) ≤ 0 → skip)
├─ near-plane cull (clip.w ≤ 0 → skip)
│
▼ perspective divide (÷ clip.w) → NDC [-1,+1]
│
▼ viewport transform → screen pixels
│
▼ triangle_shaded()
├─ barycentric fill of bounding box
├─ z-buffer depth test (depth_test_and_set)
├─ perspective-correct UV interpolation (u/w, 1/w trick)
└─ Lambertian diffuse + ambient → final pixel color
- Install SDL2 via vcpkg:
.\vcpkg install sdl2:x64-windows - CLion: Settings → CMake → CMake options, add:
-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake - Reset CMake cache (Tools → CMake → Reset Cache and Reload Project), then Run.
Drop files into models/ and the app picks them up automatically:
| File | Purpose |
|---|---|
models/scene.obj |
Wavefront OBJ mesh |
models/scene.png |
Texture (PNG, JPG, BMP, TGA ok) |
models/scene.jpg |
Fallback texture |
If neither file is found the app renders a rotating textured cube.
Supported OBJ features: v, vt, vn, f (tris, quads, convex polygons).
Flat normals are auto-computed when vn data is absent.
rasterizer/
├── CMakeLists.txt
├── vendor/
│ └── stb_image.h Header-only image loader (PNG/JPG/BMP/TGA)
├── models/ Drop scene.obj + scene.png here
├── include/
│ ├── rmath.h Vec2, Vec3, Vec4, Mat4, Point2i, color helpers
│ ├── framebuffer.h Pixel buffer + z-buffer
│ ├── draw.h 2D primitives + 3D shaded triangle
│ ├── mesh.h Triangle/Vertex structs, OBJ loader, cube generator
│ └── texture.h stb_image wrapper, checkerboard fallback
└── src/
├── main.cpp SDL setup, asset loading, render loop
├── rmath.cpp Mat4 implementations
├── framebuffer.cpp set_pixel, depth_test_and_set, clear
├── draw.cpp Bresenham, barycentric fill, triangle_shaded
├── mesh.cpp OBJ parser, make_cube
└── texture.cpp stb_image integration
Perspective-correct UV (draw.cpp — triangle_shaded):
Interpolating UVs linearly in screen space produces distortion.
The fix: store u/w and v/w at each vertex, interpolate those and 1/w
linearly, then recover u = (u/w) / (1/w). One extra division per pixel.
Z-buffer (framebuffer.cpp — depth_test_and_set):
Each pixel stores the depth of the closest fragment drawn so far.
A new fragment only writes if its depth is strictly less than the stored value.
clear() resets depth to 1.0 (far) each frame.
Barycentric fill (draw.cpp — triangle_shaded):
Walk every pixel in the triangle's bounding box. For each, compute three weights
that sum to 1 and describe "closeness to each vertex." All weights ≥ 0 → inside.
The same weights interpolate depth, UVs, and normals for free.
Back-face culling (main.cpp — render):
In world space, dot the face normal with the vector from the surface to the camera.
≤ 0 means the face points away → skip, halving draw calls for closed meshes.
Mat4 / look_at / perspective (rmath.cpp):
Standard right-handed OpenGL conventions.
look_at: camera looks along −Z in view space.perspective: maps z=−near→NDC−1, z=−far→NDC+1.- Normal matrix: for rotation-only model transforms, the model matrix itself is correct for normals (a rotation's inverse-transpose equals itself).
ESCor close window: quit
Vec2,Vec3,Vec4,Mat4— full math library with dot, cross, normalize, perspective, look_at, rotate_x/y/z, translate, scaleFramebuffer— pixel buffer + z-buffer, safe bounds checkingDraw::line— Bresenham, all 8 octantsDraw::triangle_wireframe/triangle_filled— 2D primitivesDraw::triangle_shaded— z-buffered, perspective-correct UV, diffuse lightingMesh::load_obj— OBJ parser (tris, quads, polygons, auto-normals)Mesh::make_cube— procedural unit cube with UVsTexture::load— stb_image (PNG/JPG/BMP/TGA)Texture::checkerboard— procedural fallback