spin_render.py User Guide
Use scripts/spin_render.py to render a rotating camera animation of a VTK dataset and write a PNG sequence suitable for stitching into a movie.
What it does
- Loads a VTU/VTP/VTI/VTR dataset and orbits the camera around its bounds.
- Colors by a chosen point-data scalar (default
log_field_value) using a ParaView colormap preset (defaultFast). - Writes numbered PNG frames that can be stitched into an MP4 with
ffmpeg. - Optionally animates elevation and zoom during the rotation.
Requirements
- ParaView
pvpython(e.g.,/Applications/ParaView-6.0.1.app/Contents/bin/pvpython). - A readable VTK file (VTU/VTP/VTI/VTR).
ffmpeg(optional, for MP4 stitching).
To keep pvpython on PATH whenever you activate the conda environment:
mkdir -p "$CONDA_PREFIX/etc/conda/activate.d" "$CONDA_PREFIX/etc/conda/deactivate.d"
cat > "$CONDA_PREFIX/etc/conda/activate.d/paraview_path.sh" <<'EOF'
export _PV_OLD_PATH="$PATH"
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/Applications/ParaView-6.0.1.app/Contents/bin:$PATH"
EOF
cat > "$CONDA_PREFIX/etc/conda/deactivate.d/paraview_path.sh" <<'EOF'
export PATH="$_PV_OLD_PATH"
unset _PV_OLD_PATH
EOFCLI
pvpython scripts/spin_render.py \
--input <INPUT_VTK> \
--output <OUTPUT_PATH> \
[--resolution WIDTH HEIGHT] \
[--background white|black] \
[--frames N] \
[--elev DEG] \
[--zoom Z] \
[--radius-scale R] \
[--fps FPS] \
[--scalar NAME] \
[--range-min MIN --range-max MAX] \
[--percentile-range PLOW PHIGH] \
[--colormap PRESET] \
[--animate-elev-zoom] \
[--loops N] \
[--slow-elev-amp DEG] \
[--slow-zoom-amp Z] \
[--slow-phase RAD] \
[--azimuth-offset DEG] \
[--representation surface|points] \
[--point-size N] \
[--overwrite]Key options
--input– VTK file to render.--output– Output path used as the PNG prefix; the script writes<OUTPUT_BASE>_0000.png,<OUTPUT_BASE>_0001.png, …- If
--outputends with.mp4, the script also stitches the PNGs into that movie usingffmpeg. --resolution– Output resolution in pixels (e.g.,1920 1080).--background– Background color (whiteorblack).--frames– Number of frames (default 180).--elev– Camera elevation in degrees for the orbit (default 20).--zoom– Zoom factor baked into the orbit radius (default 1.2).--radius-scale– Orbit radius scale as a fraction of dataset extent.--fps– FPS for MP4 stitching (default 30).--scalar– Point-data array to color by (defaultlog_field_value).--range-min/--range-max– Fixed color range. If omitted, auto-range is used.--percentile-range– Clamp the colormap to the given percentiles (e.g.,1 99). Mutually exclusive with fixed range.--colormap– ParaView preset name (defaultFast).--overwrite– Overwrite the MP4 if it already exists.--animate-elev-zoom– Enables smooth sinusoidal elevation and zoom modulation.--loops– Number of full rotations.--slow-elev-amp/--slow-zoom-amp– Slow modulation amplitude across the full movie.--slow-phase– Phase offset (radians) for slow modulation.--azimuth-offset– Starting azimuth offset in degrees.--representation– Render assurfaceorpoints.--point-size– Point size when--representation pointsis used.
Animated elevation + zoom
pvpython scripts/spin_render.py \
--input <INPUT_VTK> \
--output <OUTPUT_PATH> \
--frames 180 \
--animate-elev-zoom \
--elev-start 20 --elev-peak 60 \
--zoom-start 0.6 --zoom-peak 2.0This animates elevation and zoom smoothly (sinusoidal) while the camera continues to rotate.
Multi-loop variation (seamless)
pvpython scripts/spin_render.py \
--input <INPUT_VTK> \
--output <OUTPUT_PATH> \
--frames 450 \
--animate-elev-zoom \
--loops 3 \
--slow-elev-amp 20 \
--slow-zoom-amp 0.5 \
--slow-phase 0.6Slow modulation produces different loops while returning to the starting position at the end for seamless playback.
Outputs
<OUTPUT_BASE>_0000.png…<OUTPUT_BASE>_####.png– PNG frames of the rotation.
Stitch frames to MP4
ffmpeg -framerate 30 \
-i <OUTPUT_BASE>_%04d.png \
-c:v libx264 -pix_fmt yuv420p \
<OUTPUT_BASE>.mp4Example
/Applications/ParaView-6.0.1.app/Contents/bin/pvpython scripts/spin_render.py \
--input outputs/quijote_batches/crop_x500-1000_y500-1000_z0-100/crop_x500-1000_y500-1000_z0-100_s5_manifolds_JE1a_S020.vtu \
--output outputs/quijote_batches/crop_x500-1000_y500-1000_z0-100/spin.mp4 \
--frames 180 \
--scalar log_field_value \
--colormap "Inferno (matplotlib)" \
--range-min -3 --range-max 1 \
--resolution 1920 1080 \
--background blackExample (multi-loop, smooth modulation)
pvpython scripts/spin_render.py \
--input outputs/combined_for_video.vtu \
--output outputs/combined_for_video_spin.mp4 \
--scalar log_field_value \
--colormap "Inferno (matplotlib)" \
--range-min -3 \
--range-max 1 \
--resolution 1920 1080 \
--background black \
--frames 1800 \
--fps 30 \
--animate-elev-zoom \
--loops 3 \
--azimuth-offset=-120 \
--elev-start=-40 \
--elev-peak 80 \
--zoom-start 0.8 \
--zoom-peak 2.0 \
--radius-scale 0.6 \
--slow-elev-amp 20 \
--slow-zoom-amp 0.5 \
--slow-phase 0.6 \
--overwriteMore Examples
pvpython scripts/spin_render.py \
--input outputs/movie_loop_4/combined_components_4_6.vtu \
--output outputs/movie_loop_4/combined_components_4_6_spin.mp4 \
--scalar log_field_value \
--colormap "Inferno (matplotlib)" \
--range-min -4 \
--range-max 4 \
--resolution 1920 1080 \
--background black \
--frames 1800 \
--fps 30 \
--animate-elev-zoom \
--loops 3 \
--azimuth-offset=-120 \
--elev-start=-40 \
--elev-peak 80 \
--zoom-start 0.8 \
--zoom-peak 2.0 \
--radius-scale 0.6 \
--slow-elev-amp 20 \
--slow-zoom-amp 0.5 \
--slow-phase 0.6 \
--point-size 4 \
--line-width 2 \
--overwrite
/Users/fules/Documents/disperse/outputs/movie_loop_4/four_layers_4_6.vtm
pvpython scripts/spin_render_layers.py \
--input outputs/movie_loop_4/four_layers_4_6 \
--output outputs/movie_loop_4/four_layers_4_6.mp4 \
--scalar log_field_value \
--colormap "Inferno (matplotlib)" \
--range-min -4 \
--range-max 4 \
--resolution 1920 1080 \
--background black \
--frames 180 \
--fps 30 \
--animate-elev-zoom \
--loops 5 \
--azimuth-offset=-120 \
--elev-start=-40 \
--elev-peak 80 \
--zoom-start 0.8 \
--zoom-peak 2.0 \
--radius-scale 0.6 \
--slow-elev-amp 20 \
--slow-zoom-amp 0.5 \
--slow-phase 0.6 \
--point-size 4 \
--line-width 2 \
--overwrite
pvpython scripts/spin_render.py \
--input outputs/movie_loop/particle_cube.vtu \
--output outputs/movie_loop/particle_cube.mp4 \
--scalar log_field_value \
--colormap "Inferno (matplotlib)" \
--range-min -3 \
--range-max 1 \
--resolution 1920 1080 \
--background black \
--frames 1800 \
--fps 30 \
--animate-elev-zoom \
--loops 1 \
--azimuth-offset=-120 \
--elev-start=-40 \
--elev-peak 80 \
--zoom-start 0.8 \
--zoom-peak 2.0 \
--radius-scale 0.6 \
--slow-elev-amp 20 \
--slow-zoom-amp 0.5 \
--slow-phase 0.6 \
--representation points \
--point-size 2.5 \
--overwriteNotes
- Coloring uses point-data arrays. If your scalar is only in cell data, convert to point data before rendering.
- If colors look washed out, try a tighter range with
--range-min/--range-max. - If you’re re-running with different settings, change the output prefix or remove old
*_####.pngframes first. - Default colormap is
Fast; use--colormap "Inferno (matplotlib)"to match the rest of the pipeline.
redshift_evolution.py User Guide
Use scripts/redshift_evolution.py to generate a smooth movie showing the evolution of the cosmic web across redshift snapshots. Given one Delaunay VTU per redshift, the script linearly interpolates point positions and scalar values between consecutive snapshots and assembles a PNG sequence and MP4.
Must be run via pvpython.
What it does
- Reads one Delaunay VTU per snapshot (earliest universe first).
- Optionally aligns particle ordering across snapshots using chained nearest-neighbour matching (via
scipy.spatial.cKDTree), so interpolation traces the same physical particle rather than position-index correspondence. - Interpolates both coordinates and scalar values linearly between consecutive snapshots.
- Renders each frame as a point cloud coloured by
log_field_value(Inferno colormap, range −3 to 2 by default). - Shows a redshift label (e.g.
z=3) in the lower-left corner of each frame. - Writes numbered PNGs and stitches them into an MP4 via
ffmpeg.
Rendering modes
| Mode | Flag | Description |
|---|---|---|
| Interpolation | (default) | Smoothly interpolates positions and scalars between snapshots. Requires scipy. |
| Cross-fade | (automatic fallback) | Opacity blending when point counts differ after alignment. |
| Slideshow | --slideshow |
No interpolation — holds each snapshot for --frames-hold frames then cuts. Skips alignment entirely. |
Requirements
pvpython(ParaView’s Python interpreter).scipy(for proximity alignment; ships with recent ParaView builds).ffmpeg(optional, for MP4 assembly).
CLI
pvpython scripts/redshift_evolution.py \
--inputs SNAP_000.vtu SNAP_001.vtu ... \
--labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
--output-dir <OUTPUT_DIR> \
--output-prefix <PREFIX> \
[--scalar log_field_value] \
[--range-min -3.0 --range-max 2.0] \
[--colormap "Inferno (matplotlib)"] \
[--frames-per-transition 30] \
[--frames-hold 10] \
[--point-size 2.0] \
[--resolution 1920 1080] \
[--background black|white] \
[--fps 30] \
[--label-position X Y] \
[--label-font-size 28] \
[--zoom 1.0] \
[--no-align] \
[--slideshow] \
[--no-mp4]Key options
--inputs— VTU files in time order (earliest redshift first, i.e.000→004).--labels— Redshift label per snapshot, shown in the lower-left corner. Must match--inputscount.--frames-per-transition— Interpolated frames between each pair (default 30).--frames-hold— Frames to pause on each snapshot (default 10).--scalar— Point-data array to colour by (defaultlog_field_value). Usenonefor solid colour.--range-min/--range-max— Fixed colour range (default −3 / 2).--percentile-range— Derive colour range from percentiles across all snapshots (overrides fixed range).--colormap— ParaView preset name (defaultInferno (matplotlib)).--zoom— Zoom factor applied after auto-framing (default 1.0). Values > 1 zoom in (less black border around the data); < 1 zoom out.--no-align— Skip proximity alignment; assume all files share the same point ordering.--slideshow— Cut directly between snapshots with no interpolation or alignment.--no-mp4— Keep PNGs only, skip ffmpeg.
Example (5 Quijote redshift snapshots, one crop)
pvpython scripts/redshift_evolution.py \
--inputs \
outputs/quijote_batches_000_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_001_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_002_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_003_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_004_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
--labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
--scalar log_field_value \
--range-min -3.0 --range-max 2.0 \
--frames-per-transition 30 \
--frames-hold 10 \
--output-dir outputs/evolution \
--output-prefix evolutionSlideshow example
pvpython scripts/redshift_evolution.py \
--inputs \
outputs/quijote_batches_000_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_001_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_002_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_003_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
outputs/quijote_batches_004_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
--labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
--scalar log_field_value \
--range-min -3.0 --range-max 1.0 \
--slideshow \
--slideshow-duration 2.0 \
--output-dir outputs/evolution_slideshow_100 \
--output-prefix evolution_slideshow_100Slideshow example (10Mpc slab)
pvpython scripts/redshift_evolution.py \
--inputs \
outputs/evolution_slideshow_10/particle_simulation_0_10.vtu \
outputs/evolution_slideshow_10/particle_simulation_1_10.vtu \
outputs/evolution_slideshow_10/particle_simulation_2_10.vtu \
outputs/evolution_slideshow_10/particle_simulation_3_10.vtu \
outputs/evolution_slideshow_10/particle_simulation_4_10.vtu \
--labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
--scalar log_field_value \
--range-min -4.0 --range-max 4.0 \
--resolution 3840 2160 \
--label-font-size 56 \
--zoom 1.2 \
--point-size 3.0 \
--slideshow \
--slideshow-duration 2.0 \
--output-dir outputs/evolution_slideshow_10 \
--output-prefix evolution_slideshow_10_10_3Notes
- The Delaunay VTU files produced by
analyze_snapshot.pycontainlog_field_value(DTFE log-density) and atrue_indexarray — these are the recommended input files. - Proximity alignment uses chained nearest-neighbour matching (snapshot i matched to snapshot i−1), so small per-step displacements accumulate correctly across many redshifts. This requires
scipy; if unavailable the script falls back to positional ordering with a warning. - Total frame count =
(N−1) × (frames_hold + frames_per_transition) + frames_holdfor interpolation mode, or N frames (one per snapshot) for slideshow mode. - Label position is given in normalized viewport coordinates (
--label-position X Y); default is0.45 0.05(center-bottom). Requires ParaView 6.0+; position is ignored on older builds.