robolib.physics.collision

collision.py - Collision submodule between basic shapes.

  1"""collision.py - Collision submodule between basic shapes."""
  2import numpy as np
  3from microecs import Entity, QueryResult
  4from robolib.utils import Point3D, logger, fmt
  5from robolib.components import ColliderKinds
  6
  7Key3D = tuple[int, int, int]
  8
  9def make_grid_3d(entities: list[Entity], cell_size: Point3D) -> dict[Key3D, list[Entity]]:
 10    """
 11    Creates a 3d grid given all the objects based on their bounding boxes. Cell size is provided from outside.
 12    Output dict is provided (not returned), so we can create it outside (e.g. prefill fixed ones)!
 13    """
 14    res: dict[Key3D, list[Entity]] = {}
 15    assert (cell_size > 0).all(), cell_size
 16    for entity in entities:
 17        top_left, bottom_right = entity.collider_bbox[0:2]
 18        first_cell = (top_left / cell_size).astype(int) # Note: possible bug with negative positions??
 19        last_cell = (bottom_right / cell_size).astype(int)
 20        nx, ny, nz = last_cell - first_cell + 1
 21
 22        for i in range(nx):
 23            for j in range(ny):
 24                for k in range(nz):
 25                    key = (first_cell[0].item() + i, first_cell[1].item() + j, first_cell[2].item() + k)
 26                    res.setdefault(key, []).append(entity)
 27    return res
 28
 29def make_collision_cell_size(entities: QueryResult) -> Point3D:
 30    """the collision cell size is twice them edian of all bboxes of all collidables with a model"""
 31    bboxes = entities.model_bbox * entities.scale[..., None]
 32    diffs = (bboxes[:, 1] - bboxes[:, 0]).numpy()
 33    median = np.median(diffs, axis=0) # median across all 3 dimensions
 34    res = median * 2 # heuristic: twice the median
 35    logger.info(f"Median bbox size: {median}. Cell size: {res}")
 36    assert (res > 0).all(), bboxes.numpy()
 37    return res
 38
 39def _get_closest_point_on_box(top_left: Point3D, bottom_right: Point3D, other_point: Point3D) -> Point3D:
 40    _f = lambda l, p, r: l if p < l else min(p, r) # returns one of 3 cases: l, p or r on 1D line: [pl, [l, p, r], pr]
 41    px = _f(top_left[0], other_point[0], bottom_right[0])
 42    py = _f(top_left[1], other_point[1], bottom_right[1])
 43    pz = _f(top_left[2], other_point[2], bottom_right[2])
 44    return np.float32([px, py, pz])
 45
 46def sphere_sphere_collision(pos1: Point3D, radius1: float, pos2: Point3D, radius2: float, eps: float=1e-5) -> bool:
 47    """checks if two spheres are colliding"""
 48    max_dist = radius1 + radius2
 49    diff_vec = pos1 - pos2
 50    dist: float = ((diff_vec**2).sum() - max_dist**2).item() # instead of np.linalg.norm(a.pos-b.pos) < max_dist
 51    if check := dist < -eps:
 52        logger.log_every_s(f"Collision. pos1={fmt(pos1)} radius1={radius1:.3f} "
 53                           f"pos2={fmt(pos2)} radius2={radius2:.3f}", "DEBUG", True)
 54    return check
 55
 56def sphere_aabb_collision(center: Point3D, radius: float, top_left: Point3D,
 57                          bottom_right: Point3D, eps: float=1e-5) -> bool:
 58    """checks if a sphere is colliding with an aabb"""
 59    xr, yr, zr = _get_closest_point_on_box(top_left, bottom_right, center)
 60    dist_sq = (center[0] - xr)**2 + (center[1] - yr)**2 + (center[2] - zr)**2
 61    dist: float = (dist_sq - radius ** 2).item()
 62    if check := dist < -eps:
 63        logger.log_every_s(f"Collision. center={fmt(center)} radius={radius:.3f} top_left={fmt(top_left)} "
 64                           f"bottom_right={fmt(bottom_right)}", "DEBUG", True)
 65    return check
 66
 67def aabb_aabb_collision(top_left1: Point3D, bottom_right1: Point3D,
 68                        top_left2: Point3D, bottom_right2: Point3D, eps: float=1e-5) -> bool:
 69    """
 70    Checks if two aabbs are colliding. We do this by doing one check on each ax.
 71    We are thus reducing the complexity from 3D to three independent 1D checks.
 72
 73    For 1D, assume the two lines given by top_left and bottom_right: [tl1, br1], [tl2, br2]. There are 5 cases:
 74    - tl1, br1, tl2, br2 -> not overlapping
 75    - tl1, tl2, br1, br2 -> overlapping
 76    - tl1, tl2, br2, br1 -> overlapping
 77    - tl2, tl1, br1, br2 -> overlapping
 78    - tl2, tl1, br2, br1 -> overlapping
 79    - tl2, br2, tl1, br1 -> not overlapping
 80
 81    In all the cases, we can look at the midpoints c1, c2 and the actual distances between the two corners:
 82    h1 = (br1 - tl1) / 2, h2 = (br2 - tl2) / 2. The lines are overlapping only if dist(c1, c2) < h1 + h2 across all dims
 83    """
 84
 85    h1 = (bottom_right1 - top_left1) / 2
 86    h2 = (bottom_right2 - top_left2) / 2
 87
 88    c1 = top_left1 + h1
 89    c2 = top_left2 + h2
 90
 91    # signed separation on each axis, like the sphere checks above: < 0 means overlapping on that axis
 92    gap = abs(c1 - c2) - (h1 + h2)
 93    if check := (gap[0] < -eps) and (gap[1] < -eps) and (gap[2] < -eps):
 94        logger.log_every_s(f"Collision. {top_left1=} {bottom_right1=} {top_left2=} {bottom_right2=}", "DEBUG", True)
 95    return check
 96
 97def check_collision(e1: Entity, e2: Entity) -> bool:
 98    """checks the collision between two entities with the HasCollision component"""
 99    if e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.SPHERE:
100        return sphere_sphere_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
101                                       e2.candidate_pose[0:3, 3], e2.collider_radii.item())
102    elif e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.AABB:
103        return sphere_aabb_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
104                                     e2.collider_bbox[0], e2.collider_bbox[1])
105    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.SPHERE:
106        return sphere_aabb_collision(e2.candidate_pose[0:3, 3], e2.collider_radii.item(),
107                                     e1.collider_bbox[0], e1.collider_bbox[1])
108    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.AABB:
109        return aabb_aabb_collision(e1.collider_bbox[0], e1.collider_bbox[1],
110                                   e2.collider_bbox[0], e2.collider_bbox[1])
111    else:
112        raise NotImplementedError((e1.collider_kind, e2.collider_kind))
Key3D = tuple[int, int, int]
def make_grid_3d( entities: list[microecs.entity.Entity], cell_size: numpy.ndarray) -> dict[tuple[int, int, int], list[microecs.entity.Entity]]:
10def make_grid_3d(entities: list[Entity], cell_size: Point3D) -> dict[Key3D, list[Entity]]:
11    """
12    Creates a 3d grid given all the objects based on their bounding boxes. Cell size is provided from outside.
13    Output dict is provided (not returned), so we can create it outside (e.g. prefill fixed ones)!
14    """
15    res: dict[Key3D, list[Entity]] = {}
16    assert (cell_size > 0).all(), cell_size
17    for entity in entities:
18        top_left, bottom_right = entity.collider_bbox[0:2]
19        first_cell = (top_left / cell_size).astype(int) # Note: possible bug with negative positions??
20        last_cell = (bottom_right / cell_size).astype(int)
21        nx, ny, nz = last_cell - first_cell + 1
22
23        for i in range(nx):
24            for j in range(ny):
25                for k in range(nz):
26                    key = (first_cell[0].item() + i, first_cell[1].item() + j, first_cell[2].item() + k)
27                    res.setdefault(key, []).append(entity)
28    return res

Creates a 3d grid given all the objects based on their bounding boxes. Cell size is provided from outside. Output dict is provided (not returned), so we can create it outside (e.g. prefill fixed ones)!

def make_collision_cell_size(entities: microecs.query_result.QueryResult) -> numpy.ndarray:
30def make_collision_cell_size(entities: QueryResult) -> Point3D:
31    """the collision cell size is twice them edian of all bboxes of all collidables with a model"""
32    bboxes = entities.model_bbox * entities.scale[..., None]
33    diffs = (bboxes[:, 1] - bboxes[:, 0]).numpy()
34    median = np.median(diffs, axis=0) # median across all 3 dimensions
35    res = median * 2 # heuristic: twice the median
36    logger.info(f"Median bbox size: {median}. Cell size: {res}")
37    assert (res > 0).all(), bboxes.numpy()
38    return res

the collision cell size is twice them edian of all bboxes of all collidables with a model

def sphere_sphere_collision( pos1: numpy.ndarray, radius1: float, pos2: numpy.ndarray, radius2: float, eps: float = 1e-05) -> bool:
47def sphere_sphere_collision(pos1: Point3D, radius1: float, pos2: Point3D, radius2: float, eps: float=1e-5) -> bool:
48    """checks if two spheres are colliding"""
49    max_dist = radius1 + radius2
50    diff_vec = pos1 - pos2
51    dist: float = ((diff_vec**2).sum() - max_dist**2).item() # instead of np.linalg.norm(a.pos-b.pos) < max_dist
52    if check := dist < -eps:
53        logger.log_every_s(f"Collision. pos1={fmt(pos1)} radius1={radius1:.3f} "
54                           f"pos2={fmt(pos2)} radius2={radius2:.3f}", "DEBUG", True)
55    return check

checks if two spheres are colliding

def sphere_aabb_collision( center: numpy.ndarray, radius: float, top_left: numpy.ndarray, bottom_right: numpy.ndarray, eps: float = 1e-05) -> bool:
57def sphere_aabb_collision(center: Point3D, radius: float, top_left: Point3D,
58                          bottom_right: Point3D, eps: float=1e-5) -> bool:
59    """checks if a sphere is colliding with an aabb"""
60    xr, yr, zr = _get_closest_point_on_box(top_left, bottom_right, center)
61    dist_sq = (center[0] - xr)**2 + (center[1] - yr)**2 + (center[2] - zr)**2
62    dist: float = (dist_sq - radius ** 2).item()
63    if check := dist < -eps:
64        logger.log_every_s(f"Collision. center={fmt(center)} radius={radius:.3f} top_left={fmt(top_left)} "
65                           f"bottom_right={fmt(bottom_right)}", "DEBUG", True)
66    return check

checks if a sphere is colliding with an aabb

def aabb_aabb_collision( top_left1: numpy.ndarray, bottom_right1: numpy.ndarray, top_left2: numpy.ndarray, bottom_right2: numpy.ndarray, eps: float = 1e-05) -> bool:
68def aabb_aabb_collision(top_left1: Point3D, bottom_right1: Point3D,
69                        top_left2: Point3D, bottom_right2: Point3D, eps: float=1e-5) -> bool:
70    """
71    Checks if two aabbs are colliding. We do this by doing one check on each ax.
72    We are thus reducing the complexity from 3D to three independent 1D checks.
73
74    For 1D, assume the two lines given by top_left and bottom_right: [tl1, br1], [tl2, br2]. There are 5 cases:
75    - tl1, br1, tl2, br2 -> not overlapping
76    - tl1, tl2, br1, br2 -> overlapping
77    - tl1, tl2, br2, br1 -> overlapping
78    - tl2, tl1, br1, br2 -> overlapping
79    - tl2, tl1, br2, br1 -> overlapping
80    - tl2, br2, tl1, br1 -> not overlapping
81
82    In all the cases, we can look at the midpoints c1, c2 and the actual distances between the two corners:
83    h1 = (br1 - tl1) / 2, h2 = (br2 - tl2) / 2. The lines are overlapping only if dist(c1, c2) < h1 + h2 across all dims
84    """
85
86    h1 = (bottom_right1 - top_left1) / 2
87    h2 = (bottom_right2 - top_left2) / 2
88
89    c1 = top_left1 + h1
90    c2 = top_left2 + h2
91
92    # signed separation on each axis, like the sphere checks above: < 0 means overlapping on that axis
93    gap = abs(c1 - c2) - (h1 + h2)
94    if check := (gap[0] < -eps) and (gap[1] < -eps) and (gap[2] < -eps):
95        logger.log_every_s(f"Collision. {top_left1=} {bottom_right1=} {top_left2=} {bottom_right2=}", "DEBUG", True)
96    return check

Checks if two aabbs are colliding. We do this by doing one check on each ax. We are thus reducing the complexity from 3D to three independent 1D checks.

For 1D, assume the two lines given by top_left and bottom_right: [tl1, br1], [tl2, br2]. There are 5 cases:

  • tl1, br1, tl2, br2 -> not overlapping
  • tl1, tl2, br1, br2 -> overlapping
  • tl1, tl2, br2, br1 -> overlapping
  • tl2, tl1, br1, br2 -> overlapping
  • tl2, tl1, br2, br1 -> overlapping
  • tl2, br2, tl1, br1 -> not overlapping

In all the cases, we can look at the midpoints c1, c2 and the actual distances between the two corners: h1 = (br1 - tl1) / 2, h2 = (br2 - tl2) / 2. The lines are overlapping only if dist(c1, c2) < h1 + h2 across all dims

def check_collision(e1: microecs.entity.Entity, e2: microecs.entity.Entity) -> bool:
 98def check_collision(e1: Entity, e2: Entity) -> bool:
 99    """checks the collision between two entities with the HasCollision component"""
100    if e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.SPHERE:
101        return sphere_sphere_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
102                                       e2.candidate_pose[0:3, 3], e2.collider_radii.item())
103    elif e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.AABB:
104        return sphere_aabb_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
105                                     e2.collider_bbox[0], e2.collider_bbox[1])
106    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.SPHERE:
107        return sphere_aabb_collision(e2.candidate_pose[0:3, 3], e2.collider_radii.item(),
108                                     e1.collider_bbox[0], e1.collider_bbox[1])
109    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.AABB:
110        return aabb_aabb_collision(e1.collider_bbox[0], e1.collider_bbox[1],
111                                   e2.collider_bbox[0], e2.collider_bbox[1])
112    else:
113        raise NotImplementedError((e1.collider_kind, e2.collider_kind))

checks the collision between two entities with the HasCollision component