diff --git a/source/isaaclab/changelog.d/sylvesterkaczmarek-cone-primitive-axis.rst b/source/isaaclab/changelog.d/sylvesterkaczmarek-cone-primitive-axis.rst new file mode 100644 index 00000000000..b0f0080c14a --- /dev/null +++ b/source/isaaclab/changelog.d/sylvesterkaczmarek-cone-primitive-axis.rst @@ -0,0 +1,4 @@ +Fixed +^^^^^ + +* Fixed USD cone primitive conversion ignoring the configured ``axis`` and returning X/Y cones aligned to Z. diff --git a/source/isaaclab/isaaclab/utils/mesh.py b/source/isaaclab/isaaclab/utils/mesh.py index 9e6315cc83c..2abfea2c8e7 100644 --- a/source/isaaclab/isaaclab/utils/mesh.py +++ b/source/isaaclab/isaaclab/utils/mesh.py @@ -167,6 +167,14 @@ def _create_cone_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: mesh = trimesh.creation.cone(radius=radius, height=height) # shift all vertices down by height/2 for usd / trimesh cone primitive definition discrepancy mesh.apply_translation((0.0, 0.0, -height / 2.0)) + axis = prim.GetAttribute("axis").Get() + if axis == "X": + # USD cones point their apex along the positive configured axis. + R = trimesh.transformations.rotation_matrix(np.radians(90), [0, 1, 0]) + mesh.apply_transform(R) + elif axis == "Y": + R = trimesh.transformations.rotation_matrix(np.radians(-90), [1, 0, 0]) + mesh.apply_transform(R) return mesh diff --git a/source/isaaclab/test/utils/test_mesh_cone_axis.py b/source/isaaclab/test/utils/test_mesh_cone_axis.py new file mode 100644 index 00000000000..7f250fea6f1 --- /dev/null +++ b/source/isaaclab/test/utils/test_mesh_cone_axis.py @@ -0,0 +1,47 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np +import pytest + +from isaaclab.utils.mesh import create_trimesh_from_geom_shape + +pytestmark = pytest.mark.unit + + +class _Attribute: + def __init__(self, value): + self._value = value + + def Get(self): + return self._value + + +class _ConePrim: + def __init__(self, axis: str, radius: float = 1.0, height: float = 4.0): + self._attributes = {"axis": axis, "radius": radius, "height": height} + + def GetTypeName(self): + return "Cone" + + def GetAttribute(self, name): + return _Attribute(self._attributes[name]) + + def GetPath(self): + return "/World/Cone" + + +@pytest.mark.parametrize(("axis", "axis_index"), [("X", 0), ("Y", 1), ("Z", 2)]) +def test_cone_primitive_respects_axis_and_apex_direction(axis, axis_index): + height = 4.0 + mesh = create_trimesh_from_geom_shape(_ConePrim(axis=axis, height=height)) + + vertices = np.asarray(mesh.vertices) + apex = vertices[np.argmax(vertices[:, axis_index])] + transverse_indices = [index for index in range(3) if index != axis_index] + + assert np.isclose(apex[axis_index], height / 2.0) + np.testing.assert_allclose(apex[transverse_indices], 0.0, atol=1e-7) + assert np.isclose(vertices[:, axis_index].min(), -height / 2.0)