Skip to main content

Real-time 3D rendering engine with ECS architecture, built on pure wgpu

Project description

ManifoldX

PyPI version Python versions License Tests

A real-time 3D rendering engine built on pure wgpu with an Entity Component System (ECS) architecture. Written in Python with numpy for high-performance data handling.

⚠️ Beta / Academic Project — This is an experimental proof-of-concept exploring the extent to which Python can be used for high-performance graphics via wgpu. Not recommended for production use. Expect bugs, breaking changes, and missing features.

Motivation

Domain researchers need to run large-scale simulations (10⁴-10⁶ entities) and visualize results in 3D. Currently they can't do both in pure Python.

Tool Simulation Visualization Language
Matplotlib/Plotly 2D only, slow at scale Pure Python
VisPy OpenGL, steep curve Python + GLSL
PyGfx Excellent rendering Pure Python
Game engines (PyGame) 2D mostly Pure Python
Specialized (OpenMM, SUMO) Data export only Fortran/C++

The gap: No Python tool combines data-driven simulation with accessible 3D visualization in one package.

The Vision

ManifoldX gives researchers both:

  1. Simulation in pure NumPy — vectorized operations over entity arrays, no Python loops in the hot path
  2. 3D visualization without graphics knowledge — spawn entities with meshes/materials, the engine handles GPU rendering
# Write physics in pure numpy (data-driven, not OOP)
@engine.system
def nbody_physics(query, dt):
    # Magic happens here...
    forces = compute_gravity_all_pairs(query[Transform].pos.data)
    velocities += forces * dt
    query[Transform].pos += velocities * dt

# Engine handles GPU buffers, WGSL shaders, instanced draw calls
# You get 3D visualization of your simulation instantly

Target domains: Astrophysics (galaxy formation), molecular dynamics (protein folding), epidemiology (disease spread), crowd science (evacuation flows), traffic engineering (vehicle flow), weather (particle advection).

Works in: Jupyter notebooks, Quarto documents, Streamlit dashboards, standalone scripts.

⚠️ Spoiler: Python is surprisingly capable at real-time 3D. The N-body demo runs 500 bodies with N² gravity — 250,000 force pairs per frame in pure numpy.

Why Not PyGfx?

PyGfx is an excellent rendering engine and has been a key source of inspiration — we've learned a lot from its WGSL patterns, material system, and API design. However, it's fundamentally a scene graph engine (object-oriented hierarchy of transforms), not a data-driven ECS. For large-scale simulations with vectorized physics, the OOP overhead of traversing a scene graph becomes a bottleneck. ManifoldX uses a flat SoA data layout where simulation logic operates directly on numpy arrays.

Technically this is a game engine — the ECS + instanced rendering + PBR pipeline is exactly what you'd use for a game. But that's not the focus. The goal is making 3D visualization accessible to researchers who don't know anything about graphics.

Installation

pip install manifold-gfx           # Base (no rendering backends)
pip install manifold-gfx[desktop]  # + GLFW for desktop window
pip install manifold-gfx[offline]  # + imageio-ffmpeg for video
pip install manifold-gfx[all]      # Everything

# or with uv
uv add manifold-gfx
uv add manifold-gfx[desktop]       # Desktop rendering
uv add manifold-gfx[offline]      # Video rendering

Requirements:

  • Python 3.13+
  • GPU with WebGPU support (via wgpu backend)
    • Vulkan on Linux
    • Metal on macOS
    • D3D12 on Windows

Note: Install with [desktop] for interactive rendering, [offline] for video output, or [all] for both.

Quick Start

import manifoldx as mx
import numpy as np

from manifoldx.components import Transform, Mesh, Material
from manifoldx.resources import StandardMaterial, PointLight, cube, sphere

# Create engine with default settings
engine = mx.Engine("My First Scene")

# Create a cube and sphere
cube_geo = cube(1, 1, 1)
sphere_geo = sphere(0.7, 32)

# Create PBR materials (roughness: 0-1, metallic: 0-1)
red_shiny = StandardMaterial(color="#ff3333", roughness=0.15, metallic=0.9)
blue_dull = StandardMaterial(color="#3366ff", roughness=0.8, metallic=0.0)

# Spawn entities
engine.spawn(
    Mesh(cube_geo),
    Material(red_shiny),
    Transform(pos=(-1.5, 0, 0)),
)

engine.spawn(
    Mesh(sphere_geo),
    Material(blue_dull),
    Transform(pos=(1.5, 0, 0)),
)

# Add an orbiting light
light = PointLight(color="#ffffff", intensity=15.0, position=(5, 5, 5))
engine.set_lights([light])

# Animate
@engine.system
def animate_lights(query: mx.Query[Transform], dt: float):
    t = engine.elapsed
    light.position = (
        5 * np.cos(t * 0.7),
        3 + np.sin(t * 0.5) * 2,
        5 * np.sin(t * 0.7),
    )

# Auto-fit camera to view the scene
engine.camera.fit(radius=5.0, azimuth=30, elevation=35)

# CLI handles run vs render automatically
engine.cli()

Save as my_scene.py and run:

python my_scene.py              # Interactive window
python my_scene.py --render     # Render 60s video → my_scene.mp4
python my_scene.py --render --fps 60 --duration 120 --output movie.mp4

N-Body Simulation

A pure-numpy gravitational simulation running 500 bodies in real-time at a single draw call (instanced rendering).

Physics: All pairwise forces are computed with a single vectorized numpy expression — no Python loops in the hot path. For N bodies this means N² = 250,000 force computations per frame, each a 3-component vector.

@engine.system
def nbody_physics(query: mx.Query[Transform], dt: float):
    global velocities
    pos = query[Transform].pos.data

    # All-pairs position differences (N, N, 3) — one numpy broadcast
    diff = pos[None, :] - pos[:, None]

    # Pairwise distances (N, N)
    dist = np.linalg.norm(diff, axis=2)
    dist = np.maximum(dist, SOFTENING)

    # Gravitational force magnitude for every pair
    force_mag = G * (masses[None, :] * masses[:, None]) / dist**2

    # Net force on each body: sum over all other bodies
    direction = diff / dist[:, :, None]
    forces = force_mag[:, :, None] * direction
    net_force = forces.sum(axis=1)

    # Integrate: F = ma → a = F/m
    velocities += (net_force / masses[:, None]) * dt
    query[Transform].pos += velocities * dt

See examples/nbody.py for the full implementation with velocity damping and speed clamping.

Ideal Gas Simulation

The examples/gas.py demo shows the other side: no gravity, all collisions. 500 particles bounce inside an invisible box with elastic collisions and wall reflections — a kinetic theory simulation in pure numpy.

Collisions find overlapping pairs with a vectorized comparison, filter with np.where(np.triu(...)), then resolve impulse with np.add.at for safe accumulation. Wall reflections are a single vectorized mask: velocities[next_pos < wall] = np.abs(...).

See examples/gas.py for the full implementation.

Boids Flocking Simulation

The examples/boids.py demo shows emergent behavior from simple local rules: 300 boids with separation, alignment, cohesion, plus 4 wandering predators they flee from.

Flocking rules (all vectorized):

  • Separation — boids repel neighbors weighted by 1/dist²
  • Alignment — match average velocity of nearby boids
  • Cohesion — steer toward center of mass of neighbors

Predator avoidance — boids detect predators within 10 units and flee with 20x the force of any flocking rule. Fleeing boids get a speed boost (15 vs 10).

Spatial optimization — uses squared distances to avoid sqrt in the hot path. All three rules computed via masked tensor sums over axis=1 of an (N, N, 3) difference tensor.

See examples/boids.py for the full implementation.

Three Demos, Three Vectorization Patterns

Demo Entities Physics Pattern Operations/Frame
nbody.py 500 All-pairs gravity N² = 250,000 force pairs
gas.py 500 Pair collisions + walls O(N²) pair checks + wall masks
boids.py 300 + 4 Neighbor flocking + predator-flee (N,N,3) tensor sums + (N,P,3)

All three use pure numpy — zero Python loops in the hot path. The ECS overhead is ~microseconds/frame; the bottleneck is GPU fill-rate, not CPU physics.

Example Description
hello_world.py Minimal empty window
cube.py Rotating cube with Phong material
pbr_demo.py 3×2 grid demonstrating PBR materials + 3 orbiting lights
spheres.py Many spheres with physics-like behavior
nbody.py 500-body gravitational simulation with pure-numpy physics
gas.py 500-particle ideal gas with collisions and virtual walls
boids.py 300-agent flocking simulation with emergent swarm behavior

Run an example:

python examples/nbody.py              # Interactive window
python examples/nbody.py --render 60  # Render 60s video to nbody.mp4

Features

ECS Architecture

  • Structure of Arrays (SoA) layout for each component
  • Vectorized numpy operations for batch transforms
  • Free-list for efficient entity reuse
  • Component view with operator overloads (+=, *=, etc.)

Rendering

  • Instanced drawing — single draw call per (geometry, material) batch
  • Material-specific pipelines — each material type compiles its own WGSL shader
  • Transform caching — dirty-flag optimization to avoid recomputing matrices
  • Shared transform buffer — all instance transforms uploaded once per frame

Materials & Lighting

  • BasicMaterial — unlit flat color with simple diffuse
  • StandardMaterial — full PBR with GGX BRDF
    • Roughness/metallic workflow
    • Multiple point lights with inverse-square attenuation
    • Reinhard tonemapping + gamma correction
  • External lights — passed to engine like camera (not in ECS)

Camera

  • Perspective projection (WebGPU NDC)
  • Spherical coordinate orbit controls
  • Fit/fit_bounds for automatic framing

Geometries

  • Cube (with normals)
  • UV Sphere (with normals, CCW winding)
  • Plane (with normals)

Video Rendering

Render simulations to video files for sharing or CI using the built-in CLI:

python examples/nbody.py --render
python examples/nbody.py --render --fps 60 --duration 120 --output movie.mp4

Options:

  • --render — Render to video instead of showing window
  • --fps N — Frames per second (default: 30)
  • --duration N — Video duration in seconds (default: 60)
  • --output FILE — Output filename (default: <script>.mp4)
  • --quality Q — Quality: low, medium, high (default: high)

Programmatic API:

engine.render(
    output="simulation.mp4",
    fps=30,
    duration=60,
    quality="high",
)

Note: Video rendering requires the offline extra: pip install manifold-gfx[offline]

Architecture Highlights

The ECS uses numpy arrays for all component data. When you call query[Transform].pos += velocity * dt, it's a single vectorized numpy operation spanning thousands of entities.

Real-world examples: The N-body demo (examples/nbody.py) simulates 500 bodies with 250,000 pairwise gravitational force computations per frame. The ideal gas demo (examples/gas.py) runs 500 particles with elastic collisions and wall reflections. Both are pure numpy with zero Python loops.

Limitations (Known)

  • ❌ No shadows
  • ❌ No texture support
  • ❌ No environment/IBL mapping
  • ❌ Single material params per draw call (not per-instance)
  • ❌ Only point lights in PBR shader
  • ❌ Limited to ~100k entities

Future Ideas

This is an academic/experimental project. Ideas for future development:

  1. Per-instance material data — Storage buffer for varying roughness/metallic per instance in a single draw
  2. Shadow mapping — Shadow pass + PCF sampling
  3. Texture maps — Diffuse, normal, roughness textures via storage buffers
  4. Spot/Directional lights — Extend PBR shader
  5. Environment mapping — IBL with prefiltered radiance
  6. Skinned animation — Bone transforms in vertex shader
  7. Post-processing — Bloom, DOF, TAA
  8. Deferred rendering — Forward+ / clustered lighting for many lights

Contributing

Contributions welcome! This is an educational project — all skill levels encouraged.

Areas needing work:

  • Bug fixes and stability improvements
  • Additional geometry types (torus, cylinder, etc.)
  • More material types (toon, unlit with texture)
  • Shadow implementation
  • Performance profiling and optimization

Getting started:

# Clone and set up
git clone https://github.com/apiad/manifoldx.git
cd manifoldx
pip install -e ".[dev]"

# Run tests
make test

# Run an example
python -m examples.cube

Testing

# Run all tests (uses offline backend for headless CI)
make test

# Run specific test file
python -m pytest tests/test_ecs.py -v

Current test coverage: 162 tests covering ECS operations, components, materials, rendering, camera, and video output.

License

MIT License — See LICENSE file.

Credits

  • wgpu — Pure Python WebGPU bindings
  • PyGfx — Reference for WGSL shader patterns
  • rendercanvas — Window management

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

manifold_gfx-0.3.0.tar.gz (208.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

manifold_gfx-0.3.0-py3-none-any.whl (40.1 kB view details)

Uploaded Python 3

File details

Details for the file manifold_gfx-0.3.0.tar.gz.

File metadata

  • Download URL: manifold_gfx-0.3.0.tar.gz
  • Upload date:
  • Size: 208.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for manifold_gfx-0.3.0.tar.gz
Algorithm Hash digest
SHA256 17d6e3a9bece317837a768893743700d1867ac456cd37d1958b41cf75739c65b
MD5 9ab9e6307eb1f1e4429dc56f4c3a9f7d
BLAKE2b-256 bcf48dd18e083392ede20ed156bc379a3786275d95f36bb5f7a459e017c3d666

See more details on using hashes here.

File details

Details for the file manifold_gfx-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: manifold_gfx-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 40.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for manifold_gfx-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 68ce84a8fddb13d15cda771c78d15a6b9a1ced1918970fab6a02e8102a5caba2
MD5 39609c752a7e7fef6c18ef069bd1e03d
BLAKE2b-256 6020b7de321ffabac10c4ae3faca15175e7dd096f1226d7412bf28b7be3c558e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page