Skip to content

Add support for spawning meshes from obj and stl files #2379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Guidelines for modifications:
* Vladimir Fokow
* Wei Yang
* Xavier Nal
* Xudong Han
* Yang Jin
* Yanzi Zhu
* Yijie Guo
Expand Down
6 changes: 6 additions & 0 deletions docs/source/api/lab/isaaclab.sim.spawners.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ From Files
:members:
:exclude-members: __init__, func

.. autofunction:: spawn_from_mesh_file

.. autoclass:: MeshFileCfg
:members:
:exclude-members: __init__, func

.. autofunction:: spawn_ground_plane

.. autoclass:: GroundPlaneCfg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@

"""

from .from_files import spawn_from_urdf, spawn_from_usd, spawn_ground_plane
from .from_files_cfg import GroundPlaneCfg, UrdfFileCfg, UsdFileCfg
from .from_files import spawn_from_urdf, spawn_from_usd, spawn_from_mesh_file, spawn_ground_plane
from .from_files_cfg import GroundPlaneCfg, UrdfFileCfg, UsdFileCfg, MeshFileCfg
121 changes: 120 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@

from __future__ import annotations

import numpy as np
import trimesh
from typing import TYPE_CHECKING

import isaacsim.core.utils.prims as prim_utils
import isaacsim.core.utils.stage as stage_utils
import omni.kit.commands
import omni.log
from pxr import Gf, Sdf, Semantics, Usd
from pxr import Gf, Sdf, Semantics, Usd, UsdPhysics

from isaaclab.sim import converters, schemas
from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone, select_usd_variants

from ..materials import DeformableBodyMaterialCfg, RigidBodyMaterialCfg

if TYPE_CHECKING:
from . import from_files_cfg

Expand Down Expand Up @@ -101,6 +105,121 @@ def spawn_from_urdf(
# spawn asset from the generated usd file
return _spawn_from_usd_file(prim_path, urdf_loader.usd_path, cfg, translation, orientation)

@clone
def spawn_from_mesh_file(
prim_path: str,
cfg: from_files_cfg.MeshFileCfg,
translation: tuple[float, float, float] | None = None,
orientation: tuple[float, float, float, float] | None = None,
) -> Usd.Prim:
"""Create a USD-Mesh prim from the given mesh file.

.. note::
This function is decorated with :func:`clone` that resolves prim path into list of paths
if the input prim path is a regex pattern. This is done to support spawning multiple assets
from a single and cloning the USD prim at the given path expression.

Args:
prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern,
then the asset is spawned at all the matching prim paths.
cfg: The configuration instance.
translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case
this is set to the origin.
orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None,
in which case this is set to identity.

Returns:
The created prim.

Raises:
ValueError: If a prim already exists at the given path.
"""
# load the mesh from file
mesh = trimesh.load_mesh(cfg.file_path)
# spawn geometry if it doesn't exist.
if not prim_utils.is_prim_path_valid(prim_path):
prim_utils.create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation)
else:
raise ValueError(f"A prim already exists at path: '{prim_path}'.")

# check that invalid schema types are not used
if cfg.deformable_props is not None and cfg.rigid_props is not None:
raise ValueError("Cannot use both deformable and rigid properties at the same time.")
if cfg.deformable_props is not None and cfg.collision_props is not None:
raise ValueError("Cannot use both deformable and collision properties at the same time.")
# check material types are correct
if cfg.deformable_props is not None and cfg.physics_material is not None:
if not isinstance(cfg.physics_material, DeformableBodyMaterialCfg):
raise ValueError("Deformable properties require a deformable physics material.")
if cfg.rigid_props is not None and cfg.physics_material is not None:
if not isinstance(cfg.physics_material, RigidBodyMaterialCfg):
raise ValueError("Rigid properties require a rigid physics material.")

# create all the paths we need for clarity
geom_prim_path = prim_path + "/geometry"
mesh_prim_path = geom_prim_path + "/mesh"

# create the mesh prim
mesh_prim = prim_utils.create_prim(
mesh_prim_path,
prim_type="Mesh",
scale=cfg.scale,
attributes={
"points": mesh.vertices,
"faceVertexIndices": mesh.faces.flatten(),
"faceVertexCounts": np.asarray([3] * len(mesh.faces)),
"subdivisionScheme": "bilinear",
},
)

# note: in case of deformable objects, we need to apply the deformable properties to the mesh prim.
# this is different from rigid objects where we apply the properties to the parent prim.
if cfg.deformable_props is not None:
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(mesh_prim_path, cfg.mass_props)
# apply deformable body properties
schemas.define_deformable_body_properties(mesh_prim_path, cfg.deformable_props)
elif cfg.collision_props is not None:
collision_approximation = "convexHull"
# apply collision approximation to mesh
# note: for primitives, we use the convex hull approximation -- this should be sufficient for most cases.
mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(mesh_prim)
mesh_collision_api.GetApproximationAttr().Set(collision_approximation)
# apply collision properties
schemas.define_collision_properties(mesh_prim_path, cfg.collision_props)

# apply visual material
if cfg.visual_material is not None:
if not cfg.visual_material_path.startswith("/"):
material_path = f"{geom_prim_path}/{cfg.visual_material_path}"
else:
material_path = cfg.visual_material_path
# create material
cfg.visual_material.func(material_path, cfg.visual_material)
# apply material
bind_visual_material(mesh_prim_path, material_path)

# apply physics material
if cfg.physics_material is not None:
if not cfg.physics_material_path.startswith("/"):
material_path = f"{geom_prim_path}/{cfg.physics_material_path}"
else:
material_path = cfg.physics_material_path
# create material
cfg.physics_material.func(material_path, cfg.physics_material)
# apply material
bind_physics_material(mesh_prim_path, material_path)

# note: we apply the rigid properties to the parent prim in case of rigid objects.
if cfg.rigid_props is not None:
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path, cfg.mass_props)
# apply rigid properties
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props)
# return the prim
return prim_utils.get_prim_at_path(prim_path)

def spawn_ground_plane(
prim_path: str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
Note:
If None, then no visual material will be added.
"""

physics_material_path: str = "material"
"""Path to the physics material to use for the prim. Defaults to "material".

If the path is relative, then it will be relative to the prim's path.
This parameter is ignored if `physics_material` is not None.
"""

physics_material: materials.PhysicsMaterialCfg | None = None
"""Physics material properties.

Note:
If None, then no physics material will be added.
"""


@configclass
Expand Down Expand Up @@ -129,6 +143,29 @@ class UrdfFileCfg(FileCfg, converters.UrdfConverterCfg):
func: Callable = from_files.spawn_from_urdf


@configclass
class MeshFileCfg(FileCfg):
"""Mesh file to spawn asset from.

See :meth:`spawn_mesh_file` for more information.

.. note::
The configuration parameters include various properties. If not `None`, these properties
are modified on the spawned prim in a nested manner.

If they are set to a value, then the properties are modified on the spawned prim in a nested manner.
This is done by calling the respective function with the specified properties.

"""

func: Callable = from_files.spawn_from_mesh_file

file_path: str = MISSING
"""Path to the mesh file (in OBJ, STL, or FBX format)."""

scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
"""Scale of the mesh (in m)."""

"""
Spawning ground plane.
"""
Expand Down
3 changes: 2 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ def spawn_mesh_cuboid(

Raises:
ValueError: If a prim already exists at the given path.
""" # create a trimesh box
"""
# create a trimesh box
box = trimesh.creation.box(cfg.size)
# spawn the cuboid as a mesh
_spawn_mesh_geom_from_mesh(prim_path, cfg, box, translation, orientation, None)
Expand Down