55
66from __future__ import annotations
77
8+ import math
89import torch
910from abc import ABC , abstractmethod
1011from dataclasses import dataclass
1112from enum import IntEnum
1213from typing import TYPE_CHECKING
1314
15+ from isaaclab_arena .relations .collision_mode import CollisionMode
1416from isaaclab_arena .relations .loss_primitives import (
1517 interval_overlap_axis_loss ,
1618 linear_band_loss ,
2022from isaaclab_arena .utils .bounding_box import AxisAlignedBoundingBox
2123
2224if TYPE_CHECKING :
25+ import trimesh
26+
27+ from isaaclab_arena .assets .object_base import ObjectBase
2328 from isaaclab_arena .relations .relations import AtPosition , NextTo , NotNextTo , On , PositionLimits , Relation
29+ from isaaclab_arena .relations .warp_mesh_manager import WarpMeshManager
2430
2531from isaaclab_arena .relations .relations import Side
2632
@@ -427,7 +433,7 @@ def __init__(
427433 self ,
428434 slope : float = 10.0 ,
429435 debug : bool = False ,
430- collision_mode = None ,
436+ collision_mode : CollisionMode | None = None ,
431437 num_spheres : int = 30 ,
432438 ):
433439 """
@@ -437,15 +443,12 @@ def __init__(
437443 collision_mode: CollisionMode enum value (BBOX or MESH). Defaults to BBOX.
438444 num_spheres: Number of spheres for mesh decomposition (MESH mode only).
439445 """
440- from isaaclab_arena .relations .relation_solver_params import CollisionMode
441-
442446 self .slope = slope
443447 self .debug = debug
444448 self ._mode = collision_mode if collision_mode is not None else CollisionMode .BBOX
445449 self ._num_spheres = num_spheres
446- self ._CollisionMode = CollisionMode
447450 self ._warned_no_mesh : set [str ] = set ()
448- self ._mesh_managers : dict [str , object ] = {} # keyed by device string
451+ self ._mesh_managers : dict [str , WarpMeshManager ] = {}
449452
450453 if self ._mode == CollisionMode .MESH :
451454 try :
@@ -461,9 +464,11 @@ def compute_loss(
461464 child_pos : torch .Tensor ,
462465 child_bbox : AxisAlignedBoundingBox ,
463466 parent_world_bbox : AxisAlignedBoundingBox ,
464- child_obj = None ,
465- parent_obj = None ,
467+ child_obj : ObjectBase | None = None ,
468+ parent_obj : ObjectBase | None = None ,
466469 parent_pos : torch .Tensor | None = None ,
470+ child_yaw : float = 0.0 ,
471+ parent_yaw : float = 0.0 ,
467472 ) -> torch .Tensor :
468473 """Compute collision loss, dispatching based on mode and mesh availability.
469474
@@ -475,16 +480,26 @@ def compute_loss(
475480 child_obj: Object with get_collision_mesh() (MESH mode).
476481 parent_obj: Object with get_collision_mesh() (MESH mode).
477482 parent_pos: Parent position tensor, or None for anchors (MESH mode).
483+ child_yaw: Z-yaw (radians) of the child object (MESH mode).
484+ parent_yaw: Z-yaw (radians) of the parent object (MESH mode).
478485
479486 Returns:
480487 Loss tensor of shape (N,).
481488 """
482- if self ._mode == self . _CollisionMode .MESH and child_obj is not None and parent_obj is not None :
489+ if self ._mode == CollisionMode .MESH and child_obj is not None and parent_obj is not None :
483490 child_mesh = child_obj .get_collision_mesh ()
484491 parent_mesh = parent_obj .get_collision_mesh ()
485492 if child_mesh is not None and parent_mesh is not None :
486493 return self ._compute_mesh_loss (
487- clearance_m , child_pos , child_obj , child_mesh , parent_pos , parent_obj , parent_mesh
494+ clearance_m ,
495+ child_pos ,
496+ child_obj ,
497+ child_mesh ,
498+ parent_pos ,
499+ parent_obj ,
500+ parent_mesh ,
501+ child_yaw = child_yaw ,
502+ parent_yaw = parent_yaw ,
488503 )
489504 for obj , mesh in [(child_obj , child_mesh ), (parent_obj , parent_mesh )]:
490505 name = getattr (obj , "name" , "?" )
@@ -544,7 +559,7 @@ def _compute_aabb_loss(
544559
545560 return total_loss .squeeze (0 ) if single_input else total_loss
546561
547- def _get_mesh_manager (self , device : str = "cuda:0" ):
562+ def _get_mesh_manager (self , device : str = "cuda:0" ) -> WarpMeshManager :
548563 """Return a cached WarpMeshManager for the given device."""
549564 if device not in self ._mesh_managers :
550565 from isaaclab_arena .relations .warp_mesh_manager import WarpMeshManager
@@ -556,13 +571,15 @@ def _compute_mesh_loss(
556571 self ,
557572 clearance_m : float ,
558573 child_pos : torch .Tensor ,
559- child_obj ,
560- child_mesh ,
574+ child_obj : ObjectBase ,
575+ child_mesh : trimesh . Trimesh ,
561576 parent_pos : torch .Tensor | None ,
562- parent_obj ,
563- parent_mesh ,
577+ parent_obj : ObjectBase ,
578+ parent_mesh : trimesh .Trimesh ,
579+ child_yaw : float = 0.0 ,
580+ parent_yaw : float = 0.0 ,
564581 ) -> torch .Tensor :
565- """Sphere- to-SDF penetration loss using mesh geometry ."""
582+ """Per-pair sphere- to-SDF penetration loss."""
566583 from isaaclab_arena .relations .warp_sdf_kernels import sphere_penetration_loss
567584
568585 single_input = child_pos .dim () == 1
@@ -577,12 +594,11 @@ def _compute_mesh_loss(
577594 assert isinstance (
578595 pose , Pose
579596 ), f"Anchor '{ getattr (parent_obj , 'name' , '?' )} ' must have a fixed Pose for mesh collision"
580- identity = (0.0 , 0.0 , 0.0 , 1.0 )
581- assert pose .rotation_xyzw == identity , (
582- f"Mesh collision with rotated anchor '{ getattr (parent_obj , 'name' , '?' )} ' "
583- f"is not yet supported (rotation={ pose .rotation_xyzw } )"
584- )
585597 parent_pos = torch .tensor (pose .position_xyz , dtype = child_pos .dtype , device = child_pos .device )
598+ if parent_yaw == 0.0 :
599+ from isaaclab_arena .utils .pose import yaw_from_quat_xyzw
600+
601+ parent_yaw = yaw_from_quat_xyzw (pose .rotation_xyzw )
586602
587603 assert parent_pos is not None
588604 parent_pos_resolved : torch .Tensor = parent_pos
@@ -596,14 +612,31 @@ def _compute_mesh_loss(
596612 radii = spheres [:, 3 ]
597613 warp_mesh = manager .get_warp_mesh (parent_mesh , obj = parent_obj )
598614
615+ # Rotate child sphere centers by net_yaw = child_yaw - parent_yaw.
616+ net_yaw = child_yaw - parent_yaw
617+ if net_yaw != 0.0 :
618+ cos_n = math .cos (net_yaw )
619+ sin_n = math .sin (net_yaw )
620+ rx = centers_local [:, 0 ] * cos_n - centers_local [:, 1 ] * sin_n
621+ ry = centers_local [:, 0 ] * sin_n + centers_local [:, 1 ] * cos_n
622+ centers_local = torch .stack ([rx , ry , centers_local [:, 2 ]], dim = - 1 )
623+
599624 batch_size = child_pos .shape [0 ]
600625 parent_pos_resolved = parent_pos_resolved .expand (batch_size , - 1 )
601626 total_loss = torch .zeros (batch_size , device = device , dtype = child_pos .dtype )
602627
603628 for b in range (batch_size ):
604- centers_world = centers_local + child_pos [b ] - parent_pos_resolved [b ]
629+ offset = child_pos [b ] - parent_pos_resolved [b ]
630+ # Rotate offset into the parent's local frame.
631+ if parent_yaw != 0.0 :
632+ cos_p = math .cos (- parent_yaw )
633+ sin_p = math .sin (- parent_yaw )
634+ ox = offset [0 ] * cos_p - offset [1 ] * sin_p
635+ oy = offset [0 ] * sin_p + offset [1 ] * cos_p
636+ offset = torch .stack ([ox , oy , offset [2 ]])
637+ centers_in_parent = centers_local + offset
605638 total_loss [b ] = self .slope * sphere_penetration_loss (
606- centers_world , radii , warp_mesh , clearance_m = clearance_m
639+ centers_in_parent , radii , warp_mesh , clearance_m = clearance_m
607640 )
608641
609642 return total_loss .squeeze (0 ) if single_input else total_loss
0 commit comments