Fix some more bugs in sphere/capsule-convex collisions.

PiperOrigin-RevId: 617646119
Change-Id: Ie0c99ccbe1dd2a18663d45a111e77fed4112e638
This commit is contained in:
Baruch Tabanpour
2024-03-20 15:44:37 -07:00
committed by Copybara-Service
parent 07da7336e8
commit 624becca52
2 changed files with 116 additions and 62 deletions
+65 -45
View File
@@ -215,46 +215,47 @@ def sphere_convex(sphere: GeomInfo, convex: GeomInfo) -> Contact:
return jp.dot(pos - faces[0], normal) return jp.dot(pos - faces[0], normal)
support = get_support(faces, normals) support = get_support(faces, normals)
has_separating_axis = jp.any(support >= 0)
# Pick the face with minimal penetration as long as it has support. # Pick the face with the best separating axis.
support = jp.where(support >= 0, -1e12, support)
best_idx = support.argmax() best_idx = support.argmax()
face = faces[best_idx] face = faces[best_idx]
normal = normals[best_idx] face_normal = normals[best_idx]
# Get closest point between the polygon face and the sphere center point. # Get closest point between the polygon face and the sphere center point.
# Project the sphere center point onto poly plane. If it's inside polygon # Project the sphere center point onto poly plane. If it's inside polygon
# edge normals, then we're done. # side planes, then we're done.
pt = _project_pt_onto_plane(sphere_pos, face[0], normal) pt = _project_pt_onto_plane(sphere_pos, face[0], face_normal)
edge_p0 = jp.roll(face, 1, axis=0) edge_p0 = jp.roll(face, 1, axis=0)
edge_p1 = face edge_p1 = face
edge_normals = jax.vmap(jp.cross, in_axes=[0, None])( side_normals = jax.vmap(jp.cross, in_axes=[0, None])(
edge_p1 - edge_p0, edge_p1 - edge_p0,
normal, face_normal,
) )
edge_dist = jax.vmap( edge_dist = jax.vmap(
lambda plane_pt, plane_norm: (pt - plane_pt).dot(plane_norm) lambda plane_pt, plane_norm: (pt - plane_pt).dot(plane_norm)
)(edge_p0, edge_normals) )(edge_p0, side_normals)
inside = jp.all(edge_dist <= 0) # lte to handle degenerate edges pt_on_face = jp.all(edge_dist <= 0) # lte to handle degenerate edges
# If the point is outside edge normals, project onto the closest edge plane # If the point is outside side planes, project onto the closest side plane
# that the point is in front of. # that the point is in front of.
degenerate_edge = jp.all(edge_normals == 0, axis=1) degenerate_edge = jp.all(side_normals == 0, axis=1)
behind = edge_dist < 0.0 behind = edge_dist < 0.0
edge_dist = jp.where(degenerate_edge | behind, 1e12, edge_dist) edge_dist = jp.where(degenerate_edge | behind, 1e12, edge_dist)
idx = edge_dist.argmin() idx = edge_dist.argmin()
edge_pt = math.closest_segment_point(edge_p0[idx], edge_p1[idx], pt) edge_pt = math.closest_segment_point(edge_p0[idx], edge_p1[idx], pt)
pt = jp.where(pt_on_face, pt, edge_pt)
pt = jp.where(inside, pt, edge_pt)
# Get the normal, dist, and contact position. # Get the normal, dist, and contact position.
n, d = math.normalize_with_norm(pt - sphere_pos) pt_normal, d = math.normalize_with_norm(pt - sphere_pos)
n = jp.where(inside | (d < 1e-6), normal, n)
# Ensure normal points towards convex centroid. # Ensure normal points towards convex centroid.
n *= jp.where(jp.dot(pt, n) < 0, 1, -1) inside = jp.dot(pt, pt_normal) > 0
sign = jp.where(inside, -1, 1)
n = jp.where(pt_on_face | (d < 1e-6), -face_normal, sign * pt_normal)
d *= sign
spt = sphere_pos + n * sphere.size[0] spt = sphere_pos + n * sphere.size[0]
dist = d - sphere.size[0] dist = jp.where(has_separating_axis, 1.0, d - sphere.size[0])
pos = (pt + spt) * 0.5 pos = (pt + spt) * 0.5
# Go back to world frame. # Go back to world frame.
@@ -292,8 +293,7 @@ def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact:
support = get_support(faces, normals) support = get_support(faces, normals)
has_support = jp.all(support < 0) has_support = jp.all(support < 0)
# Pick the face with minimal penetration as long as it has support. # Pick the face with minimal penetration.
support = jp.where(support >= 0, -1e12, support)
best_idx = support.argmax() best_idx = support.argmax()
face = faces[best_idx] face = faces[best_idx]
normal = normals[best_idx] normal = normals[best_idx]
@@ -316,44 +316,64 @@ def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact:
# Create variables for the face contact. # Create variables for the face contact.
pos = (cap_pts_clipped + face_pts) * 0.5 pos = (cap_pts_clipped + face_pts) * 0.5
contact_normal = -jp.stack([normal] * 2, 0) contact_normal = -jp.stack([normal] * 2, 0)
penetration = jp.where( face_penetration = jp.where(
mask & has_support, jp.dot(face_pts - cap_pts_clipped, normal), -1 mask & has_support, jp.dot(face_pts - cap_pts_clipped, normal), -1
) )
# Get a potential edge contact. # Pick a potential shallow edge contact.
edge_closest, cap_closest = jax.vmap( def get_edge_axis(edge):
math.closest_segment_to_segment_points, edge_closest_pt, cap_closest_pt = math.closest_segment_to_segment_points(
in_axes=[0, 0, None, None], edge[0], edge[1], cap_pts[0], cap_pts[1]
)(edge_p0, edge_p1, cap_pts[0], cap_pts[1]) )
e_idx = ((edge_closest - cap_closest) ** 2).sum(axis=1).argmin() edge_dir = edge_closest_pt - cap_closest_pt
cap_closest_pt, edge_closest_pt = cap_closest[e_idx], edge_closest[e_idx] degenerate_edge_dir = jp.sum(jp.square(edge_dir)) < 1e-6
edge_dir = edge_closest_pt - cap_closest_pt edge_axis, edge_dist = math.normalize_with_norm(edge_dir)
degenerate_edge_dir = jp.sum(jp.square(edge_dir)) < 1e-6 return (
edge_dir, edge_dist = math.normalize_with_norm(edge_dir) edge_dist,
face_edge_normals = convex.face_edge_normal[best_idx][e_idx] edge_axis,
inside_edge_voronoi_front = ((face_edge_normals @ edge_dir) < 0).all() degenerate_edge_dir,
inside_edge_voronoi_back = ((face_edge_normals @ edge_dir) > 0).all() edge_closest_pt,
outside_edge_voronoi = ~inside_edge_voronoi_front & ~inside_edge_voronoi_back cap_closest_pt,
edge_axis = math.normalize(-edge_closest_pt) # approximate edge axis )
edge_axis = jp.where(
~degenerate_edge_dir & inside_edge_voronoi_front, edge = jp.take(convex.vert, convex.face_edge, axis=0).reshape(-1, 2, 3)
edge_dir, # shallow edge penetration face_edge_normal = convex.face_edge_normal.reshape((-1, 2, 3)) # pytype: disable=attribute-error
edge_axis, # deep edge penetration
) res = jax.vmap(get_edge_axis)(edge.reshape(-1, 2, 3))
e_idx = jp.abs(res[0]).argmin()
(
edge_dist,
edge_axis,
degenerate_edge_dir,
edge_closest_pt,
cap_closest_pt,
) = jax.tree_map(lambda x, i=e_idx: jp.take(x, i, axis=0), res)
face_edge_normals = face_edge_normal[e_idx]
edge_voronoi_front = ((face_edge_normals @ edge_axis) < 0).all()
shallow = ~degenerate_edge_dir & edge_voronoi_front
edge_penetration = jp.where(shallow, cap.size[0] - edge_dist, -1)
# Determine edge contact position. # Determine edge contact position.
edge_pos = ( edge_pos = (
edge_closest_pt + (cap_closest_pt + edge_axis * cap.size[0]) edge_closest_pt + (cap_closest_pt + edge_axis * cap.size[0])
) * 0.5 ) * 0.5
edge_penetration = cap.size[0] - edge_dist
# prefer face contact if the edge axis is parallel to the face normal
edge_dir_parallel_to_face = ( edge_dir_parallel_to_face = (
jp.abs(edge_dir.dot(normal)) > 0.99 jp.abs(edge_axis.dot(normal)) > 0.99
) & ~degenerate_edge_dir ) & ~degenerate_edge_dir
min_face_penetration = face_penetration.min()
has_edge_contact = ( has_edge_contact = (
(edge_penetration > 0) (edge_penetration > 0)
# prefer edge contact if the edge is smaller than face penetration
& jp.where(
min_face_penetration > 0,
edge_penetration < min_face_penetration,
True,
)
# prefer face contact if the edge axis is parallel to the face normal
& ~edge_dir_parallel_to_face & ~edge_dir_parallel_to_face
& ~outside_edge_voronoi # make sure we have a shallow contact
& edge_voronoi_front
) )
# Get the contact info. # Get the contact info.
@@ -367,7 +387,7 @@ def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact:
n = n @ convex.mat.T n = n @ convex.mat.T
dist = -jp.where( dist = -jp.where(
has_edge_contact, penetration.at[0].set(edge_penetration), penetration has_edge_contact, jp.array([edge_penetration, -1]), face_penetration
) )
frame = jax.vmap(math.make_frame)(n) frame = jax.vmap(math.make_frame)(n)
return dist, pos, frame return dist, pos, frame
+51 -17
View File
@@ -132,37 +132,70 @@ class SphereCollisionTest(parameterized.TestCase):
</mujoco> </mujoco>
""" """
def test_sphere_convex(self): def test_sphere_convex_face(self):
d, dx = _collide(self._SPHERE_CONVEX) # no contact
xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.55 0 0.5">'
)
d, dx = _collide(xml)
self.assertEmpty(d.contact.dist)
self.assertGreater(dx.contact.dist, 0)
# face contact
xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.51 0 0.25">'
)
d, dx = _collide(xml)
for field in dataclasses.fields(Contact): for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, 'sphere_convex', 1e-4) _assert_attr_eq(dx.contact, d.contact, field.name, 'face', 1e-4)
# test deep penetration # deep face contact
xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.48 0 0.47">'
)
d, dx = _collide(xml)
self.assertTrue((dx.contact.dist < 0).all())
self.assertTrue((d.contact.dist < 0).all())
np.testing.assert_allclose(dx.contact.dist, [-0.07], atol=1e-5)
np.testing.assert_array_almost_equal(dx.contact.pos, d.contact.pos)
np.testing.assert_array_almost_equal(
dx.contact.frame, d.contact.frame.reshape((-1, 3, 3))
)
def test_sphere_convex_edge(self):
# edge contact
d, dx = _collide(self._SPHERE_CONVEX)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, 'edge', 1e-4)
# deep edge penetration
xml = self._SPHERE_CONVEX.replace( xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.49 0 0.49">' '<body pos="0.52 0 0.52">', '<body pos="0.49 0 0.49">'
) )
d, dx = _collide(xml) d, dx = _collide(xml)
self.assertTrue((dx.contact.dist < 0).all()) self.assertTrue((dx.contact.dist < 0).all())
self.assertTrue((d.contact.dist < 0).all()) self.assertTrue((d.contact.dist < 0).all())
np.testing.assert_allclose(dx.contact.dist, [-0.06], atol=1e-5)
np.testing.assert_array_almost_equal(dx.contact.pos, d.contact.pos) np.testing.assert_array_almost_equal(dx.contact.pos, d.contact.pos)
np.testing.assert_array_almost_equal( np.testing.assert_array_almost_equal(
dx.contact.frame, d.contact.frame.reshape((-1, 3, 3)) dx.contact.frame, d.contact.frame.reshape((-1, 3, 3))
) )
# test sphere center on vertex # vertex contact
xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.5 0.52 0.51">'
)
d, dx = _collide(xml)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, 'vertex', 1e-4)
# sphere center on vertex
xml = self._SPHERE_CONVEX.replace( xml = self._SPHERE_CONVEX.replace(
'<body pos="0.52 0 0.52">', '<body pos="0.5 0 0.5">' '<body pos="0.52 0 0.52">', '<body pos="0.5 0 0.5">'
) )
d, dx = _collide(xml) d, dx = _collide(xml)
for field in dataclasses.fields(Contact):
self.assertTrue((dx.contact.dist < 0).all()) _assert_attr_eq(dx.contact, d.contact, field.name, 'vertex_center', 1e-4)
self.assertTrue((d.contact.dist < 0).all())
np.testing.assert_array_almost_equal(dx.contact.pos, d.contact.pos)
np.testing.assert_array_almost_equal(
dx.contact.frame, d.contact.frame.reshape((-1, 3, 3))
)
class CapsuleCollisionTest(parameterized.TestCase): class CapsuleCollisionTest(parameterized.TestCase):
@@ -309,16 +342,17 @@ class CapsuleCollisionTest(parameterized.TestCase):
def test_capsule_convex_edge_deep(self): def test_capsule_convex_edge_deep(self):
"""Tests deep edge penetration.""" """Tests deep edge penetration."""
xml = self._CAP_EDGE_BOX.replace( xml = self._CAP_EDGE_BOX.replace(
'<body pos="0.5 0 0.55"', '<body pos="0.5 0 0.48"' '<body pos="0.5 0 0.55"', '<body pos="0.5 0 0.42"'
) )
_, dx = _collide(xml) _, dx = _collide(xml)
np.testing.assert_array_equal(dx.contact.dist < 0, np.array([True, False])) np.testing.assert_array_equal(dx.contact.dist < 0, np.array([True, False]))
np.testing.assert_array_almost_equal(dx.contact.dist[0], np.array([-0.13]))
np.testing.assert_array_almost_equal( np.testing.assert_array_almost_equal(
dx.contact.pos[0], np.array([0.478, 0, 0.475]), decimal=3 dx.contact.pos[0], np.array([0.5, 0, 0.435]), decimal=3
) )
s2 = np.sqrt(2) / 2.0
np.testing.assert_array_almost_equal( np.testing.assert_array_almost_equal(
dx.contact.frame[0, 0], np.array([-s2, 0, -s2]), decimal=3 dx.contact.frame[0, 0], np.array([0, 0, -1]), decimal=3
) )
def test_capsule_convex_edge_shallow_tip(self): def test_capsule_convex_edge_shallow_tip(self):