Skip to content

Commit 9f752df

Browse files
authored
Fixes Newton cloner ignoring collision approximation (#6596)
# Description The Newton cloner throws away whatever `physics:approximation` an asset authors on its collision meshes: it imports USD with `skip_mesh_approximation=True`, then convex-hulls everything. So in cloned envs a concave part always collides as one convex blob, with no warning. Plain scene loading honors the attribute, so the two paths didn't even agree with each other. Two changes: - Cloner now lets Newton process authored approximations, and only runs the default convex-hull pass on meshes that don't author one. - `coacd` is now a core dependency. Without it `convexDecomposition` just warns and falls back to a single hull. Kit-based installs already get it via isaacsim (which pins 1.0.7, hence the floor); this covers kit-less installs. | Before | After | |---|---| | Cloned envs: every authored mode collapses to one convex hull, silently | `convexDecomposition` -> multiple hulls, `boundingSphere` -> sphere, `boundingCube` -> box, `meshSimplification`/`none` -> mesh | | Plain scenes: `convexDecomposition` falls back to one hull (no `coacd`) | Decomposes out of the box | | Concave L-prism (volume 3.0) collides as its filled hull (volume 3.5) | Decomposed hulls preserve the exact shape | Tests: cloner unit tests, plus an install-CI probe that runs after `./isaaclab.sh -i core` and fails on either regression (verified failing before the fix, passing after). ## Reproduction 1. Author a concave collision mesh with `physics:approximation = "convexDecomposition"` (`UsdPhysics.MeshCollisionAPI`). 2. Load it in a cloned env with the Newton backend, or run `python source/isaaclab/test/install_ci/misc/convex_decomposition_probe.py` in an installed environment. 3. Before: one convex hull, concavity filled in. After: multi-hull decomposition on both import paths. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] 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 added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package (do **not** edit `CHANGELOG.rst` or bump `extension.toml` — CI handles that) - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent 564a008 commit 9f752df

8 files changed

Lines changed: 323 additions & 24 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ dependencies = [
8484
# ----- newton (default physics engine) -----
8585
# Loose bound so the wheel co-resolves with isaacsim's newton[sim]==1.2.0 pin; the
8686
# exact git commit is forced via [tool.uv].override-dependencies (uv sync only).
87-
"newton[sim]>=1.2.0",
87+
# The importers extra carries the mesh-processing deps (coacd, fast-simplification,
88+
# ...) that honoring USD-authored ``physics:approximation`` requires.
89+
"newton[sim,importers]>=1.2.0",
8890
# Loose bound co-resolves with isaacsim's ==0.2.0; the override below forces >=0.3.1.
8991
"newton-usd-schemas>=0.2.0",
9092
"PyOpenGL-accelerate>=3.1.0",
@@ -334,7 +336,7 @@ prerelease = "allow"
334336
# the CUDA indexes via [tool.uv.sources]. Values mirror [tool.isaaclab.versions] above.
335337
override-dependencies = [
336338
"numpy>=2",
337-
"newton[sim] @ git+https://github.com/newton-physics/newton.git@c7ae7c7648cd0717df39e5c94b95d5a02c997320",
339+
"newton[sim,importers] @ git+https://github.com/newton-physics/newton.git@c7ae7c7648cd0717df39e5c94b95d5a02c997320",
338340
# Force the Newton-matched schemas over isaacsim's ==0.2.0 pin.
339341
"newton-usd-schemas>=0.3.1",
340342
"torch==2.11.0",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Internal: added an install-CI probe for USD-authored convex decomposition.

source/isaaclab/test/cli/test_install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def fake_run(cmd, *args, **kwargs):
264264
install_cmds = [cmd for cmd in calls if "install" in cmd]
265265
assert install_cmds, "expected a pip install call"
266266
install_args = install_cmds[-1]
267-
assert any(arg.startswith("newton[sim]") and arg.endswith(commit) for arg in install_args)
267+
assert any(arg.startswith("newton[sim,importers]") and arg.endswith(commit) for arg in install_args)
268268
assert any(arg.startswith("newton-usd-schemas") for arg in install_args), "schemas must be forced too"
269269

270270
def test_skips_when_commit_already_installed(self):

source/isaaclab/test/cli/test_uv_run_pyproject.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ def test_uv_run_base_dependencies_cover_newton_rsl_rl_training():
133133
dependencies = _root_pyproject()["project"]["dependencies"]
134134

135135
# Newton is the default physics engine and RSL-RL the default training library,
136-
# so both ship as core third-party requirements (not opt-in extras).
137-
assert any(dep.startswith("newton[sim]") for dep in dependencies)
136+
# so both ship as core third-party requirements (not opt-in extras). The importers
137+
# extra carries the mesh-processing deps that authored collision approximations need.
138+
assert any(dep.startswith("newton[sim,importers]") for dep in dependencies)
138139
assert any(dep.startswith("rsl-rl-lib") for dep in dependencies)
139140

140141

source/isaaclab/test/install_ci/cli/test_cli_install_core_in_uvenv_correctness.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- ./isaaclab.sh -i core -> verify all core packages importable
1111
- ./isaaclab.sh -i core -> verify optional submodules (mimic, teleop, ovrtx, ovphysx) NOT installed
1212
- ./isaaclab.sh -i core -> verify isaaclab_physx test suite passes
13+
- ./isaaclab.sh -i core -> verify the Newton importer mesh-processing dependencies
14+
(coacd, fast-simplification) are installed
1315
"""
1416

1517
from __future__ import annotations
@@ -95,6 +97,31 @@ def test_install_core_omits_optional_submodules(self, isaaclab_root):
9597
finally:
9698
self.destroy_uv_env()
9799

100+
@pytest.mark.install_path_cli
101+
@pytest.mark.uv
102+
@pytest.mark.slow
103+
@pytest.mark.timeout(1800)
104+
def test_install_core_provides_newton_importer_dependencies(self, isaaclab_root):
105+
"""Newton's mesh-processing deps ship with -i core (via newton[sim,importers]).
106+
107+
Without them, Newton warns and falls back to a single convex hull instead of
108+
honoring an authored ``physics:approximation`` (behavior is unit-tested in
109+
isaaclab_newton's test_collision_approximation.py).
110+
"""
111+
try:
112+
self.create_uv_env(isaaclab_root)
113+
114+
result = self.run_in_uv_env([str(self.cli_script), "-i", "core"], cwd=isaaclab_root)
115+
assert result.returncode == 0, f"isaaclab -i core failed:\n{result.stdout}\n{result.stderr}"
116+
117+
result = self.run_in_uv_env(["python", "-c", "import coacd, fast_simplification"])
118+
assert result.returncode == 0, (
119+
f"Newton importer mesh-processing deps missing:\n{result.stdout}\n{result.stderr}"
120+
)
121+
122+
finally:
123+
self.destroy_uv_env()
124+
98125
@pytest.mark.install_path_cli
99126
@pytest.mark.uv
100127
@pytest.mark.gpu
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed the Newton cloner discarding USD-authored ``physics:approximation`` on
5+
collision meshes: cloned environments flattened every collision mesh to a single
6+
convex hull regardless of the authored mode (``convexDecomposition``,
7+
``boundingSphere``, ``boundingCube``, ``meshSimplification``, or ``none``),
8+
without a warning. The cloner now honors authored approximations the same way
9+
non-cloned scene loading does, and applies the default convex-hull simplification
10+
only to meshes with no authored approximation. Also switched the core install to
11+
``newton[sim,importers]`` so the mesh-processing dependencies (``coacd``,
12+
``fast-simplification``, ...) ship by default and ``convexDecomposition``
13+
decomposes instead of silently falling back to a single convex hull.

source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,68 @@
55

66
from __future__ import annotations
77

8+
import warnings
89
from collections.abc import Callable, Sequence
910
from typing import Any
1011

1112
import torch
1213
import warp as wp
13-
from newton import ModelBuilder, solvers
14+
from newton import GeoType, ModelBuilder, ShapeFlags, solvers
1415

15-
from pxr import Usd
16+
from pxr import Usd, UsdPhysics
1617

1718
from isaaclab.cloner.cloner_utils import replace_path_prefix
1819
from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors
1920

21+
# USD ``physics:approximation`` token (lower case) -> Newton remeshing method.
22+
# Mirrors Newton's own importer mapping; ``none`` keeps the raw trimesh.
23+
_APPROXIMATION_TO_REMESHING_METHOD = {
24+
"convexdecomposition": "coacd",
25+
"convexhull": "convex_hull",
26+
"boundingsphere": "bounding_sphere",
27+
"boundingcube": "bounding_box",
28+
"meshsimplification": "quadratic",
29+
}
30+
31+
32+
def _authored_collision_approximations(stage: Usd.Stage) -> dict[str, str]:
33+
"""Prim path -> authored ``physics:approximation`` token (lower case).
34+
35+
SDF collision prims are excluded: the attribute has no meaning on a shape with
36+
``NewtonSDFCollisionAPI`` applied (matching Newton's importer semantics).
37+
"""
38+
authored: dict[str, str] = {}
39+
for prim in stage.Traverse():
40+
attr = UsdPhysics.MeshCollisionAPI(prim).GetApproximationAttr()
41+
if attr and attr.HasAuthoredValue() and "NewtonSDFCollisionAPI" not in prim.GetAppliedSchemas():
42+
authored[prim.GetPath().pathString] = str(attr.Get()).lower()
43+
return authored
44+
45+
46+
def _apply_authored_approximations(builder: ModelBuilder, path_shape_map: dict, authored: dict[str, str]) -> set[int]:
47+
"""Remesh authored collision shapes (visual shapes preserved); return their indices."""
48+
authored_shape_indices: set[int] = set()
49+
for path, mode in authored.items():
50+
index = path_shape_map.get(path)
51+
if index is None:
52+
continue
53+
authored_shape_indices.add(index)
54+
method = _APPROXIMATION_TO_REMESHING_METHOD.get(mode)
55+
if method is not None:
56+
builder.approximate_meshes(method, shape_indices=[index], keep_visual_shapes=True)
57+
return authored_shape_indices
58+
59+
60+
def _unauthored_collision_mesh_shapes(builder: ModelBuilder, authored_shape_indices: set[int]) -> list[int]:
61+
"""Colliding mesh shapes not covered by an authored ``physics:approximation``."""
62+
return [
63+
index
64+
for index, shape_type in enumerate(builder.shape_type)
65+
if shape_type == GeoType.MESH
66+
and (builder.shape_flags[index] & ShapeFlags.COLLIDE_SHAPES)
67+
and index not in authored_shape_indices
68+
]
69+
2070

2171
def build_source_builders(
2272
stage: Usd.Stage,
@@ -27,27 +77,76 @@ def build_source_builders(
2777
ignore_paths: Sequence[str] | None = None,
2878
simplify_meshes: bool = True,
2979
) -> dict[str, ModelBuilder]:
30-
"""Build one Newton builder for each clone source prim path."""
31-
builders: dict[str, ModelBuilder] = {}
32-
for source in sources:
33-
builder = create_builder()
34-
solvers.SolverMuJoCo.register_custom_attributes(builder)
35-
solvers.SolverKamino.register_custom_attributes(builder)
36-
builder.add_usd(
37-
stage,
38-
root_path=source,
39-
load_visual_shapes=True,
40-
skip_mesh_approximation=True,
41-
schema_resolvers=schema_resolvers,
42-
ignore_paths=ignore_paths,
80+
"""Build one Newton builder for each clone source prim path.
81+
82+
USD-authored ``physics:approximation`` modes are honored (applied after import so
83+
visual shapes are preserved for visualization/rendering). Exception: when the
84+
honored modes leave multiple sources with differing shape-type sequences (e.g.
85+
heterogeneous asset variants), every mesh falls back to the uniform convex-hull
86+
treatment, because :class:`SolverMuJoCo` requires homogeneous worlds.
87+
"""
88+
authored = _authored_collision_approximations(stage)
89+
builders = {
90+
source: _build_source_builder(
91+
stage, source, create_builder, schema_resolvers, ignore_paths, simplify_meshes, authored
4392
)
44-
if simplify_meshes:
45-
builder.approximate_meshes("convex_hull", keep_visual_shapes=True)
46-
replace_newton_builder_shape_colors(builder, stage)
47-
builders[source] = builder
93+
for source in sources
94+
}
95+
96+
if authored and len(builders) > 1:
97+
shape_sequences = {tuple(int(t) for t in b.shape_type) for b in builders.values()}
98+
if len(shape_sequences) > 1:
99+
warnings.warn(
100+
"Clone sources have differing collision shape sequences after honoring authored"
101+
" physics:approximation modes, which SolverMuJoCo's homogeneous-worlds requirement"
102+
" does not support. Falling back to uniform convex-hull approximation for all"
103+
" collision meshes.",
104+
stacklevel=2,
105+
)
106+
builders = {
107+
source: _build_source_builder(
108+
stage, source, create_builder, schema_resolvers, ignore_paths, simplify_meshes, {}
109+
)
110+
for source in sources
111+
}
48112
return builders
49113

50114

115+
def _build_source_builder(
116+
stage: Usd.Stage,
117+
source: str,
118+
create_builder: Callable[[], ModelBuilder],
119+
schema_resolvers: Sequence[Any],
120+
ignore_paths: Sequence[str] | None,
121+
simplify_meshes: bool,
122+
authored: dict[str, str],
123+
) -> ModelBuilder:
124+
"""Build one source builder; an empty ``authored`` map restores hull-everything."""
125+
builder = create_builder()
126+
solvers.SolverMuJoCo.register_custom_attributes(builder)
127+
solvers.SolverKamino.register_custom_attributes(builder)
128+
import_result = builder.add_usd(
129+
stage,
130+
root_path=source,
131+
load_visual_shapes=True,
132+
skip_mesh_approximation=True,
133+
schema_resolvers=schema_resolvers,
134+
ignore_paths=ignore_paths,
135+
)
136+
if authored:
137+
authored_shape_indices = _apply_authored_approximations(builder, import_result["path_shape_map"], authored)
138+
if simplify_meshes:
139+
builder.approximate_meshes(
140+
"convex_hull",
141+
shape_indices=_unauthored_collision_mesh_shapes(builder, authored_shape_indices),
142+
keep_visual_shapes=True,
143+
)
144+
elif simplify_meshes:
145+
builder.approximate_meshes("convex_hull", keep_visual_shapes=True)
146+
replace_newton_builder_shape_colors(builder, stage)
147+
return builder
148+
149+
51150
def replicate_builder_mapping(
52151
builder: ModelBuilder,
53152
sources: Sequence[str],

0 commit comments

Comments
 (0)