Skip to content

Commit 17edb18

Browse files
duburcqaYilingQiao
authored andcommitted
[BUG FIX] Fix terrain collision detection. (Genesis-Embodied-AI#1338)
1 parent de97a8a commit 17edb18

File tree

3 files changed

+98
-87
lines changed

3 files changed

+98
-87
lines changed

genesis/engine/solvers/rigid/collider_decomp.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -572,16 +572,14 @@ def _func_contact_mpr_terrain(self, i_ga, i_gb, i_b):
572572
)
573573
)
574574

575-
for i in range(6):
576-
i_axis = i % 3
577-
i_m = i // 3
578-
579-
sign = gs.ti_float(1 - i_m * 2)
580-
direction = ti.Vector([i_axis == 0, i_axis == 1, i_axis == 2], dt=gs.ti_float)
581-
direction = direction * sign
582-
575+
for i_axis, i_m in ti.ndrange(3, 2):
576+
direction = ti.Vector.zero(gs.ti_float, 3)
577+
if i_m == 0:
578+
direction[i_axis] = 1.0
579+
else:
580+
direction[i_axis] = -1.0
583581
v1 = self._mpr.support_driver(direction, i_ga, i_b)
584-
self.xyz_max_min[i, i_b] = v1[i_axis]
582+
self.xyz_max_min[3 * i_m + i_axis, i_b] = v1[i_axis]
585583

586584
for i in ti.static(range(3)):
587585
self.prism[i, i_b][2] = self._solver.terrain_xyz_maxmin[5]
@@ -622,24 +620,27 @@ def _func_contact_mpr_terrain(self, i_ga, i_gb, i_b):
622620
or self.prism[4, i_b][2] >= self.xyz_max_min[5, i_b]
623621
or self.prism[5, i_b][2] >= self.xyz_max_min[5, i_b]
624622
):
625-
pos = ti.Vector.zero(gs.ti_float, 3)
623+
center_a = gu.ti_transform_by_trans_quat(
624+
self._solver.geoms_info[i_ga].center, ga_pos, ga_quat
625+
)
626+
center_b = ti.Vector.zero(gs.ti_float, 3)
626627
for i_p in ti.static(range(6)):
627-
pos = pos + self.prism[i_p, i_b]
628+
center_b = center_b + self.prism[i_p, i_b]
629+
center_b = center_b / 6.0
628630

629-
self._solver.geoms_info[i_gb].center = pos / 6
630631
self._solver.geoms_state[i_gb, i_b].pos = ti.Vector.zero(gs.ti_float, 3)
631632
self._solver.geoms_state[i_gb, i_b].quat = gu.ti_identity_quat()
632633

633-
is_col, normal, penetration, contact_pos = self._mpr.func_mpr_contact(
634-
i_ga, i_gb, i_b, ti.Vector.zero(gs.ti_float, 3)
634+
is_col, normal, penetration, contact_pos = self._mpr.func_mpr_contact_from_centers(
635+
i_ga, i_gb, i_b, center_a, center_b
635636
)
636637
if is_col:
637638
normal = gu.ti_transform_by_quat(normal, gb_quat)
638639
contact_pos = gu.ti_transform_by_quat(contact_pos, gb_quat)
639640
contact_pos = contact_pos + gb_pos
640641

641-
i_col = self.n_contacts[i_b]
642642
valid = True
643+
i_col = self.n_contacts[i_b]
643644
for j in range(cnt):
644645
if (
645646
contact_pos - self.contact_data[i_col - j - 1, i_b].pos

genesis/engine/solvers/rigid/mpr_decomp.py

Lines changed: 81 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -357,76 +357,7 @@ def mpr_expand_portal(self, v, v1, v2, i_ga, i_gb, i_b):
357357
self.simplex_support[i_s, i_b].v = v
358358

359359
@ti.func
360-
def mpr_discover_portal(self, i_ga, i_gb, i_b, normal_ws):
361-
# MPR algorithm was initially design to check whether a pair of convex geometries was colliding. The author
362-
# proposed to extend its application to collision detection as it can provide the contact normal and penetration
363-
# depth in some cases, i.e. when the original of the Minkowski difference can be projected inside the refined
364-
# portal. Beyond this specific scenario, it only provides an approximation, that gets worst and worst as the
365-
# ray casting and portal normal are misaligned.
366-
# For convex shape, one can show that everything should be fine for low penetration-to-size ratio for each
367-
# geometry, and the probability to accurately estimate the contact point decreases as this ratio increases.
368-
#
369-
# This issue can be avoided by initializing the algorithm with the good seach direction, basically the one
370-
# from the previous simulation timestep would do fine, as the penetration was smaller at that time and so the
371-
# likely for this direction to be valid was larger. Alternatively, the direction of the linear velocity would
372-
# be a good option.
373-
#
374-
# Enforcing a specific search direction to vanilla MPR is not straightforward, because the direction of the ray
375-
# control by v0, which is defined as the difference between the respective centers of each geometry.
376-
# The only option is to change the way the center of each geometry are defined, so as to make the ray casting
377-
# from origin to v0 as colinear as possible with the direction we are interested, while remaining included in
378-
# their respective geometry.
379-
# The idea is to offset the original centers of each geometry by a ratio that corresponds to their respective
380-
# (rotated) bounding box size along each axe. Each center cannot be moved more than half of its bound-box size
381-
# along each axe. This could lead to a center that is outside the geometries if they do not collide, but
382-
# should be fine otherwise. Anyway, this is not a big deal in practice and MPR is robust enough to converge to
383-
# a meaningful solution and if the center is slightly off of each geometry. Nevertheless, if it turns out this
384-
# is a real issue, one way to address it is to evaluate the exact signed distance of each center wrt their
385-
# respective geometry. If one of the center is off, its offset from the original center is divided by 2 and the
386-
# signed distance is computed once again until to find a valid point. This procedure should be cheap.
387-
388-
g_state_a = self._solver.geoms_state[i_ga, i_b]
389-
g_state_b = self._solver.geoms_state[i_gb, i_b]
390-
g_info = self._solver.geoms_info[i_ga]
391-
center_a = gu.ti_transform_by_trans_quat(g_info.center, g_state_a.pos, g_state_a.quat)
392-
g_info = self._solver.geoms_info[i_gb]
393-
center_b = gu.ti_transform_by_trans_quat(g_info.center, g_state_b.pos, g_state_b.quat)
394-
395-
# Completely different center logics if a normal guess is provided
396-
if ti.static(not self._solver._enable_mujoco_compatibility):
397-
if (ti.abs(normal_ws) > self.CCD_EPS).any():
398-
# Must start from the center of each bounding box
399-
center_a_local = 0.5 * (self._solver.geoms_init_AABB[i_ga, 7] + self._solver.geoms_init_AABB[i_ga, 0])
400-
center_a = gu.ti_transform_by_trans_quat(center_a_local, g_state_a.pos, g_state_a.quat)
401-
center_b_local = 0.5 * (self._solver.geoms_init_AABB[i_gb, 7] + self._solver.geoms_init_AABB[i_gb, 0])
402-
center_b = gu.ti_transform_by_trans_quat(center_b_local, g_state_b.pos, g_state_b.quat)
403-
delta = center_a - center_b
404-
405-
# Skip offset if normal is roughly pointing in the same direction already.
406-
# Note that a threshold of 0.5 would probably make more sense, but this means that the center of each
407-
# geometry would significantly affect collision detection, which is undesirable.
408-
normal = delta.normalized()
409-
if normal_ws.cross(normal).norm() > 0.01:
410-
# Compute the target offset
411-
offset = delta.dot(normal_ws) * normal_ws - delta
412-
offset_norm = offset.norm()
413-
414-
if offset_norm > gs.EPS:
415-
# Compute the size of the bounding boxes along the target offset direction.
416-
# First, move the direction in local box frame
417-
dir_offset = offset / offset_norm
418-
dir_offset_local_a = gu.ti_inv_transform_by_quat(dir_offset, g_state_a.quat)
419-
dir_offset_local_b = gu.ti_inv_transform_by_quat(dir_offset, g_state_b.quat)
420-
box_size_a = self._solver.geoms_init_AABB[i_ga, 7] - self._solver.geoms_init_AABB[i_ga, 0]
421-
box_size_b = self._solver.geoms_init_AABB[i_gb, 7] - self._solver.geoms_init_AABB[i_gb, 0]
422-
length_a = box_size_a.dot(ti.abs(dir_offset_local_a))
423-
length_b = box_size_b.dot(ti.abs(dir_offset_local_b))
424-
425-
# Shift the center of each geometry
426-
offset_ratio = ti.min(offset_norm / (length_a + length_b), 0.5)
427-
center_a = center_a + dir_offset * length_a * offset_ratio
428-
center_b = center_b - dir_offset * length_b * offset_ratio
429-
360+
def mpr_discover_portal(self, i_ga, i_gb, i_b, center_a, center_b):
430361
self.simplex_support[0, i_b].v1 = center_a
431362
self.simplex_support[0, i_b].v2 = center_b
432363
self.simplex_support[0, i_b].v = center_a - center_b
@@ -526,8 +457,81 @@ def mpr_discover_portal(self, i_ga, i_gb, i_b, normal_ws):
526457
return ret
527458

528459
@ti.func
529-
def func_mpr_contact(self, i_ga, i_gb, i_b, normal_ws):
530-
res = self.mpr_discover_portal(i_ga, i_gb, i_b, normal_ws)
460+
def guess_geoms_center(self, i_ga, i_gb, i_b, normal_ws):
461+
# MPR algorithm was initially design to check whether a pair of convex geometries was colliding. The author
462+
# proposed to extend its application to collision detection as it can provide the contact normal and penetration
463+
# depth in some cases, i.e. when the original of the Minkowski difference can be projected inside the refined
464+
# portal. Beyond this specific scenario, it only provides an approximation, that gets worst and worst as the
465+
# ray casting and portal normal are misaligned.
466+
# For convex shape, one can show that everything should be fine for low penetration-to-size ratio for each
467+
# geometry, and the probability to accurately estimate the contact point decreases as this ratio increases.
468+
#
469+
# This issue can be avoided by initializing the algorithm with the good seach direction, basically the one
470+
# from the previous simulation timestep would do fine, as the penetration was smaller at that time and so the
471+
# likely for this direction to be valid was larger. Alternatively, the direction of the linear velocity would
472+
# be a good option.
473+
#
474+
# Enforcing a specific search direction to vanilla MPR is not straightforward, because the direction of the ray
475+
# control by v0, which is defined as the difference between the respective centers of each geometry.
476+
# The only option is to change the way the center of each geometry are defined, so as to make the ray casting
477+
# from origin to v0 as colinear as possible with the direction we are interested, while remaining included in
478+
# their respective geometry.
479+
# The idea is to offset the original centers of each geometry by a ratio that corresponds to their respective
480+
# (rotated) bounding box size along each axe. Each center cannot be moved more than half of its bound-box size
481+
# along each axe. This could lead to a center that is outside the geometries if they do not collide, but
482+
# should be fine otherwise. Anyway, this is not a big deal in practice and MPR is robust enough to converge to
483+
# a meaningful solution and if the center is slightly off of each geometry. Nevertheless, if it turns out this
484+
# is a real issue, one way to address it is to evaluate the exact signed distance of each center wrt their
485+
# respective geometry. If one of the center is off, its offset from the original center is divided by 2 and the
486+
# signed distance is computed once again until to find a valid point. This procedure should be cheap.
487+
488+
g_state_a = self._solver.geoms_state[i_ga, i_b]
489+
g_state_b = self._solver.geoms_state[i_gb, i_b]
490+
g_info = self._solver.geoms_info[i_ga]
491+
center_a = gu.ti_transform_by_trans_quat(g_info.center, g_state_a.pos, g_state_a.quat)
492+
g_info = self._solver.geoms_info[i_gb]
493+
center_b = gu.ti_transform_by_trans_quat(g_info.center, g_state_b.pos, g_state_b.quat)
494+
495+
# Completely different center logics if a normal guess is provided
496+
if ti.static(not self._solver._enable_mujoco_compatibility):
497+
if (ti.abs(normal_ws) > self.CCD_EPS).any():
498+
# Must start from the center of each bounding box
499+
center_a_local = 0.5 * (self._solver.geoms_init_AABB[i_ga, 7] + self._solver.geoms_init_AABB[i_ga, 0])
500+
center_a = gu.ti_transform_by_trans_quat(center_a_local, g_state_a.pos, g_state_a.quat)
501+
center_b_local = 0.5 * (self._solver.geoms_init_AABB[i_gb, 7] + self._solver.geoms_init_AABB[i_gb, 0])
502+
center_b = gu.ti_transform_by_trans_quat(center_b_local, g_state_b.pos, g_state_b.quat)
503+
delta = center_a - center_b
504+
505+
# Skip offset if normal is roughly pointing in the same direction already.
506+
# Note that a threshold of 0.5 would probably make more sense, but this means that the center of each
507+
# geometry would significantly affect collision detection, which is undesirable.
508+
normal = delta.normalized()
509+
if normal_ws.cross(normal).norm() > 0.01:
510+
# Compute the target offset
511+
offset = delta.dot(normal_ws) * normal_ws - delta
512+
offset_norm = offset.norm()
513+
514+
if offset_norm > gs.EPS:
515+
# Compute the size of the bounding boxes along the target offset direction.
516+
# First, move the direction in local box frame
517+
dir_offset = offset / offset_norm
518+
dir_offset_local_a = gu.ti_inv_transform_by_quat(dir_offset, g_state_a.quat)
519+
dir_offset_local_b = gu.ti_inv_transform_by_quat(dir_offset, g_state_b.quat)
520+
box_size_a = self._solver.geoms_init_AABB[i_ga, 7] - self._solver.geoms_init_AABB[i_ga, 0]
521+
box_size_b = self._solver.geoms_init_AABB[i_gb, 7] - self._solver.geoms_init_AABB[i_gb, 0]
522+
length_a = box_size_a.dot(ti.abs(dir_offset_local_a))
523+
length_b = box_size_b.dot(ti.abs(dir_offset_local_b))
524+
525+
# Shift the center of each geometry
526+
offset_ratio = ti.min(offset_norm / (length_a + length_b), 0.5)
527+
center_a = center_a + dir_offset * length_a * offset_ratio
528+
center_b = center_b - dir_offset * length_b * offset_ratio
529+
530+
return center_a, center_b
531+
532+
@ti.func
533+
def func_mpr_contact_from_centers(self, i_ga, i_gb, i_b, center_a, center_b):
534+
res = self.mpr_discover_portal(i_ga, i_gb, i_b, center_a, center_b)
531535

532536
is_col = False
533537
pos = gs.ti_vec3([0.0, 0.0, 0.0])
@@ -544,3 +548,8 @@ def func_mpr_contact(self, i_ga, i_gb, i_b, normal_ws):
544548
is_col, normal, penetration, pos = self.mpr_find_penetration(i_ga, i_gb, i_b)
545549

546550
return is_col, normal, penetration, pos
551+
552+
@ti.func
553+
def func_mpr_contact(self, i_ga, i_gb, i_b, normal_ws):
554+
center_a, center_b = self.guess_geoms_center(i_ga, i_gb, i_b, normal_ws)
555+
return self.func_mpr_contact_from_centers(i_ga, i_gb, i_b, center_a, center_b)

tests/test_rigid_physics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,6 +2007,7 @@ def test_terrain_generation(show_viewer):
20072007
camera_fov=40,
20082008
),
20092009
show_viewer=show_viewer,
2010+
show_FPS=False,
20102011
)
20112012
terrain = scene.add_entity(
20122013
morph=gs.morphs.Terrain(

0 commit comments

Comments
 (0)