A lightweight 3D wireframe renderer built from scratch using Pygame
Project description
Aiden3DRenderer
A lightweight but capable 3D renderer for Python + Pygame with custom projection math, first-person camera controls, procedural geometry, OBJ support, basic physics, and an optional GPU raster path.
Feature List (Clickable)
Every item below links to its explanation section.
- Custom 3D Projection - Projection pipeline implemented manually (no external 3D engine).
- First-Person Camera (6-DOF) - Mouse look + keyboard movement with speed control.
- 15+ Procedural Shape Generators - Mountains, cities, fractals, knots, and more.
- Real-Time Rendering - Designed for responsive live rendering workflows.
- Animated Terrains - Time-based procedural surfaces.
- Extensible Shape API - Register your own shapes with
@register_shape. - Multiple Object Support - Render multiple meshes/shapes in one scene.
- Per-Shape Colors - Custom colors for polygon/raster workflows.
- Simple Physics Engine - Basic forces and collisions for spheres/planes/camera.
- OBJ Loading - Load and render
.objfiles with triangulation and UV parsing. - Rasterization Paths - CPU fill path + GPU compute-shader raster path.
- Three Render Modes -
MESH,POLYGON_FILL, andRASTERIZE. - Raster Debug Views - Depth and heat-map diagnostics.
- Texture Mapping - UV texture sampling in raster mode.
- Multi-Texture Pipeline - Multiple texture layers selectable per OBJ.
- Rasterization Paths - CPU fill path + GPU compute-shader raster path.
- Three Render Modes -
MESH,POLYGON_FILL, andRASTERIZE. - Raster Debug Views - Depth and heat-map diagnostics.
- Texture Mapping - UV texture sampling in raster mode.
- Multi-Texture Pipeline - Multiple texture layers selectable per OBJ.
- Entities - Lightweight in-scene entities with scripted behaviour, bounding boxes, and basic collision support.
- Custom Shaders - Helper wrapper for OpenGL compute shaders with SSBO/uniform helpers.
- Runtime Shape Management - Toggle built-in defaults while running.
- Skybox Rendering - Generate cubemap skyboxes from UVs or cross atlas layouts.
- Pause + Settings UI - Tune mode, FOV, debug views, and lighting live.
- Video Renderer (Experimental) - Render OBJ animation clips to video output.
- macOS GPU Note - Compute-shader limitation and VM workaround.
Gallery
|
Ripple Effect Expanding waves from center |
Mandelbulb Slice 3D fractal cross-section |
|
Turning Spiral Screw-like surface animation |
Simple City Large procedural city terrain |
|
Tree Mesh Wireframe OBJ rendering |
Tree Solid Render Filled rendering from same scene |
|
Colored Tree Per-shape color update showcase |
Physics Demo Collision and motion in scene |
|
Skull + Skybox OBJ + environment background |
Minecraft Boat Wireframe Complex model wireframe preview |
Installation
pip install aiden3drenderer
Requirements:
- Python 3.11+
- Pygame 2.6.0+ (installed automatically)
Quick Start
Minimal Example
from aiden3drenderer import Renderer3D, renderer_type
renderer = Renderer3D()
renderer.camera.position = [0, 0, 0]
renderer.render_type = renderer_type.POLYGON_FILL
renderer.run()
Looped Run Example
Use loopable_run() when you want to integrate custom game logic each frame.
from aiden3drenderer import Renderer3D, renderer_type
renderer = Renderer3D()
renderer.render_type = renderer_type.POLYGON_FILL
while True:
# Custom logic here
renderer.loopable_run()
Runtime Pause/Settings
Press Esc during run() to open the runtime menu.
You can:
- Switch render mode (
MESH/POLYGON_FILL/RASTERIZE) - Toggle raster debug views (depth/heat)
- Adjust FOV
- Adjust lighting strictness
- Toggle OBJ render mode handling
Feature Explanations
Custom 3D Projection
The renderer uses a full world-to-screen pipeline implemented in Python:
- Translate vertices into camera space.
- Apply yaw/pitch/roll rotations.
- Perspective divide by depth with FOV scaling.
- Map to screen coordinates.
No external 3D engine is required for the core projection math.
First-Person Camera (6-DOF)
Camera movement is designed for interactive exploration:
- Movement:
W/A/S/D,Space,Left Shift - Speed boost:
Left Ctrl - Rotation: right mouse drag + arrow key nudging
- FOV tweak: mouse wheel
15+ Procedural Shape Generators
Built-in generators include mathematical and stylized surfaces such as:
- Mountain, canyon, pyramid, torus, sphere
- Mobius strip, megacity, mandelbulb slice
- Klein bottle, trefoil knot
- Animated waves/ripples/spirals/alien terrain/double helix
Real-Time Rendering
The engine is designed for live rendering loops and interactive scenes. Typical scenes can run smoothly at real-time frame rates depending on selected mode and geometry complexity.
Animated Terrains
Animated shapes use the frame parameter inside shape generators. This enables time-driven deformation without changing the API shape contract.
Extensible Shape API
Create custom shapes using @register_shape.
from aiden3drenderer import Renderer3D, register_shape
import pygame
@register_shape("My Plane", key=pygame.K_p, is_animated=False, color=(200, 255, 150))
def generate_plane(grid_size=40, frame=0):
return [
[(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)],
]
renderer = Renderer3D()
renderer.run()
Important shape rule:
- Return a rectangular matrix (
list[list[tuple | None]]) where all rows have the same length.
Multiple Object Support
You can render multiple objects at once by appending several shape/OBJ entries to the active scene list.
Per-Shape Colors
Shapes can define explicit base colors in registration, which are respected in filled/raster workflows.
Simple Physics Engine
Physics module supports basic rigid-body style interactions:
- Sphere objects
- Plane colliders
- Gravity/forces
- Sphere-sphere and sphere-plane collision handling
- Camera physics wrapper support
For a full example, see the Physics section below.
Entities
Lightweight in-scene Entity objects are provided to attach models to simple runtime behaviour. Key points:
- Entities wrap a model (vertices/faces) and expose a small API:
add_script(script_str),toggle_gravity(),update(), and helpers to add variables/functions accessible to scripts. - Scripts are plain Python strings executed with
exec()in a sandboxed-ishvariablesnamespace; the default namespace containsentityandrenderer. - Built-in gravity script and collision helpers allow snapping, terminal velocity, and simple positional resolution using the renderer's
bounding_boxes. - Entities maintain
position,rotation,velocity, abounding_box, anddelta_timeand are updated each frame viaEntity.update().
Example:
from aiden3drenderer import Renderer3D, Entity, obj_loader
renderer = Renderer3D()
obj = obj_loader.get_obj("./assets/alloy_forge_block.obj")
entity = Entity(obj, renderer)
entity.toggle_gravity()
renderer.entities.append(entity)
while True:
for e in renderer.entities:
e.update()
renderer.loopable_run()
OBJ Loading
OBJ workflow supports standard model loading with extra quality-of-life features:
- Load from file path
- Parse UV (
vt) data for raster texturing - Triangulate faces with more than 3 vertices
- Per-object offset and
texture_indexsupport
For full usage, see OBJ Loading.
Rasterization Paths
Two fill/raster workflows are available:
- CPU software triangle filling (
POLYGON_FILL) - GPU compute-shader rasterization (
RASTERIZE)
Custom Shaders
CustomShader is a small helper around a ModernGL compute shader that parses buffer/uniform declarations and exposes simple helpers:
- Create with shader source and an optional ModernGL context:
CustomShader(shader_code, context=ctx). - Allocate SSBO-style buffers via
set_buffer(name, element_count, element_size=None)which binds storage buffers by layout binding. - Write to buffers/uniforms with
write_to_buffer(name, bytes)andwrite_to_uniform(name, value_or_bytes). - Read results back from buffers with
read_from_buffer(name, num_elements, element_type='vec3')which returns a NumPy array.
Example usage:
from aiden3drenderer.custom_shader import CustomShader
shader_src = """#version 430
layout(std430, binding=0) buffer mybuf { vec4 data[]; };
void main(){ /* ... */ }
"""
cs = CustomShader(shader_src, context=renderer.ctx)
cs.set_buffer('mybuf', element_count=1024, element_size=16)
# write raw bytes (numpy.tobytes()) and dispatch via cs.compute_shader
Three Render Modes
Switch with renderer.render_type:
renderer_type.MESHrenderer_type.POLYGON_FILLrenderer_type.RASTERIZE
Raster Debug Views
When using RASTERIZE mode:
toggle_depth_view(True)for depth visualizationtoggle_heat_map(True)for heat-map diagnostics
Texture Mapping
Apply texture sampling in raster mode using image files and OBJ UV coordinates.
Multi-Texture Pipeline
Add multiple texture layers and assign each OBJ to one via texture_index.
from aiden3drenderer import Renderer3D, obj_loader, renderer_type
renderer = Renderer3D(width=1000, height=800)
renderer.render_type = renderer_type.RASTERIZE
renderer.using_obj_filetype_format = True
renderer.add_texture_for_raster("./assets/model1.png") # index 0
renderer.add_texture_for_raster("./assets/model2.png") # index 1
obj1 = obj_loader.get_obj("./assets/model1.obj", texture_index=0)
obj2 = obj_loader.get_obj("./assets/model2.obj", texture_index=1, offset=(6, 0, 0))
renderer.vertices_faces_list.append(obj1)
renderer.vertices_faces_list.append(obj2)
renderer.run()
Runtime Shape Management
Enable or disable built-in shape set at runtime:
renderer = Renderer3D(load_default_shapes=False)
renderer.set_use_default_shapes(True)
Skybox Rendering
Skyboxes can be generated from:
- Explicit cubemap UV mappings
- Cross-layout atlas images via
generate_cross_type_cubemap_skybox(...)
Pause + Settings UI
Esc opens a pause/settings UI while running. Use it for fast iteration without restarting your app.
Video Renderer
The package includes an experimental OBJ-to-video renderer:
- Uses the same projection concepts as live renderer
- Supports per-object transforms and rotation rates
- Suitable for simple pre-rendered clips
More details in Video Renderer.
macOS GPU Note
RASTERIZE mode requires OpenGL 4.3 compute shaders, which are not available natively on macOS.
Use MESH/POLYGON_FILL on macOS, or see VM Workaround for macOS.
Renderer Modes and Debugging
from aiden3drenderer import Renderer3D, renderer_type
renderer = Renderer3D()
renderer.render_type = renderer_type.RASTERIZE
renderer.set_texture_for_raster("./assets/alloy_forge_block.png")
renderer.toggle_depth_view(True)
renderer.run()
Creating Custom Shapes
Shape functions must return a rectangular vertex matrix. Jagged rows can cause IndexError during mesh traversal.
Advanced Shape Example
from aiden3drenderer import Renderer3D, register_shape
import pygame
@register_shape("My Pyramid", key=pygame.K_p, is_animated=False)
def generate_pyramid(grid_size=40, frame=0):
matrix = []
center = grid_size / 2
for x in range(grid_size):
row = []
for y in range(grid_size):
dx = abs(x - center)
dy = abs(y - center)
max_dist = max(dx, dy)
height = max(0, 10 - max_dist)
row.append((x, height, y))
matrix.append(row)
return matrix
renderer = Renderer3D()
renderer.run()
Physics
About
Physics in Aiden3DRenderer is intentionally lightweight and extensible.
You can:
- Create sphere and plane physics objects
- Apply forces and impulses
- Simulate collisions
- Attach a physics camera
- Manage everything through
PhysicsObjectHandler
Example: Two Colliding Spheres
from aiden3drenderer import Renderer3D, physics, renderer_type
def main():
renderer = Renderer3D(width=1000, height=1000, title="My 3D Renderer")
shape = physics.ShapePhysicsObject(renderer, "sphere", (0, 0, 0), (100, 0, 0), 5, 20, 20)
shape.add_forces((-0.7, 0, 0))
shape.anchor_position = [20, 0, 0]
shape1 = physics.ShapePhysicsObject(renderer, "sphere", (0, 0, 0), (50, 0, 0), 5, 10, 20)
shape1.add_forces((0.7, 0, 0))
shape1.anchor_position = [0, 0, 0]
obj_handler = physics.PhysicsObjectHandler()
obj_handler.add_shape(shape)
obj_handler.add_shape(shape1)
renderer.set_starting_shape(None)
renderer.camera.position = [0, 0, 0]
renderer.render_type = renderer_type.POLYGON_FILL
while True:
obj_handler.handle_shapes()
renderer.loopable_run()
if __name__ == "__main__":
main()
Example: Balls in a Box + Camera Physics
from aiden3drenderer import Renderer3D, physics, renderer_type
def main():
renderer = Renderer3D(width=1000, height=1000, title="My 3D Renderer")
obj_handler = physics.PhysicsObjectHandler()
plane_color = (200, 200, 200)
plane_size = 28
grid_size = 8
obj_handler.add_plane(renderer, [0, -14, 0], (0, 0, 0), plane_color, plane_size, grid_size)
obj_handler.add_plane(renderer, [-14, 0, 0], (0, 0, 90), plane_color, plane_size, grid_size)
obj_handler.add_plane(renderer, [14, 0, 0], (0, 0, 90), plane_color, plane_size, grid_size)
obj_handler.add_plane(renderer, [0, 0, -14], (90, 0, 0), plane_color, plane_size, grid_size)
obj_handler.add_plane(renderer, [0, 0, 14], (90, 0, 0), plane_color, plane_size, grid_size)
ball_color = (100, 100, 255)
ball_radius = 4
ball_mass = 2.5
ball_grid = 8
ball1 = physics.ShapePhysicsObject(renderer, "sphere", (0, 0, 0), ball_color, ball_radius, ball_mass, ball_grid)
ball1.anchor_position = [0, 0, 0]
ball2 = physics.ShapePhysicsObject(renderer, "sphere", (0, 0, 0), ball_color, ball_radius, ball_mass, ball_grid)
ball2.anchor_position = [9, 0, 0]
gravity = (0, -0.18, 0)
ball1.add_forces((1, 0, 1))
camera = physics.CameraPhysicsObject(renderer, renderer.camera, 1, 10)
obj_handler.add_camera(camera)
obj_handler.add_shape(ball1)
obj_handler.add_shape(ball2)
renderer.set_starting_shape(None)
renderer.render_type = renderer_type.POLYGON_FILL
renderer.camera.base_speed = 1.2
while True:
ball1.add_forces(gravity)
ball2.add_forces(gravity)
camera.add_forces(tuple(v * 100 for v in gravity))
obj_handler.handle_shapes()
renderer.loopable_run()
if __name__ == "__main__":
main()
OBJ Loading
Example
from aiden3drenderer import Renderer3D, obj_loader, renderer_type
def main():
renderer = Renderer3D(width=1000, height=1000, title="My 3D Renderer")
renderer.current_shape = None
renderer.camera.position = [0, 0, 0]
renderer.render_type = renderer_type.POLYGON_FILL
renderer.using_obj_filetype_format = True
obj = obj_loader.get_obj("./assets/alloy_forge_block.obj", texture_index=0)
renderer.vertices_faces_list.append(obj)
renderer.run()
if __name__ == "__main__":
main()
Notes
obj_loader.get_obj(path, texture_index, offset=(x, y, z))supports per-object texture selection and world-space offset.- N-gon faces are triangulated automatically.
- UV coordinates (
vt) are parsed for texture mapping in raster mode. - Cross-layout skybox helper:
generate_cross_type_cubemap_skybox(radius, img_path).
Video Renderer
A lightweight experimental renderer for creating video clips from OBJ scenes.
Basic Usage
from aiden3drenderer.video_renderer import VideoRenderer3D, VideoRendererObject
obj = VideoRendererObject("assets/alloy_forge_block.obj")
obj.rotations_per_seccond = [10, 25, 0]
obj.rotation = [0, 0, 0]
vr = VideoRenderer3D(width=800, height=600, fps=30, shapes=[obj])
vr.render("out.avi", duration_s=5, verbose=True)
Multiple Objects
from aiden3drenderer.video_renderer import VideoRenderer3D, VideoRendererObject
o1 = VideoRendererObject("assets/model1.obj")
o1.rotations_per_seccond = [0, 40, 0]
o2 = VideoRendererObject("assets/model2.obj")
o2.rotations_per_seccond = [10, 0, 5]
o2.anchor_pos = [4, 0, 8]
vr = VideoRenderer3D(width=1200, height=800, fps=24, shapes=[o2, o1])
vr.render("multiples.avi", duration_s=10, verbose=True)
Tips:
- Keep resolution/FPS moderate while this module is still being optimized.
- Minor seam/overdraw artifacts are known limitations at the moment.
macOS GPU Note
GPU RASTERIZE mode needs GL 4.3 compute shaders, which are unavailable on native macOS drivers.
VM Workaround for macOS
- Install UTM
- Download Ubuntu ISO
- Create a Linux VM in UTM
- Install Python:
sudo apt update
sudo apt install python3.11
python3 --version
sudo apt install python3-pip
Then install the package inside the VM:
pip install aiden3drenderer
Controls
Camera Movement
W/A/S/D- Move forward/left/backward/rightSpace- Move upLeft Shift- Move downLeft Ctrl- Speed boost (2x)- Mouse wheel - Adjust camera FOV
- Arrow keys - Fine pitch/yaw adjustment
- Right mouse + drag - Look around
Terrain Selection
1- Mountain terrain2- Animated sine waves3- Ripple effect4- Canyon valley5- Stepped pyramid6- Spiral surface7- Torus8- Sphere9- Mobius strip0- MegacityQ- Alien landscapeE- Double helixR- Mandelbulb sliceT- Klein bottleY- Trefoil knot
Other
Escape- Open/close pause menu inrun()mode
Terrain Descriptions
Static Terrains
Mountain (1) - Smooth parabolic mountain with radial falloff.
Canyon (4) - U-shaped valley with sinusoidal variation.
Pyramid (5) - Stepped pyramid using Chebyshev distance.
Torus (7) - Classic donut shape from parametric equations.
Sphere (8) - UV sphere generated from spherical coordinates.
Mobius Strip (9) - Non-orientable surface with a single continuous side.
Megacity (0) - 80x80 procedural city (6400 vertices) with roads and building variation.
Mandelbulb (R) - 2D slice through a Mandelbulb-style fractal field.
Klein Bottle (T) - Non-orientable 4D-inspired surface projected into 3D.
Trefoil Knot (Y) - Tube mesh along a classic trefoil knot path.
Animated Terrains
Waves (2) - Multi-frequency flowing sine surface.
Ripple (3) - Expanding circular wave with amplitude decay.
Spiral (6) - Rotating polar-coordinate surface animation.
Alien Landscape (Q) - Mixed procedural terrain with craters, spikes, and pulsation.
Double Helix (E) - Twin strand structure with phase offset and animation.
Technical Details
3D Projection Pipeline
- World coordinates
- Camera translation
- Camera rotation (yaw/pitch/roll)
- Perspective projection with FOV
- Screen-space mapping
Rotation Equations
Yaw (Y-axis):
x' = x*cos(theta) + z*sin(theta)
z' = -x*sin(theta) + z*cos(theta)
Pitch (X-axis):
y' = y*cos(phi) - z*sin(phi)
z' = y*sin(phi) + z*cos(phi)
Roll (Z-axis):
x' = x*cos(psi) - y*sin(psi)
y' = x*sin(psi) + y*cos(psi)
Culling
Vertices behind the camera (z <= 0.1) are culled (None) to avoid invalid perspective division and visual artifacts.
Performance Notes
- Most built-in terrains are intended to be playable in real-time.
- Large scenes (especially filled modes) are heavier;
MESHmode is best for maximum speed. Megacityis one of the largest defaults and a good stress test.
API Reference
Renderer3D
from aiden3drenderer import Renderer3D
renderer = Renderer3D(width=1200, height=800)
renderer.run()
Useful methods and attributes:
set_starting_shape(shape_name_or_none)set_use_default_shapes(bool)set_render_type(renderer_type.*)toggle_depth_view(bool)toggle_heat_map(bool)set_texture_for_raster(path)add_texture_for_raster(path)generate_cross_type_cubemap_skybox(radius, img_path)generate_cubemap_skybox(...)using_obj_filetype_formatvertices_faces_listlighting_strictnessentities: list ofEntityobjects attached to the scene (update them each frame or let your loop call them).CustomShader: helper class (seeaiden3drenderer.custom_shader.CustomShader) to run compute shaders and manage SSBO/uniform access.
Camera
from aiden3drenderer import Renderer3D
renderer = Renderer3D()
camera = renderer.camera
print(camera.position) # [x, y, z]
print(camera.rotation) # [pitch, yaw, roll]
print(camera.speed)
print(camera.base_speed)
register_shape Decorator
@register_shape(name, key=None, is_animated=False, color=None)
def generate_function(grid_size=40, frame=0):
return matrix
Expected return type:
list[list[tuple[float, float, float] | None]](rectangular matrix)
Package Structure
aiden3drenderer/
|-- __init__.py
|-- renderer.py
|-- camera.py
|-- obj_loader.py
|-- physics.py
|-- shapes.py
`-- video_renderer.py
examples/
|-- basic_usage.py
|-- custom_shape_example.py
|-- obj_example.py
`-- physics_test.py
Development
Run from Source
git clone https://github.com/AidenKielby/3D-mesh-Renderer
cd 3D-mesh-Renderer
pip install -e .
python examples/basic_usage.py
Build + Publish
pip install build twine
python -m build
python -m twine upload dist/*
Credits
Created by Aiden. Procedural terrain ideas were AI-assisted in places; core renderer/projection/camera and package engineering are authored manually.
License
MIT
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-1.9.3.tar.gz.
File metadata
- Download URL: aiden3drenderer-1.9.3.tar.gz
- Upload date:
- Size: 95.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3943ffc9149b5aee2091b15c2877320a133256b95fa251345f943a3af7602043
|
|
| MD5 |
a45e7b64aaf335557736dad71e28d7cf
|
|
| BLAKE2b-256 |
6525a4c668823c3bfa49381dc25b9e2c286268271ec4588b4c58b5f2f3539837
|
File details
Details for the file aiden3drenderer-1.9.3-py3-none-any.whl.
File metadata
- Download URL: aiden3drenderer-1.9.3-py3-none-any.whl
- Upload date:
- Size: 88.0 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 |
f1bfc08a160a9c599dcab5aa682ed3ae87a8c6f2a3131a3d83b611ceb44a085c
|
|
| MD5 |
f9b7cee70880e39cf791acdc7b0886ad
|
|
| BLAKE2b-256 |
7e565c50da5c9a14078661909a2173a20a2a9b20f74eeb8c78bf9fd658ecb448
|