2424import math
2525import os
2626import re
27- from itertools import compress
2827from pathlib import Path
2928from typing import TYPE_CHECKING , Any , NoReturn , cast
3029
@@ -251,49 +250,6 @@ def _write_file(output_dir: Path, file_name: str, content: str) -> None:
251250 logger .info ("Wrote USD file: %s" , output_path )
252251
253252
254- def _create_homogeneous_clone_plan (num_envs : int ) -> ClonePlan :
255- """Create a homogeneous fallback plan that replicates ``env_0`` to every environment."""
256- return ClonePlan (
257- sources = ("/World/envs/env_0" ,),
258- destinations = ("/World/envs/env_{}" ,),
259- clone_mask = torch .ones ((1 , num_envs ), dtype = torch .bool , device = "cpu" ),
260- )
261-
262-
263- def _resolve_clone_plan (num_envs : int ) -> ClonePlan :
264- """Resolve clone plan for local use.
265-
266- If no clone plan is published by the scene or it has no active rows, returns a homogeneous clone plan.
267- If the clone plan has some inactive rows, returns a copy of the published clone plan with only the active rows.
268- If the clone plan has all active rows, returns the published clone plan (shallow copy).
269- """
270- published_clone_plan = SimulationContext .instance ().get_clone_plan ()
271-
272- if published_clone_plan is None :
273- logger .warning ("No clone plan is published by the scene; returning homogeneous clone plan" )
274- return _create_homogeneous_clone_plan (num_envs )
275-
276- active_rows = published_clone_plan .clone_mask .any (dim = 1 )
277-
278- # If no rows are active, return a homogeneous clone plan.
279- if not active_rows .any ():
280- logger .warning ("Clone plan has no active rows, returning homogeneous clone plan" )
281- return _create_homogeneous_clone_plan (num_envs )
282-
283- # If some rows are inactive, return a copy of the published clone plan with only the active rows.
284- if not active_rows .all ():
285- logger .warning ("Clone plan has some inactive rows; returning a copy with only active rows" )
286- active = active_rows .tolist ()
287- return ClonePlan (
288- sources = tuple (compress (published_clone_plan .sources , active )),
289- destinations = tuple (compress (published_clone_plan .destinations , active )),
290- clone_mask = published_clone_plan .clone_mask [active_rows ],
291- )
292-
293- # If all rows are active, return the published clone plan (shallow copy).
294- return published_clone_plan
295-
296-
297253class OVRTXRenderData :
298254 """OVRTX-specific RenderData. Holds warp output buffers sized from :class:`CameraRenderSpec`."""
299255
@@ -435,27 +391,20 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
435391 if stage is None :
436392 return
437393
394+ self ._clone_plan = SimulationContext .instance ().get_clone_plan ()
395+ if self ._clone_plan is None or self ._clone_plan .positions is None :
396+ raise RuntimeError ("Clone plan with environment positions is required when preparing OVRTX stage" )
397+
438398 # If temp_usd_dir is set, write the pre-ovrtx stage to a temporary file.
439399 if self .cfg .temp_usd_dir is not None :
440400 _write_file (Path (self .cfg .temp_usd_dir ), "pre_ovrtx_renderer_stage.usda" , stage .ExportToString ())
441401
442402 logger .info ("Preparing stage (%d envs)..." , num_envs )
443403 create_scene_partition_attributes (stage , num_envs )
444404
445- # Resolve the clone plan for local use.
446- self ._clone_plan = _resolve_clone_plan (num_envs )
447- if self ._clone_plan is None :
448- raise RuntimeError ("Clone plan is required when preparing OVRTX stage" )
449-
450- # The ovstage path cannot read env-root transforms back after load (see
451- # _clone_sources_ovstage), so snapshot them here while the full USD stage is still live.
452- if self ._use_ovstage :
453- self ._capture_env_root_xforms_ovstage (stage , num_envs )
454-
455405 # keep_env_roots is False on the ovstage path: ovstage's ``Stage.clone`` requires each target
456406 # path to not already exist, so the non-source env roots must be trimmed from the exported
457- # stage for it to recreate them. Their xforms were captured just above, since trimming them
458- # is what makes them unreadable afterwards.
407+ # stage for it to recreate them.
459408 self ._exported_usd_string = export_stage_to_string (
460409 stage ,
461410 num_envs ,
@@ -563,35 +512,11 @@ def _initialize_from_spec_legacy(self, spec: CameraRenderSpec):
563512 def _clone_sources_in_ovrtx (self ):
564513 """Clone sources in OVRTX using the scene :class:`~isaaclab.cloner.ClonePlan`."""
565514 clone_plan = self ._clone_plan
566- if clone_plan is None :
567- raise RuntimeError ("Clone plan is required when using OVRTX cloning" )
515+ if clone_plan is None or clone_plan . positions is None :
516+ raise RuntimeError ("Clone plan with environment positions is required when using OVRTX cloning" )
568517
569518 num_envs = clone_plan .clone_mask .shape [1 ]
570519 env_prim_paths = [f"/World/envs/env_{ i } " for i in range (num_envs )]
571- xform_attr_name = "omni:xform"
572-
573- # Snapshot per-env root transforms before clone_usd overwrites them with the source env root.
574- #
575- # We only create xform bindings for prims in the body_label list, so their transforms are
576- # driven each frame from simulation data. Prims not in that list (e.g. static rigid objects
577- # such as tables) get no binding, and we never reset their xform stack. Their world placement
578- # therefore relies on every ancestor holding a correct xform in the ovrtx data. In
579- # particular, if an env root has no valid xform, these prims collapse toward env_0 frame
580- # (e.g. tables ending up at env_0 and missing from the other tiles).
581- #
582- # Snapshotting the env-root xforms here and restoring them after clone (see below) keeps those
583- # hierarchy-positioned prims correctly placed per env. This is a cheap one-off operation,
584- # preferable to forcing every static object into the body_label list just to bind its xform.
585- #
586- env_root_xforms = np .empty ((num_envs , 4 , 4 ), dtype = np .float64 )
587- self ._renderer .read_attribute (
588- xform_attr_name ,
589- env_prim_paths ,
590- prim_mode = PrimMode .MUST_EXIST ,
591- dest = env_root_xforms ,
592- )
593- logger .info ("Captured per-env root transforms before cloning" )
594-
595520 logger .info ("Cloning sources in OVRTX..." )
596521 env_ids = torch .arange (num_envs , dtype = torch .int32 , device = clone_plan .clone_mask .device )
597522
@@ -618,15 +543,15 @@ def _clone_sources_in_ovrtx(self):
618543
619544 logger .info ("Cloned %d sources successfully in OVRTX" , num_cloned_sources )
620545
621- # Restore the pre-clone xforms.
546+ env_root_xforms = np .tile (np .eye (4 , dtype = np .float64 ), (num_envs , 1 , 1 ))
547+ env_root_xforms [:, 3 , :3 ] = clone_plan .positions .cpu ().numpy ()
622548 self ._renderer .write_attribute (
623549 prim_paths = env_prim_paths ,
624- attribute_name = xform_attr_name ,
550+ attribute_name = "omni:xform" ,
625551 tensor = env_root_xforms ,
626552 semantic = Semantic .XFORM_MAT4x4 ,
627553 prim_mode = PrimMode .MUST_EXIST ,
628554 )
629- logger .info ("Restored per-env root transforms after cloning" )
630555
631556 def _update_scene_partitions_after_clone (self , num_envs : int ):
632557 """Update scene partition attributes on cloned environments and cameras in OvRTX."""
@@ -1567,7 +1492,6 @@ def _init_fields_ovstage(self) -> None:
15671492 self ._deformable_paths_list = None
15681493 self ._particle_points_query = None
15691494 self ._particle_paths_list = None
1570- self ._env_root_xforms : np .ndarray | None = None
15711495
15721496 def _initialize_from_spec_ovstage (self , spec : CameraRenderSpec ) -> None :
15731497 """Initialize the OVRTX renderer with internal environment cloning (ovstage path).
@@ -1678,55 +1602,15 @@ def _initialize_from_spec_ovstage(self, spec: CameraRenderSpec) -> None:
16781602 logger .info ("OVRTX loaded USD from string successfully via ovstage" )
16791603 self ._current_ordinal += 1
16801604
1681- def _capture_env_root_xforms_ovstage (self , stage : Any , num_envs : int ) -> None :
1682- """Capture per-env root transforms from the live USD stage before export.
1683-
1684- Must be called before :func:`export_stage_to_string`, which trims non-source
1685- env geometry so those transforms cannot be read back reliably from ovstage after load.
1686- The captured array is consumed by :meth:`_clone_sources_ovstage` and cleared after use.
1687- """
1688- from pxr import UsdGeom
1689-
1690- xform_cache = UsdGeom .XformCache ()
1691- self ._env_root_xforms = np .empty ((num_envs , 4 , 4 ), dtype = np .float64 )
1692- for i in range (num_envs ):
1693- prim = stage .GetPrimAtPath (f"/World/envs/env_{ i } " )
1694- self ._env_root_xforms [i ] = np .array (xform_cache .GetLocalToWorldTransform (prim ), dtype = np .float64 ).reshape (
1695- 4 , 4
1696- )
1697-
16981605 def _clone_sources_ovstage (self ):
16991606 """Clone sources in OVRTX using the scene :class:`~isaaclab.cloner.ClonePlan` (ovstage path)."""
17001607 clone_plan = self ._clone_plan
1701- if clone_plan is None :
1702- raise RuntimeError ("Clone plan is required when using OVRTX cloning" )
1608+ if clone_plan is None or clone_plan . positions is None :
1609+ raise RuntimeError ("Clone plan with environment positions is required when using OVRTX cloning" )
17031610
17041611 num_envs = clone_plan .clone_mask .shape [1 ]
17051612 env_prim_paths = [f"/World/envs/env_{ i } " for i in range (num_envs )]
17061613
1707- env_paths_list = self ._stage_paths .create_path_list_from_strings (env_prim_paths )
1708- env_query = self ._stage .query_from_path_list (env_paths_list )
1709-
1710- # Snapshot per-env root transforms before clone overwrites them with the source env root.
1711- #
1712- # We only create xform queries for prims in the body_label list, so their transforms are
1713- # driven each frame from simulation data. Prims not in that list (e.g. static rigid objects
1714- # such as tables) get no query, and we never reset their xform stack. Their world placement
1715- # therefore relies on every ancestor holding a correct xform in the ovstage data. In
1716- # particular, if an env root has no valid xform, these prims collapse toward env_0 frame
1717- # (e.g. tables ending up at env_0 and missing from the other tiles).
1718- #
1719- # Snapshotting the env-root xforms here and restoring them after clone (see below) keeps those
1720- # hierarchy-positioned prims correctly placed per env. This is a cheap one-off operation,
1721- # preferable to forcing every static object into the body_label list just to bind its xform.
1722- #
1723- # Transforms were captured from the live USD stage in prepare_stage before export
1724- # stripped non-source envs — they are not readable from ovstage at this point.
1725- env_root_xforms = self ._env_root_xforms
1726- if env_root_xforms is None :
1727- raise RuntimeError ("env_root_xforms not captured; ensure prepare_stage was called first" )
1728- logger .info ("Using pre-captured per-env root transforms for post-clone restore" )
1729-
17301614 logger .info ("Cloning sources in OVRTX..." )
17311615 env_ids = torch .arange (num_envs , dtype = torch .int32 , device = clone_plan .clone_mask .device )
17321616
@@ -1752,7 +1636,10 @@ def _clone_sources_ovstage(self):
17521636
17531637 logger .info ("Cloned %d sources successfully in OVRTX" , num_cloned_sources )
17541638
1755- # Restore the pre-clone xforms.
1639+ env_root_xforms = np .tile (np .eye (4 , dtype = np .float64 ), (num_envs , 1 , 1 ))
1640+ env_root_xforms [:, 3 , :3 ] = clone_plan .positions .cpu ().numpy ()
1641+ env_paths_list = self ._stage_paths .create_path_list_from_strings (env_prim_paths )
1642+ env_query = self ._stage .query_from_path_list (env_paths_list )
17561643 self ._stage .write_attribute (
17571644 env_query ,
17581645 "omni:xform" ,
@@ -1761,8 +1648,6 @@ def _clone_sources_ovstage(self):
17611648 is_array = False ,
17621649 semantic = ovstage .AttributeSemantic .MATRIX ,
17631650 ).wait ()
1764- self ._env_root_xforms = None
1765- logger .info ("Restored per-env root transforms after cloning" )
17661651
17671652 self ._stage .release_query (env_query ).wait ()
17681653 self ._stage_paths .destroy_path_list (env_paths_list )
@@ -2220,7 +2105,6 @@ def _safe_destroy_path_list(path_list, name: str) -> None:
22202105 self ._deformable_particle_counts = []
22212106 self ._particle_visual_offsets = []
22222107 self ._particle_visual_counts = []
2223- self ._env_root_xforms = None
22242108
22252109 # Detach before closing ExitStack: the renderer holds a live reference into the stage,
22262110 # so detaching first avoids a use-after-free when ExitStack destroys Stage and PathDictionary.
0 commit comments