Skip to content

Commit 0469d0e

Browse files
authored
Fix rendering shadow hand environment to preserve object scale (#7010)
1 parent 71eeeba commit 0469d0e

5 files changed

Lines changed: 80 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed :class:`~isaaclab_ov.renderers.OVRTXRenderer` dropping authored USD scale when syncing
5+
Newton body transforms into OVRTX, which rendered scaled assets (for example Shadow Hand) at
6+
unit scale.

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ def __init__(self, cfg: OVRTXRendererCfg):
355355
# _init_fields_legacy instead; the ovstage path drives the same offsets and counts
356356
# through its stage queries.
357357
self._object_newton_indices: wp.array | None = None
358+
self._object_scales: wp.array | None = None
359+
self._object_scales_by_path: dict[str, tuple[float, float, float]] = {}
358360
self._deformable_particle_offsets: list[int] = []
359361
self._deformable_particle_counts: list[int] = []
360362
self._particle_visual_offsets: list[int] = []
@@ -431,6 +433,9 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
431433
logger.info("Preparing stage (%d envs)...", num_envs)
432434
create_scene_partition_attributes(stage, num_envs)
433435

436+
# Composed scales must be read while the full stage is still live, before export trims it.
437+
self._capture_object_scales(stage)
438+
434439
# keep_env_roots is False on the ovstage path: ovstage's ``Stage.clone`` requires each target
435440
# path to not already exist, so the non-source env roots must be trimmed from the exported
436441
# stage for it to recreate them.
@@ -441,6 +446,49 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
441446
keep_env_roots=not self._use_ovstage,
442447
)
443448

449+
def _capture_object_scales(self, stage: Any) -> None:
450+
"""Record composed world scales of scaled environment prims before the stage is exported.
451+
452+
The per-frame object transform write rebuilds each body's matrix from a Newton
453+
``transformf``, which carries only translation and rotation, so any scale authored on the
454+
USD prim is lost once that write lands. Capturing the composed scale here, while the full
455+
stage is still live, lets :meth:`_create_object_scale_array` fold it back in.
456+
457+
Only prims whose scale deviates from unit are stored, keeping the mapping small for scenes
458+
with many environments.
459+
460+
Args:
461+
stage: The live USD stage, before per-environment trimming and export.
462+
"""
463+
self._object_scales_by_path.clear()
464+
465+
from pxr import Gf, Usd, UsdGeom
466+
467+
envs_prim = stage.GetPrimAtPath("/World/envs")
468+
if not envs_prim.IsValid():
469+
return
470+
471+
xform_cache = UsdGeom.XformCache()
472+
for prim in Usd.PrimRange(envs_prim):
473+
if not prim.IsA(UsdGeom.Xformable):
474+
continue
475+
scale = Gf.Transform(xform_cache.GetLocalToWorldTransform(prim)).GetScale()
476+
scale = (float(scale[0]), float(scale[1]), float(scale[2]))
477+
if not all(math.isclose(axis, 1.0, rel_tol=1e-6, abs_tol=1e-6) for axis in scale):
478+
self._object_scales_by_path[str(prim.GetPath())] = scale
479+
480+
def _create_object_scale_array(self, object_paths: list[str]) -> wp.array:
481+
"""Build the device scale array aligned with the Newton body binding order.
482+
483+
Args:
484+
object_paths: Bound body prim paths, ordered to match the Newton index array.
485+
486+
Returns:
487+
Per-body scale factors, shape ``[len(object_paths)]``, unit where no scale was authored.
488+
"""
489+
scales = [self._object_scales_by_path.get(path, (1.0, 1.0, 1.0)) for path in object_paths]
490+
return wp.array(scales, dtype=wp.vec3f, device=self._device)
491+
444492
def _init_fields_legacy(self) -> None:
445493
"""Initialize the legacy-path instance fields.
446494
@@ -654,6 +702,7 @@ def _setup_xform_bindings_legacy(self):
654702
raise RuntimeError("Failed to create OVRTX object bindings")
655703

656704
self._object_newton_indices = wp.array(newton_indices, dtype=wp.int32, device=self._device)
705+
self._object_scales = self._create_object_scale_array(object_paths)
657706

658707
def _setup_deformable_bindings_legacy(self, num_envs: int):
659708
"""Setup OVRTX bindings for Newton deformable bodies.
@@ -837,7 +886,7 @@ def set_outputs(self, render_data: OVRTXRenderData, output_data: dict[str, Proxy
837886

838887
def _update_transforms_legacy(self) -> None:
839888
"""Sync transforms to OVRTX."""
840-
if self._object_xform_binding is None or self._object_newton_indices is None:
889+
if self._object_xform_binding is None or self._object_newton_indices is None or self._object_scales is None:
841890
return
842891

843892
# If self._object_newton_indices is not None, then Newton's the current physics backend
@@ -857,7 +906,7 @@ def _update_transforms_legacy(self) -> None:
857906
wp.launch(
858907
kernel=sync_newton_transforms_kernel,
859908
dim=len(self._object_newton_indices),
860-
inputs=[ovrtx_transforms, self._object_newton_indices, body_q],
909+
inputs=[ovrtx_transforms, self._object_newton_indices, body_q, self._object_scales],
861910
device=self._device,
862911
)
863912

@@ -1767,6 +1816,7 @@ def _setup_xform_bindings_ovstage(self) -> None:
17671816
raise RuntimeError("Failed to create OVRTX object bindings")
17681817

17691818
self._object_newton_indices = wp.array(newton_indices, dtype=wp.int32, device=self._device)
1819+
self._object_scales = self._create_object_scale_array(object_paths)
17701820

17711821
def _setup_deformable_bindings_ovstage(self, num_envs: int) -> None:
17721822
"""Setup OVRTX bindings for Newton deformable bodies (ovstage path).
@@ -1912,7 +1962,7 @@ def _setup_particle_bindings_ovstage(self) -> None:
19121962
raise RuntimeError("Failed to create OVRTX particle point bindings")
19131963

19141964
def _update_transforms_ovstage(self) -> None:
1915-
if self._object_xform_query is None or self._object_newton_indices is None:
1965+
if self._object_xform_query is None or self._object_newton_indices is None or self._object_scales is None:
19161966
return
19171967

19181968
# If self._object_newton_indices is not None, then Newton's the current physics backend
@@ -1932,7 +1982,7 @@ def _update_transforms_ovstage(self) -> None:
19321982
wp.launch(
19331983
kernel=sync_newton_transforms_kernel,
19341984
dim=num_objects,
1935-
inputs=[object_transforms, self._object_newton_indices, body_q],
1985+
inputs=[object_transforms, self._object_newton_indices, body_q, self._object_scales],
19361986
device=self._device,
19371987
)
19381988
# Synchronize then copy to CPU numpy: ovstage's make_dltensor only accepts the lanes=16
@@ -2125,6 +2175,8 @@ def _safe_destroy_path_list(path_list, name: str) -> None:
21252175
self._particle_paths_list = None
21262176

21272177
self._object_newton_indices = None
2178+
self._object_scales = None
2179+
self._object_scales_by_path = {}
21282180
self._deformable_particle_offsets = []
21292181
self._deformable_particle_counts = []
21302182
self._particle_visual_offsets = []

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer_kernels.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,23 @@ def sync_newton_transforms_kernel(
180180
ovrtx_transforms: wp.array(dtype=wp.mat44d), # type: ignore
181181
newton_body_indices: wp.array(dtype=wp.int32), # type: ignore
182182
newton_body_q: wp.array(dtype=wp.transformf), # type: ignore
183+
object_scales: wp.array(dtype=wp.vec3f), # type: ignore
183184
):
184-
"""Sync Newton physics body transforms to OVRTX 4x4 column-major matrices."""
185+
"""Sync Newton physics body transforms to OVRTX 4x4 column-major matrices.
186+
187+
A Newton ``transformf`` holds only translation and rotation, so the authored USD scale is
188+
reapplied here to keep it from being overwritten with unit scale.
189+
"""
185190
i = wp.tid()
186191
body_idx = newton_body_indices[i]
187192
transform = newton_body_q[body_idx]
188-
ovrtx_transforms[i] = wp.transpose(wp.mat44d(wp.transform_to_matrix(transform)))
193+
scale = object_scales[i]
194+
ovrtx_transforms[i] = wp.mat44d(
195+
wp.transpose(
196+
wp.transform_compose(
197+
wp.transform_get_translation(transform),
198+
wp.transform_get_rotation(transform),
199+
scale,
200+
)
201+
)
202+
)

source/isaaclab_ov/test/test_ovrtx_clone_plan.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def _make_ovrtx_renderer_without_backend() -> OVRTXRenderer:
103103
renderer._exported_usd_string = None
104104
renderer._initialized_scene = False
105105
renderer._use_ovstage = False
106+
renderer._object_scales = None
107+
renderer._object_scales_by_path = {}
106108
return renderer
107109

108110

source/isaaclab_tasks/changelog.d/pbarejko-ompe-103086-refresh-shadow-hand-goldens.skip

Whitespace-only changes.

0 commit comments

Comments
 (0)