Skip to main content

THREE.js-based simulation visualization tool

Project description

SimView Visualizer

SimView is a powerful and interactive tool for visualizing 3D models and terrain data in simulations. It enables you to explore and analyze multiple simulation scenarios (batches) within a shared environment, all defined through an intuitive JSON format or a Python API.

Whether you're simulating physical objects or comparing different runs, SimView provides a flexible and efficient way to bring your data to life using a web-based interface powered by Three.js.


Features

  • Batched Simulations: Visualize multiple simulation instances side-by-side.
  • Shared Terrain: Efficient rendering with shared terrain across all batches.
  • Interactive UI: Web-based controls for playback, camera, and data inspection.
  • Python API: Easy-to-use API for generating scenes and launching the visualizer directly from your code.
  • JSON Support: Load and save simulation data using a portable JSON format.

Quick Start

The easiest way to get started is to run the provided example script:

python example.py

This script demonstrates how to use the Python API to create a simulation with wavy terrain, dynamic bodies, and time-series data.

The Python authoring API (simview.scene, simview.state, simview.model) depends on torch and einops. Install them with the optional authoring extra shown below. Only these are needed to build simulations; viewing an existing JSON file does not require torch.


Installation

Requires Python 3.12+.

To only view existing simulation JSON files:

pip install -e .

To also author simulations from Python (installs torch and einops):

pip install -e ".[authoring]"

For independent use of this repository, use venv or uv:

python -m venv .venv
source .venv/bin/activate
pip install -e ".[authoring]"
uv sync --extra authoring
source .venv/bin/activate

CLI Utilities

Cache Management

SimView caches some temporary files for visualization. It also cleans up any simview_viz_*.json temp scene files left behind by older versions (a launched viewer now serves an in-memory SimulationScene directly, without writing one). You can clear all of this using the following command:

simview clear

Visualization of exported simulations

To visualize a simulation defined in a JSON file, run the following command, replacing [path_to_json_file] with the actual path to your JSON data:

simview [path_to_json_file]

Gzip-compressed files (e.g. scene.json.gz) are detected automatically and decompressed transparently — no separate flag needed.

Useful flags:

simview scene.json --host 0.0.0.0 --port 8080  # bind to a specific host/port
simview scene.json --no-browser                # don't auto-open a browser tab
simview --version                               # print the installed version

Comparing multiple runs (e.g. real-world vs. simulated)

Pass multiple JSON files to merge them into a single scene, each file's batches appended as extra batches in the viewer:

simview real_world.json simulated.json

The files must describe the same physical setup (identical bodies and terrain grid) — that's what makes the batches comparable. They don't need to share a timeline: the first file's timestamps become the merged timeline, and every other file is resampled onto it by nearest timestamp (no interpolation), so put the recording you care most about matching frame-for-frame first. See Analysis Panel below for a way to quantify the difference between two merged batches.

Each merged file's batches are auto-named after its filename (e.g. real_world, simulated), shown in the Batch Legend. You can rename them from there — renames are saved next to the input file(s) and reloaded automatically the next time you open the same file(s). You can also set initial batch names yourself by including a batchNames array directly in the JSON's model object (see JSON Format Specification); renames from the UI take precedence over this once saved.

To merge files without launching the viewer, e.g. to inspect or re-share the merged scene, pass --save-merged:

simview real_world.json simulated.json --save-merged combined.json.gz

The output is gzipped if the path ends in .gz.


Live streaming

Instead of saving a scene and viewing it afterwards, LiveViewer starts the server immediately and pushes each state to an already-open browser tab as your simulation produces it, over a WebSocket:

from simview import LiveViewer

# `scene` needs its complete model (terrain, bodies, ...) up front; states are
# streamed in afterwards.
with LiveViewer(scene, open_browser=True) as live:
    for t in range(num_steps):
        ...  # step the simulation
        live.push_state(time=t * dt, body_states=[...], scalar_values=...)

# scene.states was appended to exactly like scene.add_state would, so it can
# still be saved once streaming is done:
scene.save("recording.json.gz", compress=True)

push_state has the same signature and validation as SimulationScene.add_state -- it delegates to it directly, then broadcasts the new frame to every connected viewer. A viewer opened after the stream has already started still gets the full history so far, replayed as a single catch-up message before it starts receiving new frames live. If no viewer is connected yet, pushed frames are simply buffered for the next one to connect. Playback in the browser follows the live frames automatically as long as you haven't scrubbed backward or started a loop; a small "LIVE" badge shows while the socket is open.

See example_live.py for a runnable end-to-end example.


Jupyter / non-blocking viewing

scene.show() starts a viewer on a background thread and returns immediately, instead of blocking like SimViewLauncher/SimViewServer.run. This is handy in a notebook: evaluating the returned handle as a cell's result embeds the viewer inline via an iframe.

handle = scene.show()  # non-blocking; scene itself is left untouched
handle  # in Jupyter, displays the viewer inline (uses _repr_html_)

# ... do other work, or just let the cell above stay interactive ...

handle.stop()  # or: `with scene.show() as handle: ...` to stop automatically

Visualization Controls

Once the visualizer is running, you can interact with the simulation using the following controls:

Camera

  • Rotate: Left-click + drag OR Ctrl (CMD on Mac) + Arrow keys
  • Pan: Right-click + drag OR Arrow keys
  • Zoom: Scroll wheel
  • Track Body: Automatically follow a specific body (via the "Camera Options" menu)
  • Split Screen: Compare two batches side-by-side (via the "Camera Options" menu, requires ≥2 batches)
  • Field of View: Adjust camera FOV (via the "Camera Options" menu)
  • Copy View Link: Click the "Copy view link" button (in the "Camera Options" menu) to copy a URL that encodes the current camera, playback time, focused batch, and visualization toggles -- opening it restores that view.

Timeline

  • Step Forward/Backward: Alt + Arrow Right / Arrow Left
  • Seek (and Pause): Click on the timeline bar
  • Play/Pause: Space or Click the Play button
  • Record: R or Click the Record button (select WEBM, MP4 -- if your browser supports recording it -- or PNG sequence via the dropdown). Recording seeks to the start, plays exactly one loop, then automatically stops and downloads the file.
  • Screenshot: S or Click the camera button next to Record to save the current frame as a PNG.
  • Playback Speed: Adjust speed (0.1x to 5x) via the dropdown next to the timeline

Batch Selection

  • Move Selection: Shift + Arrow keys

Visualization Options

  • B: Toggle Body Visualization Mode (Mesh / Wireframe / Points)
  • A: Toggle Axes Visibility
  • G: Toggle Trajectory Trails
  • I: Toggle Smooth Interpolation (on by default; interpolates position/orientation between recorded states during playback and scrubbing instead of snapping to the nearest frame)
  • C: Toggle Contact Points
  • V: Toggle Linear Velocity
  • W: Toggle Angular Velocity
  • F: Toggle Linear Force
  • T: Toggle Torque
  • P: Toggle Terrain Data Probe (interactive tooltip on hover)

You can also customize terrain colors, colormaps, and toggle surface/wireframe/normals from the "Terrain Options" menu.

Trajectory Trails

Toggling trails (G, or "Show Trails" in the Body Options panel) draws each body's path from the start of the simulation up to the current playback time, one line per batch in that batch's color. Useful for comparing the overall shape of two trajectories (e.g. real vs. simulated) at a glance instead of scrubbing frame by frame.

Analysis Panel

Scalars and Error Metrics share one collapsible panel at the top-center of the screen. When both are available, a mode switcher lets you flip between them; if only one is available (e.g. a single-batch scene has no Error Metrics), that one is shown directly without the switcher.

  • Scalars: one tab per scalar defined in the model, each plotting its value over time for every batch (colored per batch, click a line to focus that batch). An "Export CSV" button on the active tab downloads its full series as time plus one column per batch, named after each batch's current display name.
  • Error Metrics: shown once a scene has 2 or more batches. Pick a body and two batches ("Batch A" / "Batch B") to compare — e.g. the real and simulated batches produced by merging multiple files — and it computes, over the full timeline, the Euclidean position error and the quaternion angle (orientation) error between the two batches for that body. A live readout shows the current-frame values, and the chart plots both error curves over time with a marker at the current playback position. The "Per-axis" toggle swaps the combined position error curve for the signed X/Y/Z error components (Batch A minus Batch B), useful for spotting a directional bias instead of just overall magnitude. Below the readout, a compact stats block summarizes the full timeline: position RMSE, the max position error (and when it occurs), the final-frame drift, and the orientation RMSE and max angle error. An "Export CSV" button downloads the current selection's per-frame series (time, pos_error, err_x, err_y, err_z, angle_error_deg).

Batch Legend

When a scene has 2 or more batches, a toggleable "Batches" legend appears in the bottom-right corner, listing each batch's color, index, and name. Click a row to focus that batch, or click a name to rename it in place — renames persist next to the input file(s), so they survive a reload or server restart.


JSON Format Specification

If you prefer to generate data files manually or from another language, SimView uses a single JSON document with two top-level keys: model (static data, sent once) and states (an array of time-ordered snapshots). This is exactly what SimulationScene.save() produces.

{ "model": { ... }, "states": [ { ... }, { ... } ] }

Model (Static Data)

  • simBatches (integer) — number of parallel simulation instances (batches).
  • batchNames (array[string], optional) — display name for each batch, length must equal simBatches. Shown in the Batch Legend; falls back to "Batch <index>" per entry if omitted, empty, or the wrong length. Renames made from the Batch Legend are persisted server-side (see below) and take precedence over this field on subsequent loads.
  • scalarNames (array[string]) — names of per-batch scalar time-series (e.g. "energy").
  • dt (float) — simulation timestep in seconds. Used for playback timing; if omitted or invalid the viewer infers it from consecutive state times.
  • collapse (boolean) — UI hint to start with the body-state window collapsed.
  • bodies (array) — dynamic bodies. Each entry:
    • name (string) — unique identifier, referenced from each state.
    • shape (object) — geometry, keyed by a string type:
      • "box" — requires hx, hy, hz (half-extents).
      • "sphere" — requires radius.
      • "cylinder" — requires radius, height.
      • "pointcloud" — requires points (array[array[3]]) in the body's local frame.
      • "mesh" — requires vertices (array[array[3]]) and faces (array[array[3]]).
    • availableAttributes (array[string], optional) — which optional per-state fields this body provides. Any of "contacts", "velocity", "angularVelocity", "force", "torque".
    • parent (string, optional) — name of another model.bodies[] entry this body is attached to. When set, this body's pose is no longer absolute world space; see localTransform below and the bodyTransform note under States.
    • localTransform (array[7], optional)[x, y, z, w, qx, qy, qz] constant offset from parent, for bodies rigidly attached (e.g. a wheel bolted to a chassis). Set only together with parent. A body with localTransform never appears in any state's bodies[] — its world pose is derived every frame from its parent's current pose plus this fixed offset, saving the cost of repeating an unchanging transform every frame. For an articulated attachment (e.g. an arm joint) instead, set only parent and keep providing a per-frame bodyTransform in states[].bodies[] as usual — it's then interpreted as local to the parent's current-frame pose rather than world space.
  • staticObjects (array, optional) — non-moving geometry. Each entry has name, isSingleton (boolean), and either shape (when singleton) or shapes (array, one per batch) using the same shape objects as bodies.
  • terrain (object) — heightfield shared or per-batch:
    • dimensions: sizeX, sizeY (float) and resolutionX, resolutionY (int).
    • bounds: minX, maxX, minY, maxY, minZ, maxZ. When friction/stiffness data is present, also minFriction/maxFriction and/or minStiffness/maxStiffness, which the viewer uses to normalize the color map.
    • isSingleton (boolean)true when one terrain is shared by all batches; false when each batch has its own.
    • heightData (array[array[float]]) — one flattened resolutionX * resolutionY grid per batch (a single flat array is also accepted and treated as one batch).
    • normals (array[array[array[3]]]) — per-batch surface normals, one [x, y, z] per grid point.
    • frictionData, stiffnessData (array[array[float]] | null, optional) — per-batch scalar fields over the grid, selectable as terrain color modes.

States (Dynamic Data)

states is an array; each element is one snapshot:

  • time (float) — snapshot time in seconds.
  • bodies (array) — per body:
    • name (string | array[string]) — matches a model.bodies[].name. May instead be a list of names when several bodies move rigidly together (e.g. links welded to the same parent): the single entry's bodyTransform and other fields below then apply identically to every named body, instead of repeating identical data once per body. All named bodies must exist in model.bodies.
    • bodyTransform — pose. Batched: array[array[7]], one [x, y, z, w, qx, qy, qz] per batch; single: a flat [x, y, z, w, qx, qy, qz]. Absolute world-space, unless the referenced body has a parent in model.bodies (see above), in which case this is local to that parent's current-frame pose instead. A body with a constant localTransform on the model never has a bodyTransform entry here at all.
    • contacts (array[array[int]], optional) — per batch, indices of contacting points (into the body's pointcloud points). Empty array means no contacts.
    • velocity, angularVelocity, force, torque (array[array[3]], optional) — per-batch 3-vectors.
  • <scalarName> (array[float]) — for each name in model.scalarNames, one value per batch.

Binary state fields. The numeric per-body fields (bodyTransform, velocity, angularVelocity, force, torque) may alternatively be a string of the form "__b64__<base64>", where the base64 payload is the little-endian float32 bytes of the batched array in row-major order (bodyTransform is width 7, the vectors width 3). Both SimViewBodyState (used by add_state) and SimulationScene.add_trajectory emit this by default (typically ~3-4× smaller than the equivalent plain JSON floats); pass binary=False to either to emit plain JSON lists instead. The viewer and the file-merge decode binary fields transparently. contacts, scalars, and time are always plain JSON.

Server-side columnar repack. This on-disk, per-frame layout never changes (and simview merge still reads/writes it as described above); but when serving a scene to the viewer, the server repacks states at load time into whole-trajectory columns -- one binary blob per body per numeric field (and one per scalar), covering all T frames at once -- and serves a small JSON index ({"version": 4, "times", "bodies", "scalars"}) whose entries are /blob/... URLs, fetched in parallel and decoded into Float32Arrays. This avoids materializing thousands of tiny per-frame JS objects just to play back a long trajectory. The repack requires the body set, per-body field set, and field widths to be identical across every frame (contacts is exempt and may come and go per frame); if a scene doesn't meet that, the server falls back to serving the legacy per-frame JSON array unchanged, which the viewer also still supports.

Authoring whole trajectories

Building states one frame at a time (add_state) is fine for short scenes, but for long, dense trajectories prefer SimulationScene.add_trajectory, which appends an entire time-series in one call, converting each body's tensors once instead of per frame — noticeably faster save/load than the same data built frame-by-frame. Both paths pack the numeric fields as the binary blobs described above by default, so file size is comparable either way:

from simview import SimulationScene, BodyShapeType, BodyTrajectory

scene = SimulationScene(batch_size=B, scalar_names=[], dt=0.001)
scene.create_terrain(...)
scene.create_body(body_name="box", shape_type=BodyShapeType.BOX, hx=0.5, hy=0.3, hz=0.15)

# positions: (T, B, 3), orientations: (T, B, 4) as [w, x, y, z]
# (2-D (T, 3) / (T, 4) is accepted when batch_size == 1)
scene.add_trajectory(
    times=times,                                  # length-T sequence or tensor
    trajectories=[BodyTrajectory("box", positions, orientations)],
)
scene.save("scene.json")

Pass binary=False to emit plain JSON lists instead.

Both BodyTrajectory.name and SimViewBodyState's body_name accept a list of body names instead of a single string, for bodies that move rigidly together (e.g. BodyTrajectory(["link_a", "link_b"], positions, orientations)) — the same transform (and any optional attributes) is applied to every named body, so it only needs to be written once per frame instead of once per body.

For large simulations, pass compress=True to save() (or use a .gz filepath) to gzip the output — SimulationScene.load(), the CLI, and the server all detect and decompress it transparently regardless of extension.

Parent-relative bodies (rigid and articulated attachments)

create_body accepts parent/local_transform to attach a body to another body already in the model, instead of it moving in world space:

scene.create_body(body_name="chassis", shape_type=BodyShapeType.BOX, hx=0.6, hy=0.4, hz=0.2)

# Rigid attachment (e.g. a wheel bolted to the chassis): a constant offset, defined
# once, never repeated per frame. Never call add_state/add_trajectory for "left_wheel".
scene.create_body(
    body_name="left_wheel", shape_type=BodyShapeType.CYLINDER, radius=0.15, height=0.1,
    parent="chassis", local_transform=[0.4, 0.52, 0.0, 1.0, 0.0, 0.0, 0.0],
)

# Articulated attachment (e.g. an arm joint): only `parent` is set, so this body's
# pose is still supplied every frame via add_state/add_trajectory as usual -- it's
# just interpreted as local to the chassis's current-frame pose instead of world.
scene.create_body(body_name="arm_joint", shape_type=BodyShapeType.BOX, hx=0.05, hy=0.05, hz=0.2, parent="chassis")

A body's parent must already exist in the model (added before its children), which also rules out cycles. Merging files containing rigid (constant-offset) bodies works the same way — merge_simulation_files carries the parent/localTransform through in model.bodies and doesn't require or emit per-frame data for them.

Example (2 batches, one box, flat terrain)

{
  "model": {
    "simBatches": 2,
    "scalarNames": ["energy"],
    "dt": 0.1,
    "collapse": false,
    "bodies": [
      {
        "name": "Box",
        "shape": { "type": "box", "hx": 0.5, "hy": 0.5, "hz": 0.5 },
        "availableAttributes": ["velocity"]
      }
    ],
    "staticObjects": [],
    "terrain": {
      "dimensions": { "sizeX": 10.0, "sizeY": 10.0, "resolutionX": 2, "resolutionY": 2 },
      "bounds": { "minX": -5.0, "maxX": 5.0, "minY": -5.0, "maxY": 5.0, "minZ": 0.0, "maxZ": 0.0 },
      "isSingleton": true,
      "heightData": [[0.0, 0.0, 0.0, 0.0]],
      "normals": [[[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]]]
    }
  },
  "states": [
    {
      "time": 0.0,
      "bodies": [
        {
          "name": "Box",
          "bodyTransform": [
            [0, 0, 1, 1, 0, 0, 0],
            [2, 0, 1, 1, 0, 0, 0]
          ],
          "velocity": [
            [0, 0, -0.1],
            [0, 0, 0]
          ]
        }
      ],
      "energy": [1.2, 0.1]
    }
  ]
}

Notes

  • Quaternion Convention Quaternions use [w, x, y, z] (scalar-first format), packed into bodyTransform after the position.

  • Terrain Consistency Each per-batch heightData grid and normals list must contain exactly resolutionX * resolutionY elements.

  • Batch Synchronization Per-batch arrays (bodyTransform, velocity, scalar values, …) must have length simBatches. When terrain.isSingleton is true, heightData/normals hold a single batch that is reused for all instances.

  • Contact Points The contacts field lists point indices into a body's pointcloud points for each batch. An empty array means no contacts.


License and Third-Party Notices

SimView is distributed under the BSD 3-Clause License.

The web interface uses uPlot (MIT licensed) for scalar and error-metric plotting; it is vendored under simview/static/lib/. three.js (MIT licensed) and chroma-js (MIT licensed) are likewise vendored under simview/static/lib/ (in version-stamped directories, e.g. lib/three-0.174.0/) rather than loaded from a CDN, so the viewer works fully offline. All third-party libraries used by SimView are permissively licensed (MIT/BSD), so there are no licensing restrictions on commercial use.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simview-3.3.tar.gz (766.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simview-3.3-py3-none-any.whl (788.2 kB view details)

Uploaded Python 3

File details

Details for the file simview-3.3.tar.gz.

File metadata

  • Download URL: simview-3.3.tar.gz
  • Upload date:
  • Size: 766.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for simview-3.3.tar.gz
Algorithm Hash digest
SHA256 4ab05bf10b433610f11d025397bc58bf73e0868c25f03c797809d4563bc61df9
MD5 f7ae8f64343e4930b33decb3bacdd687
BLAKE2b-256 f9497735f269ab2557570a5c6943bcf33d2b657d3d31376e4a3fe1254d367c4b

See more details on using hashes here.

Provenance

The following attestation bundles were made for simview-3.3.tar.gz:

Publisher: publish.yml on vlk-jan/simview

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file simview-3.3-py3-none-any.whl.

File metadata

  • Download URL: simview-3.3-py3-none-any.whl
  • Upload date:
  • Size: 788.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for simview-3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 6eec9342d4a32ceb88e8706755dceea20def06a9ccba265eec0d9e49fd34bab9
MD5 46bec70c02ff5455c5f2ca69d1d89120
BLAKE2b-256 bca21675a38bbb0e1e6a76efb7bc1059ee5f6a93e26138b875592713feef6df8

See more details on using hashes here.

Provenance

The following attestation bundles were made for simview-3.3-py3-none-any.whl:

Publisher: publish.yml on vlk-jan/simview

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page