@@ -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 = []
0 commit comments