Skip to main content
`Ctrl`+`K`

# Rendering Handoff

ovphysx is a headless simulation SDK — it has no built-in renderer. To visualize simulation state, you read data back through tensor bindings and hand it to an external tool such as Rerun or ovrtx.

This tutorial shows the pattern using Rerun: populate an ovstage scene from USD and attach it, step the simulation, and stream rigid body transforms to a browser-based viewer.

## Key Idea

Rerun does not read USD. The sample explicitly logs all visual geometry to Rerun from the simulation data returned by ovphysx tensor bindings:

- Falling cubes — logged each frame as `rr.Boxes3D` with positions and quaternions from the `RIGID_BODY_POSE` tensor binding.

- Ground plane — logged once as a static flat box for visual context.

This is the core of the render handoff pattern: ovphysx owns the simulation, you log what you need to your visualization tool.

## Two ways to hand off, and what each populates

- Tensor handoff (this tutorial). The viewer never reads the Stage — Rerun receives transforms from tensor bindings and the sample supplies its own geometry. The sample USD is non-instanced, so `domains=PopulationDomain.PHYSICS` is enough here; for arbitrary content prefer `ALL`.

- Shared Stage. A renderer such as ovrtx draws from the same Stage, so populate `ALL` (or `PHYSICS | RENDERING`). See Population domains for the full contract, the native-instance caveat, sequencing, and cost.

## Prerequisites

- Install ovphysx: `pip install ovphysx`

- Install the Rerun SDK: `pip install rerun-sdk`

- Get the sample from the source repository: the Rerun sample lives under `tests/python_samples_extra/visual_rerunio_sample/` and is not part of the `pip install` wheel.

## Sample

    # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
    # SPDX-License-Identifier: BSD-3-Clause
    #

    # NOTE: This file is included verbatim in documentation via literalinclude (full file).

    """
    Visualize rigid body simulation with Rerun.

    Shows 11 cubes falling onto a ground plane, streaming rigid body transforms
    to a Rerun web viewer. Open http://localhost:9090 in your browser to watch.

    This demonstrates the "render handoff" pattern: ovphysx owns simulation,
    you log what you need to your visualization tool. Rerun does not read USD,
    so the sample explicitly logs all visual geometry from simulation data.
    """

    import argparse
    import time
    from pathlib import Path
    from urllib.parse import quote

    import numpy as np
    import rerun as rr
    from ovphysx.types import TensorType

    from ovphysx import PhysX

    def attach_scene(physx, usd_path, stage_name):
        import ovstage

        if not ovstage.population.available():
            raise RuntimeError("ovstage population bridge is unavailable")

        stage = ovstage.Stage(stage_name)
        ordinal = 1
        try:
            ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.PHYSICS)
            # Population does not seal: the caller owns ordinal lifecycle, and
            # attach_ovstage() reads at a sealed ordinal.
            stage.advance_write_floor(ordinal=ordinal).wait()
            physx.attach_ovstage(stage, read_ordinal=ordinal)
            return stage
        except Exception:
            stage.destroy()
            raise

    def main():
        parser = argparse.ArgumentParser(description="Visualize rigid body simulation with Rerun")
        parser.add_argument(
            "--interactive",
            action="store_true",
            help="Pause before simulation and run at real-time speed for live viewing",
        )
        args = parser.parse_args()

        # --- Rerun setup ---
        rr.init("ovphysx_rigid_bodies")
        server_uri = rr.serve_grpc()
        rr.serve_web_viewer(connect_to=server_uri, open_browser=False)
        viewer_url = f"http://localhost:9090/?url={quote(server_uri)}"
        print(f"Rerun web viewer: {viewer_url}")

        if args.interactive:
            print("Please open the web viewer in your browser. Pausing for 5 seconds...")
            time.sleep(5)

        # Set coordinate system: the USD scene is Z-up, meters
        rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)

        # --- ovphysx setup ---
        sdk = PhysX()

        script_dir = Path(__file__).resolve().parent
        usd_path = script_dir / ".." / ".." / "data" / "boxes_falling_on_groundplane.usda"
        if not usd_path.exists():
            raise RuntimeError(f"USD scene not found: {usd_path}")

        print(f"Loading USD scene: {usd_path}")
        stage = attach_scene(sdk, usd_path, "ovphysx-rerun-sample")
        sdk.wait_all()

        # Create tensor binding for rigid body poses: [N, 7] = [px, py, pz, qx, qy, qz, qw]
        pose_binding = sdk.create_tensor_binding(
            pattern="/World/Cube*",
            tensor_type=TensorType.RIGID_BODY_POSE,
        )
        num_cubes = pose_binding.count
        print(f"Bound {num_cubes} rigid bodies, shape={pose_binding.shape}")

        # --- Log static geometry ---
        # Rerun doesn't read USD, so we log colliders explicitly for visual context.
        # Ground plane: collision plane at Z=0 with a 50x50 m mesh.
        # BigBase: static box collider the cubes land on (from the USD scene).
        rr.log(
            "world/ground",
            rr.Boxes3D(
                centers=[[0.0, 0.0, -0.025]],
                half_sizes=[[25.0, 25.0, 0.025]],
                colors=[[180, 180, 180]],
            ),
            static=True,
        )
        rr.log(
            "world/big_base",
            rr.Boxes3D(
                centers=[[7.725, -5.182, 4.075]],
                half_sizes=[[17.201, 6.087, 3.896]],
                colors=[[140, 140, 160]],
            ),
            static=True,
        )

        # --- Simulate and stream ---
        poses = np.zeros(pose_binding.shape, dtype=np.float32)
        dt = 1.0 / 60.0
        num_steps = 300  # 5 seconds at 60 Hz

        print(f"Simulating {num_steps} steps...")
        wall_start = time.monotonic()
        for i in range(num_steps):
            sdk.step(dt)
            sdk.wait_all()

            pose_binding.read(poses)

            rr.set_time("step", sequence=i)
            rr.set_time("sim_time", duration=i * dt)

            # Log cubes: positions from columns 0:3, quaternions (xyzw) from columns 3:7.
            # .copy() ensures contiguous arrays for Rerun's Arrow serialization.
            rr.log(
                "world/cubes",
                rr.Boxes3D(
                    centers=poses[:, 0:3].copy(),
                    half_sizes=[0.5, 0.5, 0.5],
                    quaternions=rr.Quaternion(xyzw=poses[:, 3:7].copy()),
                ),
            )

            # In interactive mode, sleep so wall-clock time doesn't run ahead of sim time
            if args.interactive:
                sim_time = (i + 1) * dt
                elapsed = time.monotonic() - wall_start
                if elapsed < sim_time:
                    time.sleep(sim_time - elapsed)

        print(f"Visualization sample completed successfully ({num_steps} steps)")

        pose_binding.destroy()
        sdk.detach_ovstage()
        stage.destroy()
        sdk.release()
        print("Cleanup complete")

    if __name__ == "__main__":
        main()

## Running

From your ovphysx source checkout:

    cd tests/python_samples_extra/visual_rerunio_sample
    uv run visualize_rigid_bodies.py --interactive

This installs dependencies, starts a Rerun web viewer at `http://localhost:9090`, pauses for 5 seconds so you can open the URL in your browser, then runs the simulation at real-time speed.

Without `--interactive`, the simulation runs as fast as possible — useful for CI or when you only need the recorded data in the Rerun viewer’s timeline.

## Result

After this tutorial, you can stream simulation state from ovphysx to an external viewer — the same pattern applies to any visualization or rendering tool that accepts transform data.