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]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.27.12"
version = "0.27.13"
# Description
title = "Isaac Lab framework for Robot Learning"
......
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)
~~~~~~~~~~~~~~~~~~~
......
......@@ -10,7 +10,7 @@ import omni
import omni.kit.commands
import omni.usd
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.mesh_converter_cfg import MeshConverterCfg
......@@ -64,12 +64,13 @@ class MeshConverter(AssetConverterBase):
def _convert_asset(self, cfg: MeshConverterCfg):
"""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)
|- /geometry <- Made instanceable if requested
|- /Looks
|- /mesh
.. code-block:: none
mesh_file_basename (default prim)
|- /geometry/Looks
|- /geometry/mesh
Args:
cfg: The configuration for conversion of mesh to USD.
......@@ -93,15 +94,25 @@ class MeshConverter(AssetConverterBase):
# Convert USD
asyncio.get_event_loop().run_until_complete(
self._convert_mesh_to_usd(
in_file=cfg.asset_path, out_file=self.usd_path, prim_path=f"/{mesh_file_basename}"
)
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 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
# 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)
# 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)
# Get the default prim (which is the root prim) -- "/{mesh_file_basename}"
xform_prim = stage.GetDefaultPrim()
......@@ -121,6 +132,32 @@ class MeshConverter(AssetConverterBase):
)
# Delete the old Xform and make the new Xform the default 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
# Create a new Xform prim that will be the prototype prim
if cfg.make_instanceable:
......@@ -158,28 +195,18 @@ class MeshConverter(AssetConverterBase):
"""
@staticmethod
async def _convert_mesh_to_usd(
in_file: str, out_file: str, prim_path: str = "/World", load_materials: bool = True
) -> bool:
async def _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 meters.
The asset hierarchy is arranged as follows:
.. code-block:: none
prim_path (default prim)
|- /geometry/Looks
|- /geometry/mesh
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.
prim_path: The prim path of the mesh.
load_materials: Set to True to enable attaching materials defined in the input file
to the generated USD mesh. Defaults to True.
......@@ -187,11 +214,9 @@ class MeshConverter(AssetConverterBase):
True if the conversion succeeds.
"""
enable_extension("omni.kit.asset_converter")
enable_extension("omni.usd.metrics.assembler")
import omni.kit.asset_converter
import omni.usd
from omni.metrics.assembler.core import get_metrics_assembler_interface
# Create converter context
converter_context = omni.kit.asset_converter.AssetConverterContext()
......@@ -212,29 +237,9 @@ class MeshConverter(AssetConverterBase):
# Create converter task
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_non_metric, None, converter_context)
task = instance.create_converter_task(in_file, out_file, None, converter_context)
# Start conversion task and wait for it to finish
success = True
while True:
success = await task.wait_until_finished()
if not success:
await asyncio.sleep(0.1)
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)
success = await task.wait_until_finished()
if not success:
raise RuntimeError(f"Failed to convert {in_file} to USD. Error: {task.get_error_message()}")
return success
......@@ -12,21 +12,21 @@ from omni.isaac.lab.utils import configclass
class MeshConverterCfg(AssetConverterBaseCfg):
"""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.
Note:
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.
Note:
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.
Note:
......@@ -42,3 +42,12 @@ class MeshConverterCfg(AssetConverterBaseCfg):
"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
"""Rest everything follows."""
import math
import os
import random
import tempfile
import unittest
......@@ -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
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):
"""Test fixture for the MeshConverter class."""
......@@ -105,7 +117,12 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_obj(self):
"""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)
# check that mesh conversion is successful
......@@ -113,7 +130,12 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_stl(self):
"""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)
# check that mesh conversion is successful
......@@ -121,12 +143,24 @@ class TestMeshConverter(unittest.TestCase):
def test_convert_fbx(self):
"""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)
# check that mesh conversion is successful
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):
"""Convert an OBJ file using no approximation"""
collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True)
......@@ -229,6 +263,15 @@ class TestMeshConverter(unittest.TestCase):
units = UsdGeom.GetStageMetersPerUnit(stage)
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):
# Check prim can be properly spawned
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