Skip to content

Commit 889c144

Browse files
committed
Honor selfCollisionEnabled on imported cables
The USD cable importer built rod articulations via add_rod / add_rod_graph but never resolved the per-articulation self-collision flag, so authoring newton:selfCollisionEnabled on a cable had no effect while the general rigid importer honored it. This left non-adjacent segments of the same cable colliding even when self-collision was disabled. Resolve self_collision_enabled per cable articulation (newton namespace, with the physxArticulation fallback) and, when disabled, filter collisions between all of the cable's segment shape pairs, mirroring the rigid importer. Welded rod graphs disable self-collision when any member curve opts out, and warn on mixed authoring.
1 parent d124e5b commit 889c144

5 files changed

Lines changed: 132 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
- Fix masked `SolverCoupledProxy.reset()` calls clearing proxy feedback history for unselected worlds.
9292
- Fix hydroelastic primitive texture SDF generation to sample analytic primitive distances instead of temporary tessellated meshes. (#3239)
9393
- Fix MJCF, URDF, and USD imports rendering collision-only bodies as visuals when the asset authors visual geometry elsewhere. (#3291)
94+
- Fix `ModelBuilder.add_usd()` ignoring `newton:selfCollisionEnabled` on imported cables; a cable articulation with self-collision disabled now filters collisions between its non-adjacent segments.
9495
- Fix `SchemaResolverPhysx` reading every D6 translational limit gain from the `linear` instance instead of its `transX`, `transY`, or `transZ` instance.
9596
- Fix USD capsule, cylinder, and cone visuals and sites without authored `radius`/`height` to use the UsdGeom schema fallbacks, matching collision shapes.
9697
- Fix `ViewerUSD` texture consumers observing partially written PNGs by publishing generated textures atomically (#3288)

newton/_src/utils/import_usd.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4216,6 +4216,7 @@ def initialize_free_joint_velocities() -> None:
42164216
linear_unit=linear_unit,
42174217
ignore_paths=ignore_paths,
42184218
verbose=verbose,
4219+
enable_self_collisions=enable_self_collisions,
42194220
path_body_map=path_body_map,
42204221
path_shape_map=path_shape_map,
42214222
path_cable_map=path_cable_map,

newton/_src/utils/import_usd_deformable_cable.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
from __future__ import annotations
1414

15+
import itertools
1516
import math
1617
import warnings
1718
from dataclasses import replace
1819

1920
import warp as wp
2021

22+
from ..usd.schema_resolver import PrimType
2123
from .import_usd_deformable_utils import (
2224
_DEFAULT_CABLE_RADIUS,
2325
_apply_cable_masses,
@@ -41,6 +43,20 @@
4143
)
4244

4345

46+
def _apply_cable_self_collision_filter(builder, bodies, self_collision_enabled: bool) -> None:
47+
"""Filter all shape pairs among a cable articulation's bodies when self-collision is disabled.
48+
49+
Adjacent segments are already filtered by the CABLE joints (``collision_filter_parent``);
50+
this extends filtering to the non-adjacent pairs, mirroring the general rigid importer.
51+
"""
52+
if self_collision_enabled or len(bodies) < 2:
53+
return
54+
for b1, b2 in itertools.combinations(bodies, 2):
55+
for s1 in builder.body_shapes[b1]:
56+
for s2 in builder.body_shapes[b2]:
57+
builder.add_shape_collision_filter_pair(s1, s2)
58+
59+
4460
def _read_validated_curve_topology(curves, path: str, *, warn: bool = True):
4561
"""Read a cable prim's ``points`` / ``curveVertexCounts`` after validating the partition.
4662
@@ -418,6 +434,29 @@ def global_node(local: tuple[str, int]) -> int:
418434
body_frame_origin="com",
419435
)
420436

437+
# One rod graph is one articulation, so resolve self-collision per component: a welded graph
438+
# disables it if ANY member curve prim authors newton:selfCollisionEnabled=False.
439+
self_collision_states = {
440+
key: bool(
441+
ctx.resolver.get_value(
442+
curve_recs[key].prim,
443+
prim_type=PrimType.ARTICULATION,
444+
key="self_collision_enabled",
445+
default=ctx.enable_self_collisions,
446+
verbose=verbose,
447+
)
448+
)
449+
for key in comp_paths
450+
}
451+
graph_self_collision = all(self_collision_states.values())
452+
if not graph_self_collision and any(self_collision_states.values()):
453+
warnings.warn(
454+
f"cable graph '{cid}': welded cables mix self-collision-enabled and "
455+
f"self-collision-disabled curves; the whole graph disables self-collision.",
456+
stacklevel=2,
457+
)
458+
_apply_cable_self_collision_filter(builder, body_ids, graph_self_collision)
459+
421460
# Partition graph bodies back to their owning curve, and rebuild the per-prim anchor
422461
# maps the curve-to-xform attachment pass reads (point index / segment index -> body).
423462
per_prim_segments: dict[str, dict[int, tuple[int, float]]] = {}
@@ -745,6 +784,17 @@ def _deformable_import_cable(ctx: _DeformableImportContext, consumed_cable_curve
745784
flat_segment_index += curve_segment_count
746785

747786
if cable_bodies:
787+
# Honor the cable articulation's newton:selfCollisionEnabled flag, like the rigid importer.
788+
self_collision_enabled = bool(
789+
ctx.resolver.get_value(
790+
prim,
791+
prim_type=PrimType.ARTICULATION,
792+
key="self_collision_enabled",
793+
default=ctx.enable_self_collisions,
794+
verbose=ctx.verbose,
795+
)
796+
)
797+
_apply_cable_self_collision_filter(builder, cable_bodies, self_collision_enabled)
748798
_apply_cable_masses(builder, prim, cable_bodies, cable_point_runs, closed, deformable_read, len(points))
749799
path_cable_map[path] = (cable_bodies, cable_joints)
750800
# Bodies/joints for a cable prim are built back-to-back, so the index lists are contiguous.

newton/_src/utils/import_usd_deformable_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,5 +1022,7 @@ class _DeformableImportContext:
10221022
path_soft_attrs: dict
10231023
path_attachment_map: dict
10241024
path_attachment_attrs: dict
1025+
# Default for self-collision when an articulation authors no newton:selfCollisionEnabled.
1026+
enable_self_collisions: bool = True
10251027
# Filled by _scout_deformable_prims so the passes iterate buckets instead of the stage.
10261028
prims: _DeformablePrimBuckets = field(default_factory=_DeformablePrimBuckets)

newton/tests/test_import_usd_deformable_cable.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,84 @@ def test_cable_collision_gating(self):
943943
self.assertEqual(is_colliding, expected_colliding, f"shape {i}")
944944
builder.finalize()
945945

946+
def test_cable_self_collision_disabled_filters_non_adjacent_pairs(self):
947+
"""newton:selfCollisionEnabled=False filters non-adjacent cable segment shape pairs.
948+
949+
Adjacent segments are already filtered by the CABLE joints; the flag extends the
950+
filtering to non-adjacent pairs, mirroring the general rigid importer. The default
951+
(flag True or unauthored) leaves those pairs unfiltered.
952+
"""
953+
from pxr import Sdf
954+
955+
# 5 points -> 4 segment bodies, so there are non-adjacent pairs to check.
956+
pts = [(0.0, 0.0, 1.0), (0.1, 0.0, 1.0), (0.2, 0.0, 1.0), (0.3, 0.0, 1.0), (0.4, 0.0, 1.0)]
957+
for case, self_collision in (("disabled", False), ("enabled", True), ("unauthored", None)):
958+
with self.subTest(case=case):
959+
stage = _deformable_stage()
960+
curve = _add_cable_curve(stage, "/World/Cable", pts)
961+
if self_collision is not None:
962+
curve.GetPrim().CreateAttribute("newton:selfCollisionEnabled", Sdf.ValueTypeNames.Bool).Set(
963+
self_collision
964+
)
965+
builder = newton.ModelBuilder()
966+
builder.add_usd(stage)
967+
b0, b1 = group_range(builder, "cable", "/World/Cable", "body")
968+
self.assertEqual(b1 - b0, 4)
969+
# A non-adjacent segment pair (bodies b0 and b0+2).
970+
s1 = builder.body_shapes[b0][0]
971+
s2 = builder.body_shapes[b0 + 2][0]
972+
pair = (min(s1, s2), max(s1, s2))
973+
pairs = set(builder.shape_collision_filter_pairs)
974+
if case == "disabled":
975+
self.assertIn(pair, pairs)
976+
else:
977+
self.assertNotIn(pair, pairs)
978+
979+
def test_welded_graph_self_collision_disabled_filters_non_adjacent_pairs(self):
980+
"""A welded cable graph honors newton:selfCollisionEnabled=False on any member curve.
981+
982+
Disabling self-collision on one welded member filters non-adjacent segment shape pairs
983+
across the whole graph articulation (both within a curve and between welded curves), and
984+
the importer warns that the mixed authoring resolves to self-collision disabled.
985+
"""
986+
from pxr import Sdf
987+
988+
for case, author in (("disabled", True), ("default", False)):
989+
with self.subTest(case=case):
990+
# Hard, coincident junction welds CableA and CableB into one graph articulation.
991+
stage = self._author_attached_cable_pair(gap=0.0)
992+
if author:
993+
stage.GetPrimAtPath("/World/CableA").CreateAttribute(
994+
"newton:selfCollisionEnabled", Sdf.ValueTypeNames.Bool
995+
).Set(False)
996+
builder = newton.ModelBuilder()
997+
if author:
998+
# Members disagree (CableB unauthored -> default True), so the importer warns.
999+
with self.assertWarnsRegex(UserWarning, "disables self-collision"):
1000+
builder.add_usd(stage)
1001+
else:
1002+
builder.add_usd(stage)
1003+
self.assertEqual(builder.articulation_count, 1)
1004+
a0, _ = group_range(builder, "cable", "/World/CableA", "body")
1005+
b0, _ = group_range(builder, "cable", "/World/CableB", "body")
1006+
pairs = set(builder.shape_collision_filter_pairs)
1007+
# A within-curve non-adjacent pair and a cross-curve non-adjacent pair.
1008+
within = self._shape_pair(builder, a0, a0 + 2)
1009+
cross = self._shape_pair(builder, a0 + 2, b0 + 2)
1010+
if case == "disabled":
1011+
self.assertIn(within, pairs)
1012+
self.assertIn(cross, pairs)
1013+
else:
1014+
self.assertNotIn(within, pairs)
1015+
self.assertNotIn(cross, pairs)
1016+
1017+
@staticmethod
1018+
def _shape_pair(builder, body_a, body_b):
1019+
"""Canonical (min, max) shape-filter pair for the first shapes of two bodies."""
1020+
s1 = builder.body_shapes[body_a][0]
1021+
s2 = builder.body_shapes[body_b][0]
1022+
return (min(s1, s2), max(s1, s2))
1023+
9461024
def test_neg_inf_junction_stiffness_does_not_weld(self):
9471025
"""-inf is the material sentinel, not the attachment one (+inf = hard): a
9481026
junction authoring -inf stiffness is nonconforming and must not weld the

0 commit comments

Comments
 (0)