rigidformer / src /utils /intersection_handler.py
multimodalart's picture
multimodalart HF Staff
RigidFormer demo
632d805 verified
Raw History Blame Contribute Delete
7.94 kB
"""
Differentiable intersection handling.
Resolves object-ground and object-object penetrations by translating whole
objects, with a straight-through estimator (STE) keeping the correction
differentiable. The correction formulation follows UNIC (arXiv:2603.25580,
Sec. 3.3), Eq. 8:
v_corrected = v + detach(target_position - v)
Forward: moves v to target_position
Backward: gradients pass through to v unchanged
Whereas UNIC drags individual garment vertices, rigid objects are translated
as a whole:
- Ground plane: shift the object upward when its lowest vertex falls
below ground_z + buffer
- Object-object: push objects apart when their bounding spheres overlap
"""
import torch
import torch.nn.functional as F
import logging
logger = logging.getLogger(__name__)
def resolve_ground_penetration(
positions: torch.Tensor,
obj_mask: torch.Tensor,
vtx_mask: torch.Tensor,
ground_z: float = 0.0,
buffer: float = 0.005,
) -> torch.Tensor:
"""
Resolve object-ground penetrations by translating objects upward.
For each valid object, find the LOWEST valid vertex. If min_z < ground_z + buffer,
shift ALL vertices of that object upward by the deficit.
This guarantees no valid vertex penetrates the ground, while preserving rigidity.
Uses STE trick: correction is detached so gradients flow through original positions.
Args:
positions: (B, Nobj, Nv, 3) predicted vertex positions
obj_mask: (B, Nobj) boolean mask for valid objects
vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices
ground_z: ground plane height (z-up coordinate system)
buffer: relaxation buffer above ground plane
Returns:
corrected_positions: (B, Nobj, Nv, 3) with ground penetrations resolved
"""
B, Nobj, Nv, _ = positions.shape
target_z = ground_z + buffer
# Find per-object minimum z among valid vertices
# Set invalid vertices to +inf so they don't affect min
z_vals = positions[..., 2] # (B, Nobj, Nv)
z_vals_masked = z_vals.masked_fill(~vtx_mask, float('inf')) # invalid vertices -> +inf
min_z, _ = z_vals_masked.min(dim=2) # (B, Nobj) lowest valid vertex z per object
# Find objects that penetrate the ground
penetration_depth = target_z - min_z # positive means below ground
# Padding objects have min_z=+inf, giving depth=-inf; clamp to >= 0 before
# the expand below so 0*(-inf) can never produce NaN
penetration_depth = penetration_depth.clamp(min=0.0)
is_penetrating = (penetration_depth > 0) & obj_mask # (B, Nobj)
# No early exit on "nothing penetrates": the correction below is identically
# zero in that case, and the shortcut would cost a host sync per call.
# Compute correction: shift entire object upward by penetration_depth
correction = torch.zeros_like(positions) # (B, Nobj, Nv, 3)
# Only z-component needs correction
correction[..., 2] = penetration_depth.unsqueeze(-1).expand(-1, -1, Nv) # (B, Nobj, Nv)
# Zero out correction for non-penetrating objects
correction = correction * is_penetrating.float().unsqueeze(-1).unsqueeze(-1) # (B, Nobj, Nv, 3)
# STE trick (UNIC Eq. 8): forward moves position, backward passes gradient through
corrected = positions + correction.detach()
return corrected
def resolve_object_penetration(
positions: torch.Tensor,
obj_mask: torch.Tensor,
vtx_mask: torch.Tensor,
buffer: float = 0.005,
) -> torch.Tensor:
"""
Resolve object-object penetrations using bounding sphere overlap detection.
For each pair of valid objects, compute COM and bounding sphere radius.
If spheres overlap, push both objects apart along the COM-COM direction.
Uses STE trick for differentiability.
Args:
positions: (B, Nobj, Nv, 3) predicted vertex positions
obj_mask: (B, Nobj) boolean mask for valid objects
vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices
buffer: relaxation buffer between objects
Returns:
corrected_positions: (B, Nobj, Nv, 3) with object penetrations resolved
"""
B, Nobj, Nv, _ = positions.shape
if Nobj < 2:
return positions
# Compute per-object COM and bounding sphere radius
vtx_weight = vtx_mask.float().unsqueeze(-1) # (B, Nobj, Nv, 1)
num_valid = vtx_weight.sum(dim=2).clamp(min=1) # (B, Nobj, 1)
com = (positions * vtx_weight).sum(dim=2) / num_valid # (B, Nobj, 3)
# Bounding sphere radius: max distance from COM to any valid vertex
diff_to_com = positions - com.unsqueeze(2) # (B, Nobj, Nv, 3)
dist_to_com = diff_to_com.norm(dim=-1) # (B, Nobj, Nv)
# Mask invalid vertices with 0 distance
dist_to_com = dist_to_com * vtx_mask.float() # (B, Nobj, Nv)
radius = dist_to_com.max(dim=2).values # (B, Nobj)
# Accumulate corrections per object
correction = torch.zeros(B, Nobj, 3, device=positions.device, dtype=positions.dtype)
# Check all pairs (i, j) where i < j
for i in range(Nobj):
for j in range(i + 1, Nobj):
# Both objects must be valid
pair_valid = obj_mask[:, i] & obj_mask[:, j] # (B,)
if not pair_valid.any():
continue
# COM distance
com_diff = com[:, j] - com[:, i] # (B, 3)
com_dist = com_diff.norm(dim=-1).clamp(min=1e-8) # (B,)
# Minimum allowed distance
min_dist = radius[:, i] + radius[:, j] + buffer # (B,)
# Overlap amount
overlap = min_dist - com_dist # (B,) positive means penetrating
is_overlapping = (overlap > 0) & pair_valid # (B,)
if not is_overlapping.any():
continue
# Push direction: from i to j (normalized)
push_dir = com_diff / com_dist.unsqueeze(-1) # (B, 3)
# Each object gets pushed by half the overlap
push_amount = (overlap / 2).clamp(min=0) * is_overlapping.float() # (B,)
correction[:, i] -= push_amount.unsqueeze(-1) * push_dir # push i away from j
correction[:, j] += push_amount.unsqueeze(-1) * push_dir # push j away from i
if (correction.abs() < 1e-10).all():
return positions
# Expand correction to per-vertex: (B, Nobj, 3) -> (B, Nobj, Nv, 3)
correction_expanded = correction.unsqueeze(2).expand(-1, -1, Nv, -1)
# STE trick
corrected = positions + correction_expanded.detach()
return corrected
def handle_intersections(
positions: torch.Tensor,
obj_mask: torch.Tensor,
vtx_mask: torch.Tensor,
config: dict,
) -> torch.Tensor:
"""
Main entry point: apply all intersection resolution steps.
Args:
positions: (B, Nobj, Nv, 3) predicted vertex positions
obj_mask: (B, Nobj) boolean mask
vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices
config: intersection_handling config dict
Returns:
corrected positions: (B, Nobj, Nv, 3)
"""
if not config.get('enable', False):
return positions
ground_cfg = config.get('ground', {})
object_cfg = config.get('object', {})
# Step 1: Resolve ground penetrations
if ground_cfg.get('enable', True):
positions = resolve_ground_penetration(
positions, obj_mask, vtx_mask,
ground_z=ground_cfg.get('z', 0.0),
buffer=ground_cfg.get('buffer', 0.005),
)
# Step 2: Resolve object-object penetrations
if object_cfg.get('enable', False):
positions = resolve_object_penetration(
positions, obj_mask, vtx_mask,
buffer=object_cfg.get('buffer', 0.005),
)
return positions