@@ -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
328538def compute_joint_basis_lines (
329539 joint_type : wp .array [int ],
0 commit comments