Clash Detection Point Cloud API#

omni.physx.clashdetection.pointcloud is a lightweight extension that provides point cloud extraction utilities for the clash detection pipeline.

It depends on the omni.pointcloud.streaming.potree extension to stream point cloud data from Potree-format sources, then writes the result as UsdGeom.Points prims in the active stage. The extracted prims are consumed by ClashDetection when SETTING_ENABLE_POINT_CLOUDS is enabled.

Note

This extension is included in omni.physx.clashdetection.bundle. Enable the bundle to make this extension available.

Module#

The public surface of omni.physxclashdetectionpointcloud exports:

from omni.physxclashdetectionpointcloud import extract_point_cloud, ExtensionConfig

Functions#

async omni.physxclashdetectionpointcloud.extract_point_cloud(
stage: Usd.Stage,
point_cloud_prim_path: str,
destination_path: str,
aabbs: List[Gf.Range3d],
max_lod_level: int = 1000000,
extract_layer_name: str = '',
extract_to_session_layer: bool = True,
progress_fn: Callable[[float], bool] | None = None,
) → Tuple[bool, str]#

Extracts a point cloud to UsdGeom.Points prims in the given stage.

The function streams point cloud geometry from a Potree source prim (referenced by point_cloud_prim_path), applies an optional axis-aligned bounding box (AABB) filter, and writes the resulting points as UsdGeom.Points prims under destination_path.

Parameters:
  • stage (Usd.Stage) – The USD stage containing the geometry to process.

  • point_cloud_prim_path (str) – USD prim path of the point cloud source.

  • destination_path (str) – Path where extraction results are written as UsdGeom.Points.

  • aabbs (List[Gf.Range3d]) – AABBs of inclusion regions of interest. Only the first entry is currently used. Pass an empty list (or a list with a single maximum-extent AABB) to include all available points; if the list is empty, a bounding box of ±1e38 is used internally.

  • max_lod_level (int) – Maximum level-of-detail level used during extraction. Higher values include finer detail at the cost of more memory and time. Defaults to 1 000 000.

  • extract_layer_name (str) – Display name of the sublayer to use for writing the extracted prims. When empty the current edit target is used without modification.

  • extract_to_session_layer (bool) – Relevant only when extract_layer_name is non-empty. If True, the named sublayer is found or created under the session layer. If False, it is found or created under the root layer.

  • progress_fn (Optional[Callable[[float], bool]]) – Optional callback that receives the fraction of nodes fetched so far (0.0 to 1.0) each time the extraction yields a frame to Kit. Return False from it to cancel the extraction before anything is written to the stage.

Returns:

A (success, error_message) tuple. error_message is an empty string on success. Returns (True, "") also when the AABB filter matches no points (this is not treated as an error). Returns (False, error_message) on streaming or write failures, and when progress_fn cancels the extraction.

Return type:

Tuple[bool, str]

Note

Node geometry is fetched in batches, and the coroutine yields a frame to Omniverse Kit between batches, so the application keeps rendering and handling input during a long fetch. The node query that precedes the fetch and the final authoring of the UsdGeom.Points prims still each run as a single blocking call on the main thread.

Note

The coordinate space of the extracted points depends on the position of destination_path relative to the parent xform of point_cloud_prim_path. When destination_path is a child of that xform, points are kept in local space (the xform itself provides the world transform). Otherwise they are transformed to world space automatically.

Example#

Extracting a point cloud and running clash detection against meshes:

import asyncio
from pxr import Usd, UsdUtils, Gf
from omni.physxclashdetectionpointcloud import extract_point_cloud
from omni.physxclashdetectioncore.clash_detect import ClashDetection
from omni.physxclashdetectioncore.clash_detect_settings import SettingId
from omni.physxclashdetectioncore.clash_query import ClashQuery
from omni.physxclashdetectioncore.clash_data import ClashData
from omni.physxclashdetectioncore.clash_data_serializer_sqlite import ClashDataSerializerSqlite
from omni.physxclashdetectioncore.point_cloud_settings import PointCloudSettings


async def run_point_cloud_clash_detection(stage: Usd.Stage):
    stage_id = UsdUtils.StageCache.Get().GetId(stage).ToLongInt()

    # 1. Configure extraction settings
    pc_settings = PointCloudSettings(
        point_cloud_prim_path="/World/ScanData",
        destination_path="/World/ScanData/Extracted",
        save_to_session_layer=True,
        skip_rendering=True,
        max_lod_level=10,
    )

    # 2. Extract the point cloud into the stage
    roi_aabb = Gf.Range3d(Gf.Vec3d(-100, -100, -100), Gf.Vec3d(100, 100, 100))
    success, error = await extract_point_cloud(
        stage=stage,
        point_cloud_prim_path=pc_settings.point_cloud_prim_path,
        destination_path=pc_settings.destination_path,
        aabbs=[roi_aabb],
        max_lod_level=pc_settings.max_lod_level,
        extract_layer_name="ClashPointCloud",
        extract_to_session_layer=pc_settings.save_to_session_layer,
    )
    if not success:
        print(f"Extraction failed: {error}")
        return

    # 3. Set up ClashData and persist the point cloud settings
    clash_data = ClashData(ClashDataSerializerSqlite())
    clash_data.open(stage_id)
    clash_data.insert_point_cloud_settings(pc_settings)

    # 4. Create a point cloud clash query
    query = ClashQuery(
        query_name="Point Cloud and Building Clash",
        object_a_path=pc_settings.point_cloud_prim_path,  # point cloud source
        object_b_path="/World/Building",                   # mesh geometry
        clash_detect_settings={
            SettingId.SETTING_ENABLE_POINT_CLOUDS.name: True,
            SettingId.SETTING_POINT_SIZE_OVERRIDE.name: 0.0,  # auto-compute size
            SettingId.SETTING_LOGGING.name: True,
        },
    )
    clash_data.insert_query(query)

    # 5. Run clash detection
    clash_detect = ClashDetection()
    clash_detect.set_settings(query.clash_detect_settings, stage)
    clash_detect.set_scope(stage, query.object_a_path, query.object_b_path)
    num_steps = clash_detect.create_pipeline()
    for i in range(num_steps):
        clash_detect.run_pipeline_step(i)

    num_overlaps = clash_detect.get_nb_overlaps()
    print(f"Found {num_overlaps} point cloud clashes")

    # Fetch and save results
    for _ in clash_detect.fetch_and_save_overlaps(stage, clash_data, query):
        pass

    clash_data.save()
    clash_data.close()
    clash_data.destroy()


# Run with: asyncio.ensure_future(run_point_cloud_clash_detection(stage))

Dynamic Point Cloud Queries#

A point cloud query can also be run as a dynamic query, sweeping a time range instead of testing a single time. Add SETTING_DYNAMIC and the time range to the settings dictionary of step 4 above:

query = ClashQuery(
    query_name="Point Cloud and Moving Machinery",
    object_a_path=pc_settings.point_cloud_prim_path,  # point cloud source
    object_b_path="/World/Machinery",                 # animated mesh geometry
    clash_detect_settings={
        SettingId.SETTING_ENABLE_POINT_CLOUDS.name: True,
        SettingId.SETTING_POINT_SIZE_OVERRIDE.name: 0.0,  # auto-compute size
        SettingId.SETTING_DYNAMIC.name: True,
        SettingId.SETTING_DYNAMIC_START_TIME.name: 0.0,   # 0 = timeline start
        SettingId.SETTING_DYNAMIC_END_TIME.name: 0.0,     # 0 = timeline end
    },
)

Each resulting clash then carries one record per clashing frame. The clashing points of a given frame are read from the per-frame ClashFrameInfo objects:

overlaps = clash_data.find_all_overlaps_by_query_id(query.identifier, fetch_also_frame_info=True)
for clash_info in overlaps.values():
    if not clash_info.is_point_cloud_clash:
        continue
    print(f"{clash_info.object_a_path}: {clash_info.num_records} clashing frame(s)")
    for frame_info in clash_info.clash_frame_info_items:
        # usd_points_0 holds the indices of the points of the extracted UsdGeom.Points prim
        # that clash at this timecode; usd_points_0_sizes holds their widths.
        print(f"  t={frame_info.timecode}: {frame_info.overlap_points} point(s)")

Note

Only the transform of the point cloud is animated. extract_point_cloud() takes no time argument, so the extracted UsdGeom.Points prim is a single snapshot that is reused for every sampled frame. A dynamic point cloud query therefore detects clashes caused by the animation of the mesh geometry and by the animation of the ancestor xforms of the point cloud, not by the point positions themselves changing over time.

Note

The baked clash artifacts of a dynamic point cloud clash are authored in point-cloud-local space and animated with the per-frame transform of the cloud. The clashing-points box is therefore axis aligned in that local space, so for a rotated cloud it is an oriented box in world space rather than the world axis-aligned box a static point cloud clash produces. Points and widths are sampled on clashing frames only, so the highlight of the last clashing frame is held on any non-clashing frame inside the clash time range.

Note

Max local depth, penetration depth and motion vector estimation are not computed for point-cloud-to-mesh pairs, nor are clashing faces and collision outlines. SETTING_TOLERANCE is ignored as well: the point-to-mesh test has no clearance margin, so a point cloud query always reports hard clashes only and never soft ones. The static permanent-overlap purge (SETTING_PURGE_PERMANENT_STATIC_OVERLAPS) covers mesh-to-mesh pairs only, so a static point cloud that permanently intersects static mesh geometry is reported once per sampled frame of the query range. The dynamic purge (SETTING_PURGE_PERMANENT_OVERLAPS) does apply to point-cloud-to-mesh pairs: a pair that overlaps on every sampled frame of the range is discarded just like a mesh pair, so the Clash Query window keeps the checkbox available for point cloud queries.

Classes#

ExtensionConfig#

class omni.physxclashdetectionpointcloud.ExtensionConfig#

A configuration class that holds extension-wide parameters read from carb.settings at startup.

All attributes are class-level - do not instantiate this class.

SETTINGS_DEBUG_LOGGING_ENABLED = "/persistent/clashDetection/pointClouds/debugLogging"
Type:

str

carb.settings path for the debug-logging toggle. When the value at this path is True, the extractor logs verbose messages (source path, extents, LOD level) via carb.log_info.

debug_logging: bool = False

Whether debug logging is currently enabled. Updated at startup and whenever SETTINGS_DEBUG_LOGGING_ENABLED changes (via SettingChangeSubscription).

extension_path: Optional[str] = None

Absolute filesystem path to the installed extension directory. Set by ClashDetectionPointCloudExtension.on_startup; None before the extension starts.