Skip to content

Commit 3b2f92e

Browse files
authored
Backport #4005 and #4006 for 1.5.1 (#4030)
2 parents f6e8cb8 + 1d478e7 commit 3b2f92e

13 files changed

Lines changed: 697 additions & 57 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- Reset masked `ControllerNeuralLSTM` hidden and cell state without in-place writes. Network outputs are produced under `torch.inference_mode()`, so a partial reset previously raised `RuntimeError: Inplace update to inference tensor outside InferenceMode is not allowed.` ([#3923](https://github.com/newton-physics/newton/issues/3923))
1212
- Index Torch neural controller targets with the supplied `target_pos_indices`. `ControllerNeuralMLP` and `ControllerNeuralLSTM` previously selected between position and sequential indices by Python object identity, which produced wrong position errors whenever the target and position layouts differ, such as DOF-layout targets on a floating-base robot. ([#3923](https://github.com/newton-physics/newton/issues/3923))
1313
- Limit the `usd-exchange` dependency to versions below 3 for aarch64 systems. (#3996)
14+
- Keep ViewerGL depth, shading, and shadows stable for very large meshes when the camera moves. ([#3977](https://github.com/newton-physics/newton/issues/3977))
15+
- Resolve deep heightfield contacts against the cell's physical surface instead of the volume the cell is extruded into, which reported about a metre of penetration with a normal pointing into the terrain once a collider's center passed below the surface.
1416

1517
## [1.5.0] - 2026-08-11
1618

newton/_src/geometry/collision_convex.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,29 @@
2323
from .simplex_solver import create_solve_closest_distance
2424

2525

26-
def create_solve_convex_multi_contact(support_func: Any, writer_func: Any, post_process_contact: Any):
27-
"""Factory: fused MPR+GJK multi-contact solver with shared support code."""
26+
def create_solve_convex_multi_contact(
27+
support_func: Any,
28+
writer_func: Any,
29+
post_process_contact: Any,
30+
penetration_refiner: Any = None,
31+
):
32+
"""Create a fused MPR/GJK multi-contact solver.
33+
34+
Args:
35+
support_func: Support mapping function for individual shapes.
36+
writer_func: Function that writes generated contacts.
37+
post_process_contact: Function that post-processes generated contacts.
38+
penetration_refiner: Optional physical-proxy result refinement function.
39+
40+
Returns:
41+
The specialized contact solver.
42+
"""
2843

2944
# Create support functions ONCE — shared between MPR and GJK.
3045
support_funcs = create_support_map_function(support_func)
3146
solve_mpr = create_solve_mpr(support_func, _support_funcs=support_funcs)
3247
solve_gjk = create_solve_closest_distance(support_func, _support_funcs=support_funcs)
48+
has_penetration_refiner = penetration_refiner is not None
3349

3450
@wp.func
3551
def solve_convex_multi_contact(
@@ -76,6 +92,19 @@ def solve_convex_multi_contact(
7692
)
7793

7894
if collision:
95+
if wp.static(has_penetration_refiner):
96+
point_a, point_b, normal, penetration = penetration_refiner(
97+
geom_a,
98+
geom_b,
99+
relative_orientation_b,
100+
relative_position_b,
101+
enlarge,
102+
data_provider,
103+
point_a,
104+
point_b,
105+
normal,
106+
penetration,
107+
)
79108
signed_distance = -penetration + enlarge
80109
# Undo the inflate on the witness points so downstream consumers
81110
# (manifold builder, contact writer) see true-surface positions.
@@ -135,13 +164,29 @@ def solve_convex_multi_contact(
135164
return solve_convex_multi_contact
136165

137166

138-
def create_solve_convex_single_contact(support_func: Any, writer_func: Any, post_process_contact: Any):
139-
"""Factory: fused MPR+GJK single-contact solver with shared support code."""
167+
def create_solve_convex_single_contact(
168+
support_func: Any,
169+
writer_func: Any,
170+
post_process_contact: Any,
171+
penetration_refiner: Any = None,
172+
):
173+
"""Create a fused MPR/GJK single-contact solver.
174+
175+
Args:
176+
support_func: Support mapping function for individual shapes.
177+
writer_func: Function that writes generated contacts.
178+
post_process_contact: Function that post-processes generated contacts.
179+
penetration_refiner: Optional physical-proxy result refinement function.
180+
181+
Returns:
182+
The specialized contact solver.
183+
"""
140184

141185
# Create support functions ONCE — shared between MPR and GJK.
142186
support_funcs = create_support_map_function(support_func)
143187
solve_mpr = create_solve_mpr(support_func, _support_funcs=support_funcs)
144188
solve_gjk = create_solve_closest_distance(support_func, _support_funcs=support_funcs)
189+
has_penetration_refiner = penetration_refiner is not None
145190

146191
@wp.func
147192
def solve_convex_single_contact(
@@ -182,6 +227,19 @@ def solve_convex_single_contact(
182227
)
183228

184229
if collision:
230+
if wp.static(has_penetration_refiner):
231+
point_a, point_b, normal, penetration = penetration_refiner(
232+
geom_a,
233+
geom_b,
234+
relative_orientation_b,
235+
relative_position_b,
236+
enlarge,
237+
data_provider,
238+
point_a,
239+
point_b,
240+
normal,
241+
penetration,
242+
)
185243
signed_distance = -penetration + enlarge
186244
half_enlarge = enlarge * 0.5
187245
point_a = point_a - normal * half_enlarge

newton/_src/geometry/collision_core.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def create_compute_gjk_mpr_contacts(
279279
writer_func: Any,
280280
post_process_contact: Any = post_process_axial_on_discrete_contact,
281281
support_func: Any = None,
282+
penetration_refiner: Any = None,
282283
):
283284
"""
284285
Factory function to create a compute_gjk_mpr_contacts function with a specific writer function.
@@ -287,6 +288,7 @@ def create_compute_gjk_mpr_contacts(
287288
writer_func: Function to write contact data (signature: (ContactData, writer_data) -> None)
288289
post_process_contact: Function to post-process contact data
289290
support_func: Support mapping function (defaults to support_map)
291+
penetration_refiner: Optional physical-proxy result refinement function.
290292
291293
Returns:
292294
A compute_gjk_mpr_contacts function with the writer function baked in
@@ -360,7 +362,14 @@ def compute_gjk_mpr_contacts(
360362
contact_template.sort_sub_key = sort_sub_key
361363

362364
if wp.static(ENABLE_MULTI_CONTACT):
363-
wp.static(create_solve_convex_multi_contact(support_func, writer_func, post_process_contact))(
365+
wp.static(
366+
create_solve_convex_multi_contact(
367+
support_func,
368+
writer_func,
369+
post_process_contact,
370+
penetration_refiner,
371+
)
372+
)(
364373
shape_a_data,
365374
shape_b_data,
366375
rot_a,
@@ -377,7 +386,14 @@ def compute_gjk_mpr_contacts(
377386
contact_template,
378387
)
379388
else:
380-
wp.static(create_solve_convex_single_contact(support_func, writer_func, post_process_contact))(
389+
wp.static(
390+
create_solve_convex_single_contact(
391+
support_func,
392+
writer_func,
393+
post_process_contact,
394+
penetration_refiner,
395+
)
396+
)(
381397
shape_a_data,
382398
shape_b_data,
383399
rot_a,

newton/_src/geometry/contact_reduction_global.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@
7878
get_spatial_direction_2d,
7979
project_point_to_plane,
8080
)
81-
from .support_function import GeoTypeEx, extract_shape_data
81+
from .support_function import (
82+
GeoTypeEx,
83+
create_triangle_prism_penetration_refiner,
84+
extract_shape_data,
85+
support_map,
86+
)
8287
from .types import GeoType
8388

8489
# Fixed beta threshold for contact reduction - small positive value to avoid flickering
@@ -1772,8 +1777,12 @@ def mesh_triangle_contacts_to_reducer_kernel(
17721777
gap_b = shape_gap[shape_b]
17731778
gap_sum = gap_a + gap_b
17741779

1775-
# Compute and write contacts using GJK/MPR
1776-
wp.static(create_compute_gjk_mpr_contacts(write_contact_to_reducer))(
1780+
wp.static(
1781+
create_compute_gjk_mpr_contacts(
1782+
write_contact_to_reducer,
1783+
penetration_refiner=create_triangle_prism_penetration_refiner(support_map),
1784+
)
1785+
)(
17771786
shape_data_a,
17781787
shape_data_b,
17791788
quat_a,

newton/_src/geometry/mpr.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
import warp as wp
3737

38-
from .support_function import GeoTypeEx, closest_point_on_triangle, unpack_mesh_ptr
38+
from .support_function import TRIANGLE_PRISM_EXTRUSION, GeoTypeEx, closest_point_on_triangle, unpack_mesh_ptr
3939
from .types import GeoType
4040

4141
MPR_BOX_SUPPORT_TIE_EPSILON = 1.0e-6
@@ -274,6 +274,11 @@ def geometric_center(
274274
nudge_distance = 0.01 * wp.min(distance_to_centroid, wp.abs(signed_plane_distance))
275275
center_b_to_a += to_centroid * (nudge_distance / distance_to_centroid)
276276

277+
if geom_a.shape_type == int(GeoTypeEx.TRIANGLE_PRISM):
278+
# A prism has an interior, so seed MPR halfway into its
279+
# artificial extrusion rather than on the physical face.
280+
center_b_to_a -= wp.vec3(0.0, 0.0, 0.5 * TRIANGLE_PRISM_EXTRUSION)
281+
277282
else:
278283
center_b_to_a = proj - center_b_world
279284

newton/_src/geometry/narrow_phase.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
from ..geometry.support_function import (
5454
GeoTypeEx,
5555
SupportMapDataProvider,
56+
create_triangle_prism_penetration_refiner,
5657
extract_shape_data,
58+
support_map,
5759
support_map_lean,
5860
)
5961
from ..geometry.types import GeoType
@@ -998,8 +1000,12 @@ def narrow_phase_process_mesh_triangle_contacts_kernel(
9981000
gap_b = shape_gap[shape_b]
9991001
gap_sum = gap_a + gap_b
10001002

1001-
# Compute and write contacts using GJK/MPR with standard post-processing
1002-
wp.static(create_compute_gjk_mpr_contacts(writer_func))(
1003+
wp.static(
1004+
create_compute_gjk_mpr_contacts(
1005+
writer_func,
1006+
penetration_refiner=create_triangle_prism_penetration_refiner(support_map),
1007+
)
1008+
)(
10031009
shape_data_a,
10041010
shape_data_b,
10051011
quat_a,

newton/_src/geometry/support_function.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"""
2929

3030
import enum
31+
from typing import Any
3132

3233
import warp as wp
3334

@@ -37,6 +38,9 @@
3738
# Near-zero direction components (e.g. from quaternion rotation noise ~1e-14)
3839
# are treated as non-negative, biasing toward the +1 vertex.
3940
BOX_SUPPORT_DEADBAND = 1.0e-10
41+
_CENTERED_BOX_SUPPORT_TIE_EPSILON = 1.0e-6
42+
TRIANGLE_PRISM_EXTRUSION = 1.0
43+
"""Depth [m] a triangle is extruded along -Z to give a heightfield cell volume."""
4044

4145

4246
# Is not allowed to share values with GeoType
@@ -171,7 +175,7 @@ def support_map(geom: GenericShapeData, direction: wp.vec3, data_provider: Suppo
171175
# always the heightfield's down direction.
172176
if geom.shape_type == GeoTypeEx.TRIANGLE_PRISM:
173177
if direction[2] < 0.0:
174-
result = result + wp.vec3(0.0, 0.0, -1.0)
178+
result = result + wp.vec3(0.0, 0.0, -TRIANGLE_PRISM_EXTRUSION)
175179
elif geom.shape_type == GeoType.BOX:
176180
# Use a relative deadband so near-zero direction components
177181
# (from solver rotation drift ~1e-7) cannot flip the sign
@@ -347,6 +351,109 @@ def support_map_lean(geom: GenericShapeData, direction: wp.vec3, data_provider:
347351
return result
348352

349353

354+
def create_shape_support_function(support_func: Any, center_ties: bool = False):
355+
"""Create a support function with built-in shape policies."""
356+
if center_ties:
357+
358+
@wp.func
359+
def shape_support(geom: Any, direction: wp.vec3, data_provider: Any) -> wp.vec3:
360+
result = support_func(geom, direction, data_provider)
361+
if geom.shape_type == GeoType.BOX:
362+
contribution = wp.cw_mul(wp.abs(direction), geom.scale)
363+
threshold = _CENTERED_BOX_SUPPORT_TIE_EPSILON * (contribution[0] + contribution[1] + contribution[2])
364+
if contribution[0] <= threshold:
365+
result[0] = 0.0
366+
if contribution[1] <= threshold:
367+
result[1] = 0.0
368+
if contribution[2] <= threshold:
369+
result[2] = 0.0
370+
return result
371+
372+
else:
373+
374+
@wp.func
375+
def shape_support(geom: Any, direction: wp.vec3, data_provider: Any) -> wp.vec3:
376+
return support_func(geom, direction, data_provider)
377+
378+
return shape_support
379+
380+
381+
def create_triangle_prism_penetration_refiner(support_func: Any):
382+
"""Create physical-surface refinement for triangle-prism collision proxies.
383+
384+
MPR operates on closed convex proxies, but a proxy may contain artificial
385+
faces that are needed only to give it volume. The returned function maps a
386+
triangle-prism result back to its physical face.
387+
388+
Args:
389+
support_func: Support function for individual shapes.
390+
391+
Returns:
392+
A function that refines MPR witness points, normal, and penetration.
393+
"""
394+
395+
shape_support = create_shape_support_function(support_func, center_ties=True)
396+
397+
@wp.func
398+
def refine_penetration(
399+
geom_a: Any,
400+
geom_b: Any,
401+
orientation_b: wp.quat,
402+
position_b: wp.vec3,
403+
extend: float,
404+
data_provider: Any,
405+
point_a: wp.vec3,
406+
point_b: wp.vec3,
407+
normal: wp.vec3,
408+
penetration: float,
409+
) -> tuple[wp.vec3, wp.vec3, wp.vec3, float]:
410+
if geom_a.shape_type == int(GeoTypeEx.TRIANGLE_PRISM):
411+
surface_normal = wp.cross(geom_a.scale, geom_a.auxiliary)
412+
normal_length_sq = wp.length_sq(surface_normal)
413+
if normal_length_sq >= 1.0e-24:
414+
surface_normal /= wp.sqrt(normal_length_sq)
415+
if surface_normal[2] < 0.0:
416+
surface_normal = -surface_normal
417+
418+
surface_point_a = shape_support(geom_a, surface_normal, data_provider)
419+
direction_b = wp.quat_rotate_inv(orientation_b, -surface_normal)
420+
surface_point_b = shape_support(geom_b, direction_b, data_provider)
421+
surface_point_b = wp.quat_rotate(orientation_b, surface_point_b) + position_b
422+
if extend != 0.0:
423+
offset = surface_normal * extend * 0.5
424+
surface_point_a += offset
425+
surface_point_b -= offset
426+
surface_penetration = wp.dot(surface_point_a - surface_point_b, surface_normal)
427+
428+
# A finite triangle must not use a support point beyond its
429+
# footprint. A neighboring heightfield triangle may own that
430+
# point, while at the outer boundary there may be no surface.
431+
projected_b = surface_point_b - wp.dot(surface_point_b, surface_normal) * surface_normal
432+
closest_b = closest_point_on_triangle(
433+
projected_b,
434+
wp.vec3(0.0),
435+
geom_a.scale,
436+
geom_a.auxiliary,
437+
)
438+
support_on_face = wp.length_sq(projected_b - closest_b) < 1.0e-10
439+
440+
if not support_on_face:
441+
# A neighboring cell owns the deepest point, so measure this cell's overlap
442+
# at MPR's own witness instead. The face normal still applies -- the
443+
# surface is what shape B is resting on -- but the depth is only what this
444+
# triangle actually carries.
445+
surface_point_b = point_b
446+
surface_penetration = wp.dot(surface_point_a - point_b, surface_normal)
447+
normal = surface_normal
448+
penetration = surface_penetration
449+
point_b = surface_point_b
450+
point_a = point_b + penetration * normal
451+
452+
return point_a, point_b, normal, penetration
453+
454+
return refine_penetration
455+
456+
350457
@wp.func
351458
def extract_shape_data(
352459
shape_idx: int,

0 commit comments

Comments
 (0)