A lightweight 3D wireframe renderer built from scratch using Pygame
Project description
Aiden3DRenderer
A lightweight 3D wireframe renderer built with Pygame featuring custom projection, first-person camera controls, and 15+ procedural terrain generators.
Features
- Custom 3D projection - Perspective projection without using external 3D libraries
- First-person camera - Full 6-DOF camera movement with mouse look
- 15+ procedural generators - Mountains, cities, fractals, and mathematical surfaces
- Real-time rendering - 60 FPS wireframe rendering
- Animated terrains - Several terrains feature time-based animations
- Extensible API - Easy to create and register custom shapes with decorators
Gallery
|
Ripple Effect Expanding waves from center |
Mandelbulb Slice 3D fractal cross-section |
|
Turning Spiral Screw like shape spinning |
Simple City (laggy when solid render) City preset in solid render |
Installation
pip install aiden3drenderer
Requires Python 3.11+ and automatically installs Pygame 2.6.0+ (only tested with Python 3.11)
Quick Start
Running the Demo
Original Demo
from aiden3drenderer import Renderer3D
# Create and run the renderer with all built-in shapes
renderer = Renderer3D()
renderer.camera.position = [0, 0 ,0]
# is_mesh = True for mesh, False for solid colors
renderer.is_mesh = False
renderer.run()
Looped Demo
from aiden3drenderer import Renderer3D
# Create and run the renderer with all built-in shapes
renderer = Renderer3D()
renderer.camera.position = [0, 0 ,0]
# is_mesh = True for mesh, False for solid colors
renderer.is_mesh = False
while True:
renderer.loopable_run()
Looped Run Usage Example
from aiden3drenderer import Renderer3D
# Create and run the renderer with all built-in shapes
# Simple gravity with set floor height
renderer = Renderer3D()
renderer.camera.position = [0, 0 ,0]
renderer.is_mesh = False
gravity = 0.05
floor_height = 0.1
camera_height = 2
while True:
if renderer.camera.position[1] <= floor_height + camera_height:
renderer.camera.position[1] = floor_height + camera_height
else:
renderer.camera.position[1] -= gravity
renderer.loopable_run()
Creating Custom Shapes
To create custom shapes compatible with this 3D mesh renderer, you must write Python functions decorated with @register_shape, each returning a "vertex matrix"—a list of rows, where each row is a list of 3D coordinate tuples (x, y, z). Each row represents a contiguous horizontal strip of points on your 3D shape, stacked along the "y" (vertical) dimension. All rows in the matrix must have the same number of points (columns); do not generate jagged matrices or append rows of different lengths, as the renderer expects a perfect rectangle to traverse for drawing wireframes and faces.
Points are usually arranged such that matrix[row][col] gives the (x, y, z) of the point at column col in row row. Loops that fill your matrix should ensure that each sub-list (row) is always the full width—pad with None or duplicate valid coordinates if needed, but empty or missing values will cause indexing errors. Avoid using polar or arbitrary arrangements for each row: either fill every cell with an (x, y, z) tuple, or, if the shape doesn't naturally fit a rectangle (e.g., cones/canopies), pad shorter rows to the full length to maintain a perfect rectangle.
Your function should accept at least the two arguments grid_size (which determines the overall scale and discretization) and frame (for animation support; ignored if not animating), and always return the correctly-sized 2D matrix ready for rendering. If any row in your matrix is shorter than the others, or you index cells that do not exist, you will get IndexError: list index out of range. Review your logic to ensure each row always contains the expected number of vertices, and debug with simple cubes or grids first to be confident in your shape's layout.
Simple Shapes
from aiden3drenderer import Renderer3D, register_shape
import pygame
# Register a custom shape with a decorator
@register_shape("My Plane", key=pygame.K_p, is_animated=False)
def generate_pyramid(grid_size=40, frame=0):
"""Generate a simple plane."""
matrix = [
[(1,1,1), (2,1,1), (3,1,1)],
[(1,1,2), (2,1,2), (3,1,2)],
[(1,1,3), (2,1,3), (3,1,3)]
]
return matrix
# Run the renderer (your shape will be available on 'P' key)
renderer = Renderer3D()
renderer.run()
Complex Shapes
from aiden3drenderer import Renderer3D, register_shape
import pygame
# Register a custom shape with a decorator
@register_shape("My Pyramid", key=pygame.K_p, is_animated=False)
def generate_pyramid(grid_size=40, frame=0):
"""Generate a simple pyramid."""
matrix = []
center = grid_size / 2
for x in range(grid_size):
row = []
for y in range(grid_size):
# Distance from center
dx = abs(x - center)
dy = abs(y - center)
max_dist = max(dx, dy)
# Height decreases with distance
height = max(0, 10 - max_dist)
row.append(height)
matrix.append(row)
return matrix
# Run the renderer (your shape will be available on 'P' key)
renderer = Renderer3D()
renderer.run()
Controls
Camera Movement
- W/A/S/D - Move forward/left/backward/right
- Space - Move up
- Left Shift - Move down
- Left Ctrl - Speed boost (2x)
- Arrow Keys - Fine pitch/yaw adjustment
- Right Mouse + Drag - Look around (pitch and yaw)
Terrain Selection
- 1 - Mountain terrain
- 2 - Animated sine waves
- 3 - Ripple effect
- 4 - Canyon valley
- 5 - Stepped pyramid
- 6 - Spiral surface
- 7 - Torus (donut)
- 8 - Sphere
- 9 - Möbius strip
- 0 - Megacity (80×80 procedural city)
- Q - Alien landscape
- E - Double helix (DNA-like)
- R - Mandelbulb fractal slice
- T - Klein bottle
- Y - Trefoil knot
Other
- Escape - Quit application
Terrain Descriptions
Static Terrains
Mountain (1) - Smooth parabolic mountain with radial falloff
Canyon (4) - U-shaped valley with sinusoidal variations
Pyramid (5) - Stepped pyramid using Chebyshev distance
Torus (7) - Classic donut shape using parametric equations
Sphere (8) - UV sphere using spherical coordinates
Möbius Strip (9) - Non-orientable surface with a single side
Megacity (0) - 80×80 grid with hundreds of procedurally generated buildings
- Buildings get taller toward the center
- 8×8 block system with roads
- Random antenna towers on some buildings
- Most complex terrain (6400 vertices)
Mandelbulb (R) - 2D slice of 3D Mandelbulb fractal
- Uses power-8 formula
- Height based on iteration count
Klein Bottle (T) - 4D object projected into 3D
- Non-orientable surface
- No inside or outside
Trefoil Knot (Y) - Mathematical knot in 3D space
- Classic topology example
- Tube follows trefoil path
Animated Terrains
Waves (2) - Multiple overlapping sine waves
- Three different wave frequencies
- Constantly flowing motion
Ripple (3) - Expanding ripple from center
- Exponential amplitude decay
- Simulates water drop impact
Spiral (6) - Rotating spiral pattern
- Polar coordinate mathematics
- Hypnotic rotation
Alien Landscape (Q) - Complex multi-feature terrain
- Crater with parabolic profile
- Crystalline spike formations
- Rolling hills
- Procedural "vegetation" spikes
- Pulsating energy field
Double Helix (E) - DNA-like structure
- Two intertwined strands
- 180° phase offset between strands
- Rotates over time
Technical Details
3D Projection Pipeline
- World coordinates - Raw vertex positions
- Camera translation - Subtract camera position
- Camera rotation - Apply yaw, pitch, roll transformations
- Perspective projection - Divide by Z-depth with FOV
- Screen mapping - Convert to pixel coordinates
Rotation Matrices
Yaw (Y-axis):
x' = x·cos(θ) + z·sin(θ)
z' = -x·sin(θ) + z·cos(θ)
Pitch (X-axis):
y' = y·cos(φ) - z·sin(φ)
z' = y·sin(φ) + z·cos(φ)
Roll (Z-axis):
x' = x·cos(ψ) - y·sin(ψ)
y' = x·sin(ψ) + y·cos(ψ)
Culling
Points behind the camera (z ≤ 0.1) are set to None to prevent rendering artifacts and negative depth division.
Performance
- 60 FPS stable on most terrains
- Megacity (6400 vertices) - Largest terrain, still maintains 60 FPS (wireframe mesh only)
- Wireframe rendering and filled polygons from triangle partitions
API Reference
Renderer3D
Main renderer class that handles the 3D projection and rendering loop.
from aiden3drenderer import Renderer3D
renderer = Renderer3D(
width=1200, # Window width in pixels
height=800, # Window height in pixels
fov=800 # Field of view (higher = less perspective)
)
renderer.run()
Camera
Camera class for position and rotation control (automatically created by Renderer3D).
from aiden3drenderer import Camera
# Access camera through renderer
renderer = Renderer3D()
camera = renderer.cam
# Camera attributes
camera.pos # [x, y, z] position
camera.facing # [yaw, pitch, roll] in radians
camera.speed # Movement speed (default: 0.5)
register_shape Decorator
Register custom shape generators that appear in the renderer.
@register_shape(name, key=None, is_animated=False)
def generate_function(grid_size=40, frame=0):
"""
Args:
name (str): Display name for the shape
key (pygame.K_*): Keyboard key to trigger shape (optional)
is_animated (bool): Whether shape changes over time
Returns:
list[list[float]]: grid_size x grid_size matrix of heights
"""
return matrix
Package Structure
aiden3drenderer/
├── __init__.py # Package exports
├── renderer.py # Renderer3D class and projection
├── camera.py # Camera class for movement/rotation
└── shapes.py # 15+ built-in shape generators
examples/
├── basic_usage.py # Simple demo
└── custom_shape_example.py # Custom shape tutorial
Development
Running from Source
git clone https://github.com/AidenKielby/3D-mesh-Renderer
cd 3D-mesh-Renderer
pip install -e .
python examples/basic_usage.py
Building the Package
pip install build twine
python -m build
python -m twine upload dist/*
Credits
Created by Aiden. Procedural generation functions created with AI assistance (the things like mountain and megacity). All rendering, projection, and camera code written manually.
License
Free to use and modify.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiden3drenderer-0.1.3.tar.gz.
File metadata
- Download URL: aiden3drenderer-0.1.3.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e71d6b847dbd94e74615439b0d0cd26d3c3b685dab4677d7dd087be20390f671
|
|
| MD5 |
09e796f5d27e2b95768d98329868d833
|
|
| BLAKE2b-256 |
b18a1fc37abcc3f3e6a8d6ba089f26388edd86cffbd6f84ffb7345319225b7d0
|
File details
Details for the file aiden3drenderer-0.1.3-py3-none-any.whl.
File metadata
- Download URL: aiden3drenderer-0.1.3-py3-none-any.whl
- Upload date:
- Size: 12.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a6478f2bb36a4b6c2b172e1fd8c07b0f79d31ebb6395ebb2ea259edf56a0761
|
|
| MD5 |
586e0fb4b3fd10e1e631bfd6203f1ecf
|
|
| BLAKE2b-256 |
ab6b6a7feed206fc1ee432ea6319d0f07bb2db7e19851a9f82304b7a830c1f52
|