Skip to content

Commit 86d73c5

Browse files
ooctipuscursoragent
andcommitted
Add flat mesh slots for multi-mesh raycasts
Use compact per-environment mesh slots so ClonePlan-backed heterogeneous layouts avoid dummy rectangular padding while preserving closest-hit behavior across raycaster variants. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e20b82f commit 86d73c5

6 files changed

Lines changed: 162 additions & 140 deletions

File tree

source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster.py

Lines changed: 73 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,17 @@ def _initialize_warp_meshes(self):
142142
self._initialize_warp_meshes_from_clone_plan(plan)
143143

144144
def _initialize_warp_meshes_from_clone_plan(self, plan) -> None:
145-
"""Initialize rectangular mesh buffers from ClonePlan source rows.
146-
147-
The current PR keeps the existing rectangular kernel ABI. Environments
148-
with fewer meshes than the target maximum are padded with a valid mesh
149-
placed far outside the ray-cast range. A follow-up PR can replace this
150-
padded representation with a new kernel.
151-
"""
152-
target_records_by_expr: dict[
153-
str, list[list[tuple[int, tuple[float, float, float], tuple[float, float, float, float]]]]
154-
] = {}
155-
dummy_mesh_id: int | None = None
145+
"""Initialize flat mesh slots from ClonePlan source rows."""
146+
slot_env_ids: list[int] = []
147+
slot_mesh_ids: list[int] = []
148+
slot_positions: list[tuple[float, float, float]] = []
149+
slot_orientations: list[tuple[float, float, float, float]] = []
156150
self._mesh_views = []
151+
self._slot_ranges_by_target_expr: dict[str, tuple[int, int]] = {}
157152

158153
for target_cfg in self._raycast_targets_cfg:
159-
records_per_env: list[list[tuple[int, tuple[float, float, float], tuple[float, float, float, float]]]] = [
160-
[] for _ in range(self._num_envs)
161-
]
154+
target_start = len(slot_mesh_ids)
155+
meshes_added_per_env = [0 for _ in range(self._num_envs)]
162156
matches = self._collect_clone_plan_matches(plan, target_cfg.prim_expr)
163157
if matches:
164158
for row, source_root, source_expr in matches:
@@ -171,7 +165,6 @@ def _initialize_warp_meshes_from_clone_plan(self, plan) -> None:
171165
prototype_records = []
172166
for target_prim in target_prims:
173167
mesh_id = self._load_target_prim_warp_mesh(target_prim, target_cfg)
174-
dummy_mesh_id = mesh_id if dummy_mesh_id is None else dummy_mesh_id
175168
source_root_prim = self.stage.GetPrimAtPath(source_root)
176169
local_pos, local_quat = sim_utils.resolve_prim_pose(target_prim, source_root_prim)
177170
prototype_records.append((mesh_id, local_pos, local_quat))
@@ -187,33 +180,35 @@ def _initialize_warp_meshes_from_clone_plan(self, plan) -> None:
187180
mesh_pos_t, mesh_quat_t = math_utils.combine_frame_transforms(
188181
root_pos_t, root_quat_t, local_pos_t, local_quat_t
189182
)
190-
records_per_env[env_id].append(
191-
(
192-
mesh_id,
193-
tuple(float(v) for v in mesh_pos_t[0].tolist()),
194-
tuple(float(v) for v in mesh_quat_t[0].tolist()),
195-
)
196-
)
183+
slot_env_ids.append(env_id)
184+
slot_mesh_ids.append(mesh_id)
185+
slot_positions.append(tuple(float(v) for v in mesh_pos_t[0].tolist()))
186+
slot_orientations.append(tuple(float(v) for v in mesh_quat_t[0].tolist()))
187+
meshes_added_per_env[env_id] += 1
197188
else:
198189
target_prims = sim_utils.find_matching_prims(target_cfg.prim_expr)
199190
if len(target_prims) == 0:
200191
raise RuntimeError(f"Failed to find a prim at path expression: {target_cfg.prim_expr}")
201192
records = []
202193
for target_prim in target_prims:
203194
mesh_id = self._load_target_prim_warp_mesh(target_prim, target_cfg)
204-
dummy_mesh_id = mesh_id if dummy_mesh_id is None else dummy_mesh_id
205195
pos, quat = sim_utils.resolve_prim_pose(target_prim)
206196
records.append((mesh_id, tuple(float(v) for v in pos), tuple(float(v) for v in quat)))
207197
for env_id in range(self._num_envs):
208-
records_per_env[env_id].extend(records)
209-
210-
self._num_meshes_per_env[target_cfg.prim_expr] = max(len(records) for records in records_per_env)
211-
target_records_by_expr[target_cfg.prim_expr] = records_per_env
198+
for mesh_id, pos, quat in records:
199+
slot_env_ids.append(env_id)
200+
slot_mesh_ids.append(mesh_id)
201+
slot_positions.append(pos)
202+
slot_orientations.append(quat)
203+
meshes_added_per_env[env_id] += 1
204+
205+
self._num_meshes_per_env[target_cfg.prim_expr] = max(meshes_added_per_env)
206+
self._slot_ranges_by_target_expr[target_cfg.prim_expr] = (target_start, len(slot_mesh_ids))
212207
self._mesh_views.append(
213208
self._create_tracked_target_view(target_cfg.prim_expr) if target_cfg.track_mesh_transforms else None
214209
)
215210

216-
self._install_rectangular_mesh_table(target_records_by_expr, dummy_mesh_id)
211+
self._install_flat_mesh_slots(slot_env_ids, slot_mesh_ids, slot_positions, slot_orientations)
217212

218213
def _collect_clone_plan_matches(self, plan, target_expr: str) -> list[tuple[int, str, str]]:
219214
target_env0 = _target_expr_for_env(target_expr, 0)
@@ -339,22 +334,23 @@ def _create_tracked_target_view(self, target_prim_path: str):
339334
raise NotImplementedError("Tracked multi-mesh targets must be implemented by the active physics backend.")
340335

341336
def _initialize_warp_meshes_from_stage(self):
342-
"""Parse mesh prim expressions from USD and install the rectangular mesh table."""
343-
target_records_by_expr: dict[
344-
str, list[list[tuple[int, tuple[float, float, float], tuple[float, float, float, float]]]]
345-
] = {}
346-
dummy_mesh_id: int | None = None
337+
"""Parse mesh prim expressions from USD and install the flat slot table."""
338+
slot_env_ids: list[int] = []
339+
slot_mesh_ids: list[int] = []
340+
slot_positions: list[tuple[float, float, float]] = []
341+
slot_orientations: list[tuple[float, float, float, float]] = []
347342
self._mesh_views = []
343+
self._slot_ranges_by_target_expr: dict[str, tuple[int, int]] = {}
348344

349345
for target_cfg in self._raycast_targets_cfg:
346+
target_start = len(slot_mesh_ids)
350347
target_prims = sim_utils.find_matching_prims(target_cfg.prim_expr)
351348
if len(target_prims) == 0:
352349
raise RuntimeError(f"Failed to find a prim at path expression: {target_cfg.prim_expr}")
353350

354351
records = []
355352
for target_prim in target_prims:
356353
mesh_id = self._load_target_prim_warp_mesh(target_prim, target_cfg)
357-
dummy_mesh_id = mesh_id if dummy_mesh_id is None else dummy_mesh_id
358354
pos, quat = sim_utils.resolve_prim_pose(target_prim)
359355
records.append((mesh_id, tuple(float(v) for v in pos), tuple(float(v) for v in quat)))
360356

@@ -369,57 +365,39 @@ def _initialize_warp_meshes_from_stage(self):
369365
n_meshes = len(records) // self._num_envs
370366
per_env_records = [records[i * n_meshes : (i + 1) * n_meshes] for i in range(self._num_envs)]
371367

368+
for env_id, env_records in enumerate(per_env_records):
369+
for mesh_id, pos, quat in env_records:
370+
slot_env_ids.append(env_id)
371+
slot_mesh_ids.append(mesh_id)
372+
slot_positions.append(pos)
373+
slot_orientations.append(quat)
374+
372375
self._num_meshes_per_env[target_cfg.prim_expr] = max(len(env_records) for env_records in per_env_records)
373-
target_records_by_expr[target_cfg.prim_expr] = per_env_records
376+
self._slot_ranges_by_target_expr[target_cfg.prim_expr] = (target_start, len(slot_mesh_ids))
374377
self._mesh_views.append(
375378
self._create_tracked_target_view(target_cfg.prim_expr) if target_cfg.track_mesh_transforms else None
376379
)
377380

378-
self._install_rectangular_mesh_table(target_records_by_expr, dummy_mesh_id)
381+
self._install_flat_mesh_slots(slot_env_ids, slot_mesh_ids, slot_positions, slot_orientations)
379382

380-
def _install_rectangular_mesh_table(
383+
def _install_flat_mesh_slots(
381384
self,
382-
target_records_by_expr: dict[
383-
str, list[list[tuple[int, tuple[float, float, float], tuple[float, float, float, float]]]]
384-
],
385-
dummy_mesh_id: int | None,
385+
slot_env_ids: list[int],
386+
slot_mesh_ids: list[int],
387+
slot_positions: list[tuple[float, float, float]],
388+
slot_orientations: list[tuple[float, float, float, float]],
386389
) -> None:
387-
"""Pack per-target mesh records into the rectangular table used by the existing kernel."""
388-
if dummy_mesh_id is None:
390+
"""Install the compact per-environment slot arrays used by the flat kernel."""
391+
if not slot_mesh_ids:
389392
raise RuntimeError(
390393
f"No meshes found for ray-casting! Please check the mesh prim paths: {self.cfg.mesh_prim_paths}"
391394
)
392-
393-
dummy_record = (dummy_mesh_id, (1.0e9, 1.0e9, 1.0e9), (0.0, 0.0, 0.0, 1.0))
394-
multi_mesh_ids_flattened: list[list[int]] = []
395-
mesh_positions: list[list[tuple[float, float, float]]] = []
396-
mesh_orientations: list[list[tuple[float, float, float, float]]] = []
397-
398-
for env_id in range(self._num_envs):
399-
meshes_in_env: list[int] = []
400-
positions_in_env: list[tuple[float, float, float]] = []
401-
orientations_in_env: list[tuple[float, float, float, float]] = []
402-
for target_cfg in self._raycast_targets_cfg:
403-
records = list(target_records_by_expr[target_cfg.prim_expr][env_id])
404-
records.extend([dummy_record] * (self._num_meshes_per_env[target_cfg.prim_expr] - len(records)))
405-
for mesh_id, pos, quat in records:
406-
meshes_in_env.append(mesh_id)
407-
positions_in_env.append(pos)
408-
orientations_in_env.append(quat)
409-
multi_mesh_ids_flattened.append(meshes_in_env)
410-
mesh_positions.append(positions_in_env)
411-
mesh_orientations.append(orientations_in_env)
412-
413-
total_n_meshes_per_env = len(multi_mesh_ids_flattened[0])
414-
self._mesh_ids_wp = wp.array2d(multi_mesh_ids_flattened, dtype=wp.uint64, device=self.device)
415-
self._mesh_positions_w = wp.zeros((self._num_envs, total_n_meshes_per_env), dtype=wp.vec3, device=self.device)
416-
self._mesh_orientations_w = wp.zeros(
417-
(self._num_envs, total_n_meshes_per_env), dtype=wp.quat, device=self.device
418-
)
419-
self._mesh_positions_w_torch = wp.to_torch(self._mesh_positions_w)
420-
self._mesh_orientations_w_torch = wp.to_torch(self._mesh_orientations_w)
421-
self._mesh_positions_w_torch[:] = torch.tensor(mesh_positions, dtype=torch.float32, device=self.device)
422-
self._mesh_orientations_w_torch[:] = torch.tensor(mesh_orientations, dtype=torch.float32, device=self.device)
395+
self._slot_env_ids_wp = wp.array(slot_env_ids, dtype=wp.int32, device=self.device)
396+
self._slot_mesh_ids_wp = wp.array(slot_mesh_ids, dtype=wp.uint64, device=self.device)
397+
self._slot_mesh_positions_w = wp.array(slot_positions, dtype=wp.vec3, device=self.device)
398+
self._slot_mesh_orientations_w = wp.array(slot_orientations, dtype=wp.quat, device=self.device)
399+
self._slot_mesh_positions_w_torch = wp.to_torch(self._slot_mesh_positions_w)
400+
self._slot_mesh_orientations_w_torch = wp.to_torch(self._slot_mesh_orientations_w)
423401

424402
def _initialize_rays_impl(self):
425403
super()._initialize_rays_impl()
@@ -440,13 +418,11 @@ def _update_mesh_transforms(self) -> None:
440418
"""Update world-frame mesh positions and orientations for dynamically tracked targets.
441419
442420
Iterates over all tracked views and writes the current world poses into
443-
the rectangular mesh pose buffers. Static (non-tracked) targets are
444-
skipped; their initial poses were set during :meth:`_initialize_warp_meshes`.
421+
the flat slot pose buffers. Static (non-tracked) targets are skipped;
422+
their initial poses were set during :meth:`_initialize_warp_meshes`.
445423
"""
446-
mesh_idx = 0
447424
for view, target_cfg in zip(self._mesh_views, self._raycast_targets_cfg):
448425
if not target_cfg.track_mesh_transforms:
449-
mesh_idx += self._num_meshes_per_env[target_cfg.prim_expr]
450426
continue
451427

452428
# update position of the target meshes
@@ -455,15 +431,18 @@ def _update_mesh_transforms(self) -> None:
455431
pos_w = pos_w.squeeze(0) if len(pos_w.shape) == 3 else pos_w
456432
ori_w = ori_w.squeeze(0) if len(ori_w.shape) == 3 else ori_w
457433

458-
count = getattr(view, "count", pos_w.shape[0])
459-
if count != 1:
460-
count = count // self._num_envs
461-
pos_w = pos_w.view(self._num_envs, count, 3)
462-
ori_w = ori_w.view(self._num_envs, count, 4)
463-
464-
self._mesh_positions_w_torch[:, mesh_idx : mesh_idx + count] = pos_w
465-
self._mesh_orientations_w_torch[:, mesh_idx : mesh_idx + count] = ori_w
466-
mesh_idx += self._num_meshes_per_env[target_cfg.prim_expr]
434+
slot_start, slot_end = self._slot_ranges_by_target_expr[target_cfg.prim_expr]
435+
slot_count = slot_end - slot_start
436+
if pos_w.shape[0] == 1 and slot_count > 1:
437+
pos_w = pos_w.repeat(slot_count, 1)
438+
ori_w = ori_w.repeat(slot_count, 1)
439+
if pos_w.shape[0] != slot_count:
440+
raise RuntimeError(
441+
f"Tracked target '{target_cfg.prim_expr}' produced {pos_w.shape[0]} poses, "
442+
f"but the raycaster has {slot_count} mesh slots for that target."
443+
)
444+
self._slot_mesh_positions_w_torch[slot_start:slot_end] = pos_w
445+
self._slot_mesh_orientations_w_torch[slot_start:slot_end] = ori_w
467446

468447
def _update_buffers_impl(self, env_mask: wp.array):
469448
"""Fills the buffers of the sensor data."""
@@ -484,24 +463,23 @@ def _update_buffers_impl(self, env_mask: wp.array):
484463
device=self._device,
485464
)
486465

487-
n_meshes = self._mesh_ids_wp.shape[1]
488-
489-
# Ray-cast against all meshes; closest hit wins via atomic_min on ray_distance.
466+
# Ray-cast against all mesh slots; closest hit wins via atomic_min on ray_distance.
490467
wp.launch(
491-
warp_kernels.raycast_dynamic_meshes_kernel,
492-
dim=(n_meshes, self._num_envs, self.num_rays),
468+
warp_kernels.raycast_dynamic_mesh_slots_kernel,
469+
dim=(self._slot_mesh_ids_wp.shape[0], self.num_rays),
493470
inputs=[
494471
env_mask,
495-
self._mesh_ids_wp,
472+
self._slot_env_ids_wp,
473+
self._slot_mesh_ids_wp,
496474
self._ray_starts_w,
497475
self._ray_directions_w,
498476
self._data._ray_hits_w,
499477
self._ray_distance_w,
500478
self._dummy_normal_w,
501479
self._dummy_face_id_w,
502480
self._ray_mesh_id_w,
503-
self._mesh_positions_w,
504-
self._mesh_orientations_w,
481+
self._slot_mesh_positions_w,
482+
self._slot_mesh_orientations_w,
505483
float(self.cfg.max_distance),
506484
int(False),
507485
int(False),

source/isaaclab/isaaclab/sensors/ray_caster/base_multi_mesh_ray_caster_camera.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,24 +239,23 @@ def _update_buffers_impl(self, env_mask: wp.array):
239239
device=self._device,
240240
)
241241

242-
n_meshes = self._mesh_ids_wp.shape[1]
243-
244-
# Ray-cast against all meshes; closest hit wins via atomic_min on ray_distance.
242+
# Ray-cast against all mesh slots; closest hit wins via atomic_min on ray_distance.
245243
wp.launch(
246-
warp_kernels.raycast_dynamic_meshes_kernel,
247-
dim=(n_meshes, self._num_envs, self.num_rays),
244+
warp_kernels.raycast_dynamic_mesh_slots_kernel,
245+
dim=(self._slot_mesh_ids_wp.shape[0], self.num_rays),
248246
inputs=[
249247
env_mask,
250-
self._mesh_ids_wp,
248+
self._slot_env_ids_wp,
249+
self._slot_mesh_ids_wp,
251250
self._ray_starts_w,
252251
self._ray_directions_w,
253252
self._ray_hits_w_cam,
254253
self._ray_distance_cam_w,
255254
self._ray_normal_w,
256255
self._ray_face_id_w,
257256
self._ray_mesh_id_w,
258-
self._mesh_positions_w,
259-
self._mesh_orientations_w,
257+
self._slot_mesh_positions_w,
258+
self._slot_mesh_orientations_w,
260259
float(CAMERA_RAYCAST_MAX_DIST),
261260
int(return_normal),
262261
int(False),

source/isaaclab/isaaclab/utils/warp/kernels.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,55 @@ def raycast_dynamic_meshes_kernel(
326326
ray_mesh_id[tid_env, tid_ray] = wp.int16(tid_mesh_id)
327327

328328

329+
@wp.kernel(enable_backward=False)
330+
def raycast_dynamic_mesh_slots_kernel(
331+
env_mask: wp.array(dtype=wp.bool),
332+
slot_env_ids: wp.array(dtype=wp.int32),
333+
mesh: wp.array(dtype=wp.uint64),
334+
ray_starts: wp.array2d(dtype=wp.vec3),
335+
ray_directions: wp.array2d(dtype=wp.vec3),
336+
ray_hits: wp.array2d(dtype=wp.vec3),
337+
ray_distance: wp.array2d(dtype=wp.float32),
338+
ray_normal: wp.array2d(dtype=wp.vec3),
339+
ray_face_id: wp.array2d(dtype=wp.int32),
340+
ray_mesh_id: wp.array2d(dtype=wp.int16),
341+
mesh_positions: wp.array(dtype=wp.vec3),
342+
mesh_rotations: wp.array(dtype=wp.quat),
343+
max_dist: float = 1e6,
344+
return_normal: int = False,
345+
return_face_id: int = False,
346+
return_mesh_id: int = False,
347+
):
348+
"""Ray-cast against a flat list of per-environment mesh slots.
349+
350+
Launch with ``dim=(num_slots, num_rays)``. Each slot carries the owning
351+
environment id, allowing heterogeneous scenes where environments have
352+
different mesh counts without padding to a rectangular table.
353+
"""
354+
slot_id, tid_ray = wp.tid()
355+
tid_env = slot_env_ids[slot_id]
356+
if not env_mask[tid_env]:
357+
return
358+
359+
mesh_pose = wp.transform(mesh_positions[slot_id], mesh_rotations[slot_id])
360+
mesh_pose_inv = wp.transform_inverse(mesh_pose)
361+
direction = wp.transform_vector(mesh_pose_inv, ray_directions[tid_env, tid_ray])
362+
start_pos = wp.transform_point(mesh_pose_inv, ray_starts[tid_env, tid_ray])
363+
364+
mesh_query_ray_t = wp.mesh_query_ray(mesh[slot_id], start_pos, direction, max_dist)
365+
if mesh_query_ray_t.result:
366+
wp.atomic_min(ray_distance, tid_env, tid_ray, mesh_query_ray_t.t)
367+
if mesh_query_ray_t.t == ray_distance[tid_env, tid_ray]:
368+
hit_pos = start_pos + mesh_query_ray_t.t * direction
369+
ray_hits[tid_env, tid_ray] = wp.transform_point(mesh_pose, hit_pos)
370+
if return_normal == 1:
371+
ray_normal[tid_env, tid_ray] = wp.transform_vector(mesh_pose, mesh_query_ray_t.normal)
372+
if return_face_id == 1:
373+
ray_face_id[tid_env, tid_ray] = mesh_query_ray_t.face
374+
if return_mesh_id == 1:
375+
ray_mesh_id[tid_env, tid_ray] = wp.int16(slot_id)
376+
377+
329378
@wp.kernel(enable_backward=False)
330379
def reshape_tiled_image(
331380
tiled_image_buffer: Any,

0 commit comments

Comments
 (0)