Skip to content

About

A complete 3D graphics pipeline built from scratch in pure Python — no OpenGL, Vulkan, or Three.js.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Software 3D Renderer from Scratch

A complete 3D renderer built entirely by hand in Python — no OpenGL, Vulkan, DirectX, WebGL, Three.js, or any graphics library. Every stage of the modern graphics pipeline is implemented manually and executed on the CPU:

Scene → Model transform → World space → View transform → Projection
→ Clip space → Frustum clipping → Perspective divide → Screen space
→ Back-face culling → Rasterization → Barycentric interpolation
→ Perspective-correct attribute interpolation → Depth test (z-buffer)
→ Per-fragment Blinn-Phong lighting → Framebuffer → Display

The only non-standard-library dependency is tkinter, which is used purely to open a window, display the finished RGB framebuffer, and receive keyboard/mouse input. It performs no rendering. Image output (PNG/BMP/PPM) is written by hand.


Requirements

  • Python 3.8+ (uses only the standard library)
  • A display for interactive mode (tkinter ships with CPython on Windows/macOS and most Linux installs; on Debian/Ubuntu you may need python3-tk)
  • Third-party libraries: none.

No pip installs required.


Launch it in one click

The repository includes ready-made launchers for every operating system. Double-click the one for your OS:

Operating system File to double-click Notes
Windows run.bat Shows a console with logs/stats
Windows (no console window) run_gui.vbs Starts the renderer window hidden-console
macOS Run.command Opens Terminal automatically
Linux (desktop) / any run.sh chmod +x run.sh on first use
Any OS (terminal) start.py python start.py

All launchers accept the same options, e.g.:

# Windows
run.bat --width 800 --height 600
run.bat --offscreen out.png --animated --fps 15

# macOS / Linux
./run.sh --width 800 --height 600
./run.sh --offscreen out.png

# Any OS with Python
python start.py --width 800 --height 600

Extra steps for macOS / Linux

Scripts need the executable flag once (this is stored in git, so it usually already applies after cloning):

chmod +x run.sh Run.command start.py

On Ubuntu/Debian, install tkinter if interactive mode complains:

sudo apt install python3-tk

Creating a desktop shortcut / access directo

  • Windows — right-click run.bat → Send to → Desktop (create shortcut). For a shortcut without a console, use run_gui.vbs the same way.

  • macOS — double-click Run.command, or make an Alias of it on the Desktop (right-click the file → Make Alias).

  • Linux — double-click run.sh, or create ~/.local/share/applications/renderer.desktop:

    [Desktop Entry]
    Version=1.0
    Type=Application
    Name=3D Renderer from Scratch
    Exec=/absolute/path/to/run.sh
    Terminal=false

Quick start

Run the interactive demo window (default 480×360; lower resolutions update faster because the renderer is pure Python):

python src/main.py

Better control over resolution / feel:

python src/main.py --width 480 --height 360
python src/main.py --width 800 --height 600     # slower, sharper

Render a single still frame with no window:

python src/main.py --offscreen screenshots/out.png --width 800 --height 600

Render a short animated sequence (cube rotates) and save every frame:

python src/main.py --offscreen screenshots/anim.png --width 400 --height 300 \
    --animated --seconds 2 --fps 15

Run the automated smoke tests (48 checks covering every pipeline stage):

python tests/smoke_test.py

Interactive controls

Key / input Action
W / S move forward / backward
A / D move left / right
E / Q move up / down
Mouse drag rotate the camera (yaw / pitch)
P toggle wireframe overlay
C toggle back-face culling
T toggle textures
F12 save a PNG screenshot to ./screenshots
Esc quit

Architecture

The project follows the module layout requested in the specification:

src/
├── math/          Vector2, Vector3, Vector4, Matrix4   (from scratch)
├── graphics/      Color, Framebuffer, DepthBuffer, Vertex, Triangle,
│                  Mesh, Material, Texture
├── renderer/      Renderer, Rasterizer, Camera, Lighting, Clipper
├── loaders/       OBJLoader
├── scene/         Object3D, Scene, Light (directional / point)
└── main.py        entry point (interactive window + offscreen modes)
assets/            demo .obj files (pyramid, UV-sphere)
tests/             smoke_test.py
screenshots/       saved renders

Math (src/math)

  • Vector2 / Vector3 / Vector4 — basic arithmetic, dot/cross product, length, normalization (Vector3 cross is used to build surface normals).
  • Matrix4 — identity, ×, ×-vector, translation, scaling, rotation about each axis, perspective projection, plus an inverse-transpose 3×3 normal matrix so vertex normals stay correct under non-uniform scale.

Graphics (src/graphics)

  • Color — RGBA floats with clamp + byte conversion.
  • Framebuffer — a plain RGBA buffer; set_pixel, clear, gradient clear, and hand-written PPM / BMP / PNG writers.
  • DepthBuffer — one float per pixel, initialized to the farthest value.
  • Vertex — position, normal, uv.
  • Triangle — three vertex indices.
  • Mesh — vertices + triangles, procedural create_cube / create_plane, smooth and flat normal generation.
  • Material — baseColor, ambient/diffuse/specular strength, shininess, optional texture.
  • Texture — checkerboard generator + wrap-around bilinear sampling.

Renderer (src/renderer)

  • Camera — position, yaw/pitch, FOV, aspect, near/far; movement, rotation, view_matrix() and projection_matrix().
  • Renderer — drives the whole per-frame pipeline.
  • Rasterizer — bounding-box scan over each triangle using barycentric edge functions, with perspective-correct interpolation of depth, world position, normals and UVs via 1/w, then the z-buffer test.
  • Clipper — Sutherland–Hodgman clipping against the six clip-space frustum planes (near, far, left, right, top, bottom) with per-attribute interpolation.
  • Lighting — per-fragment Blinn-Phong: ambient + diffuse + specular, supporting directional and point lights (with attenuation).

Scene & loaders

  • scene.Object3D — position / rotation / scale → model matrix.
  • scene.Light — DirectionalLight and PointLight.
  • loaders.OBJLoader — parses v, vt, vn, f (all token forms, negative indices), triangulates polygons, and generates smoothed normals when the file has none (assets/pyramid.obj has no normals on purpose to exercise this).

Pipeline details

The frame is produced like this (in Renderer.render):

  1. Clear — framebuffer gets a vertical sky gradient, depth buffer is reset to the farthest depth (+1 in NDC).
  2. For each object: build the model matrix from position/rotation/scale, combine MVP = Projection × View × Model, transform every vertex to clip space and (via the model matrix + normal matrix) to world space.
  3. Back-face culling — the world-space face normal of each triangle is tested against the view vector; back faces are skipped.
  4. Frustum clipping — triangles fully inside pass through; partially outside triangles are clipped to the frustum in clip space; fully outside triangles are rejected.
  5. Rasterize — for each resulting triangle, compute the screen-space bounding box and fill inside pixels using barycentric edge functions. Depth, world position, normal and UV are interpolated perspective correctly (attr/w interpolated, then divided by interpolated 1/w).
  6. Depth test — a pixel is only written when its interpolated NDC depth is closer than the value in the z-buffer.
  7. Lighting — the fragment shader normalizes the interpolated normal and computes Blinn-Phong over every light, blending the material base color or a bilinearly-sampled texture.
  8. Display — the finished framebuffer is pushed to the window (Framebuffer.to_hex_rows + tkinter.PhotoImage), or saved to disk.

Validation

tests/smoke_test.py verifies, among other things:

  • vector / matrix / camera math (normals, perspective near/far mapping),
  • the rasterizer fills the correct pixels,
  • z-buffer occlusion (far triangles are hidden even when drawn last),
  • frustum clipping (inside kept, outside rejected, near-plane correctly cut),
  • cube / plane / OBJ geometry and normal generation,
  • texture sampling,
  • the full pipeline produces a visible framebuffer.

A known visual check is rendered into screenshots/final_800.png.


Performance notes & optimization roadmap

This is an educational, correctness-first renderer written in pure interpreted Python. Every fragment goes through a Python-level inner loop, so a full 800×600 frame takes on the order of a few seconds and interactive rate is modest (higher at 480×360 or below). The spec intentionally lists performance work as the final milestone; an incremental, documented path is:

  • already done: bounding-box rasterization, incremental edge functions, back-face culling, frustum clipping, a C-speed framebuffer→hex transfer, precomputed light vectors and color × intensity per light, inverse-transpose normal matrix;
  • next steps: incremental (scanline) attribute interpolation so each per-pixel attribute cost drops from ~5 ops to 1 add, gouraud/lambert fast paths, tiled rasterization + multithreading (subinterpreters/processes sidestep the GIL for an embarrassingly parallel renderer), flat-indexed buffers / SIMD via array, and a JIT (PyPy/Numba) for near-interactive frame rates.

Project layout (as requested)

Specification stage Where implemented
Framebuffer, setPixel, clear graphics/Framebuffer.py
Line rasterization (Bresenham) renderer/Rasterizer.py
Triangle rasterization + bounding box renderer/Rasterizer.py
Vector2 / Vector3 / Vector4 math/*
Matrix4 + transformations math/Matrix4.py
Camera + perspective projection renderer/Camera.py, math/Matrix4.py
3D cube graphics/Mesh.py
Z-buffer / depth test graphics/DepthBuffer.py, renderer/Rasterizer.py
Back-face culling renderer/Renderer.py
Vertex normals graphics/Mesh.py, math/Matrix4.py
Diffuse + specular (Blinn-Phong) lighting renderer/Lighting.py
OBJ loading loaders/OBJLoader.py
UV coordinates + texture mapping graphics/Mesh.py, graphics/Texture.py
Perspective-correct interpolation renderer/Rasterizer.py
Frustum clipping renderer/Clipper.py
Optimization / multithreading (roadmap) see "Performance notes"

About

A complete 3D graphics pipeline built from scratch in pure Python — no OpenGL, Vulkan, or Three.js.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages