Skip to content
Merged
2 changes: 1 addition & 1 deletion scripts/demos/deformables.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def design_scene() -> tuple[dict, list[list[float]]]:
)
cfg_cloth = sim_utils.MeshRectangleCfg(
size=(1.5, 1.0),
resolution=(21, 21),
edge_refinement=21,
deformable_props=DeformableBodyPropertiesCfg(),
visual_material=sim_utils.PreviewSurfaceCfg(),
physics_material=SurfaceDeformableMaterialCfg(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Added
^^^^^

* Added ``MeshCfg.edge_refinement``, defaulting to ``4.0``, to control surface mesh resolution for deformable
primitives and the automatically generated tetrahedral mesh resolution for closed volume deformables. It is ignored
when ``deformable_props`` is None, since rigid primitive collision approximations are invariant to surface
subdivision.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Changed
^^^^^^^

* Changed ``MeshCuboidCfg.edge_refinement``, added in 15.6.0 and now generalized to ``MeshCfg.edge_refinement``, to
apply only when ``deformable_props`` is set. Rigid primitives are no longer subdivided; their surface and collision
approximation are unaffected, since subdivision only inserted coplanar vertices. Callers relying on a denser rigid
visual mesh must supply their own mesh asset.

Removed
^^^^^^^

* Removed ``MeshRectangleCfg.resolution``. Deformable callers must use ``MeshCfg.edge_refinement`` to bound surface
edge length relative to the bounding-box diagonal. Rigid rectangles are now spawned as two triangles.
5 changes: 4 additions & 1 deletion source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2063,6 +2063,7 @@ def define_deformable_body_properties(
stage: Usd.Stage | None = None,
deformable_type: str = "volume",
sim_mesh_prim_path: str | None = None,
tetrahedralization_edge_length_fac: float = 0.1,
):
"""Apply the deformable body schema on the input prim and set its properties. The input prim should
have a visual surface mesh as child. Volume deformables will have their simulation tetrahedral mesh
Expand Down Expand Up @@ -2092,6 +2093,8 @@ def define_deformable_body_properties(
sim_mesh_prim_path: Optional override for the simulation mesh creation prim path.
Ignored when pre-tetrahedralized mesh is found for volume deformables.
If None, it is set to ``{prim_path}/sim_mesh``.
tetrahedralization_edge_length_fac: Relative target edge length for automatic tetrahedralization.
Defaults to ``0.1``.

Raises:
ValueError: When the prim path is not valid.
Expand Down Expand Up @@ -2221,7 +2224,7 @@ def define_deformable_body_properties(
tet_mesh_points, tet_mesh_indices = tetrahedralize(
vertices,
faces.reshape(-1, 3),
edge_length_fac=0.1,
edge_length_fac=tetrahedralization_edge_length_fac,
simplify=False,
epsilon=1e-2,
coarsen=True,
Expand Down
85 changes: 42 additions & 43 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,9 @@ def spawn_mesh_cuboid(

Raises:
ValueError: If a prim already exists at the given path.
ValueError: If :attr:`~isaaclab.sim.MeshCuboidCfg.edge_refinement` is less than ``1.0``.
"""
if cfg.edge_refinement < 1.0:
raise ValueError(f"Cuboid mesh edge refinement must be at least 1.0, got {cfg.edge_refinement}.")

# create a trimesh box
box = trimesh.creation.box(cfg.size)
if cfg.edge_refinement > 1.0:
max_edge = float(np.linalg.norm(box.bounding_box.extents)) / cfg.edge_refinement
vertices, faces = trimesh.remesh.subdivide_to_size(box.vertices, box.faces, max_edge=max_edge)
box = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)

# obtain stage handle
stage = get_current_stage()
Expand Down Expand Up @@ -302,53 +294,50 @@ def spawn_mesh_rectangle(
Raises:
ValueError: If a prim already exists at the given path.
"""
# create a 2D triangle mesh grid
vertices, faces = _create_triangle_mesh_grid(cfg.resolution)
vertices[:, 0] *= cfg.size[0]
vertices[:, 1] *= cfg.size[1]
grid = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
# create a 2D triangle mesh
half_x, half_y = cfg.size[0] / 2, cfg.size[1] / 2
vertices = np.array(
[(-half_x, -half_y, 0.0), (half_x, -half_y, 0.0), (half_x, half_y, 0.0), (-half_x, half_y, 0.0)],
dtype=np.float32,
)
rectangle = trimesh.Trimesh(vertices=vertices, faces=((0, 1, 2), (0, 2, 3)), process=False)

# obtain stage handle
stage = get_current_stage()
# spawn the rectangle as a mesh
_spawn_mesh_geom_from_mesh(prim_path, cfg, grid, translation, orientation, None, stage=stage)
_spawn_mesh_geom_from_mesh(prim_path, cfg, rectangle, translation, orientation, None, stage=stage)
# return the prim
return stage.GetPrimAtPath(prim_path)


def _create_triangle_mesh_grid(resolution: tuple[int, int]) -> tuple[np.ndarray, np.ndarray]:
"""Create a centered triangle grid for :class:`MeshRectangleCfg`."""
if resolution[0] < 1 or resolution[1] < 1:
raise ValueError(f"Rectangle mesh resolution must be positive, got {resolution}.")

num_x, num_y = resolution
xs = np.linspace(-0.5, 0.5, num_x + 1, dtype=np.float32)
ys = np.linspace(-0.5, 0.5, num_y + 1, dtype=np.float32)
vertices = np.array([(x, y, 0.0) for y in ys for x in xs], dtype=np.float32)

faces = []
row_stride = num_x + 1
for iy in range(num_y):
for ix in range(num_x):
v0 = iy * row_stride + ix
v1 = v0 + 1
v2 = v0 + row_stride
v3 = v2 + 1
if (ix % 2 == 0) != (iy % 2 == 0):
faces.append((v0, v1, v2))
faces.append((v1, v3, v2))
else:
faces.append((v0, v1, v3))
faces.append((v0, v3, v2))

return vertices, np.asarray(faces, dtype=np.int64)


"""
Helper functions.
"""


def _refine_surface_mesh(mesh: trimesh.Trimesh, cfg: meshes_cfg.MeshCfg) -> trimesh.Trimesh:
"""Subdivide a deformable's surface mesh to the configured edge-length target.

Args:
mesh: The mesh to refine.
cfg: The config carrying :attr:`~isaaclab.sim.MeshCfg.edge_refinement`.

Returns:
The refined mesh, or the input mesh when refinement does not apply.

Raises:
ValueError: If the edge refinement is less than ``1.0``.
"""
if cfg.edge_refinement < 1.0:
raise ValueError(f"Mesh edge refinement must be at least 1.0, got {cfg.edge_refinement}.")
if cfg.deformable_props is None or cfg.edge_refinement == 1.0:
return mesh

max_edge = float(np.linalg.norm(mesh.bounding_box.extents)) / cfg.edge_refinement
vertices, faces = trimesh.remesh.subdivide_to_size(mesh.vertices, mesh.faces, max_edge=max_edge)
return trimesh.Trimesh(vertices=vertices, faces=faces, process=False)


def _apply_deformable_collision_props(prim_path: str, collision_props, stage: Usd.Stage) -> None:
"""Apply collision fragments to the simulation mesh of a deformable body.

Expand Down Expand Up @@ -405,13 +394,16 @@ def _spawn_mesh_geom_from_mesh(

Raises:
ValueError: If a prim already exists at the given path.
ValueError: If edge refinement is less than ``1.0``.
ValueError: If both deformable and rigid properties are used.
ValueError: If the physics material is not of the correct type. Deformable properties require a deformable
physics material, and rigid properties require a rigid physics material.
ValueError: If deformable properties are used with non-fragment collision properties.

.. _USDGeomMesh: https://openusd.org/dev/api/class_usd_geom_mesh.html
"""
mesh = _refine_surface_mesh(mesh, cfg)

# obtain stage handle
stage = stage if stage is not None else get_current_stage()

Expand Down Expand Up @@ -471,8 +463,15 @@ def _spawn_mesh_geom_from_mesh(
deformable_type = (
"surface" if isinstance(cfg.physics_material, SurfaceDeformableBodyMaterialBaseCfg) else "volume"
)
deformable_kwargs = {}
if deformable_type == "volume":
deformable_kwargs["tetrahedralization_edge_length_fac"] = 1.0 / cfg.edge_refinement
schemas.define_deformable_body_properties(
prim_path, cfg.deformable_props, stage=stage, deformable_type=deformable_type
prim_path,
cfg.deformable_props,
stage=stage,
deformable_type=deformable_type,
**deformable_kwargs,
)
if cfg.collision_props is not None:
_apply_deformable_collision_props(prim_path, cfg.collision_props, stage)
Expand Down
18 changes: 9 additions & 9 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ class MeshCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
If None, then no physics material will be added.
"""

edge_refinement: float = 4.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Api — Public resolution field removed without deprecation

Introducing edge_refinement here deletes MeshRectangleCfg.resolution, so existing configurations passing resolution=(nx, ny) now fail at construction with a TypeError. Repository guidelines forbid removing a public API without a prior deprecation and migration path. Keep resolution for one release, warn when it is set, and map it onto edge_refinement (with defined precedence) before deleting it.

"""Mesh edge refinement factor for deformable bodies.

The maximum surface edge length is the bounding-box diagonal divided by this value. Volume deformables use the
same normalized target for automatic tetrahedralization. The factor must be at least ``1.0``. For volume
deformables, values near ``1.0`` should be avoided because they can make TetWild tetrahedralization significantly
slower. Defaults to ``4.0``.
"""


@configclass
class MeshSphereCfg(MeshCfg):
Expand All @@ -101,13 +110,6 @@ class MeshCuboidCfg(MeshCfg):
size: tuple[float, float, float] = MISSING
"""Size of the cuboid [m]."""

edge_refinement: float = 1.0
"""Surface edge refinement factor relative to the bounding-box diagonal.

The maximum edge length is the diagonal divided by this value. The factor must be at least
``1.0``. Defaults to ``1.0``, which leaves the base mesh unchanged.
"""


@configclass
class MeshCylinderCfg(MeshCfg):
Expand Down Expand Up @@ -171,5 +173,3 @@ class MeshRectangleCfg(MeshCfg):

size: tuple[float, float] = MISSING
"""Edge lengths of the rectangle along the X and Y axes [m]."""
resolution: tuple[int, int] = (5, 5)
"""Resolution of the rectangle (in elements/edges per side)."""
84 changes: 70 additions & 14 deletions source/isaaclab/test/sim/test_spawn_meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext
from isaaclab.sim.spawners.meshes import meshes as mesh_spawner

pytestmark = [pytest.mark.integration, pytest.mark.isaacsim_ci]

Expand Down Expand Up @@ -107,21 +108,68 @@ def test_spawn_cuboid(sim):
assert len(prim.GetAttribute("faceVertexCounts").Get()) == 12


def test_spawn_cuboid_with_edge_refinement(sim):
"""Test cuboid surface edge refinement."""
size = (1.0, 2.0, 3.0)
edge_refinement = 3.0
cfg = sim_utils.MeshCuboidCfg(size=size, edge_refinement=edge_refinement)
cfg.func("/World/RefinedCube", cfg)

prim = sim.stage.GetPrimAtPath("/World/RefinedCube/geometry/mesh")
def test_mesh_edge_refinement_default():
"""Test the default mesh edge refinement."""
assert sim_utils.MeshCfg().edge_refinement == 4.0
assert not hasattr(sim_utils.MeshRectangleCfg(size=(1.0, 1.0)), "resolution")


@pytest.mark.parametrize(
"cfg_type,kwargs,edge_refinement",
[
(sim_utils.MeshSphereCfg, {"radius": 1.0}, 25.0),
(sim_utils.MeshCuboidCfg, {"size": (1.0, 2.0, 3.0)}, 3.0),
(sim_utils.MeshCylinderCfg, {"radius": 1.0, "height": 2.0}, 3.0),
(sim_utils.MeshCapsuleCfg, {"radius": 1.0, "height": 2.0}, 3.0),
(sim_utils.MeshConeCfg, {"radius": 1.0, "height": 2.0}, 3.0),
(sim_utils.MeshRectangleCfg, {"size": (1.0, 1.0)}, 3.0),
],
)
def test_spawn_mesh_with_edge_refinement(sim, monkeypatch, cfg_type, kwargs, edge_refinement):
"""Test surface edge refinement for deformable mesh primitives."""
monkeypatch.setattr(mesh_spawner.schemas, "define_deformable_body_properties", lambda *a, **k: None)
cfg = cfg_type(**kwargs, edge_refinement=edge_refinement, deformable_props=sim_utils.DeformableBodyPropertiesCfg())
cfg.func("/World/Refined", cfg)
prim = sim.stage.GetPrimAtPath("/World/Refined/geometry/mesh")
points = np.asarray(prim.GetAttribute("points").Get())
faces = np.asarray(prim.GetAttribute("faceVertexIndices").Get()).reshape(-1, 3)
edges = points[faces[:, [0, 1, 1, 2, 2, 0]]].reshape(-1, 2, 3)
max_edge = np.linalg.norm(edges[:, 0] - edges[:, 1], axis=1).max()
diagonal = np.linalg.norm(points.max(axis=0) - points.min(axis=0))

assert max_edge <= diagonal / edge_refinement


@pytest.mark.parametrize(
"cfg_type,geometry_kwargs,refinement_kwargs,physics_material,expected_factor",
[
(sim_utils.MeshCuboidCfg, {"size": (1.0, 1.0, 1.0)}, {}, None, 0.25),
(sim_utils.MeshCuboidCfg, {"size": (1.0, 1.0, 1.0)}, {"edge_refinement": 2.0}, None, 0.5),
(sim_utils.MeshRectangleCfg, {"size": (1.0, 1.0)}, {}, sim_utils.PhysxSurfaceDeformableBodyMaterialCfg(), None),
],
)
def test_edge_refinement_sets_tetrahedralization_resolution(
sim, monkeypatch, cfg_type, geometry_kwargs, refinement_kwargs, physics_material, expected_factor
):
"""Test edge refinement is forwarded to volume tetrahedralization."""
captured_kwargs = {}

def capture_deformable_properties(*args, **kwargs):
captured_kwargs.update(kwargs)

monkeypatch.setattr(mesh_spawner.schemas, "define_deformable_body_properties", capture_deformable_properties)
cfg = cfg_type(
deformable_props=sim_utils.DeformableBodyPropertiesCfg(),
physics_material=physics_material,
**geometry_kwargs,
**refinement_kwargs,
)
cfg.func("/World/Deformable", cfg)

assert len(points) > 8
assert len(faces) > 12
assert np.linalg.norm(edges[:, 0] - edges[:, 1], axis=1).max() <= np.linalg.norm(size) / edge_refinement
if expected_factor is None:
assert "tetrahedralization_edge_length_fac" not in captured_kwargs
else:
assert captured_kwargs["tetrahedralization_edge_length_fac"] == pytest.approx(expected_factor)


def test_spawn_sphere(sim):
Expand All @@ -139,12 +187,11 @@ def test_spawn_sphere(sim):
assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh"


@pytest.mark.parametrize("resolution", [(1, 1), (3, 2)])
@pytest.mark.parametrize("size", [(1.0, 1.0), (1.5, 0.8)])
def test_spawn_rectangle(sim, resolution, size):
def test_spawn_rectangle(sim, size):
"""Test spawning of UsdGeomMesh as a rectangle prim."""
# Spawn rectangle
cfg = sim_utils.MeshRectangleCfg(size=size, resolution=resolution)
cfg = sim_utils.MeshRectangleCfg(size=size)
prim = cfg.func("/World/Rectangle", cfg)

# Check validity
Expand All @@ -154,6 +201,15 @@ def test_spawn_rectangle(sim, resolution, size):
# Check properties
prim = sim.stage.GetPrimAtPath("/World/Rectangle/geometry/mesh")
assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh"
assert len(prim.GetAttribute("points").Get()) == 4
assert len(prim.GetAttribute("faceVertexCounts").Get()) == 2


def test_invalid_edge_refinement(sim):
"""Test spawning with invalid edge refinement."""
cfg = sim_utils.MeshCuboidCfg(size=(1.0, 2.0, 3.0), edge_refinement=0.5)
with pytest.raises(ValueError, match="Mesh edge refinement must be at least 1.0"):
cfg.func("/World/Invalid", cfg)


"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def generate_cloth_scene(
prim_path="/World/env_[^/]+/Cloth",
spawn=sim_utils.MeshRectangleCfg(
size=(0.2, 0.2),
resolution=(3, 3),
edge_refinement=3,
deformable_props=NewtonDeformableBodyPropertiesCfg(),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.8)),
physics_material=NewtonSurfaceDeformableBodyMaterialCfg(density=0.02, particle_radius=0.005),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class DeformableCfg(PresetCfg):
init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.4, 0.0, 0.102), rot=(0.70710678, 0.0, 0.0, 0.70710678)),
spawn=sim_utils.MeshRectangleCfg(
size=(0.2, 0.2),
resolution=(8, 8),
edge_refinement=8,
deformable_props=NewtonDeformableBodyPropertiesCfg(),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.95, 0.85, 0.1)),
physics_material=NewtonSurfaceDeformableBodyMaterialCfg(
Expand All @@ -145,7 +145,7 @@ class DeformableCfg(PresetCfg):
init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.4, 0.0, 0.102), rot=(0.70710678, 0.0, 0.0, 0.70710678)),
spawn=sim_utils.MeshRectangleCfg(
size=(0.2, 0.2),
resolution=(8, 8),
edge_refinement=8,
deformable_props=PhysxDeformableBodyPropertiesCfg(),
collision_props=[PhysxCollisionCfg(rest_offset=0.002, contact_offset=0.01)],
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.95, 0.85, 0.1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class DeformableCfg(PresetCfg):
init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.5, 0.0, 0.05)),
spawn=sim_utils.MeshCuboidCfg(
size=(0.3, 0.04, 0.04),
edge_refinement=3.0,
edge_refinement=8.0,
deformable_props=NewtonDeformableBodyPropertiesCfg(),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.45, 0.45, 0.85)),
physics_material=NewtonDeformableBodyMaterialCfg(
Expand All @@ -121,7 +121,7 @@ class DeformableCfg(PresetCfg):
init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.5, 0.0, 0.05)),
spawn=sim_utils.MeshCuboidCfg(
size=(0.3, 0.04, 0.04),
edge_refinement=3.0,
edge_refinement=8.0,
deformable_props=PhysxDeformableBodyPropertiesCfg(),
collision_props=[PhysxCollisionCfg(rest_offset=0.0025, contact_offset=0.01)],
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.45, 0.45, 0.85)),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading