Set Up a Radar Sensor#

The Radar generic wpmDmat model produces detections with position, radial velocity and radar cross section. A scene needs an OmniRadar prim with OmniSensorGenericRadarWpmDmatAPI and a RenderProduct requesting PointCloud. This procedure configures a radar viewing a moving cube and a field of panels, then reads the detections.

The example ships two scenes. Its default, radar_example.usda, holds a moving steel cube and a static concrete cube and is described on Python: Radar Sensor. This page builds the alternative, radar_example_field.usda, selected with --scene radar_example_field.usda: the same moving cube crossing a field of 33 thin steel panels, scanned by a radar whose single scan is laid out explicitly, bins and limits included, rather than left at the schema defaults.

Before you start. See Radar generic wpmDmat model for the simulation model and Outputs for channel definitions. If this is your first sensor scene, start with Your First Lidar Sensor.

Run the Python radar example with the panel-field scene:

cd examples/python/radar
uv run main.py --scene radar_example_field.usda

The C radar example drives the same scene when you pass the file’s path as its first argument. Both examples ship an identical copy of the file, and the USDA snippets below are taken from the Python example’s copy.

Author the sensor prim#

Apply OmniSensorGenericRadarWpmDmatAPI to an OmniRadar prim. It includes the shared attributes and first scan, s001. The defaults produce a working radar without further attributes; see schema instances and sensor authoring.

The example below explicitly sets coordinate representation and reference frame, trace depth, CFAR mode, 10 Hz frame rate and prim transform. It also sets the scan’s bins, limits, antenna gain, CFAR neighborhood, noise and velocity sequence.

radar_example_field.usda, from the runnable Python radar example; the C radar example ships the same scene#
#usda 1.0
    # One scan whose range, velocity and angle bins are authored explicitly:
    # each panel sits in a range bin of its own, outside the one-cell CFAR
    # training neighborhood of its neighbors, with constant antenna gain.
    def OmniRadar "Radar" (
        prepend apiSchemas = ["OmniSensorGenericRadarWpmDmatAPI"]
    )
    {
        token omni:sensor:WpmDmat:elementsCoordsType = "CARTESIAN"
        token omni:sensor:WpmDmat:outputFrameOfReference = "SENSOR"
        uint omni:sensor:WpmDmat:traceTreeDepth = 1
        token omni:sensor:WpmDmat:cfarMode = "2D"
        double2 omni:sensor:frameRate = (10, 1)

        bool omni:sensor:WpmDmat:scan:s001:binsFromSpec = false
        float omni:sensor:WpmDmat:scan:s001:raysPerDeg = 8
        uint omni:sensor:WpmDmat:scan:s001:rBins = 112
        uint omni:sensor:WpmDmat:scan:s001:vBins = 161
        uint omni:sensor:WpmDmat:scan:s001:azBins = 12
        uint omni:sensor:WpmDmat:scan:s001:elBins = 2
        float omni:sensor:WpmDmat:scan:s001:maxRangeM = 90
        float omni:sensor:WpmDmat:scan:s001:maxAzAngDeg = 75
        float omni:sensor:WpmDmat:scan:s001:maxElAngDeg = 10
        token omni:sensor:WpmDmat:scan:s001:txAntennaGainMode = "CONSTANT"
        token omni:sensor:WpmDmat:scan:s001:rxAntennaGainMode = "CONSTANT"
        uint omni:sensor:WpmDmat:scan:s001:cfarRnT = 1
        uint omni:sensor:WpmDmat:scan:s001:cfarRnG = 0
        uint omni:sensor:WpmDmat:scan:s001:cfarVnT = 1
        uint omni:sensor:WpmDmat:scan:s001:cfarVnG = 0
        float omni:sensor:WpmDmat:scan:s001:cfarMinVal = 7e-17
        float omni:sensor:WpmDmat:scan:s001:cfarOffset = 1
        float omni:sensor:WpmDmat:scan:s001:cfarNoiseMean = 0
        float omni:sensor:WpmDmat:scan:s001:cfarNoiseSDev = 0
        float[] omni:sensor:WpmDmat:scan:s001:rcsTuningCoefficients = [-12, 150, 0]
        float omni:sensor:WpmDmat:scan:s001:azimuthRadNoiseMean = 0
        float omni:sensor:WpmDmat:scan:s001:azimuthRadNoiseSDev = 0
        float omni:sensor:WpmDmat:scan:s001:elevationRadNoiseMean = 0
        float omni:sensor:WpmDmat:scan:s001:elevationRadNoiseSDev = 0
        float omni:sensor:WpmDmat:scan:s001:rangeNoiseMean = 0
        float omni:sensor:WpmDmat:scan:s001:rangeNoiseSDev = 0
        float omni:sensor:WpmDmat:scan:s001:velocityNoiseMean = 0
        float omni:sensor:WpmDmat:scan:s001:velocityNoiseSDev = 0
        float[] omni:sensor:WpmDmat:scan:s001:maxVelMpsSequence = [16.33]

        double3 xformOp:translate = (0, 0, 1)
        float3 xformOp:rotateXYZ = (90, 0, -90)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"]
    }
from pxr import Gf, Sdf

world = stage.DefinePrim("/World", "Xform")

radar = stage.DefinePrim("/World/Radar", "OmniRadar")
radar.AddAppliedSchema("OmniSensorGenericRadarWpmDmatAPI")
radar.CreateAttribute(
    "omni:sensor:WpmDmat:elementsCoordsType", Sdf.ValueTypeNames.Token, custom=False
).Set("CARTESIAN")
radar.CreateAttribute(
    "omni:sensor:WpmDmat:outputFrameOfReference", Sdf.ValueTypeNames.Token, custom=False
).Set("SENSOR")
radar.CreateAttribute(
    "omni:sensor:WpmDmat:traceTreeDepth", Sdf.ValueTypeNames.UInt, custom=False
).Set(1)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:cfarMode", Sdf.ValueTypeNames.Token, custom=False
).Set("2D")
radar.CreateAttribute("omni:sensor:frameRate", Sdf.ValueTypeNames.Double2, custom=False).Set(
    Gf.Vec2d(10.0, 1.0)
)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:binsFromSpec", Sdf.ValueTypeNames.Bool, custom=False
).Set(False)
# Geometric sampling trades coverage against tracing cost.
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:raysPerDeg", Sdf.ValueTypeNames.Float, custom=False
).Set(8.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:rBins", Sdf.ValueTypeNames.UInt, custom=False
).Set(112)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:vBins", Sdf.ValueTypeNames.UInt, custom=False
).Set(161)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:azBins", Sdf.ValueTypeNames.UInt, custom=False
).Set(12)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:elBins", Sdf.ValueTypeNames.UInt, custom=False
).Set(2)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:maxRangeM", Sdf.ValueTypeNames.Float, custom=False
).Set(90.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:maxAzAngDeg", Sdf.ValueTypeNames.Float, custom=False
).Set(75.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:maxElAngDeg", Sdf.ValueTypeNames.Float, custom=False
).Set(10.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:txAntennaGainMode", Sdf.ValueTypeNames.Token, custom=False
).Set("CONSTANT")
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:rxAntennaGainMode", Sdf.ValueTypeNames.Token, custom=False
).Set("CONSTANT")
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarRnT", Sdf.ValueTypeNames.UInt, custom=False
).Set(1)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarRnG", Sdf.ValueTypeNames.UInt, custom=False
).Set(0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarVnT", Sdf.ValueTypeNames.UInt, custom=False
).Set(1)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarVnG", Sdf.ValueTypeNames.UInt, custom=False
).Set(0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarMinVal", Sdf.ValueTypeNames.Float, custom=False
).Set(7e-17)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarOffset", Sdf.ValueTypeNames.Float, custom=False
).Set(1.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarNoiseMean", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:cfarNoiseSDev", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:rcsTuningCoefficients",
    Sdf.ValueTypeNames.FloatArray,
    custom=False,
).Set([-12.0, 150.0, 0.0])
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:azimuthRadNoiseMean", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:azimuthRadNoiseSDev", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:elevationRadNoiseMean",
    Sdf.ValueTypeNames.Float,
    custom=False,
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:elevationRadNoiseSDev",
    Sdf.ValueTypeNames.Float,
    custom=False,
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:rangeNoiseMean", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:rangeNoiseSDev", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:velocityNoiseMean", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:velocityNoiseSDev", Sdf.ValueTypeNames.Float, custom=False
).Set(0.0)
radar.CreateAttribute(
    "omni:sensor:WpmDmat:scan:s001:maxVelMpsSequence",
    Sdf.ValueTypeNames.FloatArray,
    custom=False,
).Set([16.33])
radar.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Double3, custom=False).Set(
    Gf.Vec3d(0.0, 0.0, 1.0)
)
radar.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Float3, custom=False).Set(
    Gf.Vec3f(90.0, 0.0, -90.0)
)
radar.CreateAttribute(
    "xformOpOrder",
    Sdf.ValueTypeNames.TokenArray,
    custom=False,
    variability=Sdf.VariabilityUniform,
).Set(["xformOp:translate", "xformOp:rotateXYZ"])

omni:sensor:WpmDmat:elementsCoordsType chooses how the three coordinates of a detection arrive. The scene sets it to CARTESIAN in place of the SPHERICAL default, so the Coordinates channel arrives as x, y and z in meters, and writes outputFrameOfReference out at its SENSOR default; the output group on USD Attributes explains both representations and the frame of reference that goes with them. The two other sensor-level attributes the scene authors are covered on the same page, under traceTreeDepth and cfarMode. omni:sensor:frameRate is a rational number in Hz, so (10, 1) is 10 Hz: how often the radar completes a scan, which the radar frame rate on USD Attributes and frame-rate behavior on Sensor Scheduling explain.

The example authors these values under scan:s001:

  • Bins: with binsFromSpec = false, the authored counts are used directly: 112 range, 161 velocity, 12 azimuth and 2 elevation bins. See Bins.

  • Coverage: maximum range 90 m; azimuth and elevation half-angles 75° and 10°. See Field of view.

  • Antenna: both elements use CONSTANT gain. See Antenna.

  • Detection: CFAR uses one neighboring test cell on each side in range and velocity, with no guard cells. See CFAR.

  • Noise: all inputs, including CFAR noise, have zero mean and deviation. See Noise.

  • Velocity sequence: one entry, 16.33. See Velocity sequence.

Some values explicitly repeat the schema defaults. Unauthored settings, including processing mode and aliasing switches, retain the defaults listed in USD Attributes.

Orient the prim#

The sensor looks along its local -Z axis, as every sensor prim does (refer to Orienting the prim), so in a Z-up scene rotate the prim until -Z points where you want the field of view to go. The scene’s xformOp:rotateXYZ = (90, 0, -90) turns local -Z onto world +X, toward the field of panels and the moving cube, and keeps local +Y pointing up, so that the field of view stays level; xformOp:translate = (0, 0, 1) puts the radar 1 m above the ground plane, at the height of the panels’ centers. The mount is the one the default scene uses.

Request the PointCloud output#

A RenderProduct whose camera relationship targets the sensor prim renders through it, and the RenderVars in its orderedVars say what it delivers (refer to RenderProduct and RenderVar). This model has one output to request, a RenderVar with sourceName PointCloud, and the RenderVar’s channels attribute lists the per-detection tensors the frame should carry:

radar_example_field.usda, from the runnable Python radar example#
#usda 1.0
    def "Render"
    {
        def "Products"
        {
            def RenderProduct "RadarProduct"
            {
                rel camera = </World/Radar>
                rel orderedVars = [<../../Vars/PointCloud>]
            }

            def RenderProduct "CameraProduct"
            {
                rel camera = </World/PreviewCamera>
                int2 resolution = (960, 540)
                token omni:rtx:rendermode = "RealTimePathTracing"
                rel orderedVars = [<../../Vars/LdrColor>]
            }
        }

        def "Vars"
        {
            def RenderVar "PointCloud"
            {
                uniform string sourceName = "PointCloud"
                token[] channels = [
                    "Coordinates",
                    "Counts",
                    "RCS",
                    "RadialVelocityMs"
                ]
            }

            def RenderVar "LdrColor"
            {
                uniform string sourceName = "LdrColor"
            }
        }
    }
from pxr import Gf, Sdf

world = stage.DefinePrim("/World", "Xform")

render = stage.DefinePrim("/World/Render", "")

products = stage.DefinePrim("/World/Render/Products", "")

radar_product = stage.DefinePrim("/World/Render/Products/RadarProduct", "RenderProduct")
radar_product.CreateRelationship("camera", custom=False).SetTargets(["/World/Radar"])
radar_product.CreateRelationship("orderedVars", custom=False).SetTargets(
    ["/World/Render/Vars/PointCloud"]
)

camera_product = stage.DefinePrim("/World/Render/Products/CameraProduct", "RenderProduct")
camera_product.CreateRelationship("camera", custom=False).SetTargets(["/World/PreviewCamera"])
camera_product.CreateAttribute("resolution", Sdf.ValueTypeNames.Int2, custom=False).Set(
    Gf.Vec2i(960, 540)
)
camera_product.GetAttribute("resolution").SetVariability(Sdf.VariabilityVarying)
camera_product.CreateAttribute(
    "omni:rtx:rendermode", Sdf.ValueTypeNames.Token, custom=False
).Set("RealTimePathTracing")
camera_product.CreateRelationship("orderedVars", custom=False).SetTargets(
    ["/World/Render/Vars/LdrColor"]
)

vars = stage.DefinePrim("/World/Render/Vars", "")

point_cloud = stage.DefinePrim("/World/Render/Vars/PointCloud", "RenderVar")
point_cloud.CreateAttribute(
    "sourceName", Sdf.ValueTypeNames.String, custom=False, variability=Sdf.VariabilityUniform
).Set("PointCloud")
point_cloud.CreateAttribute("channels", Sdf.ValueTypeNames.TokenArray, custom=False).Set(
    ["Coordinates", "Counts", "RCS", "RadialVelocityMs"]
)

ldr_color = stage.DefinePrim("/World/Render/Vars/LdrColor", "RenderVar")
ldr_color.CreateAttribute(
    "sourceName", Sdf.ValueTypeNames.String, custom=False, variability=Sdf.VariabilityUniform
).Set("LdrColor")

The scene requests RCS as well as RadialVelocityMs, so every detection arrives with its radar cross section beside its radial velocity, and the figures under Verification color the detections by each in turn. Counts and Flags arrive whether or not you list them. Requesting the output on Outputs names every channel you can list, and its channel table says what each one holds.

The second product in the same Render scope, CameraProduct, is not a sensor output. It renders through the scene’s PreviewCamera and requests LdrColor: the camera view of the field, there so that the scene can be looked at, and the example never steps it. The camera animation under Verification was rendered through it.

Label the surfaces#

The non-visual sensors use the non-visual material labels (omni:simready:nonvisual:*) on the Material prim bound to a surface for calculating their responses, not the surface shader. The scene has two materials, labeled asphalt for the ground plane and steel for the panels and the moving cube:

The complete material definitions are in radar_example_field.usda.

Each Material prim also carries a visual shader, so one prim serves the radar and the preview camera; Non-Visual Sensor Materials explains the label vocabulary, where the labels live and how a material that also drives visual rendering keeps its shader beside them.

Place the targets#

The targets are what the scan above is laid out for. Thirty-three thin steel panels, 2 m tall, a quarter of a meter thick and wider the farther out they stand, are spread across the field of view from about 10 m out to nearly the 90 m maximum range, each turned to face the radar with its front face centered in a range bin of its own, so that no two panels share a range bin; their angular footprints do not overlap either. The moving steel cube of the default scene crosses the field with its transform time-sampled from (30, 4, 0.75) at time code 0 to (20, 4, 0.75) at time code 10, one meter closer to the radar per 0.1 s step, a closing speed of 10 m/s.

The complete target layout are in radar_example_field.usda.

Create the renderer with the motion BVH enabled#

This model needs an active motion BVH for motion effects, with AUTO or ENABLE. Create the renderer with ENABLE, as the example does. The Python snippet also creates the stage the scene is loaded into and attaches it to the renderer:

main.py, from the runnable Python radar example#
# Create the renderer. Radar defaults are provided by the ovrtx runtime.
# Enable motion BVH: required for moving-object radial velocity in the radar pipeline.
log_file_path = None
if args.log is not None:
    args.log.parent.mkdir(parents=True, exist_ok=True)
    log_file_path = str(args.log)
renderer = ovrtx.Renderer(ovrtx.RendererConfig(
    log_file_path=log_file_path,
    motion_bvh=ovrtx.MotionBvh.ENABLE))
stage = ovstage.Stage("ovrtx.example.radar")
renderer.attach_ovstage(stage)
main.cpp, from the runnable C radar example#
// The STATIC ovrtx loader resolves the ${executable_dir} token to the running
// executable's directory; ovrtx_setup_runtime() links the package bin beside
// the exe as "ovrtx/", which we hand the loader as the binary package root.
ovx_string_t ovrtx_package_root = {
    OVX_CONFIG_EXECUTABLE_DIR_TOKEN "/ovrtx",
    sizeof(OVX_CONFIG_EXECUTABLE_DIR_TOKEN "/ovrtx") - 1};
// Enable motion BVH: required for moving-object radial velocity in the radar
// pipeline.
ovrtx_config_entry_t config_entries[] = {
    ovrtx_config_entry_binary_package_root_path(ovrtx_package_root),
    ovrtx_config_entry_motion_bvh(OVRTX_MOTION_BVH_ENABLE),
};
ovrtx_config_t config = { config_entries, 2 };

// Create the renderer before attaching a stage or populating the scene.
result = ovrtx_create_renderer(&config, &renderer);
if (!check_api_result(result, "create_renderer")) {
    return 1;
}

Motion BVH describes the switch in full: the modes, the C call, its lifetime and GPU support. Motion BVH states the requirement for this model, and the motion BVH on Simulate Moving Targets says what the radar does without it.

Load the scene and warm up#

Open the USDA named by --scene into the attached stage and wait until that write has landed, so the sensor and its product exist before the first step:

# The USDA scene defines the radar, materials, render product, and animated cube.
print(f"Loading radar scene from {args.scene}...")
ordinal = 1
ovstage.population.open_usd(stage, str(args.scene), ordinal=ordinal)
stage.advance_write_floor(ordinal, ovstage.Scope.ALL).wait()
// Initialize the STATIC ovstage loader with its package root, mirroring the
// ovrtx setup above. ovstage_setup_runtime() links the package bin beside the
// exe as "ovstage/" (next to "ovrtx/"). ovrtx_create_renderer() already loaded
// the shared usd_ms runtime; ovstage.dll loads lazily on the first ovstage
// call and reuses it.
ovx_string_t ovstage_package_root = {
    OVX_CONFIG_EXECUTABLE_DIR_TOKEN "/ovstage",
    sizeof(OVX_CONFIG_EXECUTABLE_DIR_TOKEN "/ovstage") - 1};
ovstage_config_entry_t stage_config_entries[] = {
    ovstage_config_entry_binary_package_root_path(ovstage_package_root),
};
ovstage_config_t stage_config {};
stage_config.entries = stage_config_entries;
stage_config.entry_count =
    sizeof(stage_config_entries) / sizeof(stage_config_entries[0]);
ovstage_api_status_t stage_init_status = ovstage_initialize(&stage_config);
if (stage_init_status != OVSTAGE_OK) {
    print_ovstage_error(nullptr, stage_init_status, "initialize");
    return cleanup(1);
}

// Create the ovstage instance and attach it to the renderer.
ovstage_instance_desc_t stage_desc {};
stage_desc.name = "radar";
ovstage_api_status_t stage_result = ovstage_create_instance(&stage_desc, &stage);
if (stage_result != OVSTAGE_OK) {
    print_ovstage_error(stage, stage_result, "create_instance");
    return cleanup(1);
}

result = ovrtx_attach_ovstage(renderer, stage);
if (!check_api_result(result, "attach_ovstage")) {
    return cleanup(1);
}
stage_attached = true;

// Populate the stage from the local USD file at time 0 and our starting
// ordinal, then commit the write floor so the attached renderer can see it.
std::cout << "Loading radar scene from " << sceneFilePath << "...\n";
ovstage_population_enqueue_result_t populate_result =
    ovstage_population_open_usd_from_file(stage,
                                          to_ovx_string(sceneFilePath),
                                          stage_ordinal,
                                          /* time = */ 0.0,
                                          OVSTAGE_POPULATION_DOMAIN_RENDERING);
if (wait_population_op(stage, populate_result, "population_open_usd_from_file") ||
    commit_ovstage_ordinal(stage, stage_ordinal)) {
    return cleanup(1);
}

Warm the renderer up before you read a frame. The example places the scene’s animation at time zero and takes three steps there before the animated loop begins; each step renders the radar product and advances the simulation by 0.1 s, one scan period at the authored 10 Hz, so that every step fires one scan:

# Warm up the sensor before reading the animated frames. Each USD-time
# update is a stage mutation, published under its own ordinal.
ordinal += 1
ovstage.population.update_from_usd_time(stage, ordinal, 0.0)
stage.advance_write_floor(ordinal, ovstage.Scope.ALL).wait()
for _ in range(WARMUP_STEPS):
    renderer.step(render_products={RENDER_PRODUCT}, delta_time=STEP_DT_SECONDS, ordinal=ordinal)
// The initial population already sampled time 0, so render a few warmup
// frames from that committed ordinal without reading outputs. In attach mode
// we drive the renderer with ovrtx_step_with_stage().
for (int i = 0; i < kWarmupStepCount; ++i) {
    ovrtx_step_result_handle_t warmup_handle{};

    // Warmup frames let the sensor pipeline settle before reading the
    // first PointCloud.
    enqueue_result =
        ovrtx_step_with_stage(renderer,
                              render_products,
                              kStepDeltaTimeSeconds,
                              stage_ordinal,
                              &warmup_handle);
    if (!check_api_result(enqueue_result, "step") ||
        !wait_for_success(renderer, enqueue_result.op_index, "step")) {
        destroy_step_results(renderer, &warmup_handle);
        return cleanup(1);
    }
    if (!destroy_step_results(renderer, &warmup_handle)) {
        return cleanup(1);
    }
}

Note

Warming up the renderer counts the sensor pipeline among the reasons to warm the renderer up, beside texture streaming and path-tracing convergence. Why the step length matters is on the radar frame rate and, for every sensor, on Sensor Scheduling.

Step the scene and read the detections#

The Python and C examples step by 0.1 s and print the valid-entry count and radial-velocity range for each frame.

To read detections, map PointCloud to CPU memory, limit reads to Counts[0] and filter by the VALID bit. Convert the component-first Coordinates layout to one row per detection if needed. See the paired reading examples and sensor-output mapping, synchronization and lifetime rules.

Verification#

Run the example with the panel-field scene and expect one printed line per step. In the recorded run shown below, frames contained 35–37 valid points. The minimum radial velocity was near −9.9 m/s for the approaching cube; the maximum was near zero for the stationary panels. Its first line was step 1: valid points=35, radial velocity min/max=[-9.921682357788086, 0.0003972465347032994] m/s. These are example observations, not required counts or exact results for other runs.

Expect negative radial velocity from the approaching cube. Its motion is the same in both example scenes; see Doppler measurements for the derivation. The expected velocity is within the ±16.33 m/s interval configured by the scan’s one-entry velocity sequence.

The scene over the ten steps, three views of the same frames. Left, the camera view through PreviewCamera, the radar marked white: the moving cube crossing the field of panels. Center, a top view of each step’s detections with the radar at the bottom edge looking up the image and 10 m range rings, colored by RadialVelocityMs on a ±10 m/s scale, blue closing, green still, red receding: the cube’s detection comes out blue, the panels’ green. Right, the same detections colored by RCS on a fixed 15 to 55 dBsm scale. The three animations are frame-synchronous and carry the step number and time in their corner.

The example’s Rerun viewer also colors detections by signed radial velocity. Use the enabled/disabled comparison on this scene to check that motion BVH affects target velocity.

A scan that detects no targets is still published with Counts[0] = 0 and scan metadata; the example prints valid points=0. A step without a radar firing returns no radar PointCloud frame. See empty scans.

Potential issues#

  • The viewer shows two targets instead of the panel field: check which scene was loaded. The example prints the scene it opens (Loading radar scene from ...); pass --scene radar_example_field.usda from the example’s directory, or the file’s full path from anywhere else.

  • Some steps return no radar frame: the steps may be shorter than the radar’s scan period, so only a step that contains one of the radar’s acquisition times fires a scan. Step the product at the scan period, as the example does (Step at the scan period).

  • Detections arrive, but the moving cube’s radial velocity stays near zero, so the printed minimum sits near zero instead of near -10 m/s: the renderer was created without the motion BVH, or on a GPU that cannot trace against motion. The model needs the motion BVH says what the radar does without it, and Motion BVH how to set it and which GPUs support it.

  • The cloud looks plausible but lies in the wrong place, or carries angles where you expected meters: the coordinate system or the frame of reference is not the one you assumed. Read the coordsType and frameOfReference params of the frame first (Reading the output) and check the three attributes of the output group.

  • Published frames print valid points=0: the acquisitions produced no valid detections. Check the target’s position, range, materials and detection thresholds per Validate a Parameterization. An empty scan has a delivered count of zero; tensor capacity does not imply that detections are present.

  • The labels seem to make no difference: an attribute of the wrong type or a misspelled token keeps a label from being read. Non-Visual Sensor Materials describes label authoring; Symptom Index lists the checks to make, in order.