Source code for isaaclab.sim.converters.mesh_converter
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clauseimportasyncioimportosimportomniimportomni.kit.commandsimportomni.usdfromisaacsim.core.utils.extensionsimportenable_extensionfrompxrimportGf,Tf,Usd,UsdGeom,UsdPhysics,UsdUtilsfromisaaclab.sim.converters.asset_converter_baseimportAssetConverterBasefromisaaclab.sim.converters.mesh_converter_cfgimportMeshConverterCfgfromisaaclab.sim.schemasimportschemasfromisaaclab.sim.utilsimportexport_prim_to_file
[docs]classMeshConverter(AssetConverterBase):"""Converter for a mesh file in OBJ / STL / FBX format to a USD file. This class wraps around the `omni.kit.asset_converter`_ extension to provide a lazy implementation for mesh to USD conversion. It stores the output USD file in an instanceable format since that is what is typically used in all learning related applications. To make the asset instanceable, we must follow a certain structure dictated by how USD scene-graph instancing and physics work. The rigid body component must be added to each instance and not the referenced asset (i.e. the prototype prim itself). This is because the rigid body component defines properties that are specific to each instance and cannot be shared under the referenced asset. For more information, please check the `documentation <https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/rigid-bodies.html#instancing-rigid-bodies>`_. Due to the above, we follow the following structure: * ``{prim_path}`` - The root prim that is an Xform with the rigid body and mass APIs if configured. * ``{prim_path}/geometry`` - The prim that contains the mesh and optionally the materials if configured. If instancing is enabled, this prim will be an instanceable reference to the prototype prim. .. _omni.kit.asset_converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_asset-converter.html .. caution:: When converting STL files, Z-up convention is assumed, even though this is not the default for many CAD export programs. Asset orientation convention can either be modified directly in the CAD program's export process or an offset can be added within the config in Isaac Lab. """cfg:MeshConverterCfg"""The configuration instance for mesh to USD conversion."""
[docs]def__init__(self,cfg:MeshConverterCfg):"""Initializes the class. Args: cfg: The configuration instance for mesh to USD conversion. """super().__init__(cfg=cfg)
""" Implementation specific methods. """def_convert_asset(self,cfg:MeshConverterCfg):"""Generate USD from OBJ, STL or FBX. The USD file has Y-up axis and is scaled to meters. The asset hierarchy is arranged as follows: .. code-block:: none mesh_file_basename (default prim) |- /geometry/Looks |- /geometry/mesh Args: cfg: The configuration for conversion of mesh to USD. Raises: RuntimeError: If the conversion using the Omniverse asset converter fails. """# resolve mesh name and formatmesh_file_basename,mesh_file_format=os.path.basename(cfg.asset_path).split(".")mesh_file_format=mesh_file_format.lower()# Check if mesh_file_basename is a valid USD identifierifnotTf.IsValidIdentifier(mesh_file_basename):# Correct the name to a valid identifier and update the basenamemesh_file_basename_original=mesh_file_basenamemesh_file_basename=Tf.MakeValidIdentifier(mesh_file_basename)omni.log.warn(f"Input file name '{mesh_file_basename_original}' is an invalid identifier for the mesh prim path."f" Renaming it to '{mesh_file_basename}' for the conversion.")# Convert USDasyncio.get_event_loop().run_until_complete(self._convert_mesh_to_usd(in_file=cfg.asset_path,out_file=self.usd_path))# Create a new stage, set Z up and meters per unittemp_stage=Usd.Stage.CreateInMemory()UsdGeom.SetStageUpAxis(temp_stage,UsdGeom.Tokens.z)UsdGeom.SetStageMetersPerUnit(temp_stage,1.0)UsdPhysics.SetStageKilogramsPerUnit(temp_stage,1.0)# Add mesh to stagebase_prim=temp_stage.DefinePrim(f"/{mesh_file_basename}","Xform")prim=temp_stage.DefinePrim(f"/{mesh_file_basename}/geometry","Xform")prim.GetReferences().AddReference(self.usd_path)temp_stage.SetDefaultPrim(base_prim)temp_stage.Export(self.usd_path)# Open converted USD stagestage=Usd.Stage.Open(self.usd_path)# Need to reload the stage to get the new prim structure, otherwise it can be taken from the cachestage.Reload()# Add USD to stage cachestage_id=UsdUtils.StageCache.Get().Insert(stage)# Get the default prim (which is the root prim) -- "/{mesh_file_basename}"xform_prim=stage.GetDefaultPrim()geom_prim=stage.GetPrimAtPath(f"/{mesh_file_basename}/geometry")# Move all meshes to underneath new Xformforchild_mesh_primingeom_prim.GetChildren():ifchild_mesh_prim.GetTypeName()=="Mesh":# Apply collider properties to meshifcfg.collision_propsisnotNone:# -- Collision approximation to mesh# TODO: Move this to a new Schema: https://github.com/isaac-orbit/IsaacLab/issues/163mesh_collision_api=UsdPhysics.MeshCollisionAPI.Apply(child_mesh_prim)mesh_collision_api.GetApproximationAttr().Set(cfg.collision_approximation)# -- Collider properties such as offset, scale, etc.schemas.define_collision_properties(prim_path=child_mesh_prim.GetPath(),cfg=cfg.collision_props,stage=stage)# Delete the old Xform and make the new Xform the default primstage.SetDefaultPrim(xform_prim)# Apply default Xform rotation to mesh -> enable to set rotation and scaleomni.kit.commands.execute("CreateDefaultXformOnPrimCommand",prim_path=xform_prim.GetPath(),**{"stage":stage},)# Apply translation, rotation, and scale to the Xformgeom_xform=UsdGeom.Xform(geom_prim)geom_xform.ClearXformOpOrder()# Remove any existing rotation attributesrotate_attr=geom_prim.GetAttribute("xformOp:rotateXYZ")ifrotate_attr:geom_prim.RemoveProperty(rotate_attr.GetName())# translationtranslate_op=geom_xform.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble)translate_op.Set(Gf.Vec3d(*cfg.translation))# rotationorient_op=geom_xform.AddOrientOp(UsdGeom.XformOp.PrecisionDouble)orient_op.Set(Gf.Quatd(*cfg.rotation))# scalescale_op=geom_xform.AddScaleOp(UsdGeom.XformOp.PrecisionDouble)scale_op.Set(Gf.Vec3d(*cfg.scale))# Handle instanceable# Create a new Xform prim that will be the prototype primifcfg.make_instanceable:# Export Xform to a file so we can reference it from all instancesexport_prim_to_file(path=os.path.join(self.usd_dir,self.usd_instanceable_meshes_path),source_prim_path=geom_prim.GetPath(),stage=stage,)# Delete the original prim that will now be a referencegeom_prim_path=geom_prim.GetPath().pathStringomni.kit.commands.execute("DeletePrims",paths=[geom_prim_path],stage=stage)# Update references to exported Xform and make it instanceablegeom_undef_prim=stage.DefinePrim(geom_prim_path)geom_undef_prim.GetReferences().AddReference(self.usd_instanceable_meshes_path,primPath=geom_prim_path)geom_undef_prim.SetInstanceable(True)# Apply mass and rigid body properties after everything else# Properties are applied to the top level prim to avoid the case where all instances of this# asset unintentionally share the same rigid body properties# apply mass propertiesifcfg.mass_propsisnotNone:schemas.define_mass_properties(prim_path=xform_prim.GetPath(),cfg=cfg.mass_props,stage=stage)# apply rigid body propertiesifcfg.rigid_propsisnotNone:schemas.define_rigid_body_properties(prim_path=xform_prim.GetPath(),cfg=cfg.rigid_props,stage=stage)# Save changes to USD stagestage.Save()ifstage_idisnotNone:UsdUtils.StageCache.Get().Erase(stage_id)""" Helper methods. """@staticmethodasyncdef_convert_mesh_to_usd(in_file:str,out_file:str,load_materials:bool=True)->bool:"""Convert mesh from supported file types to USD. This function uses the Omniverse Asset Converter extension to convert a mesh file to USD. It is an asynchronous function and should be called using `asyncio.get_event_loop().run_until_complete()`. The converted asset is stored in the USD format in the specified output file. The USD file has Y-up axis and is scaled to cm. Args: in_file: The file to convert. out_file: The path to store the output file. load_materials: Set to True to enable attaching materials defined in the input file to the generated USD mesh. Defaults to True. Returns: True if the conversion succeeds. """enable_extension("omni.kit.asset_converter")importomni.kit.asset_converterimportomni.usd# Create converter contextconverter_context=omni.kit.asset_converter.AssetConverterContext()# Set up converter settings# Don't import/export materialsconverter_context.ignore_materials=notload_materialsconverter_context.ignore_animations=Trueconverter_context.ignore_camera=Trueconverter_context.ignore_light=True# Merge all meshes into oneconverter_context.merge_all_meshes=True# Sets world units to meters, this will also scale asset if it's centimeters model.# This does not work right now :(, so we need to scale the mesh manuallyconverter_context.use_meter_as_world_unit=Trueconverter_context.baking_scales=True# Uses double precision for all transform ops.converter_context.use_double_precision_to_usd_transform_op=True# Create converter taskinstance=omni.kit.asset_converter.get_instance()task=instance.create_converter_task(in_file,out_file,None,converter_context)# Start conversion task and wait for it to finishsuccess=awaittask.wait_until_finished()ifnotsuccess:raiseRuntimeError(f"Failed to convert {in_file} to USD. Error: {task.get_error_message()}")returnsuccess