# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Sub-module with USD-related utilities."""from__future__importannotationsimportfunctoolsimportinspectimportrefromcollections.abcimportCallablefromtypingimportTYPE_CHECKING,Anyimportisaacsim.core.utils.stageasstage_utilsimportomni.kit.commandsimportomni.logfromisaacsim.core.clonerimportClonerfrompxrimportPhysxSchema,Sdf,Usd,UsdGeom,UsdPhysics,UsdShade# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecatedtry:importSemanticsexceptModuleNotFoundError:frompxrimportSemanticsfromisaaclab.utils.stringimportto_camel_casefrom.importschemasifTYPE_CHECKING:from.spawners.spawner_cfgimportSpawnerCfg"""Attribute - Setters."""
[docs]defsafe_set_attribute_on_usd_schema(schema_api:Usd.APISchemaBase,name:str,value:Any,camel_case:bool):"""Set the value of an attribute on its USD schema if it exists. A USD API schema serves as an interface or API for authoring and extracting a set of attributes. They typically derive from the :class:`pxr.Usd.SchemaBase` class. This function checks if the attribute exists on the schema and sets the value of the attribute if it exists. Args: schema_api: The USD schema to set the attribute on. name: The name of the attribute. value: The value to set the attribute to. camel_case: Whether to convert the attribute name to camel case. Raises: TypeError: When the input attribute name does not exist on the provided schema API. """# if value is None, do nothingifvalueisNone:return# convert attribute name to camel caseifcamel_case:attr_name=to_camel_case(name,to="CC")else:attr_name=name# retrieve the attribute# reference: https://openusd.org/dev/api/_usd__page__common_idioms.html#Usd_Create_Or_Get_Propertyattr=getattr(schema_api,f"Create{attr_name}Attr",None)# check if attribute existsifattrisnotNone:attr().Set(value)else:# think: do we ever need to create the attribute if it doesn't exist?# currently, we are not doing this since the schemas are already created with some defaults.omni.log.error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.")raiseTypeError(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.")
[docs]defsafe_set_attribute_on_usd_prim(prim:Usd.Prim,attr_name:str,value:Any,camel_case:bool):"""Set the value of a attribute on its USD prim. The function creates a new attribute if it does not exist on the prim. This is because in some cases (such as with shaders), their attributes are not exposed as USD prim properties that can be altered. This function allows us to set the value of the attributes in these cases. Args: prim: The USD prim to set the attribute on. attr_name: The name of the attribute. value: The value to set the attribute to. camel_case: Whether to convert the attribute name to camel case. """# if value is None, do nothingifvalueisNone:return# convert attribute name to camel caseifcamel_case:attr_name=to_camel_case(attr_name,to="cC")# resolve sdf type based on valueifisinstance(value,bool):sdf_type=Sdf.ValueTypeNames.Boolelifisinstance(value,int):sdf_type=Sdf.ValueTypeNames.Intelifisinstance(value,float):sdf_type=Sdf.ValueTypeNames.Floatelifisinstance(value,(tuple,list))andlen(value)==3andany(isinstance(v,float)forvinvalue):sdf_type=Sdf.ValueTypeNames.Float3elifisinstance(value,(tuple,list))andlen(value)==2andany(isinstance(v,float)forvinvalue):sdf_type=Sdf.ValueTypeNames.Float2else:raiseNotImplementedError(f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type.")# change propertyomni.kit.commands.execute("ChangePropertyCommand",prop_path=Sdf.Path(f"{prim.GetPath()}.{attr_name}"),value=value,prev=None,type_to_create_if_not_exist=sdf_type,usd_context_name=prim.GetStage(),)
"""Decorators."""
[docs]defapply_nested(func:Callable)->Callable:"""Decorator to apply a function to all prims under a specified prim-path. The function iterates over the provided prim path and all its children to apply input function to all prims under the specified prim path. If the function succeeds to apply to a prim, it will not look at the children of that prim. This is based on the physics behavior that nested schemas are not allowed. For example, a parent prim and its child prim cannot both have a rigid-body schema applied on them, or it is not possible to have nested articulations. While traversing the prims under the specified prim path, the function will throw a warning if it does not succeed to apply the function to any prim. This is because the user may have intended to apply the function to a prim that does not have valid attributes, or the prim may be an instanced prim. Args: func: The function to apply to all prims under a specified prim-path. The function must take the prim-path and other arguments. It should return a boolean indicating whether the function succeeded or not. Returns: The wrapped function that applies the function to all prims under a specified prim-path. Raises: ValueError: If the prim-path does not exist on the stage. """@functools.wraps(func)defwrapper(prim_path:str|Sdf.Path,*args,**kwargs):# map args and kwargs to function signature so we can get the stage# note: we do this to check if stage is given in arg or kwargsig=inspect.signature(func)bound_args=sig.bind(prim_path,*args,**kwargs)# get current stagestage=bound_args.arguments.get("stage")ifstageisNone:stage=stage_utils.get_current_stage()# get USD primprim:Usd.Prim=stage.GetPrimAtPath(prim_path)# check if prim is validifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' is not valid.")# add iterable to check if property was applied on any of the primscount_success=0instanced_prim_paths=[]# iterate over all prims under prim-pathall_prims=[prim]whilelen(all_prims)>0:# get current primchild_prim=all_prims.pop(0)child_prim_path=child_prim.GetPath().pathString# type: ignore# check if prim is a prototypeifchild_prim.IsInstance():instanced_prim_paths.append(child_prim_path)continue# set propertiessuccess=func(child_prim_path,*args,**kwargs)# if successful, do not look at children# this is based on the physics behavior that nested schemas are not allowedifnotsuccess:all_prims+=child_prim.GetChildren()else:count_success+=1# check if we were successful in applying the function to any primifcount_success==0:omni.log.warn(f"Could not perform '{func.__name__}' on any prims under: '{prim_path}'."" This might be because of the following reasons:""\n\t(1) The desired attribute does not exist on any of the prims.""\n\t(2) The desired attribute exists on an instanced prim."f"\n\t\tDiscovered list of instanced prim paths: {instanced_prim_paths}")returnwrapper
[docs]defclone(func:Callable)->Callable:"""Decorator for cloning a prim based on matching prim paths of the prim's parent. The decorator checks if the parent prim path matches any prim paths in the stage. If so, it clones the spawned prim at each matching prim path. For example, if the input prim path is: ``/World/Table_[0-9]/Bottle``, the decorator will clone the prim at each matching prim path of the parent prim: ``/World/Table_0/Bottle``, ``/World/Table_1/Bottle``, etc. Note: For matching prim paths, the decorator assumes that valid prims exist for all matching prim paths. In case no matching prim paths are found, the decorator raises a ``RuntimeError``. Args: func: The function to decorate. Returns: The decorated function that spawns the prim and clones it at each matching prim path. It returns the spawned source prim, i.e., the first prim in the list of matching prim paths. """@functools.wraps(func)defwrapper(prim_path:str|Sdf.Path,cfg:SpawnerCfg,*args,**kwargs):# cast prim_path to str type in case its an Sdf.Pathprim_path=str(prim_path)# check prim path is globalifnotprim_path.startswith("/"):raiseValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.")# resolve: {SPAWN_NS}/AssetName# note: this assumes that the spawn namespace already exists in the stageroot_path,asset_path=prim_path.rsplit("/",1)# check if input is a regex expression# note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashesis_regex_expression=re.match(r"^[a-zA-Z0-9/_]+$",root_path)isNone# resolve matching prims for source prim path expressionifis_regex_expressionandroot_path!="":source_prim_paths=find_matching_prim_paths(root_path)# if no matching prims are found, raise an erroriflen(source_prim_paths)==0:raiseRuntimeError(f"Unable to find source prim path: '{root_path}'. Please create the prim before spawning.")else:source_prim_paths=[root_path]# resolve prim paths for spawning and cloningprim_paths=[f"{source_prim_path}/{asset_path}"forsource_prim_pathinsource_prim_paths]# spawn single instanceprim=func(prim_paths[0],cfg,*args,**kwargs)# set the prim visibilityifhasattr(cfg,"visible"):imageable=UsdGeom.Imageable(prim)ifcfg.visible:imageable.MakeVisible()else:imageable.MakeInvisible()# set the semantic annotationsifhasattr(cfg,"semantic_tags")andcfg.semantic_tagsisnotNone:# note: taken from replicator scripts.utils.utils.pyforsemantic_type,semantic_valueincfg.semantic_tags:# deal with spaces by replacing them with underscoressemantic_type_sanitized=semantic_type.replace(" ","_")semantic_value_sanitized=semantic_value.replace(" ","_")# set the semantic API for the instanceinstance_name=f"{semantic_type_sanitized}_{semantic_value_sanitized}"sem=Semantics.SemanticsAPI.Apply(prim,instance_name)# create semantic type and data attributessem.CreateSemanticTypeAttr()sem.CreateSemanticDataAttr()sem.GetSemanticTypeAttr().Set(semantic_type)sem.GetSemanticDataAttr().Set(semantic_value)# activate rigid body contact sensorsifhasattr(cfg,"activate_contact_sensors")andcfg.activate_contact_sensors:schemas.activate_contact_sensors(prim_paths[0],cfg.activate_contact_sensors)# clone asset using cloner APIiflen(prim_paths)>1:cloner=Cloner()# clone the primcloner.clone(prim_paths[0],prim_paths[1:],replicate_physics=False,copy_from_source=cfg.copy_from_source)# return the source primreturnprimreturnwrapper
"""Material bindings."""
[docs]@apply_nesteddefbind_visual_material(prim_path:str|Sdf.Path,material_path:str|Sdf.Path,stage:Usd.Stage|None=None,stronger_than_descendants:bool=True,):"""Bind a visual material to a prim. This function is a wrapper around the USD command `BindMaterialCommand`_. .. note:: The function is decorated with :meth:`apply_nested` to allow applying the function to a prim path and all its descendants. .. _BindMaterialCommand: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.BindMaterialCommand.html Args: prim_path: The prim path where to apply the material. material_path: The prim path of the material to apply. stage: The stage where the prim and material exist. Defaults to None, in which case the current stage is used. stronger_than_descendants: Whether the material should override the material of its descendants. Defaults to True. Raises: ValueError: If the provided prim paths do not exist on stage. """# resolve stageifstageisNone:stage=stage_utils.get_current_stage()# check if prim and material existsifnotstage.GetPrimAtPath(prim_path).IsValid():raiseValueError(f"Target prim '{material_path}' does not exist.")ifnotstage.GetPrimAtPath(material_path).IsValid():raiseValueError(f"Visual material '{material_path}' does not exist.")# resolve token for weaker than descendantsifstronger_than_descendants:binding_strength="strongerThanDescendants"else:binding_strength="weakerThanDescendants"# obtain material binding API# note: we prefer using the command here as it is more robust than the USD APIsuccess,_=omni.kit.commands.execute("BindMaterialCommand",prim_path=prim_path,material_path=material_path,strength=binding_strength,stage=stage,)# return successreturnsuccess
[docs]@apply_nesteddefbind_physics_material(prim_path:str|Sdf.Path,material_path:str|Sdf.Path,stage:Usd.Stage|None=None,stronger_than_descendants:bool=True,):"""Bind a physics material to a prim. `Physics material`_ can be applied only to a prim with physics-enabled on them. This includes having collision APIs, or deformable body APIs, or being a particle system. In case the prim does not have any of these APIs, the function will not apply the material and return False. .. note:: The function is decorated with :meth:`apply_nested` to allow applying the function to a prim path and all its descendants. .. _Physics material: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials Args: prim_path: The prim path where to apply the material. material_path: The prim path of the material to apply. stage: The stage where the prim and material exist. Defaults to None, in which case the current stage is used. stronger_than_descendants: Whether the material should override the material of its descendants. Defaults to True. Raises: ValueError: If the provided prim paths do not exist on stage. """# resolve stageifstageisNone:stage=stage_utils.get_current_stage()# check if prim and material existsifnotstage.GetPrimAtPath(prim_path).IsValid():raiseValueError(f"Target prim '{material_path}' does not exist.")ifnotstage.GetPrimAtPath(material_path).IsValid():raiseValueError(f"Physics material '{material_path}' does not exist.")# get USD primprim=stage.GetPrimAtPath(prim_path)# check if prim has collision applied on ithas_physics_scene_api=prim.HasAPI(PhysxSchema.PhysxSceneAPI)has_collider=prim.HasAPI(UsdPhysics.CollisionAPI)has_deformable_body=prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI)has_particle_system=prim.IsA(PhysxSchema.PhysxParticleSystem)ifnot(has_physics_scene_apiorhas_colliderorhas_deformable_bodyorhas_particle_system):omni.log.verbose(f"Cannot apply physics material '{material_path}' on prim '{prim_path}'. It is neither a"" PhysX scene, collider, a deformable body, nor a particle system.")returnFalse# obtain material binding APIifprim.HasAPI(UsdShade.MaterialBindingAPI):material_binding_api=UsdShade.MaterialBindingAPI(prim)else:material_binding_api=UsdShade.MaterialBindingAPI.Apply(prim)# obtain the material primmaterial=UsdShade.Material(stage.GetPrimAtPath(material_path))# resolve token for weaker than descendantsifstronger_than_descendants:binding_strength=UsdShade.Tokens.strongerThanDescendantselse:binding_strength=UsdShade.Tokens.weakerThanDescendants# apply the materialmaterial_binding_api.Bind(material,bindingStrength=binding_strength,materialPurpose="physics")# type: ignore# return successreturnTrue
"""Exporting."""
[docs]defexport_prim_to_file(path:str|Sdf.Path,source_prim_path:str|Sdf.Path,target_prim_path:str|Sdf.Path|None=None,stage:Usd.Stage|None=None,):"""Exports a prim from a given stage to a USD file. The function creates a new layer at the provided path and copies the prim to the layer. It sets the copied prim as the default prim in the target layer. Additionally, it updates the stage up-axis and meters-per-unit to match the current stage. Args: path: The filepath path to export the prim to. source_prim_path: The prim path to export. target_prim_path: The prim path to set as the default prim in the target layer. Defaults to None, in which case the source prim path is used. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Raises: ValueError: If the prim paths are not global (i.e: do not start with '/'). """# automatically casting to str in case args# are path typespath=str(path)source_prim_path=str(source_prim_path)iftarget_prim_pathisnotNone:target_prim_path=str(target_prim_path)ifnotsource_prim_path.startswith("/"):raiseValueError(f"Source prim path '{source_prim_path}' is not global. It must start with '/'.")iftarget_prim_pathisnotNoneandnottarget_prim_path.startswith("/"):raiseValueError(f"Target prim path '{target_prim_path}' is not global. It must start with '/'.")# get current stageifstageisNone:stage:Usd.Stage=omni.usd.get_context().get_stage()# get root layersource_layer=stage.GetRootLayer()# only create a new layer if it doesn't exist alreadytarget_layer=Sdf.Find(path)iftarget_layerisNone:target_layer=Sdf.Layer.CreateNew(path)# open the target stagetarget_stage=Usd.Stage.Open(target_layer)# update stage dataUsdGeom.SetStageUpAxis(target_stage,UsdGeom.GetStageUpAxis(stage))UsdGeom.SetStageMetersPerUnit(target_stage,UsdGeom.GetStageMetersPerUnit(stage))# specify the prim to copysource_prim_path=Sdf.Path(source_prim_path)iftarget_prim_pathisNone:target_prim_path=source_prim_path# copy the primSdf.CreatePrimInLayer(target_layer,target_prim_path)Sdf.CopySpec(source_layer,source_prim_path,target_layer,target_prim_path)# set the default primtarget_layer.defaultPrim=Sdf.Path(target_prim_path).name# resolve all paths relative to layer pathomni.usd.resolve_paths(source_layer.identifier,target_layer.identifier)# save the stagetarget_layer.Save()
"""USD Prim properties."""
[docs]defmake_uninstanceable(prim_path:str|Sdf.Path,stage:Usd.Stage|None=None):"""Check if a prim and its descendants are instanced and make them uninstanceable. 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. This is useful when we want to modify the properties of a prim that is instanced. For example, if we want to apply a different material on an instanced prim, we need to make the prim uninstanceable first. 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. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# make paths str type if they aren't alreadyprim_path=str(prim_path)# check if prim path is globalifnotprim_path.startswith("/"):raiseValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# get primprim:Usd.Prim=stage.GetPrimAtPath(prim_path)# check if prim is validifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' 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 prim is instancedifchild_prim.IsInstance():# make the prim uninstanceablechild_prim.SetInstanceable(False)# add children to listall_prims+=child_prim.GetChildren()
"""USD Stage traversal."""
[docs]defget_first_matching_child_prim(prim_path:str|Sdf.Path,predicate:Callable[[Usd.Prim],bool],stage:Usd.Stage|None=None)->Usd.Prim|None:"""Recursively get the first USD Prim at the path string that passes the predicate function Args: prim_path: The path of the prim in the stage. predicate: The function to test the prims against. It takes a prim as input and returns a boolean. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# make paths str type if they aren't alreadyprim_path=str(prim_path)# check if prim path is globalifnotprim_path.startswith("/"):raiseValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# get primprim=stage.GetPrimAtPath(prim_path)# check if prim is validifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' 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 prim passes predicateifpredicate(child_prim):returnchild_prim# add children to listall_prims+=child_prim.GetChildren()returnNone
[docs]defget_all_matching_child_prims(prim_path:str|Sdf.Path,predicate:Callable[[Usd.Prim],bool]=lambda_:True,depth:int|None=None,stage:Usd.Stage|None=None,)->list[Usd.Prim]:"""Performs a search starting from the root and returns all the prims matching the predicate. Args: prim_path: The root prim path to start the search from. predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input and returns a boolean. Defaults to a function that always returns True. depth: The maximum depth for traversal, should be bigger than zero if specified. Defaults to None (i.e: traversal happens till the end of the tree). stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: A list containing all the prims matching the predicate. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# make paths str type if they aren't alreadyprim_path=str(prim_path)# check if prim path is globalifnotprim_path.startswith("/"):raiseValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# get primprim=stage.GetPrimAtPath(prim_path)# check if prim is validifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' is not valid.")# check if depth is validifdepthisnotNoneanddepth<=0:raiseValueError(f"Depth must be bigger than zero, got {depth}.")# iterate over all prims under prim-path# list of tuples (prim, current_depth)all_prims_queue=[(prim,0)]output_prims=[]whilelen(all_prims_queue)>0:# get current primchild_prim,current_depth=all_prims_queue.pop(0)# check if prim passes predicateifpredicate(child_prim):output_prims.append(child_prim)# add children to listifdepthisNoneorcurrent_depth<depth:all_prims_queue+=[(child,current_depth+1)forchildinchild_prim.GetChildren()]returnoutput_prims
[docs]deffind_first_matching_prim(prim_path_regex:str,stage:Usd.Stage|None=None)->Usd.Prim|None:"""Find the first matching prim in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: The first prim that matches input expression. If no prim matches, returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# check prim path is globalifnotprim_path_regex.startswith("/"):raiseValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the stringpattern=f"^{prim_path_regex}$"compiled_pattern=re.compile(pattern)# obtain matching prim (depth-first search)forpriminstage.Traverse():# check if prim passes predicateifcompiled_pattern.match(prim.GetPath().pathString)isnotNone:returnprimreturnNone
[docs]deffind_matching_prims(prim_path_regex:str,stage:Usd.Stage|None=None)->list[Usd.Prim]:"""Find all the matching prims in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: A list of prims that match input expression. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# check prim path is globalifnotprim_path_regex.startswith("/"):raiseValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the stringtokens=prim_path_regex.split("/")[1:]tokens=[f"^{token}$"fortokenintokens]# iterate over all prims in stage (breath-first search)all_prims=[stage.GetPseudoRoot()]output_prims=[]forindex,tokeninenumerate(tokens):token_compiled=re.compile(token)forpriminall_prims:forchildinprim.GetAllChildren():iftoken_compiled.match(child.GetName())isnotNone:output_prims.append(child)ifindex<len(tokens)-1:all_prims=output_primsoutput_prims=[]returnoutput_prims
[docs]deffind_matching_prim_paths(prim_path_regex:str,stage:Usd.Stage|None=None)->list[str]:"""Find all the matching prim paths in the stage based on input regex expression. Args: prim_path_regex: The regex expression for prim path. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: A list of prim paths that match input expression. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """# obtain matching primsoutput_prims=find_matching_prims(prim_path_regex,stage)# convert prims to prim pathsoutput_prim_paths=[]forpriminoutput_prims:output_prim_paths.append(prim.GetPath().pathString)returnoutput_prim_paths
[docs]deffind_global_fixed_joint_prim(prim_path:str|Sdf.Path,check_enabled_only:bool=False,stage:Usd.Stage|None=None)->UsdPhysics.Joint|None:"""Find the fixed joint prim under the specified prim path that connects the target to the simulation world. A joint is a connection between two bodies. A fixed joint is a joint that does not allow relative motion between the two bodies. When a fixed joint has only one target body, it is considered to attach the body to the simulation world. This function finds the fixed joint prim that has only one target under the specified prim path. If no such fixed joint prim exists, it returns None. Args: prim_path: The prim path to search for the fixed joint prim. check_enabled_only: Whether to consider only enabled fixed joints. Defaults to False. If False, then all joints (enabled or disabled) are considered. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. Returns: The fixed joint prim that has only one target. If no such fixed joint prim exists, it returns None. Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). ValueError: If the prim path does not exist on the stage. """# check prim path is globalifnotprim_path.startswith("/"):raiseValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.")# get current stageifstageisNone:stage=stage_utils.get_current_stage()# check if prim existsprim=stage.GetPrimAtPath(prim_path)ifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' is not valid.")fixed_joint_prim=None# we check all joints under the root prim and classify the asset as fixed base if there exists# a fixed joint that has only one target (i.e. the root link).forpriminUsd.PrimRange(prim):# note: ideally checking if it is FixedJoint would have been enough, but some assets use "Joint" as the# schema name which makes it difficult to distinguish between the two.joint_prim=UsdPhysics.Joint(prim)ifjoint_prim:# if check_enabled_only is True, we only consider enabled jointsifcheck_enabled_onlyandnotjoint_prim.GetJointEnabledAttr().Get():continue# check body 0 and body 1 existbody_0_exist=joint_prim.GetBody0Rel().GetTargets()!=[]body_1_exist=joint_prim.GetBody1Rel().GetTargets()!=[]# if either body 0 or body 1 does not exist, we have a fixed joint that connects to the worldifnot(body_0_existandbody_1_exist):fixed_joint_prim=joint_primbreakreturnfixed_joint_prim
"""USD Variants."""
[docs]defselect_usd_variants(prim_path:str,variants:object|dict[str,str],stage:Usd.Stage|None=None):"""Sets the variant selections from the specified variant sets on a USD prim. `USD Variants`_ are a very powerful tool in USD composition that allows prims to have different options on a single asset. This can be done by modifying variations of the same prim parameters per variant option in a set. This function acts as a script-based utility to set the variant selections for the specified variant sets on a USD prim. The function takes a dictionary or a config class mapping variant set names to variant selections. For instance, if we have a prim at ``"/World/Table"`` with two variant sets: "color" and "size", we can set the variant selections as follows: .. code-block:: python select_usd_variants( prim_path="/World/Table", variants={ "color": "red", "size": "large", }, ) Alternatively, we can use a config class to define the variant selections: .. code-block:: python @configclass class TableVariants: color: Literal["blue", "red"] = "red" size: Literal["small", "large"] = "large" select_usd_variants( prim_path="/World/Table", variants=TableVariants(), ) Args: prim_path: The path of the USD prim. variants: A dictionary or config class mapping variant set names to variant selections. stage: The USD stage. Defaults to None, in which case, the current stage is used. Raises: ValueError: If the prim at the specified path is not valid. .. _USD Variants: https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Variant """# Resolve stageifstageisNone:stage=stage_utils.get_current_stage()# Obtain primprim=stage.GetPrimAtPath(prim_path)ifnotprim.IsValid():raiseValueError(f"Prim at path '{prim_path}' is not valid.")# Convert to dict if we have a configclass object.ifnotisinstance(variants,dict):variants=variants.to_dict()existing_variant_sets=prim.GetVariantSets()forvariant_set_name,variant_selectioninvariants.items():# Check if the variant set exists on the prim.ifnotexisting_variant_sets.HasVariantSet(variant_set_name):omni.log.warn(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.")continuevariant_set=existing_variant_sets.GetVariantSet(variant_set_name)# Only set the variant selection if it is different from the current selection.ifvariant_set.GetVariantSelection()!=variant_selection:variant_set.SetVariantSelection(variant_selection)omni.log.info(f"Setting variant selection '{variant_selection}' for variant set '{variant_set_name}' on"f" prim '{prim_path}'.")