Skip to content

Commit e6859ff

Browse files
committed
Release Software Renderer v1.0.0
1 parent 627e55b commit e6859ff

31 files changed

Lines changed: 27866 additions & 1 deletion

‎.clang-format‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
BasedOnStyle: LLVM
2+
IndentWidth: 4
3+
ColumnLimit: 100
4+
AllowShortFunctionsOnASingleLine: Inline
5+
AllowShortIfStatementsOnASingleLine: Never
6+
AllowShortLoopsOnASingleLine: false
7+
BreakBeforeBraces: Attach
8+
PointerAlignment: Left

‎.github/workflows/ci.yml‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: build-and-test
2+
on: [push, pull_request, workflow_dispatch]
3+
permissions:
4+
contents: read
5+
jobs:
6+
build:
7+
strategy:
8+
fail-fast: false
9+
matrix:
10+
os: [ubuntu-latest, windows-latest]
11+
runs-on: ${{ matrix.os }}
12+
steps:
13+
- uses: actions/checkout@v7
14+
with:
15+
persist-credentials: false
16+
- name: Configure
17+
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DRENDERER_STRICT=ON
18+
- name: Build
19+
run: cmake --build build --config Release --parallel 2
20+
- name: Test and render
21+
run: ctest --test-dir build -C Release --output-on-failure
22+
sanitizers:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v7
26+
with:
27+
persist-credentials: false
28+
- name: Configure Clang with address and undefined-behavior sanitizers
29+
run: >-
30+
cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++
31+
-DCMAKE_BUILD_TYPE=Debug -DRENDERER_STRICT=ON
32+
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
33+
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
34+
- name: Build
35+
run: cmake --build build --parallel 2
36+
- name: Test and render
37+
run: ctest --test-dir build --output-on-failure

‎.gitignore‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/build*/
2+
/main
3+
*.exe
4+
*.o
5+
*.obj
6+
!assets/**/*.obj
7+
*.a
8+
*.lib
9+
*.dll
10+
*.pdb
11+
*.ilk
12+
*.exp
13+
*.tga
14+
.vs/
15+
.vscode/
16+
.idea/
17+
*.user
18+
CMakeUserPresets.json
19+
__pycache__/

‎CMakeLists.txt‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project(SoftwareRenderer VERSION 1.0.0 LANGUAGES CXX)
3+
4+
include(CTest)
5+
set(CMAKE_CXX_EXTENSIONS OFF)
6+
option(RENDERER_STRICT "Treat project warnings as errors" OFF)
7+
8+
function(renderer_warnings target)
9+
if(MSVC)
10+
target_compile_options(${target} PRIVATE /W4 /permissive- $<$<BOOL:${RENDERER_STRICT}>:/WX>)
11+
else()
12+
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wshadow
13+
$<$<BOOL:${RENDERER_STRICT}>:-Werror>)
14+
endif()
15+
endfunction()
16+
17+
add_library(renderer_core
18+
src/math.cpp
19+
src/model.cpp
20+
src/framebuffer.cpp
21+
src/rasterizer.cpp
22+
src/pipeline.cpp
23+
src/texture.cpp
24+
src/tga_writer.cpp)
25+
target_include_directories(renderer_core PUBLIC include)
26+
target_compile_features(renderer_core PUBLIC cxx_std_20)
27+
renderer_warnings(renderer_core)
28+
29+
add_executable(renderer src/main.cpp)
30+
target_link_libraries(renderer PRIVATE renderer_core)
31+
renderer_warnings(renderer)
32+
configure_file(assets/orbital.obj assets/orbital.obj COPYONLY)
33+
34+
if(BUILD_TESTING)
35+
foreach(suite core integration)
36+
if(suite STREQUAL "core")
37+
set(test_source tests/core.cpp)
38+
else()
39+
set(test_source tests/integration.cpp)
40+
endif()
41+
add_executable(${suite}_tests ${test_source})
42+
target_link_libraries(${suite}_tests PRIVATE renderer_core)
43+
renderer_warnings(${suite}_tests)
44+
add_test(NAME ${suite} COMMAND ${suite}_tests)
45+
endforeach()
46+
add_test(NAME render_smoke COMMAND renderer assets/orbital.obj --output smoke.tga --width 96 --height 96)
47+
add_test(NAME cli_help COMMAND renderer --help)
48+
set_tests_properties(cli_help PROPERTIES PASS_REGULAR_EXPRESSION "renderer")
49+
foreach(case missing_model invalid_size oversized_size bad_number missing_value unknown duplicate_model bad_camera bad_light missing_texture output_failure)
50+
add_test(NAME cli_${case} COMMAND ${CMAKE_COMMAND}
51+
-DPROGRAM=$<TARGET_FILE:renderer> -DCASE=${case}
52+
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cli.cmake)
53+
endforeach()
54+
endif()

‎LICENSE‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2026 Corey L.
3+
Copyright (c) 2024 Corey L.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

‎README.md‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Software Renderer
2+
3+
**Runs entirely on the CPU.**
4+
5+
[![Build](https://github.com/Speed126/Software-Renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/Speed126/Software-Renderer/actions/workflows/ci.yml)
6+
![C++20](https://img.shields.io/badge/C%2B%2B-20-blue)
7+
![CMake 3.20+](https://img.shields.io/badge/CMake-3.20%2B-blue)
8+
![CPU Renderer](https://img.shields.io/badge/rendering-CPU%20only-blue)
9+
![License: MIT](https://img.shields.io/badge/license-MIT-blue)
10+
11+
![An amber and teal checker mapped onto a lit ring and sphere](docs/images/orbital.png)
12+
13+
A C++20 rasterizer that loads an OBJ, projects its triangles, shades the
14+
visible pixels, and writes a TGA image. Includes edge tests, depth comparisons, matrix math, clipping, and attribute interpolation.
15+
16+
## How to Use
17+
18+
Requires **CMake 3.20+** and a C++20 compiler. No runtime libraries beyond the
19+
C++ standard library. The framebuffer is saved by a small uncompressed TGA writer.
20+
21+
```sh
22+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
23+
cmake --build build --config Release --parallel
24+
ctest --test-dir build -C Release --output-on-failure
25+
```
26+
27+
From the repository root, with a single-configuration generator (Linux, macOS,
28+
or MinGW):
29+
30+
```sh
31+
./build/renderer assets/orbital.obj --output output.tga --width 1400 --height 1000
32+
```
33+
34+
With the Visual Studio generator, in PowerShell:
35+
36+
```powershell
37+
.\build\Release\renderer.exe assets/orbital.obj --output output.tga --width 1400 --height 1000
38+
```
39+
40+
Running with no arguments uses `assets/orbital.obj` and writes an 800×800
41+
`output.tga`. Paths are relative to your working directory. CMake also copies
42+
the demo into `build/assets`, so running from the build directory works.
43+
44+
## What It Does
45+
46+
- Bounding-box traversal with pixel-center barycentrics and shared-edge rules.
47+
- Per-pixel float depth buffer and optional counterclockwise back-face culling.
48+
- Model fitting/rotation, look-at camera, perspective and six-plane clipping.
49+
- Triangulated OBJ meshes with positions, UVs, normals, and positive or negative indices.
50+
- Perspective-correct UV/normal interpolation and directional Lambert lighting.
51+
- Procedural checker or a supplied P3 PPM texture; nearest sampling with repeat.
52+
- Tests run through CTest, with CI on Linux and Windows.
53+
54+
```mermaid
55+
flowchart TD
56+
A[OBJ] --> B[Model loader]
57+
B --> C[Model · view · projection]
58+
C --> D[Homogeneous clipping]
59+
D --> E[Perspective divide · viewport]
60+
E --> F[Triangle setup · barycentric coverage]
61+
F --> G[Interpolate depth · visibility test]
62+
G --> H[Perspective-correct attributes]
63+
H --> I[Texture sample · Lambert lighting]
64+
I --> J[Color + depth framebuffer]
65+
J --> K[TGA image]
66+
```
67+
68+
## Useful Arguments
69+
70+
```sh
71+
./build/renderer path/to/model.obj --texture path/to/texture.ppm --output render.tga
72+
./build/renderer --rotate 30 --eye 0.5 0.25 3.6
73+
./build/renderer --solid --light -0.4 0.7 1
74+
./build/renderer --no-cull
75+
```
76+
77+
| Option | Meaning |
78+
|---|---|
79+
| `--output path` | TGA destination; the parent directory must exist |
80+
| `--width N`, `--height N` | Integer dimensions from 1 to 4096; default 800 |
81+
| `--eye X Y Z` | Camera position in fitted model coordinates; looks at the origin |
82+
| `--rotate degrees` | Rotate the fitted model around Y |
83+
| `--light X Y Z` | World-space direction **toward** the light; normalized internally |
84+
| `--texture path.ppm` | Use an ASCII P3 PPM image instead of the checker |
85+
| `--solid` | Use a neutral solid surface instead of a texture |
86+
| `--no-cull` | Draw both windings; useful for inspecting open meshes |
87+
| `--help` | Show usage |
88+
89+
Invalid arguments and failed reads/writes exit with code 1.
90+
91+
## Inside a Triangle
92+
93+
[`rasterizer.cpp`](src/rasterizer.cpp) evaluates signed edge functions inside a
94+
triangle's screen-space bounding box. Their ratios to the total area give the
95+
three barycentric weights. Those weights locate the sample within the triangle
96+
and interpolate its depth and surface attributes. Pixel centers avoid a corner
97+
sampling bias; a half-open edge convention gives adjacent triangles consistent
98+
ownership of their shared boundary. Degenerate and nonfinite triangles are
99+
skipped, and traversal is clamped before converting bounds to integers.
100+
101+
The depth buffer starts at infinity. A fragment updates the color and depth only
102+
when its depth is smaller than the stored value. Hidden surfaces therefore stay
103+
hidden when triangle order changes. Exactly equal depths keep the first sample.
104+
105+
## Camera and Surface Detail
106+
107+
[`pipeline.cpp`](src/pipeline.cpp) centers the model bounds and uniformly scales
108+
the longest extent to two units. Models can arrive with different sizes and
109+
origins. Matrices use row-major storage with column vectors:
110+
`projection * view * model * position`. The camera is right-handed, looks down
111+
view-space −Z, and uses a 45° vertical field of view. Near/far planes are 0.1/100.
112+
Clipping happens before division by `w`, including triangles crossing the camera
113+
or near plane. Screen coordinates use a bottom-left origin; TGA rows are written
114+
with a top-left origin.
115+
116+
For an attribute `a`, interpolation uses
117+
`sum(barycentric[i] * a[i] / w[i]) / sum(barycentric[i] / w[i])`.
118+
This keeps textures attached to surfaces as they recede into the
119+
image. NDC depth stays affine in screen space. Interpolated normals are
120+
renormalized for Lambert shading, with a small ambient floor so unlit surfaces
121+
remain readable. The model transform uses uniform scale, so rotating normals
122+
is sufficient.
123+
124+
| Solid Surface | Textured + Alternate Light |
125+
|---|---|
126+
| ![Neutral Lambert shading](docs/images/solid.png) | ![Checker under a light from the right](docs/images/alternate-light.png) |
127+
128+
These images are direct renderer output converted to PNG.
129+
To reproduce the documentation images with an existing Python/Pillow setup:
130+
131+
Linux/macOS/MinGW:
132+
133+
```sh
134+
python tools/render_docs.py build/renderer
135+
```
136+
137+
Visual Studio:
138+
139+
```powershell
140+
python tools/render_docs.py build/Release/renderer.exe
141+
```
142+
143+
Pillow is only a documentation convenience; it is not a build or renderer dependency.
144+
145+
## Tests and Codebase
146+
147+
```sh
148+
cmake -S . -B build -DRENDERER_STRICT=ON
149+
cmake --build build --config Release --parallel
150+
ctest --test-dir build -C Release --output-on-failure
151+
```
152+
153+
The 15 CTest entries cover math, OBJ failures, shared edges, clipping,
154+
perspective-correct texture interpolation, depth/order invariance, fitted transforms, complete
155+
TGA output and CLI diagnostics. Checks run in Release builds too. The
156+
[workflow](.github/workflows/ci.yml) builds and tests on Ubuntu and Windows,
157+
with a separate Ubuntu Clang ASan/UBSan job. All three jobs currently pass.
158+
159+
| Location | Responsibility |
160+
|---|---|
161+
| `include/renderer/`, `src/` | Math, loading, pipeline, rasterizer, texture and framebuffer |
162+
| `src/main.cpp` | Argument handling and the offline render operation |
163+
| `tests/` | Small C++ test executables and CMake CLI checks |
164+
| `assets/`, `tools/` | Original demo and optional reproduction scripts |
165+
| `src/tga_writer.cpp` | Uncompressed 24-bit TGA output using the standard library |
166+
167+
## License
168+
169+
Project code, the procedural model and documentation are [MIT licensed](LICENSE).

‎assets/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Demo assets
2+
3+
`orbital.obj` is an original parametric torus and sphere generated for this
4+
project by `tools/generate_demo.py` (Python standard library). It has positions,
5+
UVs, normals and triangle faces; no imported mesh or texture data is used.
6+
The generator and generated model use the repository's MIT license.
7+
Run `python tools/generate_demo.py` from any directory to reproduce the asset.
8+
9+
The default amber/teal checker is generated in `Texture::checker()`, also MIT.

0 commit comments

Comments
 (0)