Skip to content

Commit b941b45

Browse files
authored
Merge branch 'develop' into mh/cleanup_video_alias
2 parents ff41e73 + 5131656 commit b941b45

11 files changed

Lines changed: 199 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Restored sensors to skip explicit USD replication now that source-only camera views expand from
5+
the clone plan.

source/isaaclab/isaaclab/sensors/sensor_base_cfg.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ class SensorBaseCfg:
2222
The class should inherit from :class:`isaaclab.sensors.sensor_base.SensorBase`.
2323
"""
2424

25-
cloning_contexts: tuple[str | type, ...] | None = ("isaaclab.cloner:UsdReplicateContext",)
26-
"""Cloning contexts for this sensor. Defaults to USD-only cloning.
25+
cloning_contexts: tuple[str | type, ...] | None = ()
26+
"""Cloning contexts for this sensor. Defaults to no explicit cloning context.
2727
28-
Sensors carry no physics of their own; see :attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts`.
28+
Sensors carry no physics of their own. When the sensor has a spawner, USD replication is
29+
added automatically under Kit. Listing :class:`~isaaclab.cloner.UsdReplicateContext`
30+
explicitly forces USD replication without Kit; see
31+
:attr:`~isaaclab.assets.AssetBaseCfg.cloning_contexts`.
2932
"""
3033

3134
prim_path: str = MISSING

source/isaaclab/test/cloner/test_replicate_session.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from isaaclab.sim import SimulationContext
1717

1818

19-
def test_sensor_default_requests_usd_replication():
20-
"""Sensors request USD cloning explicitly so kitless runs author per-env prims."""
19+
def test_sensor_default_does_not_request_a_cloning_context():
20+
"""Sensors rely on automatic Kit replication unless a user explicitly overrides it."""
2121

22-
assert SensorBaseCfg().cloning_contexts == ("isaaclab.cloner:UsdReplicateContext",)
22+
assert SensorBaseCfg().cloning_contexts == ()
2323

2424

2525
@pytest.mark.parametrize(

source/isaaclab/test/sim/test_newton_manager_visualization_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,9 +806,9 @@ def test_clone_visualization_builder_ignores_non_env_deformables_on_world_import
806806
fake_builder = _FakeShadowBuilder(body_count=1, cloth_delta=3, track_usd=True)
807807
clone_plan = SimpleNamespace(
808808
sources=("/World/envs/env_0",),
809-
destinations=("/World/envs/env_0", "/World/envs/env_1"),
809+
destinations=("/World/envs/env_{}",),
810810
env_ids=torch.tensor([0, 1], dtype=torch.int32),
811-
clone_mask=torch.tensor([0, 0], dtype=torch.int32),
811+
clone_mask=torch.tensor([[False, False]], dtype=torch.bool),
812812
)
813813
monkeypatch.setattr(vb, "ModelBuilder", lambda up_axis="Z": fake_builder)
814814
monkeypatch.setattr(vb, "_restore_visible_colliders_without_visual_shapes", lambda *args, **kwargs: None)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed shadow deformable visualization for clone-planned PhysX environments.

source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def build_visualization_builder_from_stage_envs(
110110
)
111111
_restore_visible_colliders_without_visual_shapes(builder, stage, import_result["path_shape_map"])
112112
shadow_entities, registry_groups = add_shadow_deformables_to_builder(
113-
builder, stage, env_paths, device=device, entries=deformable_entries
113+
builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan
114114
)
115115
return builder, (shadow_entities, registry_groups)
116116

@@ -148,6 +148,6 @@ def build_visualization_builder_from_stage_envs(
148148
replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders)
149149
rename_builder_labels(builder, sources, destinations, env_ids, mapping)
150150
shadow_entities, registry_groups = add_shadow_deformables_to_builder(
151-
builder, stage, env_paths, device=device, entries=deformable_entries
151+
builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan
152152
)
153153
return builder, (shadow_entities, registry_groups)

source/isaaclab_newton/isaaclab_newton/physics/visualization_deformables.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99

1010
import logging
1111
from collections.abc import Sequence
12-
from dataclasses import dataclass, field
12+
from dataclasses import dataclass, field, replace
1313

1414
import numpy as np
1515
import warp as wp
1616
from newton import ModelBuilder
1717

1818
from pxr import Usd
1919

20+
from isaaclab.cloner import ClonePlan
2021
from isaaclab.scene_data.deformable_discovery import (
2122
DeformableStageEntry,
2223
discover_deformables_on_stage,
@@ -115,13 +116,63 @@ def _build_volume_vis_remap(entry: DeformableStageEntry, device: str) -> VolumeV
115116
return remap
116117

117118

119+
def _expand_clone_plan_deformable_entries(
120+
entries: Sequence[DeformableStageEntry],
121+
clone_plan: ClonePlan | None,
122+
) -> list[DeformableStageEntry]:
123+
"""Expand prototype deformables into every destination selected by a clone plan.
124+
125+
Kit-less replication may retain only a source deformable on the USD stage. The
126+
shadow builder nevertheless needs one deformable particle block and one visual
127+
mesh binding for every cloned environment.
128+
"""
129+
if clone_plan is None:
130+
return list(entries)
131+
132+
env_ids = clone_plan.env_ids
133+
if env_ids is None:
134+
env_ids = range(clone_plan.clone_mask.shape[1])
135+
else:
136+
env_ids = env_ids.detach().cpu().tolist()
137+
138+
expanded: dict[str, DeformableStageEntry] = {entry.root_path: entry for entry in entries}
139+
for entry in entries:
140+
for source_idx, (source, destination) in enumerate(
141+
zip(clone_plan.sources, clone_plan.destinations, strict=True)
142+
):
143+
source = source.rstrip("/")
144+
if entry.root_path != source and not entry.root_path.startswith(f"{source}/"):
145+
continue
146+
147+
selected_env_ids = clone_plan.clone_mask[source_idx].detach().cpu().tolist()
148+
for env_id, selected in zip(env_ids, selected_env_ids, strict=True):
149+
if not selected:
150+
continue
151+
target = destination.format(int(env_id)).rstrip("/")
152+
153+
def replace_source(path: str) -> str:
154+
return f"{target}{path[len(source) :]}"
155+
156+
cloned_entry = replace(
157+
entry,
158+
root_path=replace_source(entry.root_path),
159+
sim_mesh_path=replace_source(entry.sim_mesh_path),
160+
vis_mesh_path=replace_source(entry.vis_mesh_path),
161+
)
162+
expanded.setdefault(cloned_entry.root_path, cloned_entry)
163+
break
164+
165+
return list(expanded.values())
166+
167+
118168
def add_shadow_deformables_to_builder(
119169
builder: ModelBuilder,
120170
stage: Usd.Stage,
121171
env_paths: Sequence[tuple[int, str]],
122172
*,
123173
device: str = "cpu",
124174
entries: Sequence[DeformableStageEntry] | None = None,
175+
clone_plan: ClonePlan | None = None,
125176
) -> tuple[list[ShadowDeformableEntity], list[ShadowDeformableRegistryGroup]]:
126177
"""Add PhysX/OVPhysX deformable meshes to a shadow Newton builder.
127178
@@ -134,6 +185,8 @@ def add_shadow_deformables_to_builder(
134185
stage: Current USD stage.
135186
env_paths: Sorted ``(env_id, env_prim_path)`` pairs.
136187
device: Warp device for barycentric remap tables uploaded during shadow build.
188+
clone_plan: Optional replication layout used to expand prototype-only stage
189+
entries into all destination environments.
137190
138191
Returns:
139192
Flat entity list for geometry mapping and grouped registry metadata for
@@ -143,6 +196,7 @@ def add_shadow_deformables_to_builder(
143196
entries = discover_deformables_on_stage(stage)
144197
if not entries:
145198
return [], []
199+
entries = _expand_clone_plan_deformable_entries(entries, clone_plan)
146200

147201
env_path_by_id = dict(env_paths)
148202
wildcard_groups: dict[tuple[str, str, str], list[DeformableStageEntry]] = {}

source/isaaclab_newton/test/cloner/test_rename_builder_labels.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
replicate_builder_mapping,
1919
)
2020
from isaaclab_newton.physics import visualization_builder as visualization_builder_module
21+
from isaaclab_newton.physics import visualization_deformables as visualization_deformables_module
2122
from newton.solvers import SolverMuJoCo
2223

2324
from pxr import Usd, UsdGeom
2425

2526
from isaaclab.cloner import ClonePlan
27+
from isaaclab.scene_data.deformable_discovery import DeformableStageEntry
2628

2729
_VIS_LABEL_SUFFIXES = {
2830
"body_label": "Body",
@@ -397,6 +399,33 @@ def test_inactive_source_rows_are_ignored(self):
397399

398400

399401
class TestVisualizationClonePlan(unittest.TestCase):
402+
def test_clone_plan_expands_prototype_deformables_to_selected_environments(self):
403+
entry = DeformableStageEntry(
404+
root_path="/World/envs/env_0/Deformable",
405+
sim_mesh_path="/World/envs/env_0/Deformable/simulation_mesh",
406+
vis_mesh_path="/World/envs/env_0/Deformable/visual_mesh",
407+
deformable_type="surface",
408+
vertex_count=3,
409+
vis_vertex_count=3,
410+
)
411+
clone_plan = ClonePlan(
412+
sources=("/World/envs/env_0",),
413+
destinations=("/World/envs/env_{}",),
414+
clone_mask=torch.ones((1, 4), dtype=torch.bool),
415+
env_ids=torch.arange(4),
416+
)
417+
418+
entries = visualization_deformables_module._expand_clone_plan_deformable_entries([entry], clone_plan)
419+
420+
self.assertEqual(
421+
[entry.root_path for entry in entries],
422+
[f"/World/envs/env_{env_id}/Deformable" for env_id in range(4)],
423+
)
424+
self.assertEqual(
425+
[entry.vis_mesh_path for entry in entries],
426+
[f"/World/envs/env_{env_id}/Deformable/visual_mesh" for env_id in range(4)],
427+
)
428+
400429
@staticmethod
401430
def _define_xform(stage, path, translation=None):
402431
xform = UsdGeom.Xform.Define(stage, path)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed source-only world-attached frame views to project across cloned environments without
5+
authoring destination USD prims.

source/isaaclab_ovphysx/isaaclab_ovphysx/sim/views/ovphysx_frame_view.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pxr import Gf, Usd, UsdGeom, UsdPhysics
1717

1818
import isaaclab.sim as sim_utils
19+
from isaaclab import cloner
1920
from isaaclab.physics import PhysicsEvent
2021
from isaaclab.sim.views.base_frame_view import BaseFrameView
2122
from isaaclab.sim.views.usd_frame_view import UsdFrameView
@@ -430,6 +431,7 @@ def _initialize_impl(self, physx: Any) -> None:
430431
self._pose_buf = wp.zeros((1, 7), dtype=wp.float32, device=self._device)
431432
binding_paths = []
432433

434+
world_sites = self._expand_world_sites_from_clone_plan(xform_cache) if not binding_paths else []
433435
# 5. Detect clone_usd=False expansion: binding row count > number of matched USD prims.
434436
# Replace per-prim arrays with one entry per binding row, all derived from the env_0 template.
435437
if binding_paths and len(binding_paths) > len(self._prims):
@@ -466,6 +468,14 @@ def _initialize_impl(self, physx: Any) -> None:
466468
parent_site_local.append(template_parent_site_local)
467469
synthetic_prim_paths.append(synthetic_path)
468470
self._synthetic_prim_paths: list[str] | None = synthetic_prim_paths
471+
self._prims = [self._prims[0]] * len(binding_paths)
472+
elif world_sites:
473+
_, self._prims, per_prim_site_local, parent_site_local, synthetic_paths = map(
474+
list, zip(*world_sites, strict=True)
475+
)
476+
per_prim_ancestor = [None] * len(world_sites)
477+
parent_ancestor = [None] * len(world_sites)
478+
self._synthetic_prim_paths = synthetic_paths
469479
else:
470480
self._synthetic_prim_paths = None
471481

@@ -497,6 +507,52 @@ def _initialize_impl(self, physx: Any) -> None:
497507
self._local_pos_ta = ProxyArray(self._local_pos_buf)
498508
self._local_quat_ta = ProxyArray(self._local_quat_buf)
499509

510+
def _expand_world_sites_from_clone_plan(
511+
self, xform_cache: UsdGeom.XformCache
512+
) -> list[tuple[int, Usd.Prim, list[float], list[float], str]]:
513+
"""Return row-ordered source prims and projected poses for source-only world sites."""
514+
sim = sim_utils.SimulationContext.instance()
515+
plan = sim.get_clone_plan() if sim is not None else None
516+
matches = tuple(cloner.query.iter_sources(plan, self._prim_path)) if plan is not None else ()
517+
if sum(len(env_ids) for _, _, _, env_ids in matches) <= len(self._prims):
518+
return []
519+
520+
records: list[tuple[int, Usd.Prim, list[float], list[float], str]] = []
521+
for source_root, destination_template, source_path, env_ids in matches:
522+
source_prim = self._stage.GetPrimAtPath(source_path)
523+
if not source_prim.IsValid():
524+
source_prim = sim_utils.find_first_matching_prim(source_path, self._stage)
525+
if source_prim is None or not source_prim.IsValid():
526+
raise RuntimeError(f"OvPhysxFrameView could not resolve source prim {source_path!r}.")
527+
528+
source_prim_path = source_prim.GetPath().pathString
529+
suffix = cloner.path.relative_to(source_prim_path, source_root)
530+
if suffix is None:
531+
raise RuntimeError(f"OvPhysxFrameView source prim {source_prim_path!r} is not under {source_root!r}.")
532+
source_world = xform_cache.GetLocalToWorldTransform(source_prim)
533+
source_parent_world = xform_cache.GetLocalToWorldTransform(source_prim.GetParent())
534+
535+
for env_id in env_ids:
536+
destination_root = destination_template.format(env_id)
537+
source_anchor_path, destination_anchor_path = source_root, destination_root
538+
destination_anchor = self._stage.GetPrimAtPath(destination_anchor_path)
539+
while not destination_anchor.IsValid() and destination_anchor_path != "/":
540+
source_anchor_path = source_anchor_path.rsplit("/", 1)[0] or "/"
541+
destination_anchor_path = destination_anchor_path.rsplit("/", 1)[0] or "/"
542+
destination_anchor = self._stage.GetPrimAtPath(destination_anchor_path)
543+
544+
source_anchor = self._stage.GetPrimAtPath(source_anchor_path)
545+
if not source_anchor.IsValid() or not destination_anchor.IsValid():
546+
raise RuntimeError(f"OvPhysxFrameView could not project {source_prim_path!r} into env {env_id}.")
547+
source_inverse = xform_cache.GetLocalToWorldTransform(source_anchor).GetInverse()
548+
destination_world = xform_cache.GetLocalToWorldTransform(destination_anchor)
549+
site_world = _gf_matrix_to_xform7(source_world * source_inverse * destination_world)
550+
parent_world = _gf_matrix_to_xform7(source_parent_world * source_inverse * destination_world)
551+
records.append((env_id, source_prim, site_world, parent_world, destination_root + suffix))
552+
553+
records.sort(key=lambda record: record[0])
554+
return records
555+
500556
def _resolve_rigid_body_ancestor(
501557
self,
502558
prim: Usd.Prim,
@@ -564,11 +620,10 @@ def _env_wildcardify(path: str) -> str:
564620

565621
@property
566622
def prims(self) -> list[Usd.Prim]:
567-
"""List of USD prims discovered for this view.
623+
"""List of one authored USD prim per site.
568624
569-
Under ``clone_usd=False`` scenes only ``env_0`` carries USD prims, so
570-
this list may be shorter than :attr:`count`. Use :attr:`prim_paths` to
571-
get one path per site (env-substituted for non-env_0 sites).
625+
Source-only clones repeat their source prim handle so the list stays aligned with
626+
the view count; prim_paths contains their logical destination paths.
572627
"""
573628
return self._prims
574629

@@ -873,6 +928,7 @@ def set_visibility(self, visibility, indices: wp.array | None = None) -> None:
873928

874929
def _gf_matrix_to_xform7(mat: Gf.Matrix4d) -> list[float]:
875930
"""Convert a ``Gf.Matrix4d`` to ``[tx, ty, tz, qx, qy, qz, qw]``."""
931+
mat.Orthonormalize()
876932
t = mat.ExtractTranslation()
877933
q = mat.ExtractRotationQuat()
878934
imag = q.GetImaginary()

0 commit comments

Comments
 (0)