Skip to main content
A local 3D render writes everything into its --out folder, which must not exist yet or be empty. This is the layout for the camera, lidar and radar example:
Folder and file names come from each source’s outputName. Ticks are zero-padded to eight digits. A lidar or radar review video shows the sensor’s right on the right of the picture. Review videos rendered before 13 October 2026 are mirrored left to right; the point clouds and radar files of those renders are correct.

Camera files

--passes rgb,depth (any of rgb, id, depth, semantic) writes only the passes you need. A 1280x720 frame takes about 1 MB as RGB and 3.7 MB as depth, so 7 cameras x 200 frames with all four passes come to 6.3 GB. The RGB image carries the preset’s camera look: exposure, tone mapping, bloom, a vignette and a slight lens distortion (0.008). Depth, id and semantic are drawn without them, so their pixels line up with an undistorted pinhole. For pixel-exact geometry in the RGB image too, turn the distortion off: --set lens.distortion=0. render.json records every setting in renderConfig.

Lidar files

<tick>.ply is an ASCII PLY point cloud with one row per point:
x, y and z are metres in the lidar’s own frame: x forward along the sensor’s line of sight, y up, and z to the right (unlike the rig’s vehicle frame, whose z points left). intensity is a reflectance proxy in 0 to 1, and instance_id uses the same ids as the cameras’ id pass. The renderer writes one full scan at every rendered frame, the camera rate: the example’s 10 Hz lidar gives 200 point clouds in 10 seconds at 20 fps, not 100. Every point of a scan is taken at the frame’s instant (no motion during the sweep), and rotationFrequencyHz sets how dense a scan is: each ring has pointsPerSecond / (channels x rotationFrequencyHz) samples, kept between 64 and 4096. Beams that hit nothing within rangeM return no point, so the count varies from frame to frame.

Radar files

<tick>.csv has one detection per row:

Time and synchronization

Every sensor is captured at the same instants. Frame n is at time clipStart + n / fps, in seconds from the scenario’s start: results.json gives each camera frame’s sampleTime.seconds, and observed-frames.jsonl each frame’s time. The rig’s clip sets clipStart (0 by default). A frame is an instantaneous snapshot: rolling shutter, exposure time and sensor clock drift are not modelled (results.json says so per frame under camera.sampling).

Poses and calibration

There is no separate calibration file: the rig you rendered with is the calibration.
  • Intrinsics. A pinhole camera has fx = fy = (width / 2) / tan(horizontalFovDeg / 2) and its principal point at the image centre. results.json gives each frame’s projection: kind, fovYDeg, nearM, farM and the depth convention.
  • Extrinsics. Each source’s transform in the rig: its position and rotation on the vehicle.
  • The vehicle in the world. observed-frames.jsonl gives every actor’s pose at every frame: position [x, y, z] in metres and rotation as a quaternion [x, y, z, w], in the map’s scene frame (x east, y up, z south).
  • Each camera in the world. job/render-job.json lists every camera at every frame (rig.cameraSchedule): its mount as the renderer applied it (attach), and its position (eye) and a point 50 m along its line of sight (target) in the scene frame.

Load it in Python

The toolkit has no reader for these files; NumPy and Pillow are enough:

In the cloud

A cloud 3D render (--cloud) returns each sensor’s video (video/<sensor>.mp4, lidar and radar as review videos) with render.json and results.json. It does not return the per-frame images, depth, id or semantic passes, point clouds or radar files: render locally when you need them. simforge preview frame --cloud returns the RGB frame as a PNG.