Skip to content

Commit b5c561e

Browse files
authored
Merge branch 'main' into twidmer/more-contact-improvements
Signed-off-by: nvtw <110816143+nvtw@users.noreply.github.com>
2 parents 5672028 + fd8d9d4 commit b5c561e

17 files changed

Lines changed: 566 additions & 59 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Add `ViewerUSD(points_as_spheres=...)` to render `log_points` particles as a `UsdGeom.PointInstancer` of sphere prototypes; enabled by default (opt out with `points_as_spheres=False` for flat `UsdGeom.Points` splats)
1818
- Add experimental `newton.controllers` module with `ControllerBase` base class, `ControllerJointImpedance`, and `ControllerJointImpedanceModelFree` for GPU-accelerated, vectorized joint-space impedance control.
1919
- Add list-of-pattern and explicit-index selectors to `ArticulationView`.
20+
- Add full-surface (edge/face) rigid-soft contacts to `SolverVBD` proxy-body coupling under `SolverCoupledProxy` through shared or proxy-local collision pipelines. Proxy-particle coupling rejects full-surface contacts.
2021
- Add `newton[onnx]` for ONNX policy inference through Warp-NN; `ControllerNeuralMLP`, `ControllerNeuralLSTM`, and RL policy examples can run exported `.onnx` policies without requiring PyTorch for ONNX execution.
2122
- Add three VBD contact examples — `vbd_rigid_rigid_contact`, `vbd_soft_rigid_contact`, and `vbd_soft_rigid_mix_contact` — demonstrating rigid-rigid, soft (particle-rigid), and mixed cloth-bag contacts
2223
- Add masked rigid-body reset support to `SolverVBD`. (#3256)
@@ -47,6 +48,9 @@
4748

4849
### Changed
4950

51+
- Decide collider visibility from USD `purpose` and visibility rather than from a bound render material. A collider whose `purpose` resolves to `default` is viewport geometry and is drawn; mark it `guide` to state that it is collision-only. Previously an unrelated visual elsewhere in the scene could make a collider vanish. `force_show_colliders` and `hide_collision_shapes` are unchanged.
52+
- Filter shared full-surface soft contacts per `SolverCoupled` entry, preserving them for capable solvers and dropping them for particle-only solvers or records spanning entries.
53+
- Require `warp-lang>=1.16.0`; upgrade Warp to version 1.16.0 or later.
5054
- Follow USD viewport `purpose` and visibility for imported visual geometry and colliders rather than inferring visibility from a bound render material. Visual shapes and Gaussian splats are drawn only for `default` and `proxy`; a collider whose `purpose` resolves to `default` is drawn, while `guide` identifies collision-only geometry. `force_show_colliders` and `hide_collision_shapes` are unchanged. (#3404, #3712)
5155
- Require `warp-lang>=1.16.0`; upgrade Warp to version 1.16.0 or later. (#3780)
5256
- Disable the implicit positive Dahl-friction defaults in `SolverVBD.register_custom_attributes()` (deprecated in 1.3.0): `vbd:dahl_eps_max` and `vbd:dahl_tau` now default to zero, and Dahl cable friction is enabled only where both are authored positive. Pass `dahl_defaults_enabled=True` to temporarily restore the old defaults; the compatibility mode will be removed in a future release.
@@ -57,7 +61,7 @@
5761
- Upgrade `mujoco` and `mujoco-warp` to 3.11.0. (#3725)
5862
- Optimize raycast/raytrace queries by restructuring ray-shape intersection into local-space primitives and compile specialized depth/shadow variants that skip unused surface-normal work (mesh shadows also use any-hit queries).
5963
- Speed up `SolverKamino` DVI contact coloring for dense manifolds requiring more than 64 colors.
60-
- Change experimental `SolverVBD` cable constraint slots from `[STRETCH=0, BEND=1]` to `[STRETCH=0, SHEAR=1, BEND=2, TWIST=3]`, allowing each stiffness and constraint mode to be configured independently. Existing cable calls using raw `slot=1` or `JointSlot.ANGULAR` now select shear; use `JointSlot.BEND` (now slot 2) to select bending.
64+
- Change experimental `SolverVBD` cable constraint slots from `[STRETCH=0, BEND=1]` to `[STRETCH=0, SHEAR=1, BEND=2, TWIST=3]`, allowing each stiffness and constraint mode to be configured independently while preserving the pre-split world-form parent solve Hessian for the common isotropic component of stretch/shear elasticity. Existing cable calls using raw `slot=1` or `JointSlot.ANGULAR` now select shear; use `JointSlot.BEND` (now slot 2) to select bending.
6165
- Map `shape_material_kf` to per-contact MuJoCo `solreffriction` in `SolverMuJoCo` (elliptic friction cones with Newton contacts); resolve `kf` with priority/`solmix`, treat a resolved `kf = 0` as frictionless, and use native MuJoCo contacts or a pyramidal cone to preserve the previous solref-inherited friction.
6266
- Load visual-only USD geometry outside rigid-body hierarchies as static shapes by default; pass `load_static_visual_shapes=False` to retain the previous body-associated-visuals-only behavior.
6367
- Speed up `Mesh.create_heightfield()` and `Mesh.create_terrain()` by building the vertex and index buffers in place, substantially reducing construction time and peak memory for large terrain grids such as those used by Isaac Lab.
@@ -95,6 +99,7 @@
9599
- Make deterministic collision pipelines cover hydroelastic contact generation and reduction, including unique reduced-contact sort keys and overflow-safe fixed-point pressure accumulation. (#3661)
96100
- Fix `SolverMuJoCo` retaining an invalid external-contact cache when its first step is captured in a CUDA graph. (#3768; fixes #3767)
97101
- Preserve box-box face contact manifolds under sub-microradian solver drift. (#3776)
102+
- Fix `SolverMuJoCo` overflowing MuJoCo's signed 32-bit collision masks when graph coloring requires the highest supported color; all 32 mask bits are now used before falling back to default collision masks for additional colors.
98103
- Convert `newton:mimicCoef0` from degrees to radians when the mimic follower joint is angular. Assets authored against the old behavior need the value rescaled to degrees.
99104
- Complete Kamino RCM traversal for large and disconnected systems and reuse the resulting permutation by default; set `reuse_permutation=False` to recompute it for changing matrix topology.
100105
- Bound Kamino DVI contact allocation with a per-world geometry heuristic instead of sizing every contact pair simultaneously; set `collision_detector.max_contacts_per_world` to override the inferred capacity.
@@ -106,6 +111,7 @@
106111
- Fix panel-parallel RCM-blocked LLT factorization hanging when a matrix ends in a partial tile.
107112
- Fix USD capsule, cylinder, and cone visual and site scaling to follow the authored primitive axis.
108113
- Fix MJCF contact pairs ignoring properties inherited from pair default classes.
114+
- Fix disabled USD colliders participating in particle collisions when visual shape loading is disabled.
109115
- Fix `ArticulationView.is_fixed_base` for roots with zero effective degrees of freedom, including fully locked D6 joints. (#3727)
110116
- Fix USD plane visual width and length to scale along the axes defined by the `UsdGeomPlane` schema, and orient X- and Y-axis plane visuals along the authored axis.
111117
- Validate `ArticulationView` mask shapes and devices before launching selection kernels. (#3448)

docs/concepts/coupling.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ The coupled-solver framework is useful today, but it is still experimental:
379379
custom hook is available.
380380
- USD ownership, automatic coupled-solver construction, and high-level tuning
381381
guidance are not part of the experimental public API yet.
382+
- Full-surface (edge/face) rigid-soft contacts are consumed by
383+
:class:`~newton.solvers.SolverVBD` only. The per-entry contact filter drops
384+
them for other sub-solvers while preserving particle contacts, and keeps them
385+
for VBD only when the entry owns every referenced corner. A record spanning
386+
two entries is dropped by both. Proxy particles do not support them at all.
382387

383388
Treat coupled solvers as an advanced feature for controlled experiments and
384389
solver integration work. Prefer focused regression tests and explicit scene

newton/_src/solvers/coupled/interface.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def coupling_notify_input_state_update(state, flags, *, iteration_restart=False,
4444
def coupling_supports_inertial_property_refresh() -> bool: ...
4545
4646
47+
def coupling_supports_full_surface_soft_contacts() -> bool: ...
48+
49+
4750
def coupling_rewind_proxy_body(
4851
body_local_to_proxy_global, state, coupling_forces, body_gravity_acceleration, dt
4952
) -> None: ...
@@ -252,6 +255,10 @@ def coupling_supports_inertial_property_refresh(self) -> bool:
252255
"""
253256
return False
254257

258+
def coupling_supports_full_surface_soft_contacts(self) -> bool:
259+
"""Return whether the solver consumes edge and face soft contacts."""
260+
return False
261+
255262
def coupling_eval_gravity_acceleration(
256263
self,
257264
out_body_acceleration: wp.array[wp.vec3] | None,

newton/_src/solvers/coupled/solver_coupled.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,6 +2711,7 @@ def _contacts_for_entry(self, entry: SolverEntry, contacts: Contacts | None) ->
27112711
return contacts
27122712

27132713
filtered = self._ensure_entry_contact_buffer(entry, contacts)
2714+
keep_full_surface_contacts = filtered._enable_rigid_soft_full_surface_contact
27142715
force_contact_update = int(self._entry_contact_sources.get(entry.name) is not contacts)
27152716
if force_contact_update:
27162717
self._entry_contact_sources[entry.name] = contacts
@@ -2849,6 +2850,7 @@ def _contacts_for_entry(self, entry: SolverEntry, contacts: Contacts | None) ->
28492850
entry.view.particle_flags,
28502851
int(ShapeFlags.COLLIDE_PARTICLES),
28512852
int(ParticleFlags.ACTIVE),
2853+
int(keep_full_surface_contacts),
28522854
filtered.soft_contact_count,
28532855
filtered.soft_contact_particle,
28542856
filtered.soft_contact_shape,
@@ -2917,6 +2919,10 @@ def _ensure_entry_contact_buffer(self, entry: SolverEntry, contacts: Contacts) -
29172919
device=contacts.device,
29182920
)
29192921
filtered._contact_matching_mode = contacts.contact_matching_mode
2922+
filtered._enable_rigid_soft_full_surface_contact = bool(
2923+
contacts._enable_rigid_soft_full_surface_contact
2924+
and entry.solver.coupling_supports_full_surface_soft_contacts()
2925+
)
29202926
return filtered
29212927

29222928
@staticmethod
@@ -3674,6 +3680,7 @@ def _filter_soft_contacts_global_shape_ids_kernel(
36743680
particle_flags: wp.array[wp.int32],
36753681
collide_particles_mask: int,
36763682
active_particle_mask: int,
3683+
keep_full_surface_contacts: int,
36773684
dst_count: wp.array[wp.int32],
36783685
dst_particle: wp.array[int],
36793686
dst_shape: wp.array[int],
@@ -3694,14 +3701,24 @@ def _filter_soft_contacts_global_shape_ids_kernel(
36943701

36953702
particle = src_particle[contact_id]
36963703
shape = src_shape[contact_id]
3697-
if particle < 0 or shape < 0:
3704+
if particle < 0 and keep_full_surface_contacts == 0:
36983705
return
3699-
if particle >= particle_flags.shape[0] or shape >= shape_flags.shape[0]:
3706+
if shape < 0 or shape >= shape_flags.shape[0]:
37003707
return
37013708
if (shape_flags[shape] & collide_particles_mask) == 0:
37023709
return
3703-
if (particle_flags[particle] & active_particle_mask) == 0:
3710+
3711+
# Keep records only when the entry owns every referenced corner.
3712+
corners = src_indices[contact_id]
3713+
if corners[0] < 0:
37043714
return
3715+
for i in range(3):
3716+
corner = corners[i]
3717+
if corner >= 0:
3718+
if corner >= particle_flags.shape[0]:
3719+
return
3720+
if (particle_flags[corner] & active_particle_mask) == 0:
3721+
return
37053722

37063723
dst_id = wp.atomic_add(dst_count, 0, wp.int32(1))
37073724
src_to_dst[contact_id] = dst_id

newton/_src/solvers/mujoco/solver_mujoco.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5126,10 +5126,8 @@ def _color_collision_shapes(
51265126
graph_edge_indices=wp.array(graph_edges, dtype=wp.int32),
51275127
balance_colors=False,
51285128
)
5129-
num_colors = 0
5130-
for group in color_groups:
5131-
num_colors += 1
5132-
shape_color[selected_shapes[group]] = num_colors
5129+
for color, group in enumerate(color_groups):
5130+
shape_color[selected_shapes[group]] = color
51335131
if visualize_graph:
51345132
plot_graph(
51355133
vertices=np.arange(num_shapes),
@@ -5140,6 +5138,21 @@ def _color_collision_shapes(
51405138

51415139
return shape_color
51425140

5141+
@staticmethod
5142+
def _collision_color_masks(color: int) -> tuple[int, int]:
5143+
"""Encode a collision color as signed MuJoCo ``contype`` and ``conaffinity`` masks."""
5144+
if color >= 32:
5145+
return 1, 1
5146+
5147+
uint32_mask = (1 << 32) - 1
5148+
contype = 1 << color
5149+
conaffinity = uint32_mask & ~contype
5150+
5151+
def as_signed_int32(mask: int) -> int:
5152+
return mask if mask < (1 << 31) else mask - (1 << 32)
5153+
5154+
return as_signed_int32(contype), as_signed_int32(conaffinity)
5155+
51435156
def get_max_contact_count(self) -> int:
51445157
"""Return the maximum number of rigid contacts that can be generated by MuJoCo."""
51455158
if self.use_mujoco_cpu:
@@ -5613,9 +5626,6 @@ def joint_has_raw_limit_solref(dof_idx: int) -> bool:
56135626
mimic_enabled = model.constraint_mimic_enabled.numpy()
56145627
mimic_world = model.constraint_mimic_world.numpy()
56155628

5616-
INT32_MAX = np.iinfo(np.int32).max
5617-
collision_mask_everything = INT32_MAX
5618-
56195629
# mapping from joint axis to actuator index
56205630
# axis_to_actuator[i, 0] = position actuator index
56215631
# axis_to_actuator[i, 1] = velocity actuator index
@@ -6131,11 +6141,9 @@ def add_geoms(newton_body_id: int):
61316141
geom_params["conaffinity"] = mujoco_mask_to_signed(int(compiled_collision_affinity[shape]))
61326142
else:
61336143
color = shape_color[shape]
6134-
if color < 32:
6135-
contype = 1 << color
6136-
geom_params["contype"] = contype
6137-
# collide with anything except shapes from the same color
6138-
geom_params["conaffinity"] = collision_mask_everything & ~contype
6144+
contype, conaffinity = self._collision_color_masks(int(color))
6145+
geom_params["contype"] = contype
6146+
geom_params["conaffinity"] = conaffinity
61396147

61406148
# set friction from Newton shape materials
61416149
mu = shape_mu[shape]

newton/_src/solvers/vbd/rigid_vbd_kernels.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,8 +1088,10 @@ def evaluate_cable_stretch_shear_force_hessian(
10881088
u = wp.quat_rotate_inv(q_wp, C_vec)
10891089
psi = wp.cw_mul(k_diag, u) - C0_force_local + lambda_local
10901090

1091-
h_s = k_diag[0]
1092-
h_z = k_diag[2]
1091+
k_s = k_diag[0]
1092+
k_z = k_diag[2]
1093+
h_s = k_s
1094+
h_z = k_z
10931095
if damping_active:
10941096
inv_dt = 1.0 / dt
10951097
x_p_prev = wp.transform_get_translation(X_wp_prev)
@@ -1103,11 +1105,24 @@ def evaluate_cable_stretch_shear_force_hessian(
11031105
force = f_world if is_parent else -f_world
11041106

11051107
t = _quat_rotate_local_z(q_wp)
1106-
K_eff = h_s * wp.identity(3, float) + (h_z - h_s) * wp.outer(t, t)
1107-
rx = wp.skew(r)
1108+
identity = wp.identity(3, float)
1109+
K_eff = h_s * identity + (h_z - h_s) * wp.outer(t, t)
11081110
H_ll = K_eff
1109-
H_al = rx * K_eff
1110-
H_aa = wp.transpose(rx) * K_eff * rx
1111+
if is_parent:
1112+
# Isotropic elastic energy is frame-invariant, so it takes the parent-anchor arm that
1113+
# evaluate_linear_constraint_force_hessian also uses; min() extracts the largest such block
1114+
# leaving a PSD remainder. Damping keeps the material arm: u_prev is frozen one step back.
1115+
k_iso = wp.min(k_s, k_z)
1116+
K_material = K_eff - k_iso * identity
1117+
r_elastic = x_p - com_w
1118+
rx_material = wp.skew(r)
1119+
H_al = k_iso * wp.skew(r_elastic) + rx_material * K_material
1120+
H_aa = k_iso * (wp.length_sq(r_elastic) * identity - wp.outer(r_elastic, r_elastic))
1121+
H_aa = H_aa + wp.transpose(rx_material) * K_material * rx_material
1122+
else:
1123+
rx = wp.skew(r)
1124+
H_al = rx * K_eff
1125+
H_aa = wp.transpose(rx) * K_eff * rx
11111126

11121127
torque = wp.cross(r, force)
11131128
return force, torque, H_ll, H_al, H_aa

newton/_src/solvers/vbd/solver_vbd.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,10 @@ def notify_model_changed(self, flags: ModelFlags | int) -> None:
868868
def coupling_supports_inertial_property_refresh(self) -> bool:
869869
return True
870870

871+
@override
872+
def coupling_supports_full_surface_soft_contacts(self) -> bool:
873+
return True
874+
871875
def coupling_notify_input_state_update(
872876
self,
873877
state: State,
@@ -926,19 +930,6 @@ def coupling_prepare_proxy_contacts(
926930
contacts_freshly_detected: bool = False,
927931
) -> Contacts | None:
928932
"""Update rigid history cadence for proxy contacts."""
929-
# Full-surface (edge/face) rigid-soft contacts are not yet harvested onto proxy particles:
930-
# the proxy contact-force kernels consume only per-particle records (particle >= 0), so a soft
931-
# edge/face contact's reaction on a proxy-coupled rigid body would be silently dropped. Fail
932-
# loud until proxy harvesting consumes the unified records. Standalone SolverVBD (no proxy
933-
# coupling) never reaches this hook and does support full-surface via the per-body path.
934-
if contacts is not None and getattr(contacts, "_enable_rigid_soft_full_surface_contact", False):
935-
raise NotImplementedError(
936-
"Full-surface (edge/face) rigid-soft contacts are not yet supported with VBD proxy-particle "
937-
"coupling (SolverCoupledProxy): the proxy contact-force harvest consumes only per-particle "
938-
"records, so edge/face force feedback to proxy-coupled rigid bodies would be silently dropped. "
939-
"Set enable_rigid_soft_full_surface_contact=False for the coupled proxy solve, or drive the "
940-
"rigid bodies with standalone SolverVBD (which supports full-surface via the per-body path)."
941-
)
942933
# Do not call super(); we can keep proxy-proxy collisions as we
943934
# are using a custom force harvesting hook
944935
self.set_rigid_history_update(bool(contacts_freshly_detected))
@@ -1032,7 +1023,8 @@ def coupling_harvest_proxy_wrenches(
10321023
self.body_particle_contact_material_kd,
10331024
self.body_particle_contact_material_mu,
10341025
contacts.soft_contact_count,
1035-
contacts.soft_contact_particle,
1026+
contacts.soft_contact_indices,
1027+
contacts.soft_contact_barycentric,
10361028
contacts.soft_contact_shape,
10371029
contacts.soft_contact_body_pos,
10381030
contacts.soft_contact_body_vel,
@@ -1068,6 +1060,9 @@ def coupling_harvest_proxy_particle_forces(
10681060
if self.model.particle_count == 0 or particle_local_to_proxy_global.shape[0] == 0:
10691061
return
10701062

1063+
if contacts is not None:
1064+
contacts._assert_particle_only_soft_contacts("SolverVBD proxy-particle coupling")
1065+
10711066
if (
10721067
contacts is not None
10731068
and contacts.soft_contact_max > 0

0 commit comments

Comments
 (0)