Skip to content
1 change: 0 additions & 1 deletion scripts/demos/mpm/particle_pour.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@ def create_sim_cfg():
project_outside_colliders=True,
),
use_cuda_graph=True,
simplify_meshes=False,
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added
^^^^^

* Added :attr:`~isaaclab.sim.schemas.CollisionBaseCfg.mesh_collision_property` so a
spawner config can author the collision approximation of a file-spawned USD asset,
which otherwise exposes no approximation knob.
11 changes: 11 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,17 @@ class CollisionBaseCfg:
attribute via its PhysX-bridge resolver.
"""

mesh_collision_property: MeshCollisionBaseCfg | None = None
"""Optional mesh-collision approximation to author on this collider.

When set, it is dispatched to :meth:`~isaaclab.sim.schemas.modify_mesh_collision_properties`
so the ``physics:approximation`` token (and any backend mesh-collision tuning) is
written on the collision mesh prim. Use this to override a file-spawned USD asset's
authored collision approximation (e.g. convex hull / convex decomposition) — such
assets otherwise expose no approximation knob through :attr:`collision_props`.
``None`` leaves the USD-authored approximation untouched.
"""


@configclass
class MassPropertiesCfg:
Expand Down
13 changes: 10 additions & 3 deletions source/isaaclab/test/sim/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ def test_valid_properties_cfg(setup_simulation):
# deprecation aliases are nulled by __post_init__ after forwarding to the canonical
# field; exclude them from the all-non-None check.
deprecation_aliases = {"max_velocity", "max_effort"}
# nested opt-in cfgs whose ``None`` means "leave the USD-authored value alone"
optional_nested_cfgs = {"mesh_collision_property"}
for cfg in [arti_cfg, rigid_cfg, collision_cfg, mass_cfg, joint_cfg]:
for k, v in cfg.__dict__.items():
# skip class-metadata keys (``_usd_*``) and deprecation aliases nulled in __post_init__
if k.startswith("_") or k in deprecation_aliases:
# skip class-metadata keys (``_usd_*``), deprecation aliases nulled in __post_init__,
# and nested cfgs that are meaningfully unset
if k.startswith("_") or k in deprecation_aliases or k in optional_nested_cfgs:
continue
assert v is not None, f"{cfg.__class__.__name__}:{k} is None. Please make sure schemas are valid."

Expand Down Expand Up @@ -1057,7 +1060,11 @@ def _validate_collision_properties_on_prim(prim_path: str, collision_cfg, verbos
if UsdPhysics.CollisionAPI(mesh_prim):
for attr_name, attr_value in collision_cfg.__dict__.items():
# skip names we know are not present and class-metadata keys
if attr_name.startswith("_") or attr_name in ["func", "collision_enabled"]:
if attr_name.startswith("_") or attr_name in [
"func",
"collision_enabled",
"mesh_collision_property",
]:
continue
# convert attribute name in prim to cfg name
prim_prop_name = f"physxCollision:{to_camel_case(attr_name, to='cC')}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Changed
^^^^^^^

* **Breaking:** Removed ``NewtonCfg.simplify_meshes``. Newton replication no longer
approximates mesh colliders, so a USD-authored collision approximation survives
cloning. Author the approximation on the asset instead, via
:attr:`~isaaclab.sim.schemas.CollisionBaseCfg.mesh_collision_property` on the
spawner config.
109 changes: 8 additions & 101 deletions source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from __future__ import annotations

import warnings
from collections.abc import Callable, Sequence
from typing import Any

Expand All @@ -18,55 +17,6 @@

from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors

# USD ``physics:approximation`` token (lower case) -> Newton remeshing method.
# Mirrors Newton's own importer mapping; ``none`` keeps the raw trimesh.
_APPROXIMATION_TO_REMESHING_METHOD = {
"convexdecomposition": "coacd",
"convexhull": "convex_hull",
"boundingsphere": "bounding_sphere",
"boundingcube": "bounding_box",
"meshsimplification": "quadratic",
}


def _authored_collision_approximations(stage: Usd.Stage) -> dict[str, str]:
"""Prim path -> authored ``physics:approximation`` token (lower case).

SDF collision prims are excluded: the attribute has no meaning on a shape with
``NewtonSDFCollisionAPI`` applied (matching Newton's importer semantics).
"""
authored: dict[str, str] = {}
for prim in stage.Traverse():
attr = UsdPhysics.MeshCollisionAPI(prim).GetApproximationAttr()
if attr and attr.HasAuthoredValue() and "NewtonSDFCollisionAPI" not in prim.GetAppliedSchemas():
authored[prim.GetPath().pathString] = str(attr.Get()).lower()
return authored


def _apply_authored_approximations(builder: ModelBuilder, path_shape_map: dict, authored: dict[str, str]) -> set[int]:
"""Remesh authored collision shapes (visual shapes preserved); return their indices."""
authored_shape_indices: set[int] = set()
for path, mode in authored.items():
index = path_shape_map.get(path)
if index is None:
continue
authored_shape_indices.add(index)
method = _APPROXIMATION_TO_REMESHING_METHOD.get(mode)
if method is not None:
builder.approximate_meshes(method, shape_indices=[index], keep_visual_shapes=True)
return authored_shape_indices


def _unauthored_collision_mesh_shapes(builder: ModelBuilder, authored_shape_indices: set[int]) -> list[int]:
"""Colliding mesh shapes not covered by an authored ``physics:approximation``."""
return [
index
for index, shape_type in enumerate(builder.shape_type)
if shape_type == GeoType.MESH
and (builder.shape_flags[index] & ShapeFlags.COLLIDE_SHAPES)
and index not in authored_shape_indices
]


def _has_visible_non_collision_geometry(stage: Usd.Stage, prim_path: str) -> bool:
"""Return whether a prim hierarchy contains visible geometry without collision."""
Expand Down Expand Up @@ -145,73 +95,40 @@ def build_source_builders(
schema_resolvers: Sequence[Any],
*,
ignore_paths: Sequence[str] | None = None,
simplify_meshes: bool = True,
load_visual_shapes: bool = True,
) -> dict[str, ModelBuilder]:
"""Build one Newton builder for each clone source prim path.

USD-authored ``physics:approximation`` modes are honored (applied after import so
visual shapes are preserved for visualization/rendering). Exception: when the
honored modes leave multiple sources with differing shape-type sequences (e.g.
heterogeneous asset variants), every mesh falls back to the uniform convex-hull
treatment, because :class:`SolverMuJoCo` requires homogeneous worlds.
The cloner approximates nothing. Collision geometry is whatever the asset authored:
Newton's importer applies each shape's ``physics:approximation`` while importing, and
USD defaults that token to ``none``, meaning "use the mesh as-is". Change it where it
is authored -- the mesh-collision schema fragments on the spawner -- not here.

Args:
stage: USD stage containing the source prims.
sources: Source prim paths to build a builder for.
create_builder: Factory returning a fresh :class:`ModelBuilder`.
schema_resolvers: Schema resolvers forwarded to Newton's USD importer.
ignore_paths: Prim paths skipped during import.
simplify_meshes: Whether to run convex-hull mesh approximation.
load_visual_shapes: Whether to import visual-only geometry. Importing it costs
USD parse time and memory that only pays off when the shapes are rendered
or ray cast.
"""
authored = _authored_collision_approximations(stage)
builders = {
source: _build_source_builder(
stage, source, create_builder, schema_resolvers, ignore_paths, simplify_meshes, authored, load_visual_shapes
)
return {
Comment on lines 114 to +117

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.

P1 Homogeneous-world guard is removed

When a replication mapping uses per-world alternatives whose authored approximations produce different shape sequences, these builders now flow unchanged into the combined model, violating SolverMuJoCo's homogeneous-world requirement and causing solver initialization or simulation failure.

source: _build_source_builder(stage, source, create_builder, schema_resolvers, ignore_paths, load_visual_shapes)
for source in sources
}

if authored and len(builders) > 1:
shape_sequences = {tuple(int(t) for t in b.shape_type) for b in builders.values()}
if len(shape_sequences) > 1:
warnings.warn(
"Clone sources have differing collision shape sequences after honoring authored"
" physics:approximation modes, which SolverMuJoCo's homogeneous-worlds requirement"
" does not support. Falling back to uniform convex-hull approximation for all"
" collision meshes.",
stacklevel=2,
)
builders = {
source: _build_source_builder(
stage,
source,
create_builder,
schema_resolvers,
ignore_paths,
simplify_meshes,
{},
load_visual_shapes,
)
for source in sources
}
return builders


def _build_source_builder(
stage: Usd.Stage,
source: str,
create_builder: Callable[[], ModelBuilder],
schema_resolvers: Sequence[Any],
ignore_paths: Sequence[str] | None,
simplify_meshes: bool,
authored: dict[str, str],
load_visual_shapes: bool = True,
) -> ModelBuilder:
"""Build one source builder; an empty ``authored`` map restores hull-everything."""
"""Build one source builder."""
builder = create_builder()
solvers.SolverMuJoCo.register_custom_attributes(builder)
solvers.SolverKamino.register_custom_attributes(builder)
Expand All @@ -220,23 +137,13 @@ def _build_source_builder(
root_path=source,
load_visual_shapes=load_visual_shapes,
hide_collision_shapes=True,
skip_mesh_approximation=True,
skip_mesh_approximation=False,
schema_resolvers=schema_resolvers,
ignore_paths=ignore_paths,
)
_restore_visible_colliders_without_visual_shapes(
builder, stage, import_result["path_shape_map"], load_visual_shapes
)
if authored:
authored_shape_indices = _apply_authored_approximations(builder, import_result["path_shape_map"], authored)
if simplify_meshes:
builder.approximate_meshes(
"convex_hull",
shape_indices=_unauthored_collision_mesh_shapes(builder, authored_shape_indices),
keep_visual_shapes=True,
)
elif simplify_meshes:
builder.approximate_meshes("convex_hull", keep_visual_shapes=True)
replace_newton_builder_shape_colors(builder, stage)
return builder

Expand Down
21 changes: 3 additions & 18 deletions source/isaaclab_newton/isaaclab_newton/cloner/replicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def _build_newton_builder_from_mapping(
positions: torch.Tensor | None = None,
quaternions: torch.Tensor | None = None,
up_axis: str = "Z",
simplify_meshes: bool = True,
load_visual_shapes: bool = True,
) -> tuple[ModelBuilder, object, dict, list, dict[str, ModelBuilder]]:
"""Build a Newton model builder from clone mapping inputs.
Expand Down Expand Up @@ -125,7 +124,6 @@ def _build_newton_builder_from_mapping(
lambda: manager_cls.create_builder(up_axis=up_axis),
schema_resolvers,
ignore_paths=deformable_ignore_paths or None,
simplify_meshes=simplify_meshes,
load_visual_shapes=load_visual_shapes,
)

Expand Down Expand Up @@ -169,7 +167,6 @@ def __init__(
*,
device: str = "cpu",
up_axis: str = "Z",
simplify_meshes: bool | None = None,
load_visual_shapes: bool | None = None,
commit_to_manager: bool = True,
):
Expand All @@ -179,8 +176,6 @@ def __init__(
stage: USD stage containing source assets.
device: Device used by the finalized Newton model builder.
up_axis: Up axis for the Newton model builder.
simplify_meshes: Whether to run convex-hull mesh approximation. If
``None``, read from the active :class:`NewtonCfg`.
load_visual_shapes: Whether to import visual-only geometry. If ``None``,
read from the active :class:`NewtonCfg`, which itself defaults to
importing them only when a renderer or visualizer is active.
Expand All @@ -190,16 +185,11 @@ def __init__(
self.stage = stage
self.device = device
self.up_axis = up_axis
if simplify_meshes is None or load_visual_shapes is None:
if load_visual_shapes is None:

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 simplify_meshes removed without deprecation

NewtonCfg.simplify_meshes and the simplify_meshes keyword on NewtonReplicateContext.__init__, newton_physics_replicate, and build_source_builders are deleted in one release, so existing configs and callers now fail with unknown-field or TypeError errors. Repository rules require deprecating public symbols in a prior release. Keep the field and keyword for one release as accepted-but-ignored shims that emit a deprecation warning pointing at mesh_collision_property.

from isaaclab_newton.physics import NewtonCfg

cfg = PhysicsManager._cfg
is_newton_cfg = isinstance(cfg, NewtonCfg)
if simplify_meshes is None:
simplify_meshes = cfg.simplify_meshes if is_newton_cfg else True
if load_visual_shapes is None:
load_visual_shapes = cfg.load_visual_shapes if is_newton_cfg else None
self.simplify_meshes = simplify_meshes
load_visual_shapes = cfg.load_visual_shapes if isinstance(cfg, NewtonCfg) else None
self.load_visual_shapes = _renderer_wants_visual_shapes() if load_visual_shapes is None else load_visual_shapes
self.commit_to_manager = commit_to_manager
self._queue: list[_MappingBatch] = []
Expand Down Expand Up @@ -285,7 +275,6 @@ def replicate(self) -> tuple[ModelBuilder, object, dict]:
positions=positions,
quaternions=quaternions,
up_axis=self.up_axis,
simplify_meshes=self.simplify_meshes,
load_visual_shapes=self.load_visual_shapes,
)
fabric_body_bindings = rename_builder_labels(builder, sources, destinations, env_ids, mapping)
Expand Down Expand Up @@ -315,7 +304,6 @@ def newton_physics_replicate(
quaternions: torch.Tensor | None = None,
device: str = "cpu",
up_axis: str = "Z",
simplify_meshes: bool = True,
):
"""Replicate prims into a Newton ``ModelBuilder`` using a per-source mapping.

Expand All @@ -329,14 +317,11 @@ def newton_physics_replicate(
quaternions: Optional per-environment orientations in xyzw order.
device: Device used by the finalized Newton model builder.
up_axis: Up axis for the Newton model builder.
simplify_meshes: Whether to run convex-hull mesh approximation.

Returns:
Tuple of the populated Newton model builder and stage metadata.
"""
ctx = NewtonReplicateContext(
stage, device=device, up_axis=up_axis, simplify_meshes=simplify_meshes, commit_to_manager=True
)
ctx = NewtonReplicateContext(stage, device=device, up_axis=up_axis, commit_to_manager=True)
ctx.queue_mapping(sources, destinations, env_ids, mapping, positions=positions, quaternions=quaternions)
builder, stage_info, _site_index_map = ctx.replicate()
return builder, stage_info
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,6 @@ class NewtonCfg(PhysicsCfg):
:class:`NewtonShapeCfg` for the declared fields.
"""

simplify_meshes: bool = True
"""Whether Newton replication simplifies mesh colliders to convex hulls.

Keep this enabled for most rigid-body scenes. Disable it when exact triangle
meshes are intentional, for example thin or hollow MPM colliders.
"""

load_visual_shapes: bool | None = None
"""Whether Newton replication imports visual-only geometry from USD.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ def build_visualization_builder_from_stage_envs(
lambda: ModelBuilder(up_axis=up_axis),
schema_resolvers,
ignore_paths=source_deformable_ignore_paths or None,
simplify_meshes=False,
)
replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders)
rename_builder_labels(builder, sources, destinations, env_ids, mapping)
Expand Down
Loading
Loading