Skip to content

Commit 947179b

Browse files
authored
[FEATURE] Implemented safer GJK and added GJK examples (#1357)
1 parent 1248538 commit 947179b

File tree

7 files changed

+1354
-232
lines changed

7 files changed

+1354
-232
lines changed

examples/collision/pyramid.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import numpy as np
2+
import genesis as gs
3+
import argparse
4+
5+
pile_type = "static"
6+
num_cubes = 5
7+
8+
parser = argparse.ArgumentParser()
9+
parser.add_argument("--pile_type", type=str, default=pile_type, choices=["static", "falling"])
10+
parser.add_argument("--num_cubes", type=int, default=num_cubes, choices=range(5, 11))
11+
parser.add_argument("--cpu", action="store_true", help="Use CPU backend instead of GPU")
12+
args = parser.parse_args()
13+
14+
pile_type = args.pile_type
15+
num_cubes = args.num_cubes
16+
cpu = args.cpu
17+
backend = gs.cpu if cpu else gs.gpu
18+
19+
gs.init(backend=backend, precision="32")
20+
21+
scene = gs.Scene(
22+
sim_options=gs.options.SimOptions(
23+
dt=0.01,
24+
),
25+
rigid_options=gs.options.RigidOptions(
26+
box_box_detection=False,
27+
max_collision_pairs=1000,
28+
use_gjk_collision=True,
29+
enable_mujoco_compatibility=False,
30+
),
31+
viewer_options=gs.options.ViewerOptions(
32+
camera_pos=(0, -5.5, 2.5),
33+
camera_lookat=(0, 0.0, 1.5),
34+
camera_fov=30,
35+
max_FPS=60,
36+
),
37+
show_viewer=True,
38+
)
39+
40+
plane = scene.add_entity(gs.morphs.Plane(pos=(0, 0, 0)))
41+
42+
# create pyramid of boxes
43+
box_size = 0.25
44+
if pile_type == "static":
45+
box_spacing = box_size
46+
else:
47+
box_spacing = 1.1 * box_size
48+
vec_one = np.array([1.0, 1.0, 1.0])
49+
box_pos_offset = (0 - 0.5, 1, 0.0) + 0.5 * box_size * vec_one
50+
boxes = {}
51+
for i in range(num_cubes):
52+
for j in range(num_cubes - i):
53+
box = scene.add_entity(
54+
gs.morphs.Box(size=box_size * vec_one, pos=box_pos_offset + box_spacing * np.array([i + 0.5 * j, 0, j])),
55+
)
56+
57+
scene.build()
58+
59+
for i in range(1000):
60+
scene.step()

examples/collision/tower.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import numpy as np
2+
import genesis as gs
3+
import argparse
4+
5+
object_type = "cylinder"
6+
7+
parser = argparse.ArgumentParser()
8+
parser.add_argument(
9+
"--object",
10+
type=str,
11+
default=object_type,
12+
choices=["sphere", "cylinder", "duck"],
13+
)
14+
args = parser.parse_args()
15+
object_type = args.object
16+
17+
gs.init(backend=gs.cpu, precision="32")
18+
19+
scene = gs.Scene(
20+
sim_options=gs.options.SimOptions(
21+
dt=0.005,
22+
),
23+
rigid_options=gs.options.RigidOptions(
24+
box_box_detection=False,
25+
max_collision_pairs=2000,
26+
use_gjk_collision=True,
27+
enable_mujoco_compatibility=False,
28+
),
29+
viewer_options=gs.options.ViewerOptions(
30+
camera_pos=(20, -20, 20),
31+
camera_lookat=(0.0, 0.0, 5.0),
32+
camera_fov=30,
33+
max_FPS=60,
34+
),
35+
show_viewer=True,
36+
)
37+
38+
plane = scene.add_entity(gs.morphs.Plane(pos=(0, 0, 0)))
39+
40+
# create pyramid of boxes
41+
box_width = 0.25
42+
box_length = 2.0
43+
box_height = 0.1
44+
num_stacks = 50
45+
height_offset = 0.0
46+
for i in range(num_stacks):
47+
horizontal = i % 2 == 0
48+
49+
if horizontal:
50+
box_size = np.array([box_width, box_length, box_height])
51+
box_pos_0 = (-0.4 * box_length, 0, i * (height_offset + box_size[2]) + 0.5 * box_size[2])
52+
box_pos_1 = (+0.4 * box_length, 0, i * (height_offset + box_size[2]) + 0.5 * box_size[2])
53+
else:
54+
box_size = np.array([box_length, box_width, box_height])
55+
box_pos_0 = (0, -0.4 * box_length, i * (height_offset + box_size[2]) + 0.5 * box_size[2])
56+
box_pos_1 = (0, +0.4 * box_length, i * (height_offset + box_size[2]) + 0.5 * box_size[2])
57+
58+
scene.add_entity(
59+
gs.morphs.Box(size=box_size, pos=box_pos_0),
60+
)
61+
scene.add_entity(
62+
gs.morphs.Box(size=box_size, pos=box_pos_1),
63+
)
64+
65+
# Drop a huge mesh
66+
if object_type == "duck":
67+
duck_scale = 1.0
68+
duck = scene.add_entity(
69+
morph=gs.morphs.Mesh(
70+
file="meshes/duck.obj",
71+
scale=duck_scale,
72+
pos=(0, 0, num_stacks * (height_offset + box_height) + 10 * duck_scale),
73+
),
74+
)
75+
elif object_type == "sphere":
76+
sphere_radius = 2.0
77+
scene.add_entity(
78+
morph=gs.morphs.Sphere(
79+
radius=sphere_radius, pos=(0.0, 0.0, num_stacks * (height_offset + box_height) + 5 * sphere_radius)
80+
),
81+
)
82+
elif object_type == "cylinder":
83+
cylinder_radius = 2.0
84+
cylinder_height = 1.0
85+
scene.add_entity(
86+
morph=gs.morphs.Cylinder(
87+
radius=cylinder_radius,
88+
height=cylinder_height,
89+
pos=(0.0, 0.0, num_stacks * (height_offset + box_height) + 5 * cylinder_height),
90+
),
91+
)
92+
93+
scene.build()
94+
for i in range(5000):
95+
scene.step()

genesis/engine/solvers/rigid/collider_decomp.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222

2323

2424
class CCD_ALGORITHM_CODE(IntEnum):
25+
# Our MPR (with SDF)
2526
MPR = 0
26-
MPR_SDF = 1
27+
# MuJoCo MPR
28+
MJ_MPR = 1
29+
# Our GJK
2730
GJK = 2
31+
# MuJoCo GJK
32+
MJ_GJK = 3
2833

2934

3035
@ti.func
@@ -54,15 +59,19 @@ def __init__(self, rigid_solver: "RigidSolver"):
5459

5560
# Identify the convex collision detection (ccd) algorithm
5661
if self._solver._options.use_gjk_collision:
57-
self.ccd_algorithm = CCD_ALGORITHM_CODE.GJK
58-
elif self._solver._enable_mujoco_compatibility:
59-
self.ccd_algorithm = CCD_ALGORITHM_CODE.MPR
62+
if self._solver._enable_mujoco_compatibility:
63+
self.ccd_algorithm = CCD_ALGORITHM_CODE.MJ_GJK
64+
else:
65+
self.ccd_algorithm = CCD_ALGORITHM_CODE.GJK
6066
else:
61-
self.ccd_algorithm = CCD_ALGORITHM_CODE.MPR_SDF
67+
if self._solver._enable_mujoco_compatibility:
68+
self.ccd_algorithm = CCD_ALGORITHM_CODE.MJ_MPR
69+
else:
70+
self.ccd_algorithm = CCD_ALGORITHM_CODE.MPR
6271

6372
# FIXME: MPR is necessary because it is used for terrain collision detection
6473
self._mpr = MPR(rigid_solver)
65-
self._gjk = GJK(rigid_solver) if self.ccd_algorithm == CCD_ALGORITHM_CODE.GJK else None
74+
self._gjk = GJK(rigid_solver) if self._solver._options.use_gjk_collision else None
6675

6776
# multi contact perturbation and tolerance
6877
if self._solver._enable_mujoco_compatibility:
@@ -1110,6 +1119,7 @@ def _func_plane_box_contact(self, i_ga, i_gb, i_b):
11101119

11111120
@ti.func
11121121
def _func_add_contact(self, i_ga, i_gb, normal, contact_pos, penetration, i_b):
1122+
# print(f"Adding contact {i_ga} {i_gb}, normal:", normal, "contact_pos:", contact_pos, "penetration:", penetration)
11131123
i_col = self.n_contacts[i_b]
11141124

11151125
if i_col == self._max_contact_pairs:
@@ -1265,8 +1275,8 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
12651275
contact_pos = v1 - 0.5 * penetration * normal
12661276
is_col = penetration > 0
12671277
else:
1268-
### MPR, MPR + SDF
1269-
if ti.static(self.ccd_algorithm != CCD_ALGORITHM_CODE.GJK):
1278+
### MPR, MJ_MPR
1279+
if ti.static(self.ccd_algorithm in (CCD_ALGORITHM_CODE.MPR, CCD_ALGORITHM_CODE.MJ_MPR)):
12701280
# Try using MPR before anything else
12711281
is_mpr_updated = False
12721282
is_mpr_guess_direction_available = True
@@ -1301,10 +1311,9 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
13011311
# associated with the point of deepest penetration.
13021312
try_sdf = True
13031313

1304-
### GJK
1305-
elif ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.GJK):
1306-
# If it was not the first detection, only detect single contact point.
1307-
self._gjk.func_gjk_contact(i_ga, i_gb, i_b, i_detection == 0)
1314+
### GJK, MJ_GJK
1315+
elif ti.static(self.ccd_algorithm in (CCD_ALGORITHM_CODE.GJK, CCD_ALGORITHM_CODE.MJ_GJK)):
1316+
self._gjk.func_gjk_contact(i_ga, i_gb, i_b)
13081317

13091318
is_col = self._gjk.is_col[i_b] == 1
13101319
penetration = self._gjk.penetration[i_b]
@@ -1326,7 +1335,7 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
13261335
contact_pos = self._gjk.contact_pos[i_b, 0]
13271336
normal = self._gjk.normal[i_b, 0]
13281337

1329-
if ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.MPR_SDF):
1338+
if ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.MPR):
13301339
if try_sdf:
13311340
is_col_a = False
13321341
is_col_b = False
@@ -1392,15 +1401,15 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
13921401
axis_0, axis_1 = self._func_contact_orthogonals(i_ga, i_gb, normal, i_b)
13931402
n_con = 1
13941403

1395-
if ti.static(not self._solver._enable_mujoco_compatibility):
1404+
if ti.static(self.ccd_algorithm in (CCD_ALGORITHM_CODE.MPR, CCD_ALGORITHM_CODE.GJK)):
13961405
self.contact_cache[i_ga, i_gb, i_b].normal = normal
13971406
else:
13981407
# Clear collision normal cache if not in contact
13991408
# self.contact_cache[i_ga, i_gb, i_b].i_va_ws = -1
14001409
self.contact_cache[i_ga, i_gb, i_b].normal.fill(0.0)
14011410

14021411
elif multi_contact and is_col_0 > 0 and is_col > 0:
1403-
if ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.MPR_SDF):
1412+
if ti.static(self.ccd_algorithm in (CCD_ALGORITHM_CODE.MPR, CCD_ALGORITHM_CODE.GJK)):
14041413
# 1. Project the contact point on both geometries
14051414
# 2. Revert the effect of small rotation
14061415
# 3. Update contact point
@@ -1437,9 +1446,9 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
14371446
# dynamics since zero-penetration contact points should not induce any force.
14381447
penetration = normal.dot(contact_point_b - contact_point_a)
14391448

1440-
elif ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.GJK):
1449+
elif ti.static(self.ccd_algorithm == CCD_ALGORITHM_CODE.MJ_GJK):
14411450
# Only change penetration to the initial one, because the normal vector could change abruptly
1442-
# under GJK-EPA as the nearest simplex is determined by discrete logic, unlike MPR.
1451+
# under MuJoCo's GJK-EPA.
14431452
penetration = penetration_0
14441453

14451454
# Discard contact point is repeated
@@ -1457,10 +1466,10 @@ def _func_convex_convex_contact(self, i_ga, i_gb, i_b):
14571466
self._func_add_contact(i_ga, i_gb, normal, contact_pos, penetration, i_b)
14581467
n_con = n_con + 1
14591468

1460-
self._solver.geoms_state[i_ga, i_b].pos = ga_pos
1461-
self._solver.geoms_state[i_ga, i_b].quat = ga_quat
1462-
self._solver.geoms_state[i_gb, i_b].pos = gb_pos
1463-
self._solver.geoms_state[i_gb, i_b].quat = gb_quat
1469+
self._solver.geoms_state[i_ga, i_b].pos = ga_pos
1470+
self._solver.geoms_state[i_ga, i_b].quat = ga_quat
1471+
self._solver.geoms_state[i_gb, i_b].pos = gb_pos
1472+
self._solver.geoms_state[i_gb, i_b].quat = gb_quat
14641473

14651474
@ti.func
14661475
def _func_rotate_frame(self, i_g, contact_pos, qrot, i_b):

genesis/engine/solvers/rigid/constraint_solver_decomp.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def __init__(self, rigid_solver: "RigidSolver"):
3535
self.ti_n_equalities = ti.field(gs.ti_int, shape=self._solver._batch_shape())
3636
self.ti_n_equalities.from_numpy(np.full((self._solver._B,), self._solver.n_equalities, dtype=gs.np_int))
3737

38+
jac_shape = self._solver._batch_shape((self.len_constraints_, self._solver.n_dofs_))
39+
if (jac_shape[0] * jac_shape[1] * jac_shape[2]) > np.iinfo(np.int32).max:
40+
raise ValueError(
41+
f"Jacobian shape {jac_shape} is too large for int32. "
42+
"Consider reducing the number of constraints or the number of degrees of freedom."
43+
)
44+
3845
self.jac = ti.field(
3946
dtype=gs.ti_float, shape=self._solver._batch_shape((self.len_constraints_, self._solver.n_dofs_))
4047
)

0 commit comments

Comments
 (0)