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