Tile Lidars#

Tiling combines several Lidar generic wpm model sensors into one RenderProduct and one aggregate point cloud. It reduces overhead when the sensors can share a profile and firing schedule. The first sensor in rel camera supplies the profile; each sensor retains its own transform and motion.

The Lidar generic core model requires one RenderProduct per sensor. For camera tiling, see Output Configuration.

Before you start. Start with a working wpm lidar using Configure a Firing Pattern and USD Attributes. The first-sensor example uses a core lidar but demonstrates the shared scene components; see Output Configuration for product wiring. For separate point clouds, use one product per sensor instead of tiling.

RenderProduct configuration#

List the sensor prims in one RenderProduct’s camera relationship and request PointCloud. Each prim uses OmniSensorGenericLidarWpmAPI and its own transform. Each requested point cloud contains every tile in relationship order.

In compact output, tile t occupies [TilePointOffsets[t], TilePointOffsets[t + 1]) in each point channel. Dense output uses stride params instead. See Outputs for both layouts and the per-tile pose parameters.

The following fragment authors two tiles and one product. Tile zero carries the firing pattern and a subsampling factor of 2, which the next sections explain; tile one applies the same API schema and authors nothing but its placement, eight meters to the side of tile zero and one meter higher. The stage is authored in meters (metersPerUnit = 1); its scene geometry and materials are defined elsewhere.

#usda 1.0
(
    metersPerUnit = 1
    upAxis = "Z"
)

def Xform "Sensor"
{
    def OmniLidar "Lidar" (
        prepend apiSchemas = ["OmniSensorGenericLidarWpmAPI"]
    )
    {
        token visibility = "invisible"
        double2 omni:sensor:frameRate = (10, 1)
        token omni:sensor:LidarWpm:scanType = "solidState"
        uint omni:sensor:LidarWpm:maxReturns = 1
        uint omni:sensor:LidarWpm:tiledSubSampling = 2

        float[] omni:sensor:LidarWpm:firingPattern:p001:azimuthDeg = [0, 0]
        float[] omni:sensor:LidarWpm:firingPattern:p001:elevationDeg = [0, 0]
        uint[] omni:sensor:LidarWpm:firingPattern:p001:fireTimeNs = [0, 1000]
        uint[] omni:sensor:LidarWpm:firingPattern:p001:channelId = [1, 4]
        uint[] omni:sensor:LidarWpm:firingPattern:p001:rangeId = []

        double3 xformOp:translate = (0, -4, 1)
        float3 xformOp:rotateXYZ = (90, 0, -90)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"]
    }

    def OmniLidar "LidarTile1" (
        prepend apiSchemas = ["OmniSensorGenericLidarWpmAPI"]
    )
    {
        token visibility = "invisible"
        double3 xformOp:translate = (0, 4, 2)
        float3 xformOp:rotateXYZ = (90, 0, -90)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"]
    }

    def Scope "Outputs"
    {
        def RenderProduct "Product"
        {
            rel camera = [</Sensor/Lidar>, </Sensor/LidarTile1>]
            rel orderedVars = [</Sensor/Outputs/PointCloud>]
        }

        def RenderVar "PointCloud"
        {
            uniform string sourceName = "PointCloud"
            token[] channels = []
        }
    }
}
from pxr import Gf, Sdf

# This scene uses meters and a Z-up world.
stage.SetMetadata("metersPerUnit", 1.0)
stage.SetMetadata("upAxis", "Z")

sensor = stage.DefinePrim("/Sensor", "Xform")

lidar = stage.DefinePrim("/Sensor/Lidar", "OmniLidar")
lidar.AddAppliedSchema("OmniSensorGenericLidarWpmAPI")
lidar.CreateAttribute("visibility", Sdf.ValueTypeNames.Token, custom=False).Set("invisible")
lidar.CreateAttribute("omni:sensor:frameRate", Sdf.ValueTypeNames.Double2, custom=False).Set(
    Gf.Vec2d(10.0, 1.0)
)
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:scanType", Sdf.ValueTypeNames.Token, custom=False
).Set("solidState")
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:maxReturns", Sdf.ValueTypeNames.UInt, custom=False
).Set(1)
# Tile zero supplies the shared profile; keep every second firing row.
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:tiledSubSampling", Sdf.ValueTypeNames.UInt, custom=False
).Set(2)
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:firingPattern:p001:azimuthDeg",
    Sdf.ValueTypeNames.FloatArray,
    custom=False,
).Set([0.0, 0.0])
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:firingPattern:p001:elevationDeg",
    Sdf.ValueTypeNames.FloatArray,
    custom=False,
).Set([0.0, 0.0])
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:firingPattern:p001:fireTimeNs",
    Sdf.ValueTypeNames.UIntArray,
    custom=False,
).Set([0, 1000])
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:firingPattern:p001:channelId",
    Sdf.ValueTypeNames.UIntArray,
    custom=False,
).Set([1, 4])
lidar.CreateAttribute(
    "omni:sensor:LidarWpm:firingPattern:p001:rangeId",
    Sdf.ValueTypeNames.UIntArray,
    custom=False,
).Set([])
lidar.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Double3, custom=False).Set(
    Gf.Vec3d(0.0, -4.0, 1.0)
)
lidar.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Float3, custom=False).Set(
    Gf.Vec3f(90.0, 0.0, -90.0)
)
lidar.CreateAttribute(
    "xformOpOrder",
    Sdf.ValueTypeNames.TokenArray,
    custom=False,
    variability=Sdf.VariabilityUniform,
).Set(["xformOp:translate", "xformOp:rotateXYZ"])

# The other tile contributes only its placement and motion.
lidar_tile1 = stage.DefinePrim("/Sensor/LidarTile1", "OmniLidar")
lidar_tile1.AddAppliedSchema("OmniSensorGenericLidarWpmAPI")
lidar_tile1.CreateAttribute("visibility", Sdf.ValueTypeNames.Token, custom=False).Set(
    "invisible"
)
lidar_tile1.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Double3, custom=False).Set(
    Gf.Vec3d(0.0, 4.0, 2.0)
)
lidar_tile1.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Float3, custom=False).Set(
    Gf.Vec3f(90.0, 0.0, -90.0)
)
lidar_tile1.CreateAttribute(
    "xformOpOrder",
    Sdf.ValueTypeNames.TokenArray,
    custom=False,
    variability=Sdf.VariabilityUniform,
).Set(["xformOp:translate", "xformOp:rotateXYZ"])

outputs = stage.DefinePrim("/Sensor/Outputs", "Scope")

product = stage.DefinePrim("/Sensor/Outputs/Product", "RenderProduct")
product.CreateRelationship("camera", custom=False).SetTargets(
    ["/Sensor/Lidar", "/Sensor/LidarTile1"]
)
product.CreateRelationship("orderedVars", custom=False).SetTargets(
    ["/Sensor/Outputs/PointCloud"]
)

point_cloud = stage.DefinePrim("/Sensor/Outputs/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([])

Shared profile#

The first sensor in rel camera, tile zero, supplies the scan schedule, firing patterns, return processing, subsampling and output settings for every tile. Other tiles’ model attributes are ignored, so they can omit those attributes, including firing-pattern arrays. Apply runtime parameter edits to tile zero; see Retune the Sensor While It Runs. The aggregate uses one profile and schedule, with points grouped by tile.

Each tile contributes its own transform and motion. Output params report its start and end poses; both transforms must be finite. Motion effects require motion BVH, selected at renderer creation. See output motion behavior for the enabled and disabled cases.

Subsampling#

Several tiles cost several sensors’ worth of tracing. The attribute uint omni:sensor:LidarWpm:tiledSubSampling reduces the number of emitter rows traced by every tile without shortening the scan. Its semantics, the selection rule included, are described on Tiling and subsampling; this section is the how-to.

  1. Author omni:sensor:LidarWpm:tiledSubSampling = n on tile zero, with n above 1; leave it at 0, the default, or at 1 when every tile should trace at full fidelity.

  2. Choose a factor that leaves every firing pattern, or the whole rotary scan, at least one emitter row; a factor that would not is refused (Tiling and subsampling).

In the fragment above, the pattern has two emitter rows and the factor is 2, so each tile fires one row per scan instead of two.

Keep tile order stable#

The model knows the tiles only by their position in rel camera. That position decides which range of the point cloud a tile fills, which entry of TilePointCounts is its own and which row of the per-tile pose params describes it. Adding or removing a tile changes the aggregate’s dimensions, so it restarts the aggregate and discards a scan in progress. Keep the order fixed while a scan is in progress, and change it only between scans.

Verification#

Check the following in the step’s PointCloud output. The tensors and params named here are defined on the Outputs page; mapping an output and reading its params is on Sensor Outputs.

  1. The tileCount param equals the number of sensors in rel camera. A single sensor is an aggregate of one tile, so an untiled product reports 1.

  2. In compact output, the default, TilePointCounts holds one entry per tile, in rel camera order, and the entries sum to Counts[0]. In the fragment above, a scan in which each tile sees one surface gives TilePointCounts of [1, 1], TilePointOffsets of [0, 1, 2] and a Counts[0] of 2.

  3. The receiversPerTile param is the number of emitter rows the schedule keeps for one sensor after subsampling, the same for every tile: 1 in the fragment, two rows thinned by a factor of 2.

  4. The tileFrameStartPosM param holds one position per tile, in meters, in rel camera order. On a static scene it reports each tile’s authored position, (0, -4, 1) and (0, 4, 2) for the fragment, whose stage is authored in meters; when the tiles move, it reports each tile’s pose at the start of the captured frame and tileFrameEndPosM its pose at the end.

  5. The points in a tile’s range come from where that tile stands: each tile reports its own surroundings, at its own range and height.

Potential issues#

  • Subsampling or another setting has no effect. Check that the attribute is authored on the first sensor in rel camera (tile zero). Only that sensor’s model attributes apply to the tiled product. To enable subsampling, set omni:sensor:LidarWpm:tiledSubSampling above 1 on tile zero; see tile configuration.

  • The output stops after a tile was added. The new tile’s transform is not finite; the reason is logged as an error. Give the prim a finite transform, at both ends of the captured frame if it moves.

  • Two tiles return identical ranges. They were left at the same transform. Each tile is its own prim and needs its own placement. Refer to Incorrect coordinates or units.

  • Fewer points than expected on every tile. A subsampling factor above 1 is set on tile zero and reduces the emitter rows traced by every tile. Set omni:sensor:LidarWpm:tiledSubSampling to 0 or 1 on tile zero to disable subsampling. Refer to Fewer points or pixels than expected.

  • Changing tiledSubSampling at runtime has no effect. A value that leaves a firing pattern, or the whole rotary scan, with no emitter rows is rejected. The last valid profile remains active. Reduce tiledSubSampling; see Retune the Sensor While It Runs.

  • Setting tiledSubSampling has no effect. Check that the full attribute name is omni:sensor:LidarWpm:tiledSubSampling and that it is authored on tile zero. USD attribute names are case-sensitive, including the capital S in SubSampling. See Runtime edits have no effect.

  • Runtime and memory grow with every tile. Each tile adds its transmitter and receiver workload and its share of the point cloud. Increasing tiledSubSampling on tile zero reduces the number of emitter rows traced and their storage. Refer to Performance problems.

The Symptom Index collects the configuration symptoms of every model.