Unverified Commit c3372f10 authored by Pascal Roth's avatar Pascal Roth Committed by GitHub

Adds option to scale/translate/rotate meshes in the `mesh_converter` (#1228)

# Description

Meshes can be generated with Y up-axis and in cm format. Currently,
these meshes would not be rotated or scaled when loading in our stage,
which is Z up and m scale. This PR allows scaling and rotating meshes
during the mesh converter process.

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
parent 10e7beca
[package] [package]
# Note: Semantic Versioning is used: https://semver.org/ # Note: Semantic Versioning is used: https://semver.org/
version = "0.27.12" version = "0.27.13"
# Description # Description
title = "Isaac Lab framework for Robot Learning" title = "Isaac Lab framework for Robot Learning"
......
Changelog Changelog
--------- ---------
0.27.13 (2024-10-30)
~~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added the attributes :attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.translation`, :attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.rotation`,
:attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.scale` to translate, rotate, and scale meshes
when importing them with :class:`~omni.isaac.lab.sim.converters.MeshConverter`.
0.27.12 (2024-01-04) 0.27.12 (2024-01-04)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
......
...@@ -10,7 +10,7 @@ import omni ...@@ -10,7 +10,7 @@ import omni
import omni.kit.commands import omni.kit.commands
import omni.usd import omni.usd
from omni.isaac.core.utils.extensions import enable_extension from omni.isaac.core.utils.extensions import enable_extension
from pxr import Tf, Usd, UsdGeom, UsdPhysics, UsdUtils from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils
from omni.isaac.lab.sim.converters.asset_converter_base import AssetConverterBase from omni.isaac.lab.sim.converters.asset_converter_base import AssetConverterBase
from omni.isaac.lab.sim.converters.mesh_converter_cfg import MeshConverterCfg from omni.isaac.lab.sim.converters.mesh_converter_cfg import MeshConverterCfg
...@@ -64,12 +64,13 @@ class MeshConverter(AssetConverterBase): ...@@ -64,12 +64,13 @@ class MeshConverter(AssetConverterBase):
def _convert_asset(self, cfg: MeshConverterCfg): def _convert_asset(self, cfg: MeshConverterCfg):
"""Generate USD from OBJ, STL or FBX. """Generate USD from OBJ, STL or FBX.
It stores the asset in the following format: The USD file has Y-up axis and is scaled to meters.
The asset hierarchy is arranged as follows:
/file_name (default prim) .. code-block:: none
|- /geometry <- Made instanceable if requested mesh_file_basename (default prim)
|- /Looks |- /geometry/Looks
|- /mesh |- /geometry/mesh
Args: Args:
cfg: The configuration for conversion of mesh to USD. cfg: The configuration for conversion of mesh to USD.
...@@ -93,15 +94,25 @@ class MeshConverter(AssetConverterBase): ...@@ -93,15 +94,25 @@ class MeshConverter(AssetConverterBase):
# Convert USD # Convert USD
asyncio.get_event_loop().run_until_complete( asyncio.get_event_loop().run_until_complete(
self._convert_mesh_to_usd( self._convert_mesh_to_usd(in_file=cfg.asset_path, out_file=self.usd_path)
in_file=cfg.asset_path, out_file=self.usd_path, prim_path=f"/{mesh_file_basename}"
)
) )
# Create a new stage, set Z up and meters per unit
temp_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 stage
base_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 stage # Open converted USD stage
# note: This opens a new stage and does not use the stage created earlier by the user
# create a new stage
stage = Usd.Stage.Open(self.usd_path) stage = Usd.Stage.Open(self.usd_path)
# add USD to stage cache # Need to reload the stage to get the new prim structure, otherwise it can be taken from the cache
stage.Reload()
# Add USD to stage cache
stage_id = UsdUtils.StageCache.Get().Insert(stage) stage_id = UsdUtils.StageCache.Get().Insert(stage)
# Get the default prim (which is the root prim) -- "/{mesh_file_basename}" # Get the default prim (which is the root prim) -- "/{mesh_file_basename}"
xform_prim = stage.GetDefaultPrim() xform_prim = stage.GetDefaultPrim()
...@@ -121,6 +132,32 @@ class MeshConverter(AssetConverterBase): ...@@ -121,6 +132,32 @@ class MeshConverter(AssetConverterBase):
) )
# Delete the old Xform and make the new Xform the default prim # Delete the old Xform and make the new Xform the default prim
stage.SetDefaultPrim(xform_prim) stage.SetDefaultPrim(xform_prim)
# Apply default Xform rotation to mesh -> enable to set rotation and scale
omni.kit.commands.execute(
"CreateDefaultXformOnPrimCommand",
prim_path=xform_prim.GetPath(),
**{"stage": stage},
)
# Apply translation, rotation, and scale to the Xform
geom_xform = UsdGeom.Xform(geom_prim)
geom_xform.ClearXformOpOrder()
# Remove any existing rotation attributes
rotate_attr = geom_prim.GetAttribute("xformOp:rotateXYZ")
if rotate_attr:
geom_prim.RemoveProperty(rotate_attr.GetName())
# translation
translate_op = geom_xform.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble)
translate_op.Set(Gf.Vec3d(*cfg.translation))
# rotation
orient_op = geom_xform.AddOrientOp(UsdGeom.XformOp.PrecisionDouble)
orient_op.Set(Gf.Quatd(*cfg.rotation))
# scale
scale_op = geom_xform.AddScaleOp(UsdGeom.XformOp.PrecisionDouble)
scale_op.Set(Gf.Vec3d(*cfg.scale))
# Handle instanceable # Handle instanceable
# Create a new Xform prim that will be the prototype prim # Create a new Xform prim that will be the prototype prim
if cfg.make_instanceable: if cfg.make_instanceable:
...@@ -158,28 +195,18 @@ class MeshConverter(AssetConverterBase): ...@@ -158,28 +195,18 @@ class MeshConverter(AssetConverterBase):
""" """
@staticmethod @staticmethod
async def _convert_mesh_to_usd( async def _convert_mesh_to_usd(in_file: str, out_file: str, load_materials: bool = True) -> bool:
in_file: str, out_file: str, prim_path: str = "/World", load_materials: bool = True
) -> bool:
"""Convert mesh from supported file types to USD. """Convert mesh from supported file types to USD.
This function uses the Omniverse Asset Converter extension to convert a mesh file 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()`. 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 converted asset is stored in the USD format in the specified output file.
The USD file has Y-up axis and is scaled to meters. The USD file has Y-up axis and is scaled to cm.
The asset hierarchy is arranged as follows:
.. code-block:: none
prim_path (default prim)
|- /geometry/Looks
|- /geometry/mesh
Args: Args:
in_file: The file to convert. in_file: The file to convert.
out_file: The path to store the output file. out_file: The path to store the output file.
prim_path: The prim path of the mesh.
load_materials: Set to True to enable attaching materials defined in the input file load_materials: Set to True to enable attaching materials defined in the input file
to the generated USD mesh. Defaults to True. to the generated USD mesh. Defaults to True.
...@@ -187,11 +214,9 @@ class MeshConverter(AssetConverterBase): ...@@ -187,11 +214,9 @@ class MeshConverter(AssetConverterBase):
True if the conversion succeeds. True if the conversion succeeds.
""" """
enable_extension("omni.kit.asset_converter") enable_extension("omni.kit.asset_converter")
enable_extension("omni.usd.metrics.assembler")
import omni.kit.asset_converter import omni.kit.asset_converter
import omni.usd import omni.usd
from omni.metrics.assembler.core import get_metrics_assembler_interface
# Create converter context # Create converter context
converter_context = omni.kit.asset_converter.AssetConverterContext() converter_context = omni.kit.asset_converter.AssetConverterContext()
...@@ -212,29 +237,9 @@ class MeshConverter(AssetConverterBase): ...@@ -212,29 +237,9 @@ class MeshConverter(AssetConverterBase):
# Create converter task # Create converter task
instance = omni.kit.asset_converter.get_instance() instance = omni.kit.asset_converter.get_instance()
out_file_non_metric = out_file.replace(".usd", "_non_metric.usd") task = instance.create_converter_task(in_file, out_file, None, converter_context)
task = instance.create_converter_task(in_file, out_file_non_metric, None, converter_context)
# Start conversion task and wait for it to finish # Start conversion task and wait for it to finish
success = True
while True:
success = await task.wait_until_finished() success = await task.wait_until_finished()
if not success: if not success:
await asyncio.sleep(0.1) raise RuntimeError(f"Failed to convert {in_file} to USD. Error: {task.get_error_message()}")
else:
break
temp_stage = Usd.Stage.CreateInMemory()
UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.Tokens.z)
UsdGeom.SetStageMetersPerUnit(temp_stage, 1.0)
UsdPhysics.SetStageKilogramsPerUnit(temp_stage, 1.0)
base_prim = temp_stage.DefinePrim(prim_path, "Xform")
prim = temp_stage.DefinePrim(f"{prim_path}/geometry", "Xform")
prim.GetReferences().AddReference(out_file_non_metric)
cache = UsdUtils.StageCache.Get()
cache.Insert(temp_stage)
stage_id = cache.GetId(temp_stage).ToLongInt()
get_metrics_assembler_interface().resolve_stage(stage_id)
temp_stage.SetDefaultPrim(base_prim)
temp_stage.Export(out_file)
return success return success
...@@ -12,21 +12,21 @@ from omni.isaac.lab.utils import configclass ...@@ -12,21 +12,21 @@ from omni.isaac.lab.utils import configclass
class MeshConverterCfg(AssetConverterBaseCfg): class MeshConverterCfg(AssetConverterBaseCfg):
"""The configuration class for MeshConverter.""" """The configuration class for MeshConverter."""
mass_props: schemas_cfg.MassPropertiesCfg = None mass_props: schemas_cfg.MassPropertiesCfg | None = None
"""Mass properties to apply to the USD. Defaults to None. """Mass properties to apply to the USD. Defaults to None.
Note: Note:
If None, then no mass properties will be added. If None, then no mass properties will be added.
""" """
rigid_props: schemas_cfg.RigidBodyPropertiesCfg = None rigid_props: schemas_cfg.RigidBodyPropertiesCfg | None = None
"""Rigid body properties to apply to the USD. Defaults to None. """Rigid body properties to apply to the USD. Defaults to None.
Note: Note:
If None, then no rigid body properties will be added. If None, then no rigid body properties will be added.
""" """
collision_props: schemas_cfg.CollisionPropertiesCfg = None collision_props: schemas_cfg.CollisionPropertiesCfg | None = None
"""Collision properties to apply to the USD. Defaults to None. """Collision properties to apply to the USD. Defaults to None.
Note: Note:
...@@ -42,3 +42,12 @@ class MeshConverterCfg(AssetConverterBaseCfg): ...@@ -42,3 +42,12 @@ class MeshConverterCfg(AssetConverterBaseCfg):
"none" causes no collision mesh to be added. "none" causes no collision mesh to be added.
""" """
translation: tuple[float, float, float] = (0.0, 0.0, 0.0)
"""The translation of the mesh to the origin. Defaults to (0.0, 0.0, 0.0)."""
rotation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
"""The rotation of the mesh in quaternion format (w, x, y, z). Defaults to (1.0, 0.0, 0.0, 0.0)."""
scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
"""The scale of the mesh. Defaults to (1.0, 1.0, 1.0)."""
...@@ -12,7 +12,9 @@ simulation_app = AppLauncher(headless=True).app ...@@ -12,7 +12,9 @@ simulation_app = AppLauncher(headless=True).app
"""Rest everything follows.""" """Rest everything follows."""
import math
import os import os
import random
import tempfile import tempfile
import unittest import unittest
...@@ -27,6 +29,16 @@ from omni.isaac.lab.sim.schemas import schemas_cfg ...@@ -27,6 +29,16 @@ from omni.isaac.lab.sim.schemas import schemas_cfg
from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path
def random_quaternion():
# Generate four random numbers for the quaternion
u1, u2, u3 = random.random(), random.random(), random.random()
w = math.sqrt(1 - u1) * math.sin(2 * math.pi * u2)
x = math.sqrt(1 - u1) * math.cos(2 * math.pi * u2)
y = math.sqrt(u1) * math.sin(2 * math.pi * u3)
z = math.sqrt(u1) * math.cos(2 * math.pi * u3)
return (w, x, y, z)
class TestMeshConverter(unittest.TestCase): class TestMeshConverter(unittest.TestCase):
"""Test fixture for the MeshConverter class.""" """Test fixture for the MeshConverter class."""
...@@ -105,7 +117,12 @@ class TestMeshConverter(unittest.TestCase): ...@@ -105,7 +117,12 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_obj(self): def test_convert_obj(self):
"""Convert an OBJ file""" """Convert an OBJ file"""
mesh_config = MeshConverterCfg(asset_path=self.assets["obj"]) mesh_config = MeshConverterCfg(
asset_path=self.assets["obj"],
scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)),
translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)),
rotation=random_quaternion(),
)
mesh_converter = MeshConverter(mesh_config) mesh_converter = MeshConverter(mesh_config)
# check that mesh conversion is successful # check that mesh conversion is successful
...@@ -113,7 +130,12 @@ class TestMeshConverter(unittest.TestCase): ...@@ -113,7 +130,12 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_stl(self): def test_convert_stl(self):
"""Convert an STL file""" """Convert an STL file"""
mesh_config = MeshConverterCfg(asset_path=self.assets["stl"]) mesh_config = MeshConverterCfg(
asset_path=self.assets["stl"],
scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)),
translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)),
rotation=random_quaternion(),
)
mesh_converter = MeshConverter(mesh_config) mesh_converter = MeshConverter(mesh_config)
# check that mesh conversion is successful # check that mesh conversion is successful
...@@ -121,12 +143,24 @@ class TestMeshConverter(unittest.TestCase): ...@@ -121,12 +143,24 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_fbx(self): def test_convert_fbx(self):
"""Convert an FBX file""" """Convert an FBX file"""
mesh_config = MeshConverterCfg(asset_path=self.assets["fbx"]) mesh_config = MeshConverterCfg(
asset_path=self.assets["fbx"],
scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)),
translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)),
rotation=random_quaternion(),
)
mesh_converter = MeshConverter(mesh_config) mesh_converter = MeshConverter(mesh_config)
# check that mesh conversion is successful # check that mesh conversion is successful
self._check_mesh_conversion(mesh_converter) self._check_mesh_conversion(mesh_converter)
def test_convert_default_xform_transforms(self):
"""Convert an OBJ file and check that default xform transforms are applied correctly"""
mesh_config = MeshConverterCfg(asset_path=self.assets["obj"])
mesh_converter = MeshConverter(mesh_config)
# check that mesh conversion is successful
self._check_mesh_conversion(mesh_converter)
def test_collider_no_approximation(self): def test_collider_no_approximation(self):
"""Convert an OBJ file using no approximation""" """Convert an OBJ file using no approximation"""
collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True)
...@@ -229,6 +263,15 @@ class TestMeshConverter(unittest.TestCase): ...@@ -229,6 +263,15 @@ class TestMeshConverter(unittest.TestCase):
units = UsdGeom.GetStageMetersPerUnit(stage) units = UsdGeom.GetStageMetersPerUnit(stage)
self.assertEqual(units, 1.0) self.assertEqual(units, 1.0)
# Check mesh settings
pos = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:translate").Get())
self.assertEqual(pos, mesh_converter.cfg.translation)
quat = prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:orient").Get()
quat = (quat.GetReal(), quat.GetImaginary()[0], quat.GetImaginary()[1], quat.GetImaginary()[2])
self.assertEqual(quat, mesh_converter.cfg.rotation)
scale = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:scale").Get())
self.assertEqual(scale, mesh_converter.cfg.scale)
def _check_mesh_collider_settings(self, mesh_converter: MeshConverter): def _check_mesh_collider_settings(self, mesh_converter: MeshConverter):
# Check prim can be properly spawned # Check prim can be properly spawned
prim_path = "/World/Object" prim_path = "/World/Object"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment