2222import logging
2323import math
2424import os
25+ from itertools import compress
2526from pathlib import Path
2627from typing import TYPE_CHECKING , Any , NoReturn
2728
5152 "(or, manually: pip install --extra-index-url https://pypi.nvidia.com -e 'source/isaaclab_ov[ovrtx]')."
5253 ) from exc
5354
55+ from isaaclab .cloner .clone_plan import ClonePlan
5456from isaaclab .renderers import BaseRenderer , RenderBufferKind , RenderBufferSpec
5557from isaaclab .sim import SimulationContext
5658from isaaclab .utils .warp .warp_math import convert_camera_frame_orientation_convention_wp
@@ -130,6 +132,67 @@ def _resolve_rtx_minimal_mode(data_types: list[str]) -> int | None:
130132 return _RTX_MINIMAL_MODES [filtered_data_types [0 ]]
131133
132134
135+ def _write_file (output_dir : Path , file_name : str , content : str ) -> None :
136+ """Write ``content`` to ``output_dir / file_name``.
137+
138+ Creates ``output_dir`` and any missing parents when needed.
139+
140+ Args:
141+ output_dir: Directory that receives the file.
142+ file_name: Base name of the file to write.
143+ content: Text content written with UTF-8 encoding.
144+ """
145+ output_dir .mkdir (parents = True , exist_ok = True )
146+ output_path = output_dir / file_name
147+
148+ with open (output_path , "w" , encoding = "utf-8" ) as file :
149+ file .write (content )
150+ logger .info ("Wrote USD file: %s" , output_path )
151+
152+
153+ def _create_homogeneous_clone_plan (num_envs : int ) -> ClonePlan :
154+ """Create a homogeneous fallback plan that replicates ``env_0`` to every environment."""
155+ return ClonePlan (
156+ sources = ("/World/envs/env_0" ,),
157+ destinations = ("/World/envs/env_{}" ,),
158+ clone_mask = torch .ones ((1 , num_envs ), dtype = torch .bool , device = "cpu" ),
159+ )
160+
161+
162+ def _resolve_clone_plan (num_envs : int ) -> ClonePlan :
163+ """Resolve clone plan for local use.
164+
165+ If no clone plan is published by the scene or it has no active rows, returns a homogeneous clone plan.
166+ If the clone plan has some inactive rows, returns a copy of the published clone plan with only the active rows.
167+ If the clone plan has all active rows, returns the published clone plan (shallow copy).
168+ """
169+ published_clone_plan = SimulationContext .instance ().get_clone_plan ()
170+
171+ if published_clone_plan is None :
172+ logger .warning ("No clone plan is published by the scene; returning homogeneous clone plan" )
173+ return _create_homogeneous_clone_plan (num_envs )
174+
175+ active_rows = published_clone_plan .clone_mask .any (dim = 1 )
176+
177+ # If no rows are active, return a homogeneous clone plan.
178+ if not active_rows .any ():
179+ logger .warning ("Clone plan has no active rows, returning homogeneous clone plan" )
180+ return _create_homogeneous_clone_plan (num_envs )
181+
182+ # If some rows are inactive, return a copy of the published clone plan with only the active rows.
183+ if not active_rows .all ():
184+ logger .warning ("Clone plan has some inactive rows; returning a copy with only active rows" )
185+ active = active_rows .tolist ()
186+ return ClonePlan (
187+ sources = tuple (compress (published_clone_plan .sources , active )),
188+ destinations = tuple (compress (published_clone_plan .destinations , active )),
189+ clone_mask = published_clone_plan .clone_mask [active_rows ],
190+ )
191+
192+ # If all rows are active, return the published clone plan (shallow copy).
193+ return published_clone_plan
194+
195+
133196class OVRTXRenderData :
134197 """OVRTX-specific RenderData. Holds warp output buffers sized from :class:`CameraRenderSpec`."""
135198
@@ -197,14 +260,7 @@ def __init__(self, cfg: OVRTXRendererCfg):
197260 self ._exported_usd_string : str | None = None
198261 self ._camera_rel_path : str | None = None
199262 self ._output_semantic_color_buffer : wp .array | None = None
200-
201- self ._use_ovrtx_cloning = self .cfg .use_ovrtx_cloning
202-
203- if self ._use_ovrtx_cloning :
204- clone_plan = SimulationContext .instance ().get_clone_plan ()
205- if clone_plan and not clone_plan .clone_mask .all ().item ():
206- logger .warning ("OVRTX cloning disabled because the simulation uses a heterogeneous env setup" )
207- self ._use_ovrtx_cloning = False
263+ self ._clone_plan : ClonePlan | None = None
208264
209265 logger .info ("Creating OVRTX renderer..." )
210266 OVRTX_CONFIG = RendererConfig (
@@ -246,16 +302,27 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None:
246302 def prepare_stage (self , stage : Any , num_envs : int ) -> None :
247303 """Prepare the USD stage for OVRTX before :meth:`create_render_data`.
248304
249- Adds cloning attributes and exports the stage to a string held on the renderer until
305+ Adds scene partition attributes and exports the stage to a string held on the renderer until
250306 :meth:`create_render_data` is called.
251307 """
252308 if stage is None :
253309 return
254310
255- logger .info ("Preparing stage for export (%d envs, cloning=%s)..." , num_envs , self ._use_ovrtx_cloning )
256- create_scene_partition_attributes (stage , num_envs , self ._use_ovrtx_cloning )
311+ # If temp_usd_dir is set, write the pre-ovrtx stage to a temporary file.
312+ if self .cfg .temp_usd_dir is not None :
313+ _write_file (Path (self .cfg .temp_usd_dir ), "pre_ovrtx_renderer_stage.usda" , stage .ExportToString ())
257314
258- self ._exported_usd_string = export_stage_to_string (stage , num_envs , self ._use_ovrtx_cloning )
315+ logger .info ("Preparing stage (%d envs)..." , num_envs )
316+ create_scene_partition_attributes (stage , num_envs )
317+
318+ # Resolve the clone plan for local use.
319+ self ._clone_plan = _resolve_clone_plan (num_envs )
320+
321+ self ._exported_usd_string = export_stage_to_string (
322+ stage ,
323+ num_envs ,
324+ source_paths = self ._clone_plan .sources ,
325+ )
259326
260327 def _initialize_from_spec (self , spec : CameraRenderSpec ):
261328 """Initialize the OVRTX renderer with internal environment cloning.
@@ -294,12 +361,7 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
294361
295362 # If temp_usd_dir is set, write the combined USD stage to a temporary file.
296363 if self .cfg .temp_usd_dir is not None :
297- temp_usd_dir = Path (self .cfg .temp_usd_dir )
298- temp_usd_dir .mkdir (parents = True , exist_ok = True )
299- temp_usd_path = temp_usd_dir / "ovrtx_renderer_stage.usda"
300- with open (temp_usd_path , "w" , encoding = "utf-8" ) as f :
301- f .write (combined_usd_string )
302- logger .info ("Wrote combined USD stage to %s" , temp_usd_path )
364+ _write_file (Path (self .cfg .temp_usd_dir ), "ovrtx_renderer_stage.usda" , combined_usd_string )
303365
304366 logger .info ("Loading USD into OvRTX..." )
305367 try :
@@ -309,9 +371,8 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
309371 logger .exception ("Error loading USD: %s" , e )
310372 raise
311373
312- if self ._use_ovrtx_cloning and num_envs > 1 :
313- logger .info ("Using OVRTX internal cloning" )
314- self ._clone_environments_in_ovrtx (num_envs )
374+ if num_envs > 1 :
375+ self ._clone_sources_in_ovrtx ()
315376 self ._update_scene_partitions_after_clone (num_envs )
316377
317378 self ._initialized_scene = True
@@ -341,17 +402,39 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
341402
342403 self ._setup_object_bindings ()
343404
344- def _clone_environments_in_ovrtx (self , num_envs : int ):
345- """Clone base environment (env_0) to all other environments using OvRTX."""
346- logger .info ("Cloning base environment to %d targets..." , num_envs - 1 )
347- source_path = "/World/envs/env_0"
348- target_paths = [f"/World/envs/env_{ i } " for i in range (1 , num_envs )]
349- try :
350- self ._renderer .clone_usd (source_path , target_paths )
351- logger .info ("Cloned %d environments successfully" , len (target_paths ))
352- except Exception as e :
353- logger .error ("Failed to clone environments: %s" , e )
354- raise RuntimeError (f"OvRTX environment cloning failed: { e } " )
405+ def _clone_sources_in_ovrtx (self ):
406+ """Clone sources in OVRTX using the scene :class:`~isaaclab.cloner.ClonePlan`."""
407+ clone_plan = self ._clone_plan
408+ if clone_plan is None :
409+ raise RuntimeError ("Clone plan is required when using OVRTX cloning" )
410+
411+ logger .info ("Cloning sources in OVRTX..." )
412+
413+ num_envs = clone_plan .clone_mask .shape [1 ]
414+ env_ids = torch .arange (num_envs , dtype = torch .int32 , device = clone_plan .clone_mask .device )
415+
416+ num_cloned_sources = 0
417+
418+ for row_idx , (source , destination ) in enumerate (zip (clone_plan .sources , clone_plan .destinations , strict = True )):
419+ target_env_ids = env_ids [clone_plan .clone_mask [row_idx ]].tolist ()
420+
421+ target_paths = []
422+ for env_id in target_env_ids :
423+ resolved_destination = destination .format (env_id )
424+ if resolved_destination != source :
425+ target_paths .append (resolved_destination )
426+
427+ if target_paths :
428+ logger .debug ("Cloning row %d: %s -> %d target(s)" , row_idx , source , len (target_paths ))
429+ try :
430+ self ._renderer .clone_usd (source , target_paths )
431+ num_cloned_sources += 1
432+ except Exception as e :
433+ error_msg = f"Failed to clone row { row_idx } from { source } : { e } "
434+ logger .error (error_msg )
435+ raise RuntimeError (error_msg )
436+
437+ logger .info ("Cloned %d sources successfully in OVRTX" , num_cloned_sources )
355438
356439 def _update_scene_partitions_after_clone (self , num_envs : int ):
357440 """Update scene partition attributes on cloned environments and cameras in OvRTX."""
0 commit comments