Skip to content

AidenKielby/3D-mesh-Renderer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

170 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PyPI Downloads PyPI version Python Versions Per Month License: MIT PyPI status GitHub stars Last commit

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.

Gallery

Ripple Animation
Ripple Effect
Expanding waves from center
Mandelbulb Fractal
Mandelbulb Slice
3D fractal cross-section
Turning Spiral
Turning Spiral
Screw-like surface animation
Simple City (laggy when solid render)
Simple City
Large procedural city terrain
Tree Mesh
Tree Mesh
Wireframe OBJ rendering
Tree Solid Render
Tree Solid Render
Filled rendering from same scene
Colored Tree
Colored Tree
Per-shape color update showcase
Physics Demo
Physics Demo
Collision and motion in scene
Skull and Skybox
Skull + Skybox
OBJ + environment background
Minecraft Boat Wireframe
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:

  1. Translate vertices into camera space.
  2. Apply yaw/pitch/roll rotations.
  3. Perspective divide by depth with FOV scaling.
  4. 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-ish variables namespace; the default namespace contains entity and renderer.
  • 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, a bounding_box, and delta_time and are updated each frame via Entity.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_index support

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) and write_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.MESH
  • renderer_type.POLYGON_FILL
  • renderer_type.RASTERIZE

Raster Debug Views

When using RASTERIZE mode:

  • toggle_depth_view(True) for depth visualization
  • toggle_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

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/right
  • Space - Move up
  • Left Shift - Move down
  • Left Ctrl - Speed boost (2x)
  • Mouse wheel - Adjust camera FOV
  • Arrow keys - Fine pitch/yaw adjustment
  • Right mouse + drag - Look around

Terrain Selection

  • 1 - Mountain terrain
  • 2 - Animated sine waves
  • 3 - Ripple effect
  • 4 - Canyon valley
  • 5 - Stepped pyramid
  • 6 - Spiral surface
  • 7 - Torus
  • 8 - Sphere
  • 9 - Mobius strip
  • 0 - Megacity
  • Q - Alien landscape
  • E - Double helix
  • R - Mandelbulb slice
  • T - Klein bottle
  • Y - Trefoil knot

Other

  • Escape - Open/close pause menu in run() 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

  1. World coordinates
  2. Camera translation
  3. Camera rotation (yaw/pitch/roll)
  4. Perspective projection with FOV
  5. 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; MESH mode is best for maximum speed.
  • Megacity is 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_format
  • vertices_faces_list
  • lighting_strictness
  • entities: list of Entity objects attached to the scene (update them each frame or let your loop call them).
  • CustomShader: helper class (see aiden3drenderer.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