ManifoldX
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:
- Simulation in pure NumPy — vectorized operations over entity arrays, no Python loops in the hot path
- 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.pyfor 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.pyfor 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.pyfor 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:
- Per-instance material data — Storage buffer for varying roughness/metallic per instance in a single draw
- Shadow mapping — Shadow pass + PCF sampling
- Texture maps — Diffuse, normal, roughness textures via storage buffers
- Spot/Directional lights — Extend PBR shader
- Environment mapping — IBL with prefiltered radiance
- Skinned animation — Bone transforms in vertex shader
- Post-processing — Bloom, DOF, TAA
- 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
Metadata
Release files for manifold-gfx 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| manifold_gfx-0.3.0.tar.gz | 208.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| manifold_gfx-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 248.4 kB
Release files / manifold_gfx-0.3.0.tar.gz
| Download URL | manifold_gfx-0.3.0.tar.gz |
|---|---|
| Size | 208.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
17d6e3a9bece317837a768893743700d1867ac456cd37d1958b41cf75739c65b
|
|
BLAKE2b-256 checksum How to use checksums |
bcf48dd18e083392ede20ed156bc379a3786275d95f36bb5f7a459e017c3d666
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / manifold_gfx-0.3.0-py3-none-any.whl
| Download URL | manifold_gfx-0.3.0-py3-none-any.whl |
|---|---|
| Size | 40.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
68ce84a8fddb13d15cda771c78d15a6b9a1ced1918970fab6a02e8102a5caba2
|
|
BLAKE2b-256 checksum How to use checksums |
6020b7de321ffabac10c4ae3faca15175e7dd096f1226d7412bf28b7be3c558e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|