Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 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 @@ -76,6 +76,9 @@
- 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 `ViewerGL.log_scalar()` for live scalar time-series plots in the viewer
Comment thread
nvtw marked this conversation as resolved.
Outdated

### 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 @@ -55,6 +55,7 @@ newton
ParticleFlags
SDF
ShapeFlags
SpeculativeContactConfig
State
TetMesh

Expand Down
42 changes: 42 additions & 0 deletions docs/concepts/collisions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,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
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
211 changes: 116 additions & 95 deletions newton/_src/geometry/contact_reduction_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,106 +1452,127 @@ def export_reduced_contacts_kernel(
return export_reduced_contacts_kernel


@wp.kernel(enable_backward=False, module="unique")
def mesh_triangle_contacts_to_reducer_kernel(
shape_types: wp.array[int],
shape_data: wp.array[wp.vec4],
shape_transform: wp.array[wp.transform],
shape_source: wp.array[wp.uint64],
shape_gap: wp.array[float],
shape_heightfield_index: wp.array[wp.int32],
heightfield_data: wp.array[HeightfieldData],
heightfield_elevations: wp.array[wp.float32],
triangle_pairs: wp.array[wp.vec3i],
triangle_pairs_count: wp.array[int],
reducer_data: GlobalContactReducerData,
total_num_threads: int,
):
"""Process mesh/heightfield-triangle contacts and store them in GlobalContactReducer.
def create_mesh_triangle_contacts_to_reducer_kernel(speculative: bool = False):
"""Factory for the mesh/heightfield-triangle → reducer kernel.

This kernel processes triangle pairs (mesh-or-hfield shape, convex-shape, triangle_index)
and computes contacts using GJK/MPR, storing results in the GlobalContactReducer for
subsequent reduction and export.

Uses grid stride loop over triangle pairs.
When *speculative* is True the kernel reads per-shape velocity arrays and
extends ``gap_sum`` by a scalar speculative margin. When False the extra
code is eliminated at compile time via ``wp.static``.
"""
tid = wp.tid()

num_triangle_pairs = triangle_pairs_count[0]
@wp.kernel(enable_backward=False, module="unique")
def mesh_triangle_contacts_to_reducer_kernel(
shape_types: wp.array[int],
shape_data: wp.array[wp.vec4],
shape_transform: wp.array[wp.transform],
shape_source: wp.array[wp.uint64],
shape_gap: wp.array[float],
shape_heightfield_index: wp.array[wp.int32],
heightfield_data: wp.array[HeightfieldData],
heightfield_elevations: wp.array[wp.float32],
triangle_pairs: wp.array[wp.vec3i],
triangle_pairs_count: wp.array[int],
reducer_data: GlobalContactReducerData,
total_num_threads: int,
shape_lin_vel: wp.array[wp.vec3],
shape_ang_speed_bound: wp.array[float],
speculative_dt: float,
max_speculative_extension: float,
):
"""Process mesh/heightfield-triangle contacts and store them in GlobalContactReducer.

for i in range(tid, num_triangle_pairs, total_num_threads):
if i >= triangle_pairs.shape[0]:
break
This kernel processes triangle pairs (mesh-or-hfield shape, convex-shape, triangle_index)
and computes contacts using GJK/MPR, storing results in the GlobalContactReducer for
subsequent reduction and export.

triple = triangle_pairs[i]
shape_a = triple[0] # Mesh or heightfield shape
shape_b = triple[1] # Convex shape
tri_idx = triple[2]
Uses grid stride loop over triangle pairs.
"""
tid = wp.tid()

type_a = shape_types[shape_a]
num_triangle_pairs = triangle_pairs_count[0]

if type_a == GeoType.HFIELD:
# Heightfield triangle
hfd = heightfield_data[shape_heightfield_index[shape_a]]
X_ws_a = shape_transform[shape_a]
shape_data_a, v0_world = get_triangle_shape_from_heightfield(hfd, heightfield_elevations, X_ws_a, tri_idx)
else:
# Mesh triangle (mesh_id already validated by midphase)
mesh_id_a = shape_source[shape_a]
scale_data_a = shape_data[shape_a]
mesh_scale_a = wp.vec3(scale_data_a[0], scale_data_a[1], scale_data_a[2])
X_ws_a = shape_transform[shape_a]
shape_data_a, v0_world = get_triangle_shape_from_mesh(mesh_id_a, mesh_scale_a, X_ws_a, tri_idx)

# Extract shape B data
pos_b, quat_b, shape_data_b, _scale_b, margin_offset_b = extract_shape_data(
shape_b,
shape_transform,
shape_types,
shape_data,
shape_source,
)
for i in range(tid, num_triangle_pairs, total_num_threads):
if i >= triangle_pairs.shape[0]:
break

# Triangle position is vertex A in world space.
# For heightfield prisms, edges are in heightfield-local space
# so we pass the heightfield rotation to let MPR/GJK work in
# that frame (where -Z is always the down axis).
pos_a = v0_world
if type_a == GeoType.HFIELD:
quat_a = wp.transform_get_rotation(shape_transform[shape_a])
else:
quat_a = wp.quat_identity()

# Back-face culling: skip when the convex center is behind the
# triangle face. TRIANGLE_PRISM (heightfields) handles this
# via its extruded support function.
if shape_data_a.shape_type == int(GeoTypeEx.TRIANGLE):
face_normal = wp.cross(shape_data_a.scale, shape_data_a.auxiliary)
center_dist = wp.dot(face_normal, pos_b - pos_a)
if center_dist < 0.0:
continue

# Extract margin offset for shape A (signed distance padding)
margin_offset_a = shape_data[shape_a][3]

# Use additive per-shape contact gap for detection threshold
gap_a = shape_gap[shape_a]
gap_b = shape_gap[shape_b]
gap_sum = gap_a + gap_b

# Compute and write contacts using GJK/MPR
wp.static(create_compute_gjk_mpr_contacts(write_contact_to_reducer))(
shape_data_a,
shape_data_b,
quat_a,
quat_b,
pos_a,
pos_b,
gap_sum,
shape_a,
shape_b,
margin_offset_a,
margin_offset_b,
reducer_data,
(tri_idx << 1) | 1,
)
triple = triangle_pairs[i]
shape_a = triple[0] # Mesh or heightfield shape
shape_b = triple[1] # Convex shape
tri_idx = triple[2]

type_a = shape_types[shape_a]

if type_a == GeoType.HFIELD:
# Heightfield triangle
hfd = heightfield_data[shape_heightfield_index[shape_a]]
X_ws_a = shape_transform[shape_a]
shape_data_a, v0_world = get_triangle_shape_from_heightfield(
hfd, heightfield_elevations, X_ws_a, tri_idx
)
else:
# Mesh triangle (mesh_id already validated by midphase)
mesh_id_a = shape_source[shape_a]
scale_data_a = shape_data[shape_a]
mesh_scale_a = wp.vec3(scale_data_a[0], scale_data_a[1], scale_data_a[2])
X_ws_a = shape_transform[shape_a]
shape_data_a, v0_world = get_triangle_shape_from_mesh(mesh_id_a, mesh_scale_a, X_ws_a, tri_idx)

# Extract shape B data
pos_b, quat_b, shape_data_b, _scale_b, margin_offset_b = extract_shape_data(
shape_b,
shape_transform,
shape_types,
shape_data,
shape_source,
)

# Triangle position is vertex A in world space.
# For heightfield prisms, edges are in heightfield-local space
# so we pass the heightfield rotation to let MPR/GJK work in
# that frame (where -Z is always the down axis).
pos_a = v0_world
if type_a == GeoType.HFIELD:
quat_a = wp.transform_get_rotation(shape_transform[shape_a])
else:
quat_a = wp.quat_identity()

# Back-face culling: skip when the convex center is behind the
# triangle face. TRIANGLE_PRISM (heightfields) handles this
# via its extruded support function.
if shape_data_a.shape_type == int(GeoTypeEx.TRIANGLE):
face_normal = wp.cross(shape_data_a.scale, shape_data_a.auxiliary)
center_dist = wp.dot(face_normal, pos_b - pos_a)
if center_dist < 0.0:
continue

# Extract margin offset for shape A (signed distance padding)
margin_offset_a = shape_data[shape_a][3]

# Use additive per-shape contact gap for detection threshold
gap_a = shape_gap[shape_a]
gap_b = shape_gap[shape_b]
gap_sum = gap_a + gap_b

if wp.static(speculative):
vel_rel = shape_lin_vel[shape_b] - shape_lin_vel[shape_a]
rel_speed = wp.length(vel_rel) + shape_ang_speed_bound[shape_a] + shape_ang_speed_bound[shape_b]
gap_sum = gap_sum + wp.min(rel_speed * speculative_dt, max_speculative_extension)

# Compute and write contacts using GJK/MPR
wp.static(create_compute_gjk_mpr_contacts(write_contact_to_reducer))(
shape_data_a,
shape_data_b,
quat_a,
quat_b,
pos_a,
pos_b,
gap_sum,
shape_a,
shape_b,
margin_offset_a,
margin_offset_b,
reducer_data,
(tri_idx << 1) | 1,
)

return mesh_triangle_contacts_to_reducer_kernel
Loading
Loading