Recursive AI

View a physics replay in C#

OneScope is a free, open-source viewer for deterministic libraries from Recursive AI. MIT or Apache-2.0.

View OnePhysics and OnePath replays with bundled modules; other formats need a module.

Terminal
dotnet tool install --global RecursiveAI.OneScope.Tool

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Terminal
onescope open run.oppr

Opens run.oppr from the download with a timeline and body inspector.

Unzip the example, then dotnet run.

The full program

run.oppr is in the download: a 2D recording of one ball falling onto a static ground box, with 120 simulation steps. The ball’s entity ID is 72057598332895233. The ground is 20 × 1 meters at (0, −1); the ball starts at (0, 4), with radius 0.5 meters. It was recorded with OnePhysics 1.0.0 at 1/60 second per step.

The C# launcher installs OneScope.Tool 1.0.0 into its build folder, runs the operations headless, checks the exit code and prints selected JSON fields. Headless still needs a working graphics renderer.

C#
using System.Diagnostics;
using System.Text.Json;
 
// Install the published command-line tool into the build folder.
string toolDir = Path.GetFullPath("obj/onescope-tool");
string tool = Path.Combine(toolDir,
    OperatingSystem.IsWindows() ? "onescope.exe" : "onescope");
if (!File.Exists(tool))
    Run("dotnet", "tool", "install", "RecursiveAI.OneScope.Tool",
        "--version", "1.0.0", "--tool-path", toolDir);
 
// Results: [0] opens run.oppr, [1] seeks, [2] lists bodies, [3] inspects the ball.
var results = Open(
    "seek {\"step\":60}",
    "entities {\"kind\":\"body\"}",
    "inspect {\"entity\":72057598332895233}");
var time = results[1];
var body = results[2].GetProperty("entities")[1];
Console.WriteLine($"step {time.GetProperty("step")}: " +
    time.GetProperty("sources")[0].GetProperty("hash"));
Console.WriteLine($"body {body.GetProperty("id")}: {body.GetProperty("name")}");
var position = results[3].GetProperty("properties")
    .EnumerateArray().First(p => p.GetProperty("name").GetString() == "Position");
Console.WriteLine($"Position: {position.GetProperty("value")}");
 
JsonElement[] Open(params string[] operations)
{
    var arguments = new List<string>
    {
        "open", "run.oppr", "--headless", "--size", "640x480"
    };
    foreach (string operation in operations)
        arguments.AddRange(["--run", operation]);
    string output = Run(tool, arguments.ToArray());
    return output.Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Select(line => JsonDocument.Parse(line).RootElement.Clone())
        .ToArray();
}
 
string Run(string command, params string[] arguments)
{
    var start = new ProcessStartInfo(command)
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true
    };
    foreach (string argument in arguments) start.ArgumentList.Add(argument);
    using var process = Process.Start(start)!;
    var output = process.StandardOutput.ReadToEndAsync();
    var errors = process.StandardError.ReadToEndAsync();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException(output.Result + errors.Result);
    return output.Result;
}

dotnet run prints:

Text
step 60: cb0162b65afd19f4
body 72057598332895233: Body 1 (Awake)
Position: [0,-4.162779E-05,0]

If an absolute seek is outside the recording, the operation fails with bad_args; use the start and end reported when opening the source.

Variations

Seek and inspect without a window

Terminal
onescope open run.oppr --headless \
  --run 'seek {"step":60}' \
  --run 'inspect {"entity":72057598332895233}'

The seek result is:

JSON
{"step":60,"start":0,"steps":120,"sources":[{"source":0,"step":60,"hash":"cb0162b65afd19f4"}]}

The following inspector result contains the ball’s transform, motion, body and solver properties:

JSON
{
  "source": 0,
  "step": 60,
  "entity": 72057598332895233,
  "kind": "body",
  "name": "Body 1 (Awake)",
  "properties": [
    {
      "group": "Transform",
      "name": "Position",
      "kind": "vector",
      "value": [
        0,
        -4.162779e-05,
        0
      ]
    },
    {
      "group": "Transform",
      "name": "Angle",
      "kind": "number",
      "value": 0
    },
    {
      "group": "Motion",
      "name": "Linear velocity",
      "kind": "vector",
      "value": [
        0,
        0,
        0
      ]
    },
    {
      "group": "Motion",
      "name": "Angular velocity",
      "kind": "number",
      "value": 0
    },
    {
      "group": "Body",
      "name": "Type",
      "kind": "text",
      "value": "Dynamic"
    },
    {
      "group": "Body",
      "name": "Mass",
      "kind": "number",
      "value": 0.7853981852531433
    },
    {
      "group": "Body",
      "name": "Awake",
      "kind": "bool",
      "value": true
    },
    {
      "group": "Solver",
      "name": "Island",
      "kind": "integer",
      "value": 0
    },
    {
      "group": "Solver",
      "name": "Sleep time",
      "kind": "number",
      "value": 0.10000000149011612
    },
    {
      "group": "Solver",
      "name": "Constraints",
      "kind": "integer",
      "value": 1
    },
    {
      "group": "Solver",
      "name": "Constraint colours",
      "kind": "integer",
      "value": 1048576
    },
    {
      "group": "Solver",
      "name": "External impulse",
      "kind": "vector",
      "value": [
        0,
        0,
        0
      ]
    }
  ]
}

To record your own .oppr, wrap your simulation steps in OnePhysics’s world.StartRecording(stream). Record a replay shows the complete recording program.


The short answer

Install the onescope .NET tool and open a OnePhysics .oppr file. OneScope loads its bundled physics module, draws the world and provides a timeline, entity tree and inspector. No extra module assembly is needed for OnePhysics recordings. The command is a separate viewer; your simulation does not need to reference the viewer package.

To explore the supplied replay interactively, run the snippet at the top from the unzipped folder.

How do I view a replay?

Open the recording in the window, then move the timeline cursor to the step you want. Time in OneScope is a simulation step index, not a video timestamp. Selecting a body in the entity tree shows its properties in the inspector. Selecting it in a scene view outlines the same entity in every view of that source. The window can hold both 2D and 3D views; the bundled module supports both kinds of OnePhysics world.

For the supplied file, select Body 1 rather than Body 0: Body 0 is the static ground. At step 60, Body 1 is near the ground. The inspector reports position and velocity, along with body and solver properties. This gives you the numbers behind the picture rather than requiring you to estimate a position from pixels.

The source declares overlays for shapes, bounds, contacts, joints, centers of mass and joint limits. Turn on the ones relevant to the question you are investigating. For a collision question, contacts and bounds help connect the geometry in the view to the properties in the inspector. Opening a replay is read-only; changing the camera or the cursor does not change the recorded simulation.

How do I inspect a physics replay without a window?

Use --headless and pass operations with --run, as in the variation. Each operation prints a JSON result, and the process exits after the requested operations. entities lists IDs; inspect takes an entity ID from that list. IDs belong to the recording, so the ball ID shown here is not a general way to select the first dynamic body in another file.

seek moves to an absolute step. step moves by a delta and clamps to the recording’s range. A seek outside the time axis fails instead of silently choosing a different absolute step. Read the source’s start and end from the opening result before choosing a step in your own recording.

Seeking backward and then forward reconstructs the same state as playing straight to that step. The reported hash is the physics library’s world hash, not a checksum of the screenshot. Playback speed affects which step is presented, not the simulation’s recorded time step. Those distinctions matter when you are trying to inspect one reproducible collision rather than simply watch an animation. The time model explains how keyframes and replay support that behavior.

All onescope recipes