PyVR is a GPU-accelerated 3D volume rendering toolkit for real-time interactive visualization using OpenGL. Built with ModernGL, it provides high-performance volume rendering with a modern, modular architecture.
โ ๏ธ Pre-1.0 Development: Breaking changes are expected. API stability comes at v1.0.0.
- โก GPU-Accelerated Rendering: Real-time OpenGL volume rendering via ModernGL at 64+ FPS
- ๐ High-Performance RGBA Textures: Single-texture transfer function lookups for optimal performance
- โ๏ธ Quality Presets: Easy performance/quality tradeoff with RenderConfig presets (fast, balanced, high_quality)
- ๐งฉ Pipeline-Aligned Architecture: Clean separation of Application, Geometry, and Fragment stages
- ๐ฆ Backend-Agnostic Volume Data: Unified Volume class for data, normals, and bounds management
- ๐น Advanced Camera System: Matrix generation, spherical coordinates, animation paths, and presets
- ๐ก Flexible Lighting System: Directional, point, and ambient light presets with easy configuration
- ๐จ Sophisticated Transfer Functions: Color and opacity mappings with matplotlib integration
- ๐ฎ Interactive Interface: Real-time volume visualization with transfer function editing (v0.3.0+)
- v0.3.1: FPS counter, quality presets, camera-linked lighting, histogram background
- NEW in v0.3.2: 3-column layout for better display of all interface information
- Trackball Camera Control - Intuitive 3D rotation (default) with orbit mode available
- ๐ Synthetic Datasets: Built-in generators for testing and development
- โ Comprehensive Testing: 398 tests with 86%+ coverage
# Clone the repository
git clone https://github.com/JixianLi/pyvr.git
cd pyvr
# Install with Poetry (recommended)
poetry install
# Or install dependencies manually
pip install moderngl numpy matplotlib pillow scipyimport numpy as np
import matplotlib.pyplot as plt
from pyvr.moderngl_renderer import VolumeRenderer
from pyvr.config import RenderConfig
from pyvr.transferfunctions import ColorTransferFunction, OpacityTransferFunction
from pyvr.camera import Camera
from pyvr.lighting import Light
from pyvr.volume import Volume
from pyvr.datasets import create_sample_volume, compute_normal_volume
# Create volume data with Volume class
volume_data = create_sample_volume(256, 'double_sphere')
normals = compute_normal_volume(volume_data)
volume = Volume(
data=volume_data,
normals=normals,
min_bounds=np.array([-1.0, -1.0, -1.0], dtype=np.float32),
max_bounds=np.array([1.0, 1.0, 1.0], dtype=np.float32)
)
# Create camera
camera = Camera.from_spherical(
target=np.array([0.0, 0.0, 0.0]),
distance=3.0,
azimuth=np.pi/4, # 45 degrees
elevation=np.pi/6, # 30 degrees
roll=0.0
)
# Create light
light = Light.directional(direction=[1, -1, 0], ambient=0.2, diffuse=0.8)
# Create renderer with high quality preset
config = RenderConfig.high_quality() # Or: fast(), balanced(), preview()
renderer = VolumeRenderer(width=512, height=512, config=config, light=light)
renderer.load_volume(volume)
renderer.set_camera(camera)
# Set up transfer functions
ctf = ColorTransferFunction.from_colormap('viridis')
otf = OpacityTransferFunction.linear(0.0, 0.1)
renderer.set_transfer_functions(ctf, otf)
# Render
data = renderer.render()
image = np.frombuffer(data, dtype=np.uint8).reshape((512, 512, 4))
plt.imshow(image, origin='lower')
plt.show()PyVR includes an interactive matplotlib-based interface for real-time volume visualization and transfer function editing (v0.3.0+):
from pyvr.interface import InteractiveVolumeRenderer
from pyvr.datasets import create_sample_volume
from pyvr.volume import Volume
import numpy as np
# Create volume
volume_data = create_sample_volume(128, 'sphere')
volume = Volume(data=volume_data)
# Launch interactive interface
interface = InteractiveVolumeRenderer(volume=volume)
# NEW in v0.3.1: Enable camera-linked lighting
interface.set_camera_linked_lighting(azimuth_offset=np.pi/4)
# Launch (FPS counter and histogram enabled by default)
interface.show()
# After closing: Capture ultra-quality image
# path = interface.capture_high_quality_image("render.png")Features:
- ๐ฅ Real-time camera controls: Mouse drag to orbit, scroll to zoom
- ๐จ Interactive opacity transfer function editor: Add, remove, and drag control points
- ๐ Colormap selection: Choose from 12+ matplotlib colormaps
- โก Performance optimized: Render throttling, caching, and smart updates
- NEW in v0.3.1:
- ๐ FPS Counter: Real-time performance monitoring with rolling average
- โ๏ธ Quality Presets: 5 rendering quality levels (preview โ ultra)
- ๐ก Camera-Linked Lighting: Light follows camera for consistent illumination
- ๐ Histogram Background: Log-scale data distribution in opacity editor
Trackball mode (default):
- Image display: Drag to rotate camera (like rotating a ball), scroll to zoom
- Opacity editor: Left-click to add/select control points, right-click to remove, drag to move
Orbit mode (press 't' to toggle):
- Image display: Drag left/right for azimuth, up/down for elevation, scroll to zoom
- Opacity editor: Same as trackball mode
Keyboard Shortcuts:
r: Reset camera to isometric views: Save current rendering to PNG filet: Toggle camera control mode (trackball โ orbit)f: Toggle FPS counter โจ NEW in v0.3.1h: Toggle histogram background โจ NEW in v0.3.1l: Toggle light linking to camera โจ NEW in v0.3.1q: Toggle automatic quality switching โจ NEW in v0.3.1Esc: Deselect control pointDelete/Backspace: Remove selected control point
Quality Presets (v0.3.1):
- Preview: Extremely fast (~50 samples/ray)
- Fast: Interactive quality (~86 samples/ray)
- Balanced: Default quality (~173 samples/ray)
- High Quality: Publication quality (~346 samples/ray)
- Ultra: Maximum quality (~1732 samples/ray)
Performance Features (v0.3.1):
- Auto-quality switching: Automatically uses "fast" preset during camera interaction
- Histogram caching: >5x speedup with persistent cache
- All monitoring features: <1% overhead
Note: This is a testing/development interface. For production use, consider implementing a custom backend.
See examples/basic_rendering.py for a complete basic example.
PyVR follows a pipeline-aligned architecture based on traditional rendering pipeline stages:
pyvr/
โโโ volume/ # Application Stage - Volume data management
โ โโโ data.py # Volume class with properties and operations
โโโ camera/ # Geometry Stage - Camera transformations
โ โโโ camera.py # Camera class with matrix generation
โ โโโ control.py # Camera controllers and animation
โโโ lighting/ # Application Stage - Light configuration
โ โโโ light.py # Light class with presets and camera linking
โโโ config.py # Rasterization Stage - Rendering configuration
โโโ transferfunctions/ # Application Stage - Material properties
โ โโโ color.py # Color transfer functions
โ โโโ opacity.py # Opacity transfer functions
โโโ moderngl_renderer/ # OpenGL Volume Renderer
โ โโโ renderer.py # ModernGLVolumeRenderer (main renderer)
โ โโโ manager.py # Low-level OpenGL resource management
โโโ interface/ # Interactive Interface (v0.3.0+)
โ โโโ matplotlib_interface.py # InteractiveVolumeRenderer with all features
โ โโโ widgets.py # UI components (ImageDisplay, OpacityEditor, etc.)
โ โโโ state.py # InterfaceState for state management
โ โโโ cache.py # Histogram caching (v0.3.1)
โโโ shaders/ # Fragment Stage - Shading operations
โ โโโ volume.vert.glsl # Vertex shader
โ โโโ volume.frag.glsl # Fragment shader with RGBA lookups
โโโ datasets/ # Application Stage - Volume data utilities
โโโ synthetic.py # Synthetic volume generators
PyVR provides a unified Volume class for backend-agnostic data management:
from pyvr.volume import Volume
import numpy as np
# Create Volume with all attributes
volume = Volume(
data=volume_data, # 3D numpy array
normals=normal_data, # Optional 4D array (D,H,W,3)
min_bounds=np.array([-1.0, -1.0, -1.0], dtype=np.float32),
max_bounds=np.array([1.0, 1.0, 1.0], dtype=np.float32),
name="my_volume" # Optional name
)
# Simple volume (default bounds: [-0.5, -0.5, -0.5] to [0.5, 0.5, 0.5])
volume = Volume(data=volume_data)# Access volume metadata
print(volume.shape) # (256, 256, 256) - voxel dimensions
print(volume.dimensions) # [2.0, 2.0, 2.0] - physical size (max - min)
print(volume.center) # [0.0, 0.0, 0.0] - bounding box center
print(volume.has_normals) # True/False - check if normals present
print(volume.voxel_spacing) # [0.0078, 0.0078, 0.0078] - spacing between voxels# Compute normals from volume data
volume.compute_normals() # Generates normals using gradient
assert volume.has_normals # True after computation
# Normalize volume data
normalized_vol = volume.normalize(method="minmax") # Scale to [0, 1]
normalized_vol = volume.normalize(method="zscore") # Z-score normalization
# Create independent copy
vol_copy = volume.copy()
vol_copy.data[0, 0, 0] = 1.0 # Doesn't affect originalfrom pyvr.moderngl_renderer import VolumeRenderer
# Create and load volume (single call)
renderer = VolumeRenderer()
renderer.load_volume(volume)
# Get current volume
current_volume = renderer.get_volume()PyVR includes built-in synthetic volume generators:
from pyvr.datasets import create_sample_volume, compute_normal_volume
# Create various synthetic volumes
sphere = create_sample_volume(256, 'sphere')
torus = create_sample_volume(256, 'torus')
double_sphere = create_sample_volume(256, 'double_sphere')
helix = create_sample_volume(256, 'helix')
cube = create_sample_volume(256, 'cube')
blob = create_sample_volume(256, 'random_blob')
# Compute normal vectors for lighting
normals = compute_normal_volume(sphere)PyVR supports loading scientific volume data from VTK ImageData (.vti) files:
from pyvr.dataloaders import load_vtk_volume
# Load VTK file - returns normalized, render-ready Volume
volume = load_vtk_volume("example_data/hydrogen.vti")
# Use with any renderer
from pyvr.interface import InteractiveVolumeRenderer
interface = InteractiveVolumeRenderer(volume)
interface.show()Features:
- Automatic normalization to [0, 1] range
- Preserves physical aspect ratio from VTK spacing
- Centered at [0, 0, 0] in world space
- Normal vectors computed automatically
- Supports custom scalar array names
Example:
# Load with custom scalar array name
volume = load_vtk_volume("data.vti", scalars_name="Temperature")
# Volume properties
print(f"Shape: {volume.shape}")
print(f"Bounds: {volume.min_bounds} to {volume.max_bounds}")
print(f"Physical dimensions: {volume.dimensions}")Requirements: VTK is included as a core dependency (required for VTK data loading).
PyVR provides quality presets for easy performance/quality tradeoffs via the RenderConfig class:
from pyvr.config import RenderConfig
from pyvr.moderngl_renderer import VolumeRenderer
# Use a preset
renderer = VolumeRenderer(config=RenderConfig.fast()) # Fast, interactive
renderer = VolumeRenderer(config=RenderConfig.balanced()) # Default, good balance
renderer = VolumeRenderer(config=RenderConfig.high_quality()) # High quality, slower
renderer = VolumeRenderer(config=RenderConfig.preview()) # Very fast, low quality
renderer = VolumeRenderer(config=RenderConfig.ultra_quality()) # Maximum quality, very slow| Preset | Step Size | Max Steps | Est. Speed | Use Case |
|---|---|---|---|---|
| preview | 0.05 | 50 | ~15x faster | Quick iteration |
| fast | 0.02 | 100 | ~5x faster | Interactive exploration |
| balanced | 0.01 | 500 | 1x (baseline) | General use (default) |
| high_quality | 0.005 | 1000 | ~5x slower | Final renders |
| ultra_quality | 0.001 | 2000 | ~20x slower | Publication quality |
# Create custom config
config = RenderConfig(
step_size=0.015,
max_steps=300,
early_ray_termination=True,
opacity_threshold=0.95
)
renderer = VolumeRenderer(config=config)
# Or modify a preset
config = RenderConfig.balanced().with_step_size(0.008)
config = RenderConfig.fast().with_max_steps(200)# Change quality on the fly
renderer.set_config(RenderConfig.fast()) # Switch to fast rendering
renderer.set_config(RenderConfig.high_quality()) # Switch to high quality
# Get current config
current_config = renderer.get_config()
print(current_config) # Shows current settings# Estimate rendering performance
config = RenderConfig.high_quality()
samples = config.estimate_samples_per_ray() # ~346 samples
relative_time = config.estimate_render_time_relative() # ~5.0x slower than balancedPyVR implements Beer-Lambert law for physically correct opacity accumulation (v0.3.3+). This ensures all quality presets produce consistent visual appearance.
How It Works:
Transfer functions define opacity at a reference step size. When rendering at different step sizes, opacity is automatically corrected:
# Formula: alpha_corrected = 1.0 - exp(-alpha_tf * step_size / reference_step_size)Default Behavior:
# All presets use reference_step_size=0.01 by default
# This means transfer functions are designed for "balanced" quality
config = RenderConfig.high_quality() # Works correctly, looks same as balancedCustomizing for Your Data:
# Feature-dense volumes (medical, turbulence): use smaller reference
config = RenderConfig(
step_size=0.01,
max_steps=500,
reference_step_size=0.005 # Denser sampling reference
)
# Simple volumes (synthetic, smooth): use larger reference
config = RenderConfig(
step_size=0.01,
max_steps=500,
reference_step_size=0.02 # Sparser sampling reference
)Benefits:
- All presets produce same overall appearance
- Switch quality without changing how it looks
- Physically accurate (Beer-Lambert law)
- Industry standard (matches VTK, ParaView)
from pyvr.transferfunctions import ColorTransferFunction
# From matplotlib colormaps
ctf = ColorTransferFunction.from_colormap('viridis')
ctf = ColorTransferFunction.from_colormap('plasma', value_range=(0.2, 0.8))
# Custom control points
ctf = ColorTransferFunction(control_points=[
(0.0, [0.0, 0.0, 1.0]), # Blue at low values
(0.5, [0.0, 1.0, 0.0]), # Green at mid values
(1.0, [1.0, 0.0, 0.0]) # Red at high values
])
# Convenience methods
ctf = ColorTransferFunction.grayscale(intensity=0.8)
ctf = ColorTransferFunction.single_color([1.0, 0.5, 0.0])from pyvr.transferfunctions import OpacityTransferFunction
# Linear opacity ramp
otf = OpacityTransferFunction.linear(min_opacity=0.0, max_opacity=0.5)
# Step function
otf = OpacityTransferFunction.step(threshold=0.3, low_opacity=0.0, high_opacity=1.0)
# Multiple peaks
otf = OpacityTransferFunction.peaks(
peaks=[0.3, 0.7],
opacity=0.8,
eps=0.1
)
# Custom control points
otf = OpacityTransferFunction(control_points=[
(0.0, 0.0),
(0.2, 0.1),
(0.5, 0.8), # Peak
(0.8, 0.2),
(1.0, 0.0)
])from pyvr.camera import Camera, CameraController, CameraPath
import numpy as np
# Create camera with spherical coordinates
camera = Camera.from_spherical(
target=np.array([0.0, 0.0, 0.0]),
distance=5.0,
azimuth=np.pi/4, # 45ยฐ horizontal
elevation=np.pi/6, # 30ยฐ vertical
roll=0.0
)
# Use preset views
camera = Camera.front_view(distance=3.0)
camera = Camera.side_view(distance=3.0)
camera = Camera.top_view(distance=3.0)
camera = Camera.isometric_view(distance=4.0)
# Camera generates transformation matrices
view_matrix = camera.get_view_matrix()
projection_matrix = camera.get_projection_matrix(aspect_ratio=16/9)
position, up = camera.get_camera_vectors()
# Set camera in renderer
renderer.set_camera(camera)# Create smooth camera paths
cameras = [
Camera.front_view(distance=3.0),
Camera.isometric_view(distance=3.0),
Camera.side_view(distance=3.0)
]
path = CameraPath(keyframes=cameras)
# Interpolate between keyframes
interpolated_camera = path.interpolate(t=0.5) # Halfway
# Generate animation frames
frames = path.generate_frames(n_frames=30)
# Interactive camera controller
controller = CameraController(initial_params=camera)
controller.orbit(delta_azimuth=0.1, delta_elevation=0.05)
controller.zoom(factor=1.1)
controller.pan(delta=np.array([0.1, 0, 0]))
controller.roll_camera(delta_roll=0.1)from pyvr.lighting import Light
# Directional light (like sunlight)
light = Light.directional(
direction=[1, -1, 0], # Normalized automatically
ambient=0.2, # Ambient intensity (0.0-1.0)
diffuse=0.8 # Diffuse intensity (0.0-1.0)
)
# Point light at specific position
light = Light.point_light(
position=[5, 5, 5],
target=[0, 0, 0],
ambient=0.1,
diffuse=0.7
)
# Ambient-only lighting (no directional component)
light = Light.ambient_only(intensity=0.5)
# Default light
light = Light.default()# Pass light to renderer constructor
light = Light.directional(direction=[1, -1, 0], ambient=0.3)
renderer = VolumeRenderer(width=512, height=512, light=light)
# Or set/update later
light = Light.point_light(position=[5, 5, 5])
renderer.set_light(light)
# Get current light
current_light = renderer.get_light()
# Access light properties
direction = light.get_direction()
print(f"Position: {light.position}")
print(f"Ambient: {light.ambient_intensity}")
print(f"Diffuse: {light.diffuse_intensity}")
# Copy and modify
new_light = light.copy()
new_light.ambient_intensity = 0.5The examples/ directory contains complete working examples:
basic_rendering.py: Minimal rendering pipeline with sphere datasetcamera_demo.py: Advanced camera system with presets and pathstransfer_functions_demo.py: Transfer function demonstration with interactive controlsbenchmark.py: Performance comparison across quality presetsvtk_loading_demo.py: VTK data loader demonstration with hydrogen.vti dataset
Run examples:
python examples/basic_rendering.py
python examples/camera_demo.py
python examples/transfer_functions_demo.py
python examples/benchmark.py
python examples/vtk_loading_demo.py- Rendering Performance: 64+ FPS at 512ร512 resolution (15.6ms avg render time)
- Pixel Throughput: 16.8+ MPix/s on modern GPUs
- Memory Efficiency: Single RGBA texture lookup vs dual RGB+Alpha operations
- Scalability: Real-time for volumes up to 512ยณ voxels on modern hardware
High Quality (slower):
renderer = VolumeRenderer(1024, 1024, step_size=0.001, max_steps=1000)Balanced:
renderer = VolumeRenderer(512, 512, step_size=0.01, max_steps=500)High Performance (faster):
renderer = VolumeRenderer(256, 256, step_size=0.02, max_steps=100)PyVR has comprehensive test coverage for reliability:
# Run all tests (398 tests)
pytest tests/
# Run with coverage report
pytest --cov=pyvr --cov-report=term-missing tests/
# Run specific test modules
pytest tests/test_camera/ # Camera system tests (42 tests)
pytest tests/test_config.py # RenderConfig tests (33 tests)
pytest tests/test_lighting/ # Lighting tests (22 tests)
pytest tests/test_moderngl_renderer/ # ModernGL renderer tests (71 tests)
pytest tests/test_transferfunctions/ # Transfer function tests (36 tests)Test Coverage Breakdown:
Module Tests Coverage
----------------------------------------------
๐ท Camera System 42 95-97%
โ๏ธ RenderConfig 63 100%
๐ก Lighting System 22 100%
๐จ Transfer Functions 36 88-100%
๐ฅ๏ธ ModernGL Renderer 101 93-98%
๐ฎ Interactive Interface 80 >90%
๐ Volume & Datasets 54 56-93%
----------------------------------------------
๐ Total 398 ~86%
Key Testing Features:
- Zero abstract base tests (removed in v0.2.7)
- Comprehensive RenderConfig preset testing
- Full integration test coverage
- Type checking and validation tests
- Edge case and error handling tests
from pyvr.moderngl_renderer import VolumeRenderer
from pyvr.config import RenderConfig
from pyvr.volume import Volume
from pyvr.camera import Camera
from pyvr.lighting import Light
from pyvr.transferfunctions import ColorTransferFunction, OpacityTransferFunction
from pyvr.datasets import create_sample_volume, compute_normal_volume
import numpy as np
# 1. Create RenderConfig (v0.2.6)
config = RenderConfig.high_quality() # Or: fast(), balanced(), preview()
# 2. Create Volume (v0.2.5)
volume_data = create_sample_volume(256, 'double_sphere')
normals = compute_normal_volume(volume_data)
volume = Volume(
data=volume_data,
normals=normals,
min_bounds=np.array([-1.0, -1.0, -1.0], dtype=np.float32),
max_bounds=np.array([1.0, 1.0, 1.0], dtype=np.float32)
)
# 3. Create Camera (v0.2.3)
camera = Camera.from_spherical(
target=np.array([0, 0, 0]),
distance=5.0,
azimuth=np.pi/4,
elevation=np.pi/6,
roll=0.0
)
# 4. Create Light (v0.2.4)
light = Light.directional(direction=[1, -1, 0], ambient=0.2, diffuse=0.8)
# 5. Create Renderer (v0.2.7 - no abstract base)
renderer = VolumeRenderer(width=512, height=512, config=config, light=light)
renderer.set_camera(camera)
renderer.load_volume(volume)
# 6. Configure Transfer Functions
ctf = ColorTransferFunction.from_colormap('viridis')
otf = OpacityTransferFunction.linear(0.0, 0.1)
renderer.set_transfer_functions(ctf, otf)
# 7. Render
data = renderer.render()
# RenderConfig operations (v0.2.6)
renderer.set_config(RenderConfig.fast()) # Change quality at runtime
current_config = renderer.get_config()
samples = config.estimate_samples_per_ray() # ~346 samples
# Volume operations (v0.2.5)
print(f"Volume shape: {volume.shape}")
print(f"Volume dimensions: {volume.dimensions}")
normalized_vol = volume.normalize(method="minmax")
current_volume = renderer.get_volume()
# Camera operations (v0.2.3)
view_matrix = camera.get_view_matrix()
projection_matrix = camera.get_projection_matrix(aspect_ratio=16/9)
current_camera = renderer.get_camera()
# Camera animation (v0.2.3)
from pyvr.camera import CameraController, CameraPath
controller = CameraController(camera)
controller.orbit(delta_azimuth=0.1, delta_elevation=0.05)
path = CameraPath(keyframes=[camera1, camera2])
frames = path.generate_frames(30)- Python: 3.11+
- Core Dependencies:
- NumPy >= 2.3
- Matplotlib >= 3.10
- Pillow >= 11.0
- SciPy >= 1.16
- OpenGL Backend:
- ModernGL >= 5.0
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and add tests
- Ensure all tests pass (
poetry run pytest) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Clone and install in development mode
git clone https://github.com/JixianLi/pyvr.git
cd pyvr
poetry install --with dev
# Run tests
poetry run pytest
# Format code
poetry run black pyvr/
poetry run isort pyvr/See CHANGELOG.md for detailed version history and links to version notes.
This project is licensed under the WTFPL (Do What The F*ck You Want To Public License) - see the LICENSE file for details.
- Issues: GitHub Issues
- Email: jixianli@sci.utah.edu
- Version Notes: See
version_notes/directory for detailed release information
- Claude Sonnet 4.5 model from Claude Code is responsible for code after v0.2.3
- Claude Sonnet 4 model from GitHub Copilot for the creation of almost all code/documentation/test (before v0.2.3) in this repository (some code was created by Claude Sonnet 3.5)
- ModernGL community for excellent OpenGL bindings
- The scientific visualization community
PyVR - High-performance OpenGL volume rendering for real-time interactive visualization! ๐