https://github.com/kmarchais/pyvista-blender
Render PyVista plotter scenes through Blender (bpy) for photoreal output.
https://github.com/kmarchais/pyvista-blender
blender bpy cycles eevee python pyvista rendering scientific-visualization
Last synced: 2 days ago
JSON representation
Render PyVista plotter scenes through Blender (bpy) for photoreal output.
- Host: GitHub
- URL: https://github.com/kmarchais/pyvista-blender
- Owner: kmarchais
- License: gpl-3.0
- Created: 2026-05-23T18:47:49.000Z (10 days ago)
- Default Branch: main
- Last Pushed: 2026-05-23T18:53:59.000Z (10 days ago)
- Last Synced: 2026-05-23T20:24:48.642Z (10 days ago)
- Topics: blender, bpy, cycles, eevee, python, pyvista, rendering, scientific-visualization
- Language: Python
- Homepage: https://kmarchais.github.io/pyvista-blender
- Size: 15.4 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# pyvista-blender
> [!WARNING]
> **Very early development.** APIs, defaults, and module layout will
> change without notice until the first stable release. Bug reports,
> missing-feature requests, and PRs are all very welcome — please open
> an [issue](https://github.com/kmarchais/pyvista-blender/issues) if
> something doesn't work or behaves surprisingly.
Render [PyVista](https://docs.pyvista.org/) plotter scenes through
[Blender](https://www.blender.org/) (`bpy`) — photoreal output, full
animation export, no file round-trip.
Build the scene once in PyVista. Pick VTK's preview or Blender's
Cycles / Eevee Next for the final render. Export entire deforming,
colour-evolving, lit animations to a single `.blend` that plays
natively in Blender with **no Python script inside**.
## Status
Pre-release (`0.1.0.dev0`). The offline render path, the desktop and
browser interactive viewports, the Jupyter inline backend, and the
full animation export to `.blend` are implemented and tested. The full
ruff (`select = ["ALL"]`) + ty + format gate is clean.
See the [features list in the docs](./docs/index.md#features) for the
full surface and the [architecture page](./docs/architecture.md) for
the translation pipeline.
## Install
```bash
pip install pyvista-blender
# or
uv add pyvista-blender
```
For the Jupyter inline backend and the Trame-served browser viewport
(`pl.blender.show(backend="web")`), install `pyvista[jupyter]`
alongside; the bridge reuses pyvista's Trame stack rather than
shipping its own extras.
```bash
pip install pyvista-blender 'pyvista[jupyter]'
```
Requires Python **3.11** (Blender 4.5 LTS via `bpy>=4.5,<5`) **or
3.13** (Blender 5.x via `bpy>=5.0,<6`). Python 3.10, 3.12, and 3.14
have no matching `bpy` wheel on PyPI; `requires-python` blocks them.
## Quick start
```python
import pyvista as pv
pl = pv.Plotter(off_screen=True, window_size=(1920, 1080))
pl.add_mesh(pv.read("data.vtu"), scalars="von_mises", cmap="viridis", pbr=True)
pl.add_light(pv.Light(position=(5, -5, 5), light_type="scene light"))
pl.camera_position = "iso"
# Offline render via Cycles + OptiX denoise. No `import pyvista_blender`
# needed — the namespace registers itself on install through PyVista
# 0.48's plotter-component entry point.
pl.blender.render("frame.png", samples=128)
# Interactive hybrid viewport: VTK 60 fps during drag, Cycles render
# on mouse release. One window, mouse-driven, no auto-exec prompts.
pl.blender.show()
```
## API surface
| Entry point | What it does |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `pl.blender.render(path, ...)` | Render a single frame to PNG via Cycles or Eevee Next. |
| `pl.blender.animate(path, updater, frames, fps=...)` | Render an animation as gif / mp4 / webm / mov / mkv via `imageio` + `imageio-ffmpeg`. |
| `pl.blender.show()` | Hybrid VTK / Cycles interactive viewport — VTK during drag, settled Cycles on release. |
| `pl.blender.add_glyph(source, geom, *, orient, scale, factor)` | Geometry-Nodes-instanced glyphs; memory cost scales `N + V` instead of `N · V`. |
| `pl.blender.export_blend(path)` | Save the live PyVista scene as a `.blend` for finishing in Blender. |
| `pl.blender.export_animation_blend(path, updater, frames, fps, **bake)` | Bake a full animation into a `.blend` that plays natively on file open. |
| `pl.blender.orbit_camera(n_frames=...)` | Build an `updater` for `animate` / `export_animation_blend`. |
Three-tier config resolution everywhere: per-call kwarg → component
attribute (`pl.blender.engine = ...`) → module default
(`pyvista_blender.config.*`).
## Animation export to `.blend`
`export_animation_blend` bakes each animation channel through its own
opt-in kwarg. Every backend plays back natively when the file is
opened in Blender — **no Python in the `.blend`, no auto-execution
prompt**, no need to "Run Scripts" on load.
| Kwarg | Mechanism | File layout |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `bake_camera=True` (default) | Keyframes on `scene.camera.location` + `rotation_quaternion` per frame | inside `.blend` |
| `bake_deformation="shape_keys"` | One Shape Key per frame, value-keyframed with linear interpolation | inside `.blend` |
| `bake_deformation="mdd"` (= `True`) | Lightwave MDD sidecar + `MESH_CACHE` modifier | `.blend` + `__.mdd` |
| `bake_scalars=True` | Packed PNG (rows = frames, columns = vertices / cells) + Geometry Nodes graph; works for point-data and cell-data scalars | inside `.blend` |
| `bake_lights=True` | Per-light keyframes on location, rotation, `energy`, `color` | inside `.blend` |
Example: full multi-channel animation in one self-contained file.
```python
import numpy as np
import pyvista as pv
pl = pv.Plotter(off_screen=True, window_size=(1280, 720))
plane = pv.Plane(i_resolution=60, j_resolution=60)
plane.cell_data["heat"] = np.zeros(plane.n_cells, dtype=np.float32)
pl.add_mesh(plane, scalars="heat", cmap="inferno", clim=[-1.0, 1.0])
light = pv.Light(position=(5, 0, 4), light_type="scene light", intensity=1.0)
pl.add_light(light)
n_frames = 60
orbit = pl.blender.orbit_camera(n_frames=n_frames)
centers_xy = plane.cell_centers().points[:, :2]
def update(frame: int) -> None:
orbit(frame)
angle = -frame * (2.0 * np.pi / n_frames)
light.position = (5.0 * np.cos(angle), 5.0 * np.sin(angle), 5.0)
light.intensity = 0.4 + 0.6 * (0.5 + 0.5 * np.sin(frame * 0.3))
r = np.linalg.norm(centers_xy, axis=1)
plane.cell_data["heat"] = np.sin(2.0 * r - frame * 0.2).astype(np.float32)
pl.blender.export_animation_blend(
"scene.blend",
update,
frames=range(n_frames),
fps=30,
bake_camera=True,
bake_lights=True,
bake_scalars=True,
)
pl.close()
```
Open `scene.blend` in Blender 4.5+ / 5.x and press Space:
camera orbits, the explicit light orbits in the opposite direction
while pulsing, the heat field travels as a radial wave with flat per-
cell shading. One file, ~500 KB, nothing to ship alongside.
## Development
```bash
uv sync --group dev --no-install-package bpy # fast local sync (no 200 MB bpy wheel)
uvx prek install # install pre-commit hooks
uv run pytest # ~10 s with bpy installed
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run ty check
```
Real `bpy` is only needed for the `@pytest.mark.bpy` tests. CI runs
the full suite against the live wheel.
## License
GPL-3.0-or-later. Forced by `bpy`'s license; every source file
carries an SPDX header. See [`LICENSE`](./LICENSE).