Skip to content

Commit 51642ea

Browse files
mmichelisisaaclab-bot[bot]
authored andcommitted
Unify primitive mesh refinement controls (#7357)
## Description - Add `MeshCfg.edge_refinement`, defaulting to `4.0`, to control surface subdivision for all primitive mesh spawners using a bounding-box-diagonal target. - Forward `1.0 / edge_refinement` to automatic tetrahedralization for volume deformables spawned through `MeshCfg` primitive spawners. Direct callers of `define_deformable_body_properties` retain its existing default factor of `0.1`. - Document that values near `1.0` should be avoided for volume deformables because they can make TetWild tetrahedralization significantly slower. - Replace `MeshRectangleCfg.resolution` with scalar `edge_refinement`. This removes independent per-axis resolution control, and subdivision counts can be discontinuous for non-power-of-two factors. - Migrate existing cloth callsites and examples, and set the Franka soft-lift cuboid refinement to `8.0`. This is a breaking change. Unconfigured primitive surfaces now use an edge-refinement factor of `4.0`, and unconfigured volume primitive spawners use a tetrahedralization factor of `0.25` instead of `0.1`. Rectangles now use the common recursive edge-length subdivision instead of the previous per-axis grid. ## Type of change - [x] Breaking change - [x] New feature ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Testing - [x] Ruff, format, compilation, and diff checks - [x] Mesh-spawner integration suite: 21 tests - [x] Newton surface-deformable initialization and freefall integration test - [x] PhysX deformables demo: completed setup and simulation - [x] Franka cloth environment: 20 steps each on Newton, PhysX, and OvPhysX - [x] Franka cloth camera environment: 5 PhysX/RTX steps with image observations - [x] Scripted Franka cloth lift: 50 steps each on Newton and PhysX - [x] Franka soft lift with refinement `8.0`: 10 steps each on Newton and Isaac Sim PhysX - [x] Seed-42 RSL-RL scratch smoke at 2,048 environments: 55 soft updates and 11 cloth updates with finite losses - [x] Seed-42 matched checkpoint continuation at 2,048 environments: 52 updates each on develop and the PR mesh; final-20 success was 67.1% versus 81.5% for soft and 85.3% versus 87.6% for cloth - [x] TetWild timing check for all five volume primitives at the default factor of `0.25` PhysX reports an invalid surface-deformable-view warning in the demo and cloth environment. The same warning is present on untouched `develop` with the same runtime and is not introduced by this change. ## Checklist - [x] Changelog fragments added - [x] Existing callsites migrated (cherry picked from commit 3fbbd15)
1 parent 5f7234e commit 51642ea

13 files changed

Lines changed: 153 additions & 75 deletions

File tree

scripts/demos/deformables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def design_scene() -> tuple[dict, list[list[float]]]:
156156
)
157157
cfg_cloth = sim_utils.MeshRectangleCfg(
158158
size=(1.5, 1.0),
159-
resolution=(21, 21),
159+
edge_refinement=21,
160160
deformable_props=DeformableBodyPropertiesCfg(),
161161
visual_material=sim_utils.PreviewSurfaceCfg(),
162162
physics_material=SurfaceDeformableMaterialCfg(),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added
2+
^^^^^
3+
4+
* Added ``MeshCfg.edge_refinement``, defaulting to ``4.0``, to control surface mesh resolution for deformable
5+
primitives and the automatically generated tetrahedral mesh resolution for closed volume deformables. It is ignored
6+
when ``deformable_props`` is None, since rigid primitive collision approximations are invariant to surface
7+
subdivision.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Changed ``MeshCuboidCfg.edge_refinement``, added in 15.6.0 and now generalized to ``MeshCfg.edge_refinement``, to
5+
apply only when ``deformable_props`` is set. Rigid primitives are no longer subdivided; their surface and collision
6+
approximation are unaffected, since subdivision only inserted coplanar vertices. Callers relying on a denser rigid
7+
visual mesh must supply their own mesh asset.
8+
9+
Removed
10+
^^^^^^^
11+
12+
* Removed ``MeshRectangleCfg.resolution``. Deformable callers must use ``MeshCfg.edge_refinement`` to bound surface
13+
edge length relative to the bounding-box diagonal. Rigid rectangles are now spawned as two triangles.

source/isaaclab/isaaclab/sim/schemas/schemas.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,7 @@ def define_deformable_body_properties(
20082008
stage: Usd.Stage | None = None,
20092009
deformable_type: str = "volume",
20102010
sim_mesh_prim_path: str | None = None,
2011+
tetrahedralization_edge_length_fac: float = 0.1,
20112012
):
20122013
"""Apply the deformable body schema on the input prim and set its properties. The input prim should
20132014
have a visual surface mesh as child. Volume deformables will have their simulation tetrahedral mesh
@@ -2037,6 +2038,8 @@ def define_deformable_body_properties(
20372038
sim_mesh_prim_path: Optional override for the simulation mesh creation prim path.
20382039
Ignored when pre-tetrahedralized mesh is found for volume deformables.
20392040
If None, it is set to ``{prim_path}/sim_mesh``.
2041+
tetrahedralization_edge_length_fac: Relative target edge length for automatic tetrahedralization.
2042+
Defaults to ``0.1``.
20402043
20412044
Raises:
20422045
ValueError: When the prim path is not valid.
@@ -2166,7 +2169,7 @@ def define_deformable_body_properties(
21662169
tet_mesh_points, tet_mesh_indices = tetrahedralize(
21672170
vertices,
21682171
faces.reshape(-1, 3),
2169-
edge_length_fac=0.1,
2172+
edge_length_fac=tetrahedralization_edge_length_fac,
21702173
simplify=False,
21712174
epsilon=1e-2,
21722175
coarsen=True,

source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,9 @@ def spawn_mesh_cuboid(
101101
102102
Raises:
103103
ValueError: If a prim already exists at the given path.
104-
ValueError: If :attr:`~isaaclab.sim.MeshCuboidCfg.edge_refinement` is less than ``1.0``.
105104
"""
106-
if cfg.edge_refinement < 1.0:
107-
raise ValueError(f"Cuboid mesh edge refinement must be at least 1.0, got {cfg.edge_refinement}.")
108-
109105
# create a trimesh box
110106
box = trimesh.creation.box(cfg.size)
111-
if cfg.edge_refinement > 1.0:
112-
max_edge = float(np.linalg.norm(box.bounding_box.extents)) / cfg.edge_refinement
113-
vertices, faces = trimesh.remesh.subdivide_to_size(box.vertices, box.faces, max_edge=max_edge)
114-
box = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
115107

116108
# obtain stage handle
117109
stage = get_current_stage()
@@ -302,53 +294,50 @@ def spawn_mesh_rectangle(
302294
Raises:
303295
ValueError: If a prim already exists at the given path.
304296
"""
305-
# create a 2D triangle mesh grid
306-
vertices, faces = _create_triangle_mesh_grid(cfg.resolution)
307-
vertices[:, 0] *= cfg.size[0]
308-
vertices[:, 1] *= cfg.size[1]
309-
grid = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
297+
# create a 2D triangle mesh
298+
half_x, half_y = cfg.size[0] / 2, cfg.size[1] / 2
299+
vertices = np.array(
300+
[(-half_x, -half_y, 0.0), (half_x, -half_y, 0.0), (half_x, half_y, 0.0), (-half_x, half_y, 0.0)],
301+
dtype=np.float32,
302+
)
303+
rectangle = trimesh.Trimesh(vertices=vertices, faces=((0, 1, 2), (0, 2, 3)), process=False)
310304

311305
# obtain stage handle
312306
stage = get_current_stage()
313307
# spawn the rectangle as a mesh
314-
_spawn_mesh_geom_from_mesh(prim_path, cfg, grid, translation, orientation, None, stage=stage)
308+
_spawn_mesh_geom_from_mesh(prim_path, cfg, rectangle, translation, orientation, None, stage=stage)
315309
# return the prim
316310
return stage.GetPrimAtPath(prim_path)
317311

318312

319-
def _create_triangle_mesh_grid(resolution: tuple[int, int]) -> tuple[np.ndarray, np.ndarray]:
320-
"""Create a centered triangle grid for :class:`MeshRectangleCfg`."""
321-
if resolution[0] < 1 or resolution[1] < 1:
322-
raise ValueError(f"Rectangle mesh resolution must be positive, got {resolution}.")
323-
324-
num_x, num_y = resolution
325-
xs = np.linspace(-0.5, 0.5, num_x + 1, dtype=np.float32)
326-
ys = np.linspace(-0.5, 0.5, num_y + 1, dtype=np.float32)
327-
vertices = np.array([(x, y, 0.0) for y in ys for x in xs], dtype=np.float32)
328-
329-
faces = []
330-
row_stride = num_x + 1
331-
for iy in range(num_y):
332-
for ix in range(num_x):
333-
v0 = iy * row_stride + ix
334-
v1 = v0 + 1
335-
v2 = v0 + row_stride
336-
v3 = v2 + 1
337-
if (ix % 2 == 0) != (iy % 2 == 0):
338-
faces.append((v0, v1, v2))
339-
faces.append((v1, v3, v2))
340-
else:
341-
faces.append((v0, v1, v3))
342-
faces.append((v0, v3, v2))
343-
344-
return vertices, np.asarray(faces, dtype=np.int64)
345-
346-
347313
"""
348314
Helper functions.
349315
"""
350316

351317

318+
def _refine_surface_mesh(mesh: trimesh.Trimesh, cfg: meshes_cfg.MeshCfg) -> trimesh.Trimesh:
319+
"""Subdivide a deformable's surface mesh to the configured edge-length target.
320+
321+
Args:
322+
mesh: The mesh to refine.
323+
cfg: The config carrying :attr:`~isaaclab.sim.MeshCfg.edge_refinement`.
324+
325+
Returns:
326+
The refined mesh, or the input mesh when refinement does not apply.
327+
328+
Raises:
329+
ValueError: If the edge refinement is less than ``1.0``.
330+
"""
331+
if cfg.edge_refinement < 1.0:
332+
raise ValueError(f"Mesh edge refinement must be at least 1.0, got {cfg.edge_refinement}.")
333+
if cfg.deformable_props is None or cfg.edge_refinement == 1.0:
334+
return mesh
335+
336+
max_edge = float(np.linalg.norm(mesh.bounding_box.extents)) / cfg.edge_refinement
337+
vertices, faces = trimesh.remesh.subdivide_to_size(mesh.vertices, mesh.faces, max_edge=max_edge)
338+
return trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
339+
340+
352341
def _apply_deformable_collision_props(prim_path: str, collision_props, stage: Usd.Stage) -> None:
353342
"""Apply collision fragments to the simulation mesh of a deformable body.
354343
@@ -405,13 +394,16 @@ def _spawn_mesh_geom_from_mesh(
405394
406395
Raises:
407396
ValueError: If a prim already exists at the given path.
397+
ValueError: If edge refinement is less than ``1.0``.
408398
ValueError: If both deformable and rigid properties are used.
409399
ValueError: If the physics material is not of the correct type. Deformable properties require a deformable
410400
physics material, and rigid properties require a rigid physics material.
411401
ValueError: If deformable properties are used with non-fragment collision properties.
412402
413403
.. _USDGeomMesh: https://openusd.org/dev/api/class_usd_geom_mesh.html
414404
"""
405+
mesh = _refine_surface_mesh(mesh, cfg)
406+
415407
# obtain stage handle
416408
stage = stage if stage is not None else get_current_stage()
417409

@@ -471,8 +463,15 @@ def _spawn_mesh_geom_from_mesh(
471463
deformable_type = (
472464
"surface" if isinstance(cfg.physics_material, SurfaceDeformableBodyMaterialBaseCfg) else "volume"
473465
)
466+
deformable_kwargs = {}
467+
if deformable_type == "volume":
468+
deformable_kwargs["tetrahedralization_edge_length_fac"] = 1.0 / cfg.edge_refinement
474469
schemas.define_deformable_body_properties(
475-
prim_path, cfg.deformable_props, stage=stage, deformable_type=deformable_type
470+
prim_path,
471+
cfg.deformable_props,
472+
stage=stage,
473+
deformable_type=deformable_type,
474+
**deformable_kwargs,
476475
)
477476
if cfg.collision_props is not None:
478477
_apply_deformable_collision_props(prim_path, cfg.collision_props, stage)

source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ class MeshCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
7575
If None, then no physics material will be added.
7676
"""
7777

78+
edge_refinement: float = 4.0
79+
"""Mesh edge refinement factor for deformable bodies.
80+
81+
The maximum surface edge length is the bounding-box diagonal divided by this value. Volume deformables use the
82+
same normalized target for automatic tetrahedralization. The factor must be at least ``1.0``. For volume
83+
deformables, values near ``1.0`` should be avoided because they can make TetWild tetrahedralization significantly
84+
slower. Defaults to ``4.0``.
85+
"""
86+
7887

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

104-
edge_refinement: float = 1.0
105-
"""Surface edge refinement factor relative to the bounding-box diagonal.
106-
107-
The maximum edge length is the diagonal divided by this value. The factor must be at least
108-
``1.0``. Defaults to ``1.0``, which leaves the base mesh unchanged.
109-
"""
110-
111113

112114
@configclass
113115
class MeshCylinderCfg(MeshCfg):
@@ -171,5 +173,3 @@ class MeshRectangleCfg(MeshCfg):
171173

172174
size: tuple[float, float] = MISSING
173175
"""Edge lengths of the rectangle along the X and Y axes [m]."""
174-
resolution: tuple[int, int] = (5, 5)
175-
"""Resolution of the rectangle (in elements/edges per side)."""

source/isaaclab/test/sim/test_spawn_meshes.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import isaaclab.sim as sim_utils
2020
from isaaclab.sim import SimulationCfg, SimulationContext
21+
from isaaclab.sim.spawners.meshes import meshes as mesh_spawner
2122

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

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

109110

110-
def test_spawn_cuboid_with_edge_refinement(sim):
111-
"""Test cuboid surface edge refinement."""
112-
size = (1.0, 2.0, 3.0)
113-
edge_refinement = 3.0
114-
cfg = sim_utils.MeshCuboidCfg(size=size, edge_refinement=edge_refinement)
115-
cfg.func("/World/RefinedCube", cfg)
116-
117-
prim = sim.stage.GetPrimAtPath("/World/RefinedCube/geometry/mesh")
111+
def test_mesh_edge_refinement_default():
112+
"""Test the default mesh edge refinement."""
113+
assert sim_utils.MeshCfg().edge_refinement == 4.0
114+
assert not hasattr(sim_utils.MeshRectangleCfg(size=(1.0, 1.0)), "resolution")
115+
116+
117+
@pytest.mark.parametrize(
118+
"cfg_type,kwargs,edge_refinement",
119+
[
120+
(sim_utils.MeshSphereCfg, {"radius": 1.0}, 25.0),
121+
(sim_utils.MeshCuboidCfg, {"size": (1.0, 2.0, 3.0)}, 3.0),
122+
(sim_utils.MeshCylinderCfg, {"radius": 1.0, "height": 2.0}, 3.0),
123+
(sim_utils.MeshCapsuleCfg, {"radius": 1.0, "height": 2.0}, 3.0),
124+
(sim_utils.MeshConeCfg, {"radius": 1.0, "height": 2.0}, 3.0),
125+
(sim_utils.MeshRectangleCfg, {"size": (1.0, 1.0)}, 3.0),
126+
],
127+
)
128+
def test_spawn_mesh_with_edge_refinement(sim, monkeypatch, cfg_type, kwargs, edge_refinement):
129+
"""Test surface edge refinement for deformable mesh primitives."""
130+
monkeypatch.setattr(mesh_spawner.schemas, "define_deformable_body_properties", lambda *a, **k: None)
131+
cfg = cfg_type(**kwargs, edge_refinement=edge_refinement, deformable_props=sim_utils.DeformableBodyPropertiesCfg())
132+
cfg.func("/World/Refined", cfg)
133+
prim = sim.stage.GetPrimAtPath("/World/Refined/geometry/mesh")
118134
points = np.asarray(prim.GetAttribute("points").Get())
119135
faces = np.asarray(prim.GetAttribute("faceVertexIndices").Get()).reshape(-1, 3)
120136
edges = points[faces[:, [0, 1, 1, 2, 2, 0]]].reshape(-1, 2, 3)
137+
max_edge = np.linalg.norm(edges[:, 0] - edges[:, 1], axis=1).max()
138+
diagonal = np.linalg.norm(points.max(axis=0) - points.min(axis=0))
139+
140+
assert max_edge <= diagonal / edge_refinement
141+
142+
143+
@pytest.mark.parametrize(
144+
"cfg_type,geometry_kwargs,refinement_kwargs,physics_material,expected_factor",
145+
[
146+
(sim_utils.MeshCuboidCfg, {"size": (1.0, 1.0, 1.0)}, {}, None, 0.25),
147+
(sim_utils.MeshCuboidCfg, {"size": (1.0, 1.0, 1.0)}, {"edge_refinement": 2.0}, None, 0.5),
148+
(sim_utils.MeshRectangleCfg, {"size": (1.0, 1.0)}, {}, sim_utils.PhysxSurfaceDeformableBodyMaterialCfg(), None),
149+
],
150+
)
151+
def test_edge_refinement_sets_tetrahedralization_resolution(
152+
sim, monkeypatch, cfg_type, geometry_kwargs, refinement_kwargs, physics_material, expected_factor
153+
):
154+
"""Test edge refinement is forwarded to volume tetrahedralization."""
155+
captured_kwargs = {}
156+
157+
def capture_deformable_properties(*args, **kwargs):
158+
captured_kwargs.update(kwargs)
159+
160+
monkeypatch.setattr(mesh_spawner.schemas, "define_deformable_body_properties", capture_deformable_properties)
161+
cfg = cfg_type(
162+
deformable_props=sim_utils.DeformableBodyPropertiesCfg(),
163+
physics_material=physics_material,
164+
**geometry_kwargs,
165+
**refinement_kwargs,
166+
)
167+
cfg.func("/World/Deformable", cfg)
121168

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

126174

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

141189

142-
@pytest.mark.parametrize("resolution", [(1, 1), (3, 2)])
143190
@pytest.mark.parametrize("size", [(1.0, 1.0), (1.5, 0.8)])
144-
def test_spawn_rectangle(sim, resolution, size):
191+
def test_spawn_rectangle(sim, size):
145192
"""Test spawning of UsdGeomMesh as a rectangle prim."""
146193
# Spawn rectangle
147-
cfg = sim_utils.MeshRectangleCfg(size=size, resolution=resolution)
194+
cfg = sim_utils.MeshRectangleCfg(size=size)
148195
prim = cfg.func("/World/Rectangle", cfg)
149196

150197
# Check validity
@@ -154,6 +201,15 @@ def test_spawn_rectangle(sim, resolution, size):
154201
# Check properties
155202
prim = sim.stage.GetPrimAtPath("/World/Rectangle/geometry/mesh")
156203
assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh"
204+
assert len(prim.GetAttribute("points").Get()) == 4
205+
assert len(prim.GetAttribute("faceVertexCounts").Get()) == 2
206+
207+
208+
def test_invalid_edge_refinement(sim):
209+
"""Test spawning with invalid edge refinement."""
210+
cfg = sim_utils.MeshCuboidCfg(size=(1.0, 2.0, 3.0), edge_refinement=0.5)
211+
with pytest.raises(ValueError, match="Mesh edge refinement must be at least 1.0"):
212+
cfg.func("/World/Invalid", cfg)
157213

158214

159215
"""

source/isaaclab_contrib/changelog.d/deformable-primitive-mesh-resolution.skip

Whitespace-only changes.

source/isaaclab_contrib/test/deformable/test_deformable_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def generate_cloth_scene(
110110
prim_path="/World/env_[^/]+/Cloth",
111111
spawn=sim_utils.MeshRectangleCfg(
112112
size=(0.2, 0.2),
113-
resolution=(3, 3),
113+
edge_refinement=3,
114114
deformable_props=NewtonDeformableBodyPropertiesCfg(),
115115
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.8)),
116116
physics_material=NewtonSurfaceDeformableBodyMaterialCfg(density=0.02, particle_radius=0.005),

source/isaaclab_tasks/changelog.d/deformable-primitive-mesh-resolution.skip

Whitespace-only changes.

0 commit comments

Comments
 (0)