Source code for isaaclab.markers.visualization_markers
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""A class to coordinate groups of visual markers (such as spheres, frames or arrows)using `UsdGeom.PointInstancer`_ class.The class :class:`VisualizationMarkers` is used to create a group of visual markers andvisualize them in the viewport. The markers are represented as :class:`UsdGeom.PointInstancer` primsin the USD stage. The markers are created as prototypes in the :class:`UsdGeom.PointInstancer` primand are instanced in the :class:`UsdGeom.PointInstancer` prim. The markers can be visualized bypassing the indices of the marker prototypes and their translations, orientations and scales.The marker prototypes can be configured with the :class:`VisualizationMarkersCfg` class... _UsdGeom.PointInstancer: https://graphics.pixar.com/usd/dev/api/class_usd_geom_point_instancer.html"""# needed to import for allowing type-hinting: np.ndarray | torch.Tensor | Nonefrom__future__importannotationsimportnumpyasnpimporttorchfromdataclassesimportMISSINGimportisaacsim.core.utils.stageasstage_utilsimportomni.kit.commandsimportomni.physx.scripts.utilsasphysx_utilsfrompxrimportGf,PhysxSchema,Sdf,Usd,UsdGeom,UsdPhysics,Vtimportisaaclab.simassim_utilsfromisaaclab.sim.spawnersimportSpawnerCfgfromisaaclab.utils.configclassimportconfigclassfromisaaclab.utils.mathimportconvert_quat
[docs]@configclassclassVisualizationMarkersCfg:"""A class to configure a :class:`VisualizationMarkers`."""prim_path:str=MISSING"""The prim path where the :class:`UsdGeom.PointInstancer` will be created."""markers:dict[str,SpawnerCfg]=MISSING"""The dictionary of marker configurations. The key is the name of the marker, and the value is the configuration of the marker. The key is used to identify the marker in the class. """
[docs]classVisualizationMarkers:"""A class to coordinate groups of visual markers (loaded from USD). This class allows visualization of different UI markers in the scene, such as points and frames. The class wraps around the `UsdGeom.PointInstancer`_ for efficient handling of objects in the stage via instancing the created marker prototype prims. A marker prototype prim is a reusable template prim used for defining variations of objects in the scene. For example, a sphere prim can be used as a marker prototype prim to create multiple sphere prims in the scene at different locations. Thus, prototype prims are useful for creating multiple instances of the same prim in the scene. The class parses the configuration to create different the marker prototypes into the stage. Each marker prototype prim is created as a child of the :class:`UsdGeom.PointInstancer` prim. The prim path for the the marker prim is resolved using the key of the marker in the :attr:`VisualizationMarkersCfg.markers` dictionary. The marker prototypes are created using the :meth:`isaacsim.core.utils.create_prim` function, and then then instanced using :class:`UsdGeom.PointInstancer` prim to allow creating multiple instances of the marker prims. Switching between different marker prototypes is possible by calling the :meth:`visualize` method with the prototype indices corresponding to the marker prototype. The prototype indices are based on the order in the :attr:`VisualizationMarkersCfg.markers` dictionary. For example, if the dictionary has two markers, "marker1" and "marker2", then their prototype indices are 0 and 1 respectively. The prototype indices can be passed as a list or array of integers. Usage: The following snippet shows how to create 24 sphere markers with a radius of 1.0 at random translations within the range [-1.0, 1.0]. The first 12 markers will be colored red and the rest will be colored green. .. code-block:: python import isaaclab.sim as sim_utils from isaaclab.markers import VisualizationMarkersCfg, VisualizationMarkers # Create the markers configuration # This creates two marker prototypes, "marker1" and "marker2" which are spheres with a radius of 1.0. # The color of "marker1" is red and the color of "marker2" is green. cfg = VisualizationMarkersCfg( prim_path="/World/Visuals/testMarkers", markers={ "marker1": sim_utils.SphereCfg( radius=1.0, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)), ), "marker2": VisualizationMarkersCfg.SphereCfg( radius=1.0, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), ), } ) # Create the markers instance # This will create a UsdGeom.PointInstancer prim at the given path along with the marker prototypes. marker = VisualizationMarkers(cfg) # Set position of the marker # -- randomly sample translations between -1.0 and 1.0 marker_translations = np.random.uniform(-1.0, 1.0, (24, 3)) # -- this will create 24 markers at the given translations # note: the markers will all be `marker1` since the marker indices are not given marker.visualize(translations=marker_translations) # alter the markers based on their prototypes indices # first 12 markers will be marker1 and the rest will be marker2 # 0 -> marker1, 1 -> marker2 marker_indices = [0] * 12 + [1] * 12 # this will change the marker prototypes at the given indices # note: the translations of the markers will not be changed from the previous call # since the translations are not given. marker.visualize(marker_indices=marker_indices) # alter the markers based on their prototypes indices and translations marker.visualize(marker_indices=marker_indices, translations=marker_translations) .. _UsdGeom.PointInstancer: https://graphics.pixar.com/usd/dev/api/class_usd_geom_point_instancer.html """
[docs]def__init__(self,cfg:VisualizationMarkersCfg):"""Initialize the class. When the class is initialized, the :class:`UsdGeom.PointInstancer` is created into the stage and the marker prims are registered into it. .. note:: If a prim already exists at the given path, the function will find the next free path and create the :class:`UsdGeom.PointInstancer` prim there. Args: cfg: The configuration for the markers. Raises: ValueError: When no markers are provided in the :obj:`cfg`. """# get next free path for the primprim_path=stage_utils.get_next_free_path(cfg.prim_path)# create a new primstage=stage_utils.get_current_stage()self._instancer_manager=UsdGeom.PointInstancer.Define(stage,prim_path)# store inputsself.prim_path=prim_pathself.cfg=cfg# check if any markers is providediflen(self.cfg.markers)==0:raiseValueError(f"The `cfg.markers` cannot be empty. Received: {self.cfg.markers}")# create a child prim for the markerself._add_markers_prototypes(self.cfg.markers)# Note: We need to do this the first time to initialize the instancer.# Otherwise, the instancer will not be "created" and the function `GetInstanceIndices()` will fail.self._instancer_manager.GetProtoIndicesAttr().Set(list(range(self.num_prototypes)))self._instancer_manager.GetPositionsAttr().Set([Gf.Vec3f(0.0)]*self.num_prototypes)self._count=self.num_prototypes
def__str__(self)->str:"""Return: A string representation of the class."""msg=f"VisualizationMarkers(prim_path={self.prim_path})"msg+=f"\n\tCount: {self.count}"msg+=f"\n\tNumber of prototypes: {self.num_prototypes}"msg+="\n\tMarkers Prototypes:"forindex,(name,marker)inenumerate(self.cfg.markers.items()):msg+=f"\n\t\t[Index: {index}]: {name}: {marker.to_dict()}"returnmsg""" Properties. """@propertydefnum_prototypes(self)->int:"""The number of marker prototypes available."""returnlen(self.cfg.markers)@propertydefcount(self)->int:"""The total number of marker instances."""# TODO: Update this when the USD API is available (Isaac Sim 2023.1)# return self._instancer_manager.GetInstanceCount()returnself._count""" Operations. """
[docs]defset_visibility(self,visible:bool):"""Sets the visibility of the markers. The method does this through the USD API. Args: visible: flag to set the visibility. """imageable=UsdGeom.Imageable(self._instancer_manager)ifvisible:imageable.MakeVisible()else:imageable.MakeInvisible()
[docs]defis_visible(self)->bool:"""Checks the visibility of the markers. Returns: True if the markers are visible, False otherwise. """returnself._instancer_manager.GetVisibilityAttr().Get()!=UsdGeom.Tokens.invisible
[docs]defvisualize(self,translations:np.ndarray|torch.Tensor|None=None,orientations:np.ndarray|torch.Tensor|None=None,scales:np.ndarray|torch.Tensor|None=None,marker_indices:list[int]|np.ndarray|torch.Tensor|None=None,):"""Update markers in the viewport. .. note:: If the prim `PointInstancer` is hidden in the stage, the function will simply return without updating the markers. This helps in unnecessary computation when the markers are not visible. Whenever updating the markers, the input arrays must have the same number of elements in the first dimension. If the number of elements is different, the `UsdGeom.PointInstancer` will raise an error complaining about the mismatch. Additionally, the function supports dynamic update of the markers. This means that the number of markers can change between calls. For example, if you have 24 points that you want to visualize, you can pass 24 translations, orientations, and scales. If you want to visualize only 12 points, you can pass 12 translations, orientations, and scales. The function will automatically update the number of markers in the scene. The function will also update the marker prototypes based on their prototype indices. For instance, if you have two marker prototypes, and you pass the following marker indices: [0, 1, 0, 1], the function will update the first and third markers with the first prototype, and the second and fourth markers with the second prototype. This is useful when you want to visualize different markers in the same scene. The list of marker indices must have the same number of elements as the translations, orientations, or scales. If the number of elements is different, the function will raise an error. .. caution:: This function will update all the markers instanced from the prototypes. That means if you have 24 markers, you will need to pass 24 translations, orientations, and scales. If you want to update only a subset of the markers, you will need to handle the indices yourself and pass the complete arrays to this function. Args: translations: Translations w.r.t. parent prim frame. Shape is (M, 3). Defaults to None, which means left unchanged. orientations: Quaternion orientations (w, x, y, z) w.r.t. parent prim frame. Shape is (M, 4). Defaults to None, which means left unchanged. scales: Scale applied before any rotation is applied. Shape is (M, 3). Defaults to None, which means left unchanged. marker_indices: Decides which marker prototype to visualize. Shape is (M). Defaults to None, which means left unchanged provided that the total number of markers is the same as the previous call. If the number of markers is different, the function will update the number of markers in the scene. Raises: ValueError: When input arrays do not follow the expected shapes. ValueError: When the function is called with all None arguments. """# check if it is visible (if not then let's not waste time)ifnotself.is_visible():return# check if we have any markers to visualizenum_markers=0# resolve inputs# -- positioniftranslationsisnotNone:ifisinstance(translations,torch.Tensor):translations=translations.detach().cpu().numpy()# check that shape is correctiftranslations.shape[1]!=3orlen(translations.shape)!=2:raiseValueError(f"Expected `translations` to have shape (M, 3). Received: {translations.shape}.")# apply translationsself._instancer_manager.GetPositionsAttr().Set(Vt.Vec3fArray.FromNumpy(translations))# update number of markersnum_markers=translations.shape[0]# -- orientationiforientationsisnotNone:ifisinstance(orientations,torch.Tensor):orientations=orientations.detach().cpu().numpy()# check that shape is correctiforientations.shape[1]!=4orlen(orientations.shape)!=2:raiseValueError(f"Expected `orientations` to have shape (M, 4). Received: {orientations.shape}.")# roll orientations from (w, x, y, z) to (x, y, z, w)# internally USD expects (x, y, z, w)orientations=convert_quat(orientations,to="xyzw")# apply orientationsself._instancer_manager.GetOrientationsAttr().Set(Vt.QuathArray.FromNumpy(orientations))# update number of markersnum_markers=orientations.shape[0]# -- scalesifscalesisnotNone:ifisinstance(scales,torch.Tensor):scales=scales.detach().cpu().numpy()# check that shape is correctifscales.shape[1]!=3orlen(scales.shape)!=2:raiseValueError(f"Expected `scales` to have shape (M, 3). Received: {scales.shape}.")# apply scalesself._instancer_manager.GetScalesAttr().Set(Vt.Vec3fArray.FromNumpy(scales))# update number of markersnum_markers=scales.shape[0]# -- statusifmarker_indicesisnotNoneornum_markers!=self._count:# apply marker indicesifmarker_indicesisnotNone:ifisinstance(marker_indices,torch.Tensor):marker_indices=marker_indices.detach().cpu().numpy()elifisinstance(marker_indices,list):marker_indices=np.array(marker_indices)# check that shape is correctiflen(marker_indices.shape)!=1:raiseValueError(f"Expected `marker_indices` to have shape (M,). Received: {marker_indices.shape}.")# apply proto indicesself._instancer_manager.GetProtoIndicesAttr().Set(Vt.IntArray.FromNumpy(marker_indices))# update number of markersnum_markers=marker_indices.shape[0]else:# check that number of markers is not zeroifnum_markers==0:raiseValueError("Number of markers cannot be zero! Hint: The function was called with no inputs?")# set all markers to be the first prototypeself._instancer_manager.GetProtoIndicesAttr().Set([0]*num_markers)# set number of markersself._count=num_markers
""" Helper functions. """def_add_markers_prototypes(self,markers_cfg:dict[str,sim_utils.SpawnerCfg]):"""Adds markers prototypes to the scene and sets the markers instancer to use them."""# add markers based on configforname,cfginmarkers_cfg.items():# resolve prim pathmarker_prim_path=f"{self.prim_path}/{name}"# create a child prim for the markermarker_prim=cfg.func(prim_path=marker_prim_path,cfg=cfg)# make the asset uninstanceable (in case it is)# point instancer defines its own prototypes so if an asset is already instanced, this doesn't work.self._process_prototype_prim(marker_prim)# add child reference to point instancerself._instancer_manager.GetPrototypesRel().AddTarget(marker_prim_path)# check that we loaded all the prototypesprototypes=self._instancer_manager.GetPrototypesRel().GetTargets()iflen(prototypes)!=len(markers_cfg):raiseRuntimeError(f"Failed to load all the prototypes. Expected: {len(markers_cfg)}. Received: {len(prototypes)}.")def_process_prototype_prim(self,prim:Usd.Prim):"""Process a prim and its descendants to make them suitable for defining prototypes. Point instancer defines its own prototypes so if an asset is already instanced, this doesn't work. This function checks if the prim at the specified prim path and its descendants are instanced. If so, it makes the respective prim uninstanceable by disabling instancing on the prim. Additionally, it makes the prim invisible to secondary rays. This is useful when we do not want to see the marker prims on camera images. Args: prim_path: The prim path to check. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. """# check if prim is validifnotprim.IsValid():raiseValueError(f"Prim at path '{prim.GetPrimAtPath()}' is not valid.")# iterate over all prims under prim-pathall_prims=[prim]whilelen(all_prims)>0:# get current primchild_prim=all_prims.pop(0)# check if it is physics body -> if so, remove itifchild_prim.HasAPI(UsdPhysics.ArticulationRootAPI):child_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI)child_prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI)ifchild_prim.HasAPI(UsdPhysics.RigidBodyAPI):child_prim.RemoveAPI(UsdPhysics.RigidBodyAPI)child_prim.RemoveAPI(PhysxSchema.PhysxRigidBodyAPI)ifchild_prim.IsA(UsdPhysics.Joint):child_prim.GetAttribute("physics:jointEnabled").Set(False)# check if prim is instanced -> if so, make it uninstanceableifchild_prim.IsInstance():child_prim.SetInstanceable(False)# check if prim is a mesh -> if so, make it invisible to secondary raysifchild_prim.IsA(UsdGeom.Gprim):# invisible to secondary rays such as depth imagesomni.kit.commands.execute("ChangePropertyCommand",prop_path=Sdf.Path(f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays"),value=True,prev=None,type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool,)# add children to listall_prims+=child_prim.GetChildren()# remove any physics on the markers because they are only for visualization!physx_utils.removeRigidBodySubtree(prim)