Skip to content
Draft
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
749b0e0
First attempt to bring support for speculative contacts into the coll…
nvtw Apr 2, 2026
41ca9d7
Add more unit tests for speculative contacts
nvtw Apr 2, 2026
8881183
Ran ruff
nvtw Apr 2, 2026
8f6dd7b
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 8, 2026
fed33dc
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 8, 2026
de66f90
Implement MR comments
nvtw Apr 8, 2026
223a5b4
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 8, 2026
2e8bf6b
Update the docs
nvtw Apr 8, 2026
5e3aece
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 8, 2026
0907140
Improve the docs
nvtw Apr 9, 2026
89cbdf6
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 9, 2026
5113899
Extend speculative contacts to support meshes
nvtw Apr 10, 2026
3e19e78
Deterministic contacts start to work for convex vs convex and convex …
nvtw Apr 10, 2026
e67a7a9
Determinism starts to work for meshes
nvtw Apr 10, 2026
f2d7647
Fix issues in contact pre-pruning
nvtw Apr 10, 2026
73849b1
Add more unit tests
nvtw Apr 10, 2026
50858c7
Ran ruff
nvtw Apr 10, 2026
ad5c0f7
Remove debug printf that slipped in
nvtw Apr 10, 2026
7623a82
Forgot to add new file
nvtw Apr 10, 2026
4f15e42
Ran ruff
nvtw Apr 10, 2026
f9ef6c4
Update the docs
nvtw Apr 10, 2026
c38b83e
Fix sort issue in contact determinism
nvtw Apr 10, 2026
92d9399
Fix hte docs
nvtw Apr 10, 2026
587be69
Fix the performance regression
nvtw Apr 10, 2026
ed6ee76
Implement more improvements
nvtw Apr 10, 2026
41ed6c8
Implement CodeRabbit comments
nvtw Apr 13, 2026
d2f7a09
Update the changelog and the docs
nvtw Apr 13, 2026
c7bc73c
Implement MR comments
nvtw Apr 13, 2026
5df8e2c
Merge branch 'main' into dev/tw3/deterministic_contacts
nvtw Apr 13, 2026
5e89322
Merge branch 'main' into dev/tw3/deterministic_contacts
nvtw Apr 13, 2026
ac66d07
Implement more MR comments
nvtw Apr 14, 2026
4d56f4f
Merge branch 'main' into dev/tw3/deterministic_contacts
nvtw Apr 14, 2026
45cfbf0
Implement more CodeRabbit comments
nvtw Apr 14, 2026
77b5331
Improve sorting
nvtw Apr 14, 2026
bb3ec2a
Improve the unit tests
nvtw Apr 14, 2026
c37a749
Attempt to fix some remaining non-determinism
nvtw Apr 14, 2026
f5f78e4
First attempt towards contact matching
nvtw Apr 14, 2026
a367f23
Reduce memory footprint
nvtw Apr 14, 2026
f6c61eb
Bugfix
nvtw Apr 14, 2026
975d973
Improve the test coverage
nvtw Apr 14, 2026
77882a0
Add more diagnostics to the tests.
nvtw Apr 14, 2026
3a8ed92
Disable prepruning for determinism experiments.
nvtw Apr 14, 2026
c639770
Attempt to fix CI
nvtw Apr 14, 2026
a0b1462
Remove unnecessary file
nvtw Apr 14, 2026
d10983c
Merge remote-tracking branch 'origin/dev/tw3/deterministic_contacts' …
nvtw Apr 14, 2026
1a5b370
Merge branch 'main' into dev/tw3/deterministic_contacts
nvtw Apr 14, 2026
94eef59
Merge remote-tracking branch 'refs/remotes/origin/dev/tw3/determinist…
nvtw Apr 14, 2026
3e833e9
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 15, 2026
1370429
Update the changelog
nvtw Apr 15, 2026
ed4c6e1
Update the documentation
nvtw Apr 15, 2026
b531b49
Implement CodeRabbit comments
nvtw Apr 15, 2026
a0073fc
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 15, 2026
8751e22
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 16, 2026
69d3491
Ran ruff
nvtw Apr 16, 2026
a76d50f
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 16, 2026
b739859
Implement CodeRabbit comments
nvtw Apr 16, 2026
e994d6b
Implement MR comments
nvtw Apr 17, 2026
631b7a3
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 17, 2026
2c33eee
Implement CodeRabbit comments
nvtw Apr 17, 2026
cb52409
Implement more MR comments
nvtw Apr 17, 2026
1fc6ea7
Update docs
nvtw Apr 17, 2026
4379204
Fix CI
nvtw Apr 17, 2026
1295271
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 17, 2026
6691056
Test better matching - not done yet
nvtw Apr 17, 2026
d8f89c7
Ensure that only one contact can claim a contact from the last frame
nvtw Apr 17, 2026
2c82a1d
Small performance improvement
nvtw Apr 17, 2026
287ce43
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 20, 2026
cf4e985
Add sticky contact matching mode
nvtw Apr 21, 2026
99b0e2c
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 21, 2026
bd6ed7a
Handle contact matching break distance evaluation in a more symmetric…
nvtw Apr 21, 2026
f1dca6e
Improve the docs
nvtw Apr 21, 2026
4fec7f5
Attempt to fix CI docs
nvtw Apr 21, 2026
dcd693b
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 21, 2026
bfb4dc3
Contact matching experiments
nvtw Apr 21, 2026
ac5c049
Add more diagnostic output
nvtw Apr 21, 2026
49ce8bd
Revert "Add more diagnostic output"
nvtw Apr 21, 2026
743bac9
Revert "Contact matching experiments"
nvtw Apr 21, 2026
011d75c
Fix normal misalignment issue
nvtw Apr 21, 2026
e6f0542
Unit test fixes
nvtw Apr 21, 2026
f47d3a0
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 23, 2026
e33ca55
Fix too long changelog entry and mark sticky mode as experimental
nvtw Apr 23, 2026
dbe98ef
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 23, 2026
c05201f
Remove superfluous enum
nvtw Apr 24, 2026
690bf2c
Implement more MR comments
nvtw Apr 24, 2026
49712a0
Merge branch 'main' into dev/tw3/contact_matching
nvtw Apr 24, 2026
3c67048
Merge branch 'dev/tw3/contact_matching' into dev/tw2/speculative_cont…
nvtw Apr 24, 2026
1ae123a
Merge branch 'main' into dev/tw2/speculative_contacts
nvtw Apr 24, 2026
32e0bff
Implement the MR comments
nvtw Apr 24, 2026
312d9a1
Attempt to fix CI
nvtw Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Add `deterministic` flag to `CollisionPipeline` and `NarrowPhase` for GPU-thread-scheduling-independent contact ordering via radix sort and deterministic fingerprint tiebreaking in contact reduction
- Add fast parity-based SDF construction path for watertight meshes in `SDF.create_from_mesh`, using `wp.mesh_query_point_sign_parity` instead of winding numbers; selected via the new `sign_method` argument (`"auto"` — the default — picks parity when `Mesh.is_watertight` is true, or `"parity"` / `"winding"` to force either strategy)
- Add `ViewerBase.log_arrows()` for arrow rendering (wide line + arrowhead) in the GL viewer with a dedicated geometry shader
- Add frame-to-frame contact matching via `CollisionPipeline(contact_matching=...)` with modes `"latest"` (populates `contacts.rigid_contact_match_index`) and `"sticky"` (experimental; additionally replays previous-frame contact geometry on matched contacts — the sticky update strategy may change without warning). Optional `contact_report=True` exposes new/broken contact index lists on `Contacts`.
- Add `enable_multiccd` parameter to `SolverMuJoCo` for multi-CCD contact generation (up to 4 contact points per geom pair)
- Support `<joint type="ball"/>` in the MJCF importer, and preserve authored damping, stiffness, and frictionloss when exporting ball joints to MuJoCo specs (previously silently dropped)
- Add `ViewerViser.log_scalar()` for live scalar time-series plots via uPlot
Expand Down Expand Up @@ -100,6 +101,8 @@
- Export `ViewerBase` from `newton.viewer` public API
- Add `custom_attributes` argument to `ModelBuilder.add_shape_convex_hull()`
- Add RJ45 plug-socket insertion example with SDF contacts, latch joint, and interactive gizmo
- Add `SpeculativeContactConfig` and speculative contact support in `CollisionPipeline` to detect contacts for fast-moving objects before they tunnel through thin geometry
- Add `TRIANGLE_PRISM` support-function type for heightfield triangles, extruding 1 m along the heightfield's local -Z so GJK/MPR naturally resolves shapes on the back side
Comment thread
nvtw marked this conversation as resolved.
Outdated
- Add site-targeted actuator support to MuJoCo solver

### Changed
Expand Down
1 change: 1 addition & 0 deletions docs/api/newton.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ newton
ParticleFlags
SDF
ShapeFlags
SpeculativeContactConfig
State
TetMesh

Expand Down
12 changes: 12 additions & 0 deletions docs/api/newton_geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,15 @@ newton.geometry
sdf_plane
sdf_sphere
transform_inertia

.. rubric:: Constants

.. list-table::
:header-rows: 1

* - Name
- Value
* - ``MATCH_BROKEN``
- ``-2``
* - ``MATCH_NOT_FOUND``
- ``-1``
187 changes: 187 additions & 0 deletions docs/concepts/collisions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,48 @@ Example (mesh SDF workflow):

The builder's ``rigid_gap`` (default 0.1) applies to shapes without explicit ``gap``. Alternatively, use ``builder.default_shape_cfg.gap``.

.. _Speculative Contacts:

Speculative Contacts
--------------------

Fast-moving objects can travel farther than the contact gap in a single time
step, causing them to tunnel through thin geometry. **Speculative contacts**
widen the detection window based on per-shape velocity so that contacts which
*will* occur within the next collision update interval are caught early.

Enable speculative contacts by passing a :class:`SpeculativeContactConfig` to
:class:`CollisionPipeline`:

.. code-block:: python

config = newton.SpeculativeContactConfig(
max_speculative_extension=0.5, # cap per-axis AABB growth [m]
collision_update_dt=1.0 / 60.0, # expected interval between collide() calls
)
pipeline = newton.CollisionPipeline(model, speculative_config=config)

At each ``collide()`` call the pipeline:

1. Computes per-shape linear velocity and an angular-speed bound from
``State.body_qd``.
2. Expands each shape AABB by the clamped velocity contribution (capped by
``max_speculative_extension``) so the broad phase returns candidate pairs
that are about to collide.
3. In the narrow phase, recomputes the contact gap using the normal-projected
approach speed of the relative linear velocity **plus** per-shape
angular-speed bounds (clamped by ``max_speculative_extension``), so only
genuinely approaching pairs are accepted.

The ``collision_update_dt`` can be overridden per call:

.. code-block:: python

pipeline.collide(state, contacts, dt=sim_dt)

When ``speculative_config`` is ``None`` (the default), all speculative code
paths are eliminated at compile time with zero runtime overhead.

.. _Common Patterns:

Common Patterns
Expand Down Expand Up @@ -1263,6 +1305,19 @@ and is consumed by the solver :meth:`~solvers.SolverBase.step` method for contac
- Contact normal, pointing from shape 0 toward shape 1 (world frame).
* - ``rigid_contact_margin0``, ``rigid_contact_margin1``
- Per-shape thickness: effective radius + margin (scalar).
* - ``rigid_contact_match_index``
- Per-contact frame-to-frame match result (int32). ``>= 0``: matched old
index, ``-1``: new, ``-2``: broken. Only allocated when
``contact_matching`` is not ``"disabled"``.
See :ref:`Contact Matching`.
* - ``rigid_contact_new_indices``, ``rigid_contact_new_count``
- Compact index list of new contacts in the current sorted buffer (where
``match_index < 0``). Only allocated when ``contact_report=True``.
See :ref:`Contact Reports`.
* - ``rigid_contact_broken_indices``, ``rigid_contact_broken_count``
- Compact index list of contacts from the previous frame that no current
contact matched. Only allocated when ``contact_report=True``.
See :ref:`Contact Reports`.

**Soft contacts (particle-shape):**

Expand Down Expand Up @@ -1679,6 +1734,138 @@ fully CUDA-graph-capturable.

Hydroelastic contacts are not yet covered by deterministic ordering.

.. _Contact Matching:

Contact Matching
----------------

Contact matching tracks contacts across frames, identifying which contacts
persist, which are new, and which have broken. The ``contact_matching``
argument on :class:`~CollisionPipeline` selects one of three modes:

- ``"disabled"`` (default) — no matching, no extra buffers.
- ``"latest"`` — match current contacts against the previous
frame and populate :attr:`Contacts.rigid_contact_match_index`, but keep the
current frame's freshly generated contact geometry in the returned
:class:`Contacts` buffer.
- ``"sticky"`` (experimental) — match like ``"latest"``, then overwrite
each matched contact's body-frame contact points (``point0``/``point1``),
offsets (``offset0``/``offset1``), and world-frame ``normal`` with the
saved previous-frame values. The remaining contact fields
(``shape0``/``shape1``, ``margin0``/``margin1``) are either key-derived
or per-shape constants and so are already identical for a matched
contact — no extra state is kept for them. Unmatched contacts pass
through with their fresh narrow-phase geometry. Useful for stacking
scenarios where small frame-to-frame geometric jitter on persistent
contacts degrades stability.

.. warning::
Sticky mode is experimental. The way sticky contacts are updated
across frames may change in the future without warning.

Any non-disabled mode implies ``deterministic=True``.

.. testsetup:: contact-matching

import warp as wp
import newton

builder = newton.ModelBuilder()
builder.add_ground_plane()
body = builder.add_body(xform=wp.transform((0.0, 0.0, 2.0), wp.quat_identity()))
builder.add_shape_sphere(body, radius=0.5)
model = builder.finalize()
state = model.state()

.. testcode:: contact-matching

pipeline = newton.CollisionPipeline(
model,
contact_matching="latest",
contact_matching_pos_threshold=0.005, # metres (default 0.0005)
contact_matching_normal_dot_threshold=0.9, # cos(~25°)
)
contacts = pipeline.contacts()

pipeline.collide(state, contacts)

# Per-contact match index (int32):
# >= 0 : index of the matched contact in the previous frame
# -1 : new contact (no match found)
# -2 : key matched but position/normal thresholds exceeded (broken)
match_idx = contacts.rigid_contact_match_index.numpy()

Each frame, the matcher binary-searches the current contacts against the
previous frame's sorted keys, then verifies candidates against a world-space
distance threshold and a normal dot-product threshold. The sort key encodes
``(shape_a, shape_b, sub_key)`` so only contacts between the same shape pair
are compared.

The distance metric is the world-space **contact midpoint**
``0.5 * (world(point0) + world(point1))`` — symmetric in shape 0 and shape 1
— which means swapping the two shapes of a pair does not change whether a
contact matches. It also means pure changes in penetration depth register
as motion on both sides of the contact, not just one.

**Thresholds**

- ``contact_matching_pos_threshold`` — maximum world-space distance [m]
between the previous and current contact midpoints for a match. Contacts
that moved more than this between frames are considered broken. Defaults
to ``0.0005`` m.
- ``contact_matching_normal_dot_threshold`` — minimum dot product between old
and new contact normals. Below this the contact is reported as broken even
if the key and position match.

**Sticky mode**

Replay of the matched previous-frame geometry happens after the deterministic
sort, so ``match_index`` already addresses the final sorted layout. Unmatched
rows (``MATCH_NOT_FOUND`` / ``MATCH_BROKEN``) are left untouched, so new and
threshold-broken contacts keep their fresh narrow-phase geometry. Because
matching requires both a position delta below the threshold and a normal dot
product above the threshold, the saved values are guaranteed to be a close
approximation of the current geometry and are safe to reuse. The extra
per-contact buffers (four ``vec3`` columns for the body-frame points and
offsets) are only allocated when the mode is ``"sticky"``; ``"latest"`` and
``"disabled"`` pay zero additional memory and launch no additional kernels.

.. _Contact Reports:

Contact Reports
^^^^^^^^^^^^^^^

Pass ``contact_report=True`` to also collect compact index lists of new and
broken contacts each frame. ``contact_report=True`` requires a non-disabled
matching mode:

.. testcode:: contact-matching

pipeline = newton.CollisionPipeline(
model,
contact_matching="latest",
contact_report=True,
)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)

n_new = contacts.rigid_contact_new_count.numpy()[0]
new_indices = contacts.rigid_contact_new_indices.numpy()[:n_new]

n_broken = contacts.rigid_contact_broken_count.numpy()[0]
broken_indices = contacts.rigid_contact_broken_indices.numpy()[:n_broken]

``rigid_contact_new_indices`` holds indices into the current frame's sorted
contact buffer for every contact with ``match_index < 0``. This includes both
genuinely new contacts (``MATCH_NOT_FOUND``, ``match_index == -1``) and
threshold-broken contacts whose sort key matched a previous-frame contact but
whose position or normal exceeded the configured thresholds
(``MATCH_BROKEN``, ``match_index == -2``). Inspect
``contacts.rigid_contact_match_index`` to distinguish the two cases.

``rigid_contact_broken_indices`` holds indices into the *previous* frame's
sorted buffer for contacts that no current contact matched.

.. _Performance:

Performance
Expand Down
2 changes: 2 additions & 0 deletions newton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
JointType,
Model,
ModelBuilder,
SpeculativeContactConfig,
State,
eval_fk,
eval_ik,
Expand All @@ -73,6 +74,7 @@
"JointType",
"Model",
"ModelBuilder",
"SpeculativeContactConfig",
"State",
"eval_fk",
"eval_ik",
Expand Down
3 changes: 3 additions & 0 deletions newton/_src/geometry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
collide_sphere_cylinder,
collide_sphere_sphere,
)
from .contact_match import MATCH_BROKEN, MATCH_NOT_FOUND
from .flags import ParticleFlags, ShapeFlags
from .inertia import compute_inertia_shape, compute_inertia_sphere, transform_inertia
from .sdf_utils import SDF
Expand All @@ -32,6 +33,8 @@
from .utils import compute_shape_radius

__all__ = [
"MATCH_BROKEN",
"MATCH_NOT_FOUND",
"SDF",
"BroadPhaseAllPairs",
"BroadPhaseExplicit",
Expand Down
Loading
Loading