Skip to content

A ParaView session -> a hologram

Command: quiltwright paraview Module: quiltwright.paraview (render_paraview_quilt, probe_paraview_state, depth_report)

If you have a ParaView state file, you have a hologram. The pipeline, the color maps and the camera all come with it, because the sweep runs inside ParaView.

ParaView is where a great deal of simulation output gets looked at: CFD, climate, astrophysics, finite-element results. A session there is a filter pipeline (slice, contour, clip, streamlines), a set of color maps and opacity transfer functions, representation settings, and a camera, all of which ParaView saves as a .pvsm state file. None of that survives the obvious route of exporting the data and reading it into PyVista: the geometry comes across, and everything the user spent the afternoon on does not.

Quiltwright takes the other route. It hands the state file back to ParaView's own Python interpreter, pvpython, and sweeps the render view's camera in place. The frames are of the session the user actually built.


The one command

quiltwright paraview session.pvsm

That probes the state for its camera and depth range, prints the depth budget, sweeps the camera across the device's view cone, and writes a quilt under renders/quilts/. Common variations:

# Fast single-view still, to check what the state looks like at the device's aspect
quiltwright paraview session.pvsm --still

# A finished portrait quilt, pulled in a little, cast straight to the display
quiltwright paraview session.pvsm --device 27-portrait --zoom 1.4 --cast

# Keep the per-view frames and the camera probe for inspection
quiltwright paraview session.pvsm --keep-views renders/views/session

# Is ParaView reachable at all?
quiltwright paraview --check
Flag What it does
--device Target display preset (16-landscape default; portrait, 27-portrait, go, ...)
--fov Vertical field of view (14 default). The camera is dollied back so the state's framing is kept; 0 keeps the state's own FOV and distance
--zoom Dolly factor after framing. Values above 1 fill more of each tile, which is what drives perceived depth
--view-cone View cone in degrees. Defaults to the device's own, capped at 35: 16-landscape declares 50, which overruns the disparity budget. Narrowing it lets --zoom go higher at the same depth
--orientation-axes Keep ParaView's corner axes widget. Hidden by default: it is pinned to the screen, so it would sit on the glass in every view
--still One center view as a flat image at the device's aspect, instead of a quilt
--preview Quarter-size quilt, for iterating
--keep-views DIR Keep the per-view PNGs and camera.json
--out STEM Output stem; defaults to the state's name under renders/quilts/ (or gallery/ with --still)
--cast Send the finished quilt to Looking Glass Bridge
--check Report where pvpython was found and exit

What comes across

Everything in the state, because nothing is converted. Sources, filters, color maps, opacity transfer functions, representations (surface, wireframe, points, volume rendering), scalar bars, lights, background: whatever the render view showed when the state was saved is what each view of the quilt shows. Volume rendering works because WindowCenter is a property of the VTK camera, not of any particular mapper.

Two things about a state file are worth knowing before the first run:

The camera is the one you saved. The state's camera is the center view and its focal point becomes the holographic focal plane, exactly as render_quilt() treats a PyVista plotter. Frame the scene in ParaView the way you want it seen, put the focal point on the thing that should sit at the glass (Adjust Camera in the toolbar sets it directly), and save state. A state with several render views is swept on the active one if it is a render view, otherwise the first.

Screen-pinned things sit on the glass. Scalar bars, text annotations and the orientation axes are 2-D widgets at fixed screen positions. Across a sweep they land at the same tile position in every view, so the display puts them exactly at the focal plane. That is harmless for a scalar bar; it is noise for the orientation axes, which is why those are hidden by default.

How the sweep works

It is the same sweep every Quiltwright backend makes, applied to the vtkCamera ParaView owns rather than the one PyVista owns. For each view the camera position and focal point translate together along the camera's right vector, no rotation, and vtkCamera.SetWindowCenter shears the frustum back so the original focal point stays centered. That is an off-axis projection, never a toe-in; see lfd.md for why the distinction is what makes a display fuse the views. The frames are tiled by assemble_quilt(), as the POV-Ray and Cycles frames are.

The whole sweep runs in one pvpython process, so the state is loaded once. The command runs pvpython twice: a probe that loads the state without rendering, so the depth budget can be printed before committing, and then the sweep. ParaView's startup is about three seconds on an M3 Max; the sweep's cost is whatever your dataset costs to render, times the view count.

The script that runs inside ParaView cannot import Quiltwright (ParaView bundles its own Python), so it carries its own copy of the offset and shear arithmetic. A test execs that copy and checks it against view_offsets() and window_shear(), which every other backend uses, so the two cannot drift.

Verified, and not

SetWindowCenter survives ParaView's Render() in the default builtin server mode, which is what pvpython runs on its own -- a laptop session -- and it survives over a genuine client-server connection too: a separate pvserver process reached with Connect(), which is the shape this module actually asks for. The sheared frame differs from the unsheared one by the same shift either way.

What is still untested is IceT compositing across multiple MPI ranks (mpiexec -n N pvserver), the mode a cluster uses to split one frame's rendering across processes. ParaView syncs camera position, focal point, view-up and view angle to each rank's remote server through named proxy properties, and WindowCenter is not among them, so a distributed-render sweep may come back un-sheared. If you run one, look at the outer views: an un-sheared sweep has the subject drifting toward the tile edge rather than staying centered on the focal plane.

In Python

from quiltwright import QUILT_PRESETS, save_quilt
from quiltwright.paraview import depth_report, probe_paraview_state, render_paraview_quilt

spec = QUILT_PRESETS["portrait"]

camera = probe_paraview_state("session.pvsm", spec, fov=14.0)
print(depth_report(camera, spec))          # near / focal / far disparity, before rendering

quilt = render_paraview_quilt("session.pvsm", spec, fov=14.0)
save_quilt(quilt, "session", spec)         # -> session_qs8x6a0.75.png

render_paraview_views() writes the per-view PNGs to a directory instead of tiling them, for a hologram printer's sweep or for inspection. Pass the same fov and zoom to the probe and the render, so the budget describes the render you are about to make.

Installing ParaView

ParaView is a desktop application with its own Python, never a Python dependency of Quiltwright. pvpython is found through PARAVIEW_BINARY, then PATH, then the macOS application bundle (/Applications/ParaView-*.app/Contents/bin/pvpython), so the cask install works with no further setup:

brew install --cask paraview       # macOS
quiltwright paraview --check       # reports the pvpython it will use

Binaries for every platform are at paraview.org/download. The [openvkl] initialization errors ParaView prints at startup on macOS are its OSPRay module failing to load, which the sweep never uses; they are filtered out of Quiltwright's error output.

Slicer, and other VTK applications

The same argument applies to any application built on VTK, since the sweep is twenty lines against vtkCamera. Nothing here drives 3D Slicer yet, but Slicer's native .nrrd volumes read straight into PyVista with pv.read(), so a Slicer volume already reaches the display through the PyVista backend with no conversion.