Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

graphics-rasterizer 📐

A software rasterizer written from scratch in C++20 — no OpenGL, no Vulkan, no third-party libraries. It walks the whole pipeline itself: load a Wavefront OBJ, transform vertices through model-view / perspective / viewport matrices, rasterize triangles with barycentric coordinates, depth-test against a z-buffer, and shade each fragment with diffuse, normal, and specular maps. The result is written out as a TGA image.

Inspired by ssloy/tinyrenderer.

CI

African head rendered with Phong shading Diablo 3 model rendered with Phong shading

Features

  • Pure CPU rasterization — bounding-box triangle fill with barycentric interpolation
  • Z-buffering for correct hidden-surface removal
  • Perspective projection and a lookAt camera you can position from the command line
  • Backface culling via the sign of the screen-space triangle determinant
  • Texture mapping — diffuse, object-space normal, and specular maps
  • Four shading modes — Phong, Gouraud, flat, and a depth-buffer visualization
  • OpenMP parallelism across scanline columns, used automatically when available
  • Full TGA support — reads and writes both raw and RLE-compressed 8/24/32-bit files
  • Zero dependencies beyond a C++20 compiler and CMake

Requirements

Compiler Any C++20 compiler — GCC 11+, Clang 14+, or MSVC 19.30+ (VS 2022)
Build system CMake 3.24 or newer
Optional OpenMP for multi-threaded rasterization; Python 3.8+ to convert output to PNG

Building

Linux / macOS

git clone https://github.com/raphaeldickinson/graphics-rasterizer.git
cd graphics-rasterizer

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

The binary lands at build/rasterizer.

If you have Ninja installed, -G Ninja builds noticeably faster:

cmake -S . -B build -G Ninja
cmake --build build

Windows

The project builds with either MSVC or MinGW. Pick whichever toolchain you already have.

Route 1 — Visual Studio 2022 (or Build Tools)

CMake and Ninja ship inside Visual Studio, so nothing extra is needed. From a Developer PowerShell for VS 2022:

cmake -S . -B build
cmake --build build --config Release

The binary lands at build\Release\rasterizer.exe.

If cmake is not on your PATH, Visual Studio's copy lives at:

C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe

(Swap BuildTools for Community, Professional, or Enterprise to match your install.)

Route 2 — MSYS2 / MinGW-w64
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja

Then from the UCRT64 shell:

cmake -S . -B build -G Ninja
cmake --build build

The binary lands at build/rasterizer.exe.

To drive a MinGW build from PowerShell using Visual Studio's bundled CMake and Ninja:

$vs    = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake"
$cmake = "$vs\CMake\bin\cmake.exe"
$ninja = "$vs\Ninja\ninja.exe"

& $cmake -S . -B build -G Ninja -DCMAKE_MAKE_PROGRAM="$ninja" -DCMAKE_CXX_COMPILER=g++
& $cmake --build build
Route 3 — standalone CMake
winget install Kitware.CMake

Restart your shell, then follow the Linux/macOS instructions above.

Running the tests

ctest --test-dir build --output-on-failure

This exercises the vector/matrix maths, the TGA reader and writer (raw and RLE, at every supported bit depth), and the OBJ parser.


Rendering your first image

From the repository root:

./build/rasterizer obj/african_head/african_head.obj

That writes assets/output.tga — an 800×800 Phong-shaded render of the head model, using the diffuse, normal, and specular maps that sit beside the .obj. On Windows the command is .\build\Release\rasterizer.exe (MSVC) or .\build\rasterizer.exe (MinGW).

Expect something like this on stderr:

# v# 1258 f# 2492 vt# 1339 vn# 1258
texture file obj/african_head/african_head_diffuse.tga loading ok
texture file obj/african_head/african_head_nm.tga loading ok
texture file obj/african_head/african_head_spec.tga loading ok
rendered 2492 triangles at 800x800 with the phong shader in 210 ms -> assets/output.tga

Viewing the result

TGA is not natively viewable in Windows Photos or a browser. Convert it to PNG with the bundled script (standard library only — no Pillow needed):

python tools/tga2png.py assets/output.tga
# -> assets/output.png

Other options: GIMP, IrfanView, Krita, and ffmpeg -i output.tga output.png all read TGA directly.


Command-line reference

rasterizer [options] <model.obj> [model2.obj ...]

Passing several .obj files renders them into the same frame, sharing one z-buffer, so they occlude each other correctly.

Option Default Description
-o, --output <file> assets/output.tga Where to write the image. Parent directories are created automatically.
-w, --width <n> 800 Image width in pixels.
-H, --height <n> 800 Image height in pixels.
--shader <mode> phong One of phong, gouraud, flat, depth.
--eye x,y,z -1,0,2 Camera position.
--center x,y,z 0,0,0 The point the camera looks at.
--up x,y,z 0,1,0 Camera up vector; controls roll.
--light x,y,z 1,1,1 Direction the light travels from.
-h, --help Print usage and exit.

Note that -H is height and -h is help.

Shading modes

Mode What it does
phong Ambient + Lambert diffuse + specular highlight. Reads per-fragment normals from the normal map and highlight strength from the specular map. The default, and the most detailed.
gouraud Lighting computed once per vertex and interpolated across the triangle. Faster, visibly smoother/softer, no specular.
flat One normal per triangle, so every facet reads as a flat plane. Makes the underlying mesh topology obvious.
depth Grayscale visualization of the z-buffer, normalized to the model's own near and far extents. White is nearest. Useful for confirming depth testing is behaving.
Phong shading
phong
Gouraud shading
gouraud
Flat shading
flat
Depth buffer
depth

Note the detail Phong recovers from the normal map — pores, forehead creases, and specular highlights on the nose and lips — none of which exist in the 2,492-triangle mesh itself. Gouraud interpolates a single intensity per vertex, so the same geometry reads as soft and matte.

Worked examples

# Default Phong render of the head
./build/rasterizer obj/african_head/african_head.obj

# The Diablo 3 model at 1600x1600, written somewhere specific
./build/rasterizer -w 1600 -H 1600 -o renders/diablo.tga obj/diablo3/diablo3.obj

# Compare the shading models
for s in phong gouraud flat depth; do
  ./build/rasterizer --shader $s -o "renders/head_$s.tga" obj/african_head/african_head.obj
done

# Orbit the camera to the other side and move the light with it
./build/rasterizer --eye 1,0.5,2 --light -1,1,1 -w 1000 -H 1000 \
  -o renders/head_alt.tga obj/african_head/african_head.obj

# Look up from below
./build/rasterizer --eye 0,-1.5,1.5 --center 0,0,0 obj/diablo3/diablo3.obj

# Straight-on portrait with a hard side light
./build/rasterizer --eye 0,0,2.5 --light 1,0,0.2 obj/african_head/african_head.obj

# Both models in one frame, sharing a z-buffer
./build/rasterizer obj/african_head/african_head.obj obj/diablo3/diablo3.obj

Positioning the camera

The bundled models sit inside roughly the [-1, 1] cube centred on the origin. --eye is therefore in the same units: 0,0,2 is straight in front, two units back. The perspective strength is derived automatically from ‖eye − center‖, so pulling the camera back flattens the projection and moving it in exaggerates it. Setting --eye closer than about 1.2 units puts geometry behind the camera plane and produces artifacts — this renderer has no near-plane clipping, which is the main simplification it inherits from tinyrenderer.


Rendering your own models

Point the binary at any triangulated (or convex-polygon) Wavefront OBJ:

./build/rasterizer -o renders/mine.tga path/to/mine.obj

Supported OBJ directives are v, vt, vn, and f, including the v/vt/vn, v//vn, and bare v face forms, negative (relative) indices, and polygons with more than three vertices, which are fan-triangulated on load.

Textures are discovered by filename, next to the .obj:

File Purpose If missing
<name>_diffuse.tga Base colour Model renders white
<name>_nm.tga Object-space normal map, RGB ↔ XYZ Falls back to the OBJ's own vertex normals
<name>_spec.tga Specular intensity, read from the red channel No specular highlight

So mine.obj picks up mine_diffuse.tga, mine_nm.tga, and mine_spec.tga. Note the normal maps must be object-space, not tangent-space — tangent-space maps will shade incorrectly.

Models are used as-is with no auto-scaling, so geometry far outside the [-1, 1] cube will need a matching --eye distance to fit in frame.


How it works

Each triangle takes the following path, which mirrors a real GPU pipeline:

  1. Vertex stageIShader::vertex() transforms an object-space vertex by ModelView and then Perspective, returning a clip-space vec4. It also stashes any varyings — UVs, normals, per-vertex light intensity — that the fragment stage will interpolate.
  2. Perspective divide — dividing by w maps clip space into the normalized device cube. The perspective matrix writes −z/f into w, so this is what makes distant geometry shrink.
  3. Viewport transformViewport maps NDC onto pixel coordinates.
  4. Culling — the screen-space triangle determinant is negative for back-facing triangles and near zero for degenerate ones. Both are dropped.
  5. Rasterization — for every pixel in the triangle's bounding box, barycentric coordinates come from multiplying the inverse-transpose of the vertex matrix by the pixel position. Any negative coordinate means the pixel is outside the triangle.
  6. Depth test — the barycentric-interpolated depth is compared against the z-buffer, and the fragment is discarded if something nearer was already drawn.
  7. Fragment stageIShader::fragment() receives the barycentric weights, interpolates its varyings, samples the textures, and returns a colour (or discards the fragment).

Adding a shading model means writing one struct that implements IShader — see src/shaders.hpp for the four that ship.

Project layout

src/
  geometry.hpp    Header-only vec<n> / mat<rows,cols> with determinants and inverses
  tgaimage.{hpp,cpp}   TGA reader and writer, raw and RLE, 8/24/32-bit
  model.{hpp,cpp}      OBJ parser plus automatic texture discovery
  gl.{hpp,cpp}         Pipeline matrices, the IShader interface, and rasterize()
  shaders.hpp          Phong, Gouraud, flat, and depth shaders
  main.cpp             Argument parsing and the render loop
tests/
  test_main.cpp   Dependency-free unit tests for the maths, TGA I/O, and OBJ parsing
tools/
  tga2png.py      Standard-library TGA to PNG converter
obj/              Models and textures from tinyrenderer
assets/           Default output directory

Troubleshooting

error: model not found: ... Paths are relative to your current directory, not the binary's. Run from the repository root.

The image is entirely black, or the model is missing The camera is probably inside or behind the geometry. Reset with --eye 0,0,2 and increase the distance from there.

Windows: "An Application Control policy has blocked this file" Smart App Control is enabled on your machine and blocks unsigned executables — including ones you just compiled yourself. Check with:

(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy').VerifiedAndReputablePolicyState

1 means enforced, 2 means evaluation, 0 means off. Turning it off is done in Windows Security → App & browser control → Smart App Control, but be aware Microsoft makes this a one-way switch: re-enabling it requires reinstalling Windows. If you would rather not, build and run inside WSL instead (wsl --install), where the policy does not apply.

Renders are slow Make sure you built in Release (-DCMAKE_BUILD_TYPE=Release; it is the default here for single-config generators, but Visual Studio needs --config Release). Confirm OpenMP was found — CMake prints Found OpenMP_CXX during configuration. AppleClang does not ship OpenMP; install it with brew install libomp.

cmake: command not found on Windows See Route 1 above for Visual Studio's bundled copy, or winget install Kitware.CMake.


Credits

  • ssloy/tinyrenderer — the course this pipeline follows, and the source of the models and textures under obj/

License

MIT — see LICENSE. Third-party attribution for the models and textures under obj/ is in NOTICE; each model directory also keeps its upstream readme.txt.

About

A CPU software rasterizer in C++20 - OBJ loading, z-buffering, perspective projection, and Phong shading with diffuse/normal/specular maps. No graphics API, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages