Skip to content

Commit 1372eb2

Browse files
committed
Route OVRTX scale projection through clone queries
1 parent 0174e13 commit 1372eb2

2 files changed

Lines changed: 25 additions & 32 deletions

File tree

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
) from exc
6868

6969
from isaaclab.cloner import ClonePlan
70+
from isaaclab.cloner import query as clone_query
7071
from isaaclab.renderers import BaseRenderer, RenderBufferKind, RenderBufferSpec
7172
from isaaclab.sim import SimulationContext
7273
from isaaclab.utils.warp.warp_math import convert_camera_frame_orientation_convention_wp
@@ -433,7 +434,7 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
433434
create_scene_partition_attributes(stage, num_envs)
434435

435436
# Composed scales must be read while the full stage is still live, before export trims it.
436-
self._capture_object_scales(stage)
437+
self._capture_object_scales(stage, self._clone_plan)
437438

438439
# The clone plan already identifies every source row. Keep those rows independent so
439440
# backend bindings for dynamic assets retain the paths they were compiled against.
@@ -444,7 +445,7 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
444445
keep_env_roots=not self._use_ovstage,
445446
)
446447

447-
def _capture_object_scales(self, stage: Any) -> None:
448+
def _capture_object_scales(self, stage: Any, plan: ClonePlan) -> None:
448449
"""Record composed world scales of scaled environment prims before the stage is exported.
449450
450451
The per-frame object transform write rebuilds each body's matrix from a Newton
@@ -453,11 +454,12 @@ def _capture_object_scales(self, stage: Any) -> None:
453454
stage is still live, lets :meth:`_create_object_scale_array` fold it back in.
454455
455456
Only paths whose scale deviates from unit are stored. Scales found under clone-plan source
456-
paths are projected onto their active destinations because OVRTX creates those prims only
457-
after the host stage is exported.
457+
paths are projected through the cloner query boundary because OVRTX creates their active
458+
destinations only after the host stage is exported.
458459
459460
Args:
460461
stage: The live USD stage, before per-environment trimming and export.
462+
plan: Validated plan describing the active prototype-to-clone relation.
461463
"""
462464
self._object_scales_by_path.clear()
463465

@@ -477,24 +479,12 @@ def _capture_object_scales(self, stage: Any) -> None:
477479
self._object_scales_by_path[str(prim.GetPath())] = scale
478480

479481
# OVRTX creates non-source rows after this stage is exported, so those destination prims
480-
# cannot be traversed above. The clone plan is the authority for their path correspondence.
481-
clone_plan = self._clone_plan
482-
if clone_plan is None or clone_plan.env_ids is None:
483-
return
484-
captured_scales = tuple(self._object_scales_by_path.items())
485-
env_ids = clone_plan.env_ids.detach().cpu()
486-
clone_mask = clone_plan.clone_mask.detach().cpu()
487-
for row, (source, destination) in enumerate(zip(clone_plan.sources, clone_plan.destinations, strict=True)):
488-
source_root = source.rstrip("/")
489-
source_scales = [
490-
(path, scale)
491-
for path, scale in captured_scales
492-
if path == source_root or path.startswith(f"{source_root}/")
493-
]
494-
for env_id in env_ids[clone_mask[row]].tolist():
495-
target_root = destination.format(int(env_id)).rstrip("/")
496-
for path, scale in source_scales:
497-
self._object_scales_by_path[f"{target_root}{path.removeprefix(source_root)}"] = scale
482+
# cannot be traversed above. Clone queries retain the plan's nearest-owner semantics.
483+
for source_path, scale in tuple(self._object_scales_by_path.items()):
484+
for env_id in clone_query.path_env_ids(plan, source_path):
485+
clone_path = clone_query.path_to_clone(plan, source_path, env_id)
486+
assert clone_path is not None
487+
self._object_scales_by_path.setdefault(clone_path, scale)
498488

499489
def _create_object_scale_array(self, object_paths: list[str]) -> wp.array:
500490
"""Build the device scale array aligned with the Newton body binding order.

source/isaaclab_ov/test/test_ovrtx_clone_plan.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -350,28 +350,31 @@ def test_prepare_stage_rejects_non_dense_environment_ids(monkeypatch: pytest.Mon
350350
_make_ovrtx_renderer_without_backend().prepare_stage(_make_multi_env_stage(2), 2)
351351

352352

353-
def test_capture_object_scales_expands_clone_plan_destinations():
354-
"""Authored source scale is retained for destinations cloned inside OVRTX."""
353+
def test_capture_object_scales_populates_source_and_destination_scale_array():
354+
"""Projected source scales reach the body array without replacing a real destination scale."""
355355
stage = Usd.Stage.CreateInMemory()
356356
UsdGeom.Xform.Define(stage, "/World")
357357
UsdGeom.Xform.Define(stage, "/World/envs")
358358
UsdGeom.Xform.Define(stage, "/World/envs/env_0")
359359
UsdGeom.Xform.Define(stage, "/World/envs/env_1")
360+
UsdGeom.Xform.Define(stage, "/World/envs/env_2")
360361
UsdGeom.Xform.Define(stage, "/World/envs/env_0/Object").AddScaleOp().Set(Gf.Vec3d(1.0, 1.0, 8.0))
362+
UsdGeom.Xform.Define(stage, "/World/envs/env_1/Object").AddScaleOp().Set(Gf.Vec3d(1.0, 1.0, 4.0))
361363
renderer = _make_ovrtx_renderer_without_backend()
362-
renderer._clone_plan = ClonePlan(
364+
renderer._device = "cpu"
365+
plan = ClonePlan(
363366
sources=("/World/envs/env_0",),
364367
destinations=("/World/envs/env_{}",),
365-
clone_mask=torch.ones((1, 2), dtype=torch.bool),
366-
env_ids=torch.arange(2),
368+
clone_mask=torch.ones((1, 3), dtype=torch.bool),
369+
env_ids=torch.arange(3),
367370
)
368371

369-
renderer._capture_object_scales(stage)
372+
renderer._capture_object_scales(stage, plan)
373+
scales = renderer._create_object_scale_array(
374+
["/World/envs/env_0/Object", "/World/envs/env_1/Object", "/World/envs/env_2/Object"]
375+
)
370376

371-
assert renderer._object_scales_by_path == {
372-
"/World/envs/env_0/Object": (1.0, 1.0, 8.0),
373-
"/World/envs/env_1/Object": (1.0, 1.0, 8.0),
374-
}
377+
np.testing.assert_allclose(scales.numpy(), np.array([[1.0, 1.0, 8.0], [1.0, 1.0, 4.0], [1.0, 1.0, 8.0]]))
375378

376379

377380
def test_prepare_stage_keeps_material_binding_inside_clone_source(monkeypatch: pytest.MonkeyPatch):

0 commit comments

Comments
 (0)