Skip to content

Commit 238c2f6

Browse files
Extended contact visualizations (#3615)
1 parent fd8d9d4 commit 238c2f6

10 files changed

Lines changed: 483 additions & 31 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
- Reject invalid `ModelBuilder.ShapeConfig` SDF and density values during shape validation. Use finite nonnegative density and SDF padding, a finite positive target voxel size, a narrow-band range satisfying `inner < 0 < outer`, and a positive maximum resolution below 65536 that is divisible by 8; set either maximum resolution or target voxel size, not both. (#3311)
7575
- Reject runtime changes that alter `SolverKamino`'s as-built joint constraint counts, passive/actuated partition, or finite-limit structure; recreate the solver after making one of these structural changes. (#3532)
7676
- Cull positive-distance speculative contacts by default when converting Newton contacts for `SolverKamino` as a temporary workaround for restitution issues. No public compatibility option restores the old behavior; if a scene needs contact forces before geometry surfaces touch, increase `ModelBuilder.ShapeConfig.margin` so margin-shifted surfaces overlap at the desired force onset, and re-test contact behavior. (#3779)
77+
- Refine contact visualizations (showing contact force and color-coded contact mode) in Newton viewer.
7778

7879
### Deprecated
7980

newton/_src/solvers/kamino/_src/utils/sim/viewer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ def render_contacts_kamino(self, contacts):
433433
# ======================================================================
434434

435435
# Allocate buffers for force arrows
436-
if not hasattr(self, "_contact_force_starts"):
436+
if not hasattr(self, "_contact_force_colors"):
437437
self._contact_force_starts = wp.zeros(max_contacts, dtype=wp.vec3, device=self.device)
438438
self._contact_force_ends = wp.zeros(max_contacts, dtype=wp.vec3, device=self.device)
439439
self._contact_force_colors = wp.zeros(max_contacts, dtype=wp.vec3, device=self.device)

newton/_src/viewer/kernels.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,216 @@ def compute_contact_lines(
324324
line_end[tid] = contact_center + line_vector
325325

326326

327+
@wp.func
328+
def _quat_from_normal_z(normal: wp.vec3) -> wp.quat:
329+
"""Build a rotation quaternion whose local +Z axis aligns with ``normal``.
330+
331+
The tangent axes are arbitrary. Implemented with :func:`orthonormal_basis`
332+
for numerical stability near the poles.
333+
"""
334+
n = wp.normalize(normal)
335+
t1, t2 = orthonormal_basis(n)
336+
R = wp.matrix_from_cols(t1, t2, n)
337+
return wp.quat_from_matrix(R)
338+
339+
340+
@wp.kernel
341+
def compute_contact_disk_transforms(
342+
body_q: wp.array[wp.transform],
343+
body_qd: wp.array[wp.spatial_vector],
344+
body_com: wp.array[wp.vec3],
345+
shape_body: wp.array[int],
346+
shape_world: wp.array[int],
347+
world_offsets: wp.array[wp.vec3],
348+
visible_worlds_mask: wp.array[int],
349+
contact_count: wp.array[int],
350+
contact_shape0: wp.array[int],
351+
contact_shape1: wp.array[int],
352+
contact_point0: wp.array[wp.vec3],
353+
contact_point1: wp.array[wp.vec3],
354+
contact_offset0: wp.array[wp.vec3],
355+
contact_normal: wp.array[wp.vec3],
356+
contact_force: wp.array[wp.spatial_vector],
357+
disk_radius: float,
358+
disk_thickness: float,
359+
eps_force: float,
360+
eps_velocity: float,
361+
color_open: wp.vec3,
362+
color_stick: wp.vec3,
363+
color_slip: wp.vec3,
364+
# outputs
365+
transforms: wp.array[wp.transform],
366+
scales: wp.array[wp.vec3],
367+
colors: wp.array[wp.vec3],
368+
):
369+
"""Compute per-contact disk transforms, scales, and mode-coloured colors.
370+
371+
A thin oriented disk (rendered via a unit cylinder mesh whose local +Z
372+
axis is the cylinder axis) is placed at each active contact, oriented so
373+
that its axis matches ``contact_normal``.
374+
375+
When ``contact_force`` is provided (non-null), the disk is colored by an
376+
inferred contact mode computed from the linear contact force magnitude and
377+
the tangential relative velocity at the contact point:
378+
379+
* ``|F| < eps_force`` -> ``color_open``
380+
* ``|F| >= eps_force and |v_tan| < eps_velocity`` -> ``color_stick``
381+
* ``|F| >= eps_force and |v_tan| >= eps_velocity`` -> ``color_slip``
382+
383+
When ``contact_force`` is null, every active contact uses ``color_open``.
384+
385+
Inactive slots (beyond ``contact_count[0]`` or hidden by the visible-worlds
386+
mask) receive a degenerate zero-scale transform and a black color.
387+
"""
388+
tid = wp.tid()
389+
390+
zero_xform = wp.transform(wp.vec3(0.0, 0.0, 0.0), wp.quat_identity())
391+
zero_vec = wp.vec3(0.0, 0.0, 0.0)
392+
393+
count = contact_count[0]
394+
if tid >= count:
395+
transforms[tid] = zero_xform
396+
scales[tid] = zero_vec
397+
colors[tid] = zero_vec
398+
return
399+
400+
shape_a = contact_shape0[tid]
401+
shape_b = contact_shape1[tid]
402+
if shape_a == shape_b:
403+
transforms[tid] = zero_xform
404+
scales[tid] = zero_vec
405+
colors[tid] = zero_vec
406+
return
407+
408+
world_a = shape_world[shape_a]
409+
world_b = shape_world[shape_b]
410+
if visible_worlds_mask:
411+
w = world_a if world_a >= 0 else world_b
412+
if w >= 0:
413+
if visible_worlds_mask[w] == 0:
414+
transforms[tid] = zero_xform
415+
scales[tid] = zero_vec
416+
colors[tid] = zero_vec
417+
return
418+
419+
body_a = shape_body[shape_a]
420+
body_b = shape_body[shape_b]
421+
422+
X_wb_a = wp.transform_identity()
423+
if body_a >= 0:
424+
X_wb_a = body_q[body_a]
425+
426+
world_pos0 = wp.transform_point(X_wb_a, contact_point0[tid] + contact_offset0[tid])
427+
428+
contact_center = world_pos0
429+
if world_a >= 0 or world_b >= 0:
430+
contact_center += world_offsets[world_a if world_a >= 0 else world_b]
431+
432+
n = contact_normal[tid]
433+
q = _quat_from_normal_z(n)
434+
435+
# Mode coloring (default to "color_open" when force is unavailable).
436+
color = color_open
437+
thickness_scaling = 1.0 # Apply slightly different thickness based on color to avoid visible z-fighting
438+
if contact_force:
439+
f_lin = wp.spatial_top(contact_force[tid])
440+
f_mag = wp.length(f_lin)
441+
if f_mag < eps_force:
442+
color = color_open
443+
else:
444+
# Relative tangential velocity at the contact point.
445+
v_a = wp.vec3(0.0, 0.0, 0.0)
446+
if body_a >= 0:
447+
world_com_a = wp.transform_point(body_q[body_a], body_com[body_a])
448+
r_a = world_pos0 - world_com_a
449+
v_a = velocity_at_point(body_qd[body_a], r_a)
450+
v_b = wp.vec3(0.0, 0.0, 0.0)
451+
if body_b >= 0:
452+
X_wb_b = body_q[body_b]
453+
world_pos1 = wp.transform_point(X_wb_b, contact_point1[tid])
454+
world_com_b = wp.transform_point(X_wb_b, body_com[body_b])
455+
r_b = world_pos1 - world_com_b
456+
v_b = velocity_at_point(body_qd[body_b], r_b)
457+
v_rel = v_a - v_b
458+
v_t = v_rel - wp.dot(v_rel, n) * n
459+
if wp.length(v_t) < eps_velocity:
460+
color = color_stick
461+
thickness_scaling = 1.02
462+
else:
463+
color = color_slip
464+
thickness_scaling = 1.01
465+
466+
transforms[tid] = wp.transform(contact_center, q)
467+
scales[tid] = wp.vec3(disk_radius, disk_radius, disk_thickness * thickness_scaling)
468+
colors[tid] = color
469+
470+
471+
@wp.kernel
472+
def compute_contact_force_arrows(
473+
body_q: wp.array[wp.transform],
474+
shape_body: wp.array[int],
475+
shape_world: wp.array[int],
476+
world_offsets: wp.array[wp.vec3],
477+
visible_worlds_mask: wp.array[int],
478+
contact_count: wp.array[int],
479+
contact_shape0: wp.array[int],
480+
contact_shape1: wp.array[int],
481+
contact_point0: wp.array[wp.vec3],
482+
contact_offset0: wp.array[wp.vec3],
483+
contact_force: wp.array[wp.spatial_vector],
484+
force_scale: float,
485+
# outputs
486+
line_start: wp.array[wp.vec3],
487+
line_end: wp.array[wp.vec3],
488+
):
489+
"""Create world-space line segments visualizing the linear part of contact wrenches.
490+
491+
The arrow starts at the world contact point on shape 0 and points along
492+
``F = wp.spatial_top(contact_force[i])`` (the world-frame linear force on
493+
body 0), with length ``force_scale * |F|``. Inactive slots produce
494+
degenerate (NaN) line segments that the renderer culls.
495+
"""
496+
tid = wp.tid()
497+
nan_line = wp.vec3(wp.nan, wp.nan, wp.nan)
498+
499+
count = contact_count[0]
500+
if tid >= count:
501+
line_start[tid] = nan_line
502+
line_end[tid] = nan_line
503+
return
504+
505+
shape_a = contact_shape0[tid]
506+
shape_b = contact_shape1[tid]
507+
if shape_a == shape_b:
508+
line_start[tid] = nan_line
509+
line_end[tid] = nan_line
510+
return
511+
512+
world_a = shape_world[shape_a]
513+
world_b = shape_world[shape_b]
514+
if visible_worlds_mask:
515+
w = world_a if world_a >= 0 else world_b
516+
if w >= 0:
517+
if visible_worlds_mask[w] == 0:
518+
line_start[tid] = nan_line
519+
line_end[tid] = nan_line
520+
return
521+
522+
body_a = shape_body[shape_a]
523+
X_wb_a = wp.transform_identity()
524+
if body_a >= 0:
525+
X_wb_a = body_q[body_a]
526+
527+
world_pos0 = wp.transform_point(X_wb_a, contact_point0[tid] + contact_offset0[tid])
528+
contact_center = world_pos0
529+
if world_a >= 0 or world_b >= 0:
530+
contact_center += world_offsets[world_a if world_a >= 0 else world_b]
531+
532+
f_lin = -wp.spatial_top(contact_force[tid]) # Flip sign so positive force is along normal
533+
line_start[tid] = contact_center
534+
line_end[tid] = contact_center + force_scale * f_lin
535+
536+
327537
@wp.kernel
328538
def compute_joint_basis_lines(
329539
joint_type: wp.array[int],

0 commit comments

Comments
 (0)