Visualization and Recording¶
NovaPhy exposes one renderer-independent lifecycle under
novaphy.viewer. The same simulation driver can target an interactive
ModernGL window, a bounded headless loop, an offline recording, or an
optional remote viewer without changing the solver contract.
Install all optional viewer backends with:
Run that command from a NovaPhy source checkout; there is no published PyPI package in the current distribution workflow.
ViewerNull and JSON ViewerFile recordings only require NovaPhy's core
NumPy dependency. Individual optional backends fail with an actionable
ImportError when their package is missing.
Common lifecycle¶
Every backend implements ViewerBase and follows the same frame calls:
import novaphy
from novaphy.viewer import ViewerGL
dt = 1.0 / 120.0
state_in = model.state()
state_out = model.state()
control = model.control()
collision_pipeline = novaphy.CollisionPipeline(model)
contacts = collision_pipeline.contacts()
viewer = ViewerGL(model=model, width=1280, height=720)
sim_time = 0.0
try:
while viewer.is_running():
if viewer.should_step():
state_in.clear_forces()
viewer.apply_forces(state_in) # mouse picking / wind, when enabled
collision_pipeline.collide(state_in, contacts)
solver.step(state_in, state_out, control, contacts, dt)
state_in, state_out = state_out, state_in
sim_time += dt
viewer.begin_frame(sim_time)
viewer.log_state(state_in)
viewer.end_frame()
finally:
viewer.close()
should_step() respects pause and single-step state on interactive
backends. Rendering should still run while paused so the window can process
events. close() is important for flushing files and shutting down external
viewer servers.
The shared logging surface also includes log_mesh(), log_instances(),
log_lines(), log_points(), log_image(), log_array(), and
log_scalar(). Backend support is intentionally asymmetric: for example,
USD records geometry but ignores scalar and image logs.
Choose a backend¶
| Backend | Use it for | Important behavior |
|---|---|---|
ViewerGL |
Interactive desktop rendering, UI, screenshots, picking | ModernGL/GLFW backend. Viewer and OpenGLViewer are aliases. |
ViewerNull |
CI, tests, batch runs, timing | No rasterization. num_frames bounds the loop; optional benchmark results are available after the run. NullViewer is an alias. |
ViewerFile |
Experimental host snapshot serialization | Writes .json; .bin additionally needs cbor2. Current C++ SimState scalar metadata has the limitation below. |
ViewerRerun |
Rerun desktop/web streaming or .rrd capture |
Requires rerun-sdk; can connect to an address, serve a web viewer, or save with record_to_rrd. |
ViewerUSD |
Time-sampled scene export | Requires usd-core; writes meshes, instances, lines, and points to a USD stage. |
ViewerViser |
Browser/Jupyter visualization and Viser recording | Requires viser; texture paths also use the viewer extra's trimesh and Pillow dependencies. Collada/DAE meshes additionally use pycollada. |
The default alias is deliberately explicit:
For applications with command-line backend selection, construct the desired
class and then call set_model(model):
from novaphy import viewer
def make_viewer(name, model, *, frames=240, output_path="output.usd"):
if name == "gl":
result = viewer.ViewerGL()
elif name == "null":
result = viewer.ViewerNull(num_frames=frames)
elif name == "file":
result = viewer.ViewerFile("run.json", auto_save=False)
elif name == "rerun":
result = viewer.ViewerRerun()
elif name == "usd":
result = viewer.ViewerUSD(output_path, num_frames=frames)
elif name == "viser":
result = viewer.ViewerViser(browser=True)
else:
raise ValueError(f"unknown viewer backend: {name}")
result.set_model(model)
return result
This is the pattern used by the VBD demos, whose --viewer choices are
gl, usd, rerun, null, and viser.
Headless execution¶
Use ViewerNull when no pixels are needed:
from novaphy.viewer import ViewerNull
viewer = ViewerNull(num_frames=600, benchmark=True)
viewer.set_model(model)
while viewer.is_running():
if viewer.should_step():
step_simulation()
viewer.begin_frame(sim_time)
viewer.log_state(state)
viewer.end_frame()
result = viewer.benchmark_result()
viewer.close()
ViewerNull still runs model/state conversion and debug logging, so it is a
useful API and data-path check. Its benchmark is viewer-loop timing, not a
portable NovaPhy solver benchmark.
ViewerGL(headless=True) is different: it creates a rendering backend
without a visible window and can return rasterized frames through
get_frame(). It still needs a working OpenGL context. Prefer
ViewerNull for display-free CI.
Recordings¶
NovaPhy snapshots¶
ViewerFile implements JSON/CBOR serialization, a bounded RingBuffer, and
playback into caller-owned objects. The basic API is:
from types import SimpleNamespace
import numpy as np
from novaphy.viewer import ViewerFile
simple_array_state = SimpleNamespace(
body_q=np.asarray(state.body_q, dtype=np.float32).copy(),
body_qd=np.asarray(state.body_qd, dtype=np.float32).copy(),
)
recorder = ViewerFile("run.json", auto_save=False)
recorder.record(simple_array_state)
recorder.save_recording()
loaded = ViewerFile("run.json", auto_save=False)
loaded.load_recording()
loaded.load_state(simple_array_state, frame_id=0)
simple_array_state must expose serializable host array fields. JSON is
self-contained and readable. Binary .bin recordings use CBOR and require
pip install cbor2.
Current SimState recording limitation
A real C++ SimState exposes scalar metadata such as host_revision.
ViewerFile.record() currently converts that scalar to a zero-dimensional
NumPy array, which its serializer does not handle; log_state(sim_state)
followed by save_recording() / close() therefore raises TypeError.
Use ViewerUSD, ViewerRerun, or ViewerViser for their supported
visualization/export output, or pass a Python snapshot object containing
only the arrays you need. Do not rely on ViewerFile as a production
checkpoint format yet.
USD, Rerun, and Viser¶
from novaphy.viewer import ViewerRerun, ViewerUSD, ViewerViser
usd = ViewerUSD("trajectory.usda", fps=60, num_frames=300, model=model)
rerun = ViewerRerun(model=model, record_to_rrd="trajectory.rrd")
viser = ViewerViser(model=model, browser=False, record_to="trajectory.viser")
Drive each object with the common frame lifecycle. ViewerUSD.close() saves
the stage, ViewerRerun.close() disconnects the stream, and
ViewerViser.close() saves an active recording and stops the server.
These backends are visualization/export sinks; their apply_forces() methods
do not provide mouse interaction.
Picking and force application¶
ViewerGL.set_model() creates a Picking helper automatically. Right-click
a dynamic rigid body or supported soft-particle surface, drag to update its
target, and release to stop. The driver must call
viewer.apply_forces(state) after state.clear_forces() and before
solver.step(...), as shown in the lifecycle example.
Useful controls include:
viewer.picking_enabled = True
viewer.set_picking_linear_only_bodies([base_body])
# Later:
viewer.clear_picking_linear_only_bodies()
Linear-only bodies receive no picking torque. Kinematic rigid bodies are not
pickable. Advanced integrations may construct Picking(model) directly and
call pick(state, ray_start, ray_dir), update(...), and release(), but
the GL backend already wires these calls to its mouse events.
CPU states use NovaPhy's model ray caster. CUDA device states require the GL
renderer's CUDA picking/upload support. If a required CUDA path is absent,
the viewer raises a RuntimeError instead of silently copying the complete
state to the host.
CPU and CUDA data boundaries¶
All backends accept host NumPy data. ViewerGL additionally has direct
CUDA/GL paths for supported state, particle, contact, and geometry buffers.
Coverage is not universal: an unsupported CUDA DeviceArray logging path
fails explicitly. Rerun, USD, Viser, and file recording are host-oriented
and may snapshot device data to the CPU.
Selecting a viewer never selects a physics device. Configure the model and solver device independently, and treat any viewer readback as a potential synchronization point when measuring GPU simulation performance.
Legacy Polyscope compatibility¶
novaphy.viz and the novaphy[viz] extra remain available for compatibility.
The IPC showcases keep their existing Polyscope compatibility path. The
retained non-IPC demos, including dam break, unified collision, and the fluid
examples, now use the common ViewerGL/ViewerNull lifecycle.
New drivers should use novaphy.viewer. See
python/demos/demo_viewer_api_basic.py for the basic GL/null path and
python/demos/demo_ik_arm.py for ViewerGL gizmo interaction.