robosim.physics.collision

collision.py - Collision submodule between basic shapes.

 1"""collision.py - Collision submodule between basic shapes."""
 2import numpy as np
 3from microecs import World, Entity
 4from robosim.utils import Point3D, logger
 5from robosim.components import HasModel, HasCollision, 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(world: World) -> Point3D:
30    """the collision cell size is twice them edian of all bboxes of all collidables with a model"""
31    world.update() # update so we know for sure we use all entities even those recently added via world.add_entity
32    qr = world.query(HasModel, HasCollision)
33    bboxes = qr.model_bbox * qr.scale[..., None]
34    diffs = (bboxes[:, 1] - bboxes[:, 0]).numpy()
35    median = np.median(diffs, axis=0) # median across all 3 dimensions
36    res = median * 2 # heuristic: twice the median
37    logger.info(f"Median bbox size: {median}. Cell size: {res}")
38    return res
39
40def _get_closest_point_on_box(top_left: Point3D, bottom_right: Point3D, other_point: Point3D) -> Point3D:
41    _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]
42    px = _f(top_left[0], other_point[0], bottom_right[0])
43    py = _f(top_left[1], other_point[1], bottom_right[1])
44    pz = _f(top_left[2], other_point[2], bottom_right[2])
45    return np.float32([px, py, pz])
46
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=} {radius1=} {pos2=} {radius2=}", "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 collidng 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=} {radius=} {top_left=} {bottom_right=}", "DEBUG", True)
64    return check
65
66# pylint: disable=unused-argument
67def aabb_aabb_collision(top_left1: Point3D, bottom_right1: Point3D,
68                        top_left2: Point3D, bottom_right2: Point3D, eps: float=1e-5) -> bool:
69    """checks if two aabbs are colliding"""
70    return False
71
72def check_collision(e1: Entity | HasCollision, e2: Entity | HasCollision) -> bool:
73    """checks the collision between two entities with the HasCollision component"""
74    if e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.SPHERE:
75        return sphere_sphere_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
76                                       e2.candidate_pose[0:3, 3], e2.collider_radii.item())
77    elif e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.AABB:
78        return sphere_aabb_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
79                                     e2.collider_bbox[0], e2.collider_bbox[1])
80    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.SPHERE:
81        return sphere_aabb_collision(e2.candidate_pose[0:3, 3], e2.collider_radii.item(),
82                                     e1.collider_bbox[0], e1.collider_bbox[1])
83    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.AABB:
84        return aabb_aabb_collision(e1.collider_bbox[0], e1.collider_bbox[1],
85                                   e2.collider_bbox[0], e2.collider_bbox[1])
86    else:
87        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(world: microecs.world.World) -> numpy.ndarray:
30def make_collision_cell_size(world: World) -> Point3D:
31    """the collision cell size is twice them edian of all bboxes of all collidables with a model"""
32    world.update() # update so we know for sure we use all entities even those recently added via world.add_entity
33    qr = world.query(HasModel, HasCollision)
34    bboxes = qr.model_bbox * qr.scale[..., None]
35    diffs = (bboxes[:, 1] - bboxes[:, 0]).numpy()
36    median = np.median(diffs, axis=0) # median across all 3 dimensions
37    res = median * 2 # heuristic: twice the median
38    logger.info(f"Median bbox size: {median}. Cell size: {res}")
39    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:
48def sphere_sphere_collision(pos1: Point3D, radius1: float, pos2: Point3D, radius2: float, eps: float=1e-5) -> bool:
49    """checks if two spheres are colliding"""
50    max_dist = radius1 + radius2
51    diff_vec = pos1 - pos2
52    dist: float = ((diff_vec**2).sum() - max_dist**2).item() # instead of np.linalg.norm(a.pos-b.pos) < max_dist
53    if check := dist < -eps:
54        logger.log_every_s(f"Collision. {pos1=} {radius1=} {pos2=} {radius2=}", "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 collidng 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=} {radius=} {top_left=} {bottom_right=}", "DEBUG", True)
65    return check

checks if a sphere is collidng 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    """checks if two aabbs are colliding"""
71    return False

checks if two aabbs are colliding

def check_collision( e1: microecs.entity.Entity | robosim.components.HasCollision, e2: microecs.entity.Entity | robosim.components.HasCollision) -> bool:
73def check_collision(e1: Entity | HasCollision, e2: Entity | HasCollision) -> bool:
74    """checks the collision between two entities with the HasCollision component"""
75    if e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.SPHERE:
76        return sphere_sphere_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
77                                       e2.candidate_pose[0:3, 3], e2.collider_radii.item())
78    elif e1.collider_kind == ColliderKinds.SPHERE and e2.collider_kind == ColliderKinds.AABB:
79        return sphere_aabb_collision(e1.candidate_pose[0:3, 3], e1.collider_radii.item(),
80                                     e2.collider_bbox[0], e2.collider_bbox[1])
81    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.SPHERE:
82        return sphere_aabb_collision(e2.candidate_pose[0:3, 3], e2.collider_radii.item(),
83                                     e1.collider_bbox[0], e1.collider_bbox[1])
84    elif e1.collider_kind == ColliderKinds.AABB and e2.collider_kind == ColliderKinds.AABB:
85        return aabb_aabb_collision(e1.collider_bbox[0], e1.collider_bbox[1],
86                                   e2.collider_bbox[0], e2.collider_bbox[1])
87    else:
88        raise NotImplementedError((e1.collider_kind, e2.collider_kind))

checks the collision between two entities with the HasCollision component