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([])
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.
Author
omni:sensor:LidarWpm:tiledSubSampling = non tile zero, with n above 1; leave it at0, the default, or at1when every tile should trace at full fidelity.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.
The
tileCountparam equals the number of sensors inrel camera. A single sensor is an aggregate of one tile, so an untiled product reports1.In compact output, the default,
TilePointCountsholds one entry per tile, inrel cameraorder, and the entries sum toCounts[0]. In the fragment above, a scan in which each tile sees one surface givesTilePointCountsof[1, 1],TilePointOffsetsof[0, 1, 2]and aCounts[0]of2.The
receiversPerTileparam is the number of emitter rows the schedule keeps for one sensor after subsampling, the same for every tile:1in the fragment, two rows thinned by a factor of 2.The
tileFrameStartPosMparam holds one position per tile, in meters, inrel cameraorder. 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 andtileFrameEndPosMits pose at the end.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, setomni:sensor:LidarWpm:tiledSubSamplingabove1on 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
1is set on tile zero and reduces the emitter rows traced by every tile. Setomni:sensor:LidarWpm:tiledSubSamplingto0or1on 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:tiledSubSamplingand that it is authored on tile zero. USD attribute names are case-sensitive, including the capitalSinSubSampling. 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
tiledSubSamplingon 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.