Recursive AI

Render replay frames to PNG headless

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

Capture OnePhysics or OnePath replays without a window; a graphics renderer is still required.

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 --headless \
  --run 'capture_frame {
    "path":"frame.png"}'

Writes artifacts/frame.png at the recording’s initial step; run.oppr is in the download.

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] captures a frame, [3] captures a sequence.
var results = Open(
    "seek {\"step\":60}",
    "capture_frame {\"path\":\"frame.png\"}",
    "capture_sequence {\"dir\":\"frames\",\"count\":3,\"stride\":10}");
var frame = results[2];
var sequence = results[3];
Console.WriteLine($"frame.png: {frame.GetProperty("width")}x" +
    $"{frame.GetProperty("height")}, step {frame.GetProperty("step")}");
Console.WriteLine($"frames: {sequence.GetProperty("frames")} PNGs, " +
    $"final step {sequence.GetProperty("step")}");
 
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
frame.png: 640x480, step 60
frames: 3 PNGs, final step 80

OneScope capture at step 60, showing the falling ball, timeline and inspector

If the graphics stack cannot run, the shell exits with code 4; headless mode does not remove the renderer requirement.

Variations

Capture a sequence from a chosen step

Terminal
onescope open run.oppr --headless \
  --size 640x480 \
  --run 'seek {"step":60}' \
  --run 'capture_sequence {
    "dir":"frames","count":3,"stride":10}'

Writes PNGs for steps 60, 70 and 80, plus artifacts/frames/manifest.json.

Capture only one scene view

Terminal
onescope open run.oppr --headless \
  --run 'capture_frame {
    "path":"view.png","view":0,"size":"640x480"}'

Writes a 640 × 480 image of view 0 rather than the whole shell.


The short answer

Open a supported recording with --headless, then call capture_frame to write a PNG. Use capture_sequence for several simulation steps. OneScope’s shell exposes the same operations in the window and on the command line, so the offscreen capture can include the timeline and inspector as well as the scene. Set view when you want just one scene view instead.

The recording in this example is a OnePhysics file. The bundled module reads its simulation state and supplies the shapes to draw. The command does not turn an arbitrary replay format into images: a source needs a module that can open it. Physics and navigation modules are bundled with the tool.

How do I render a simulation replay to PNG headless?

Choose the simulation step before capturing. A lone capture_frame draws the initial state; placing seek before it draws the requested step. The full program seeks to step 60 and captures that exact state.

For a sequence, choose a starting step, a count and a stride. In the variation, count 3 and stride 10 capture steps 60, 70 and 80. The sequence result reports how many frames were written, the ending step and whether it reached the end of the source. It also gives the manifest path. The manifest records each image’s simulation step, position, world hash and SHA-256, so consumers do not have to infer time from a filename.

Output paths are inside the output folder, artifacts by default. The --artifacts option changes that folder. Keep output separate from your input recording: the replay supplies the state, while the PNGs and manifest are new artifacts. A capture can use a size such as 640x480, a named size such as 720p, or a scale. The shell’s operation reference lists the arguments and result fields.

Can a screenshot be PNG?

Yes. OneScope’s capture operations produce PNG files. The result includes the image dimensions, renderer, simulation step and SHA-256. A full-shell image shows the surrounding controls and views; a view-only image isolates the selected scene. Choose the latter for an illustration where the inspector or timeline would distract from the geometry.

Offscreen rendering and capturing the window’s presented image are different operations. presented:true asks for the window’s own frame and is rejected in headless mode. Leave it unset for an offscreen image. That avoids making a batch capture depend on whether a window is covered or minimized.

The published documentation says captures of the same state are byte-identical from run to run on the same renderer. It does not promise the same bytes across different renderers or driver builds. The world hash identifies the simulation state; the PNG’s SHA-256 identifies the image bytes. A changed image checksum alone does not establish a physics desync.

For a build machine, provide a working graphics stack even when no desktop window is shown. The documented ONEGAME_GPU_ADAPTER setting can select software, discrete or integrated rendering, or part of a device name. Keep that choice consistent when comparing image outputs.

All onescope recipes