3D Renderer
Real-time 3D rendering software built from scratch in pure Python without external graphics libraries
- Date
- Domain
- software
- Stack
- Python · NumPy · pygame
![]()
A real-time 3D renderer for .obj files built in pure Python, deliberately without using GPU APIs like OpenGL. The project was completed as part of my self-imposed challenge to learn Python in Fall 2025. The repo is public and you can access it here.
Stack
- NumPy for all matrix math and vectorized rendering operations
- Pygame for the display window and input handling
- Tkinter for the file picker dialog
Process
The rendering pipeline follows the standard path: parse vertex and face data from a .obj file, transform world-space points into camera space using a view matrix, then project them into 2D screen coordinates with a perspective divide. The first working version was a point cloud viewer.
From there I added face rendering, which introduced the need for a depth sort (painter’s algorithm) so closer polygons draw over farther ones, backface culling to skip faces pointing away from the camera, and frustum culling to skip anything behind the camera plane. Lighting came last—each face’s screen-space normal is dot-producted against a light direction vector to get a grayscale intensity, with an option to use averaged per-vertex normals from the .obj file instead for smoother shading.
The biggest challenge was performance. On a ~50k polygon model, early iterations ran at around 0.5 FPS because the draw loop was iterating over every polygon in Python one at a time. Profiling confirmed that was the bottleneck, so I restructured the entire pipeline to operate on NumPy arrays in batch—culling, depth sorting, normal computation, and projection all run across all faces simultaneously. That alone brought the frame rate up to around 20 FPS.
Outcomes
I was able to produce a fully interactive viewer with intuitive user controls based on my experience with various 3D modeling softwares. The vectorization work was the most valuable part. It made the difference between an unusable prototype and something that actually runs.
For my personal journey, this project was crucial for giving me the inspiration to continue with my self-guided programming challenges, and for giving me the confidence to learn independently.