diff --git a/doc/changelog.rst b/doc/changelog.rst
index bd0d0e4e..78a076e7 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -10,7 +10,7 @@ General
- Added :ref:`mjv_connector` which has identical functionality to :ref:`mjv_makeConnector`, but with more convenient
"from-to" argument parametrization. :ref:`mjv_makeConnector` is now deprecated.
-
+- Added primitive collider for sphere-cylinder contacts, previously this pair used the generic convex-convex collider.
Version 2.3.6 (June 20, 2023)
-----------------------------
diff --git a/src/engine/engine_collision_driver.c b/src/engine/engine_collision_driver.c
index 980f25bf..1dc70ecd 100644
--- a/src/engine/engine_collision_driver.c
+++ b/src/engine/engine_collision_driver.c
@@ -37,7 +37,7 @@ mjfCollision mjCOLLISIONFUNC[mjNGEOMTYPES][mjNGEOMTYPES] = {
/* PLANE HFIELD SPHERE CAPSULE ELLIPSOID CYLINDER BOX MESH */
/*PLANE */ {0, 0, mjc_PlaneSphere, mjc_PlaneCapsule, mjc_PlaneConvex, mjc_PlaneCylinder, mjc_PlaneBox, mjc_PlaneConvex},
/*HFIELD */ {0, 0, mjc_ConvexHField, mjc_ConvexHField, mjc_ConvexHField, mjc_ConvexHField, mjc_ConvexHField, mjc_ConvexHField},
- /*SHPERE */ {0, 0, mjc_SphereSphere, mjc_SphereCapsule, mjc_Convex, mjc_Convex, mjc_SphereBox, mjc_Convex},
+ /*SHPERE */ {0, 0, mjc_SphereSphere, mjc_SphereCapsule, mjc_Convex, mjc_SphereCylinder, mjc_SphereBox, mjc_Convex},
/*CAPSULE */ {0, 0, 0, mjc_CapsuleCapsule, mjc_Convex, mjc_Convex, mjc_CapsuleBox, mjc_Convex},
/*ELLIPSOID */ {0, 0, 0, 0, mjc_Convex, mjc_Convex, mjc_Convex, mjc_Convex},
/*CYLINDER */ {0, 0, 0, 0, 0, mjc_Convex, mjc_Convex, mjc_Convex},
diff --git a/src/engine/engine_collision_primitive.c b/src/engine/engine_collision_primitive.c
index ad03a9ba..ba077e35 100644
--- a/src/engine/engine_collision_primitive.c
+++ b/src/engine/engine_collision_primitive.c
@@ -314,6 +314,80 @@ int mjc_SphereCapsule(const mjModel* m, const mjData* d,
+// sphere : cylinder
+int mjc_SphereCylinder(const mjModel* m, const mjData* d,
+ mjContact* con, int g1, int g2, mjtNum margin) {
+ mjGETINFO
+
+ // get cylinder sizes and axis
+ mjtNum radius = size2[0];
+ mjtNum height = size2[1];
+ mjtNum axis[3] = {mat2[2], mat2[5], mat2[8]};
+
+ // find sphere projection onto cylinder axis and plane
+ mjtNum vec[3] = {pos1[0] - pos2[0], pos1[1] - pos2[1], pos1[2] - pos2[2]};
+ mjtNum x = mju_dot3(axis, vec);
+ mjtNum a_proj[3], p_proj[3];
+ mju_scl3(a_proj, axis, x);
+ mju_sub3(p_proj, vec, a_proj);
+ mjtNum p_proj_sqr = mju_dot3(p_proj, p_proj);
+
+ // get collision type
+ int collide_side = mju_abs(x) < height;
+ int collide_cap = p_proj_sqr < radius*radius;
+ if (collide_side && collide_cap) { // deep penetration (sphere origin inside cylinder)
+ mjtNum dist_cap = height - mju_abs(x);
+ mjtNum dist_radius = radius - mju_sqrt(p_proj_sqr);
+ if (dist_cap < dist_radius) { // disable one collision type
+ collide_side = 0;
+ } else {
+ collide_cap = 0;
+ }
+ }
+
+ // side collision: use sphere-sphere
+ if (collide_side) {
+ mju_addTo3(a_proj, pos2);
+ return _SphereSphere(con, margin, pos1, mat1, size1, a_proj, mat2, size2);
+ }
+
+ // cap collision: use plane-sphere
+ if (collide_cap) {
+ mjtNum flipmat[9] = {
+ -mat2[0], mat2[1], -mat2[2],
+ -mat2[3], mat2[4], -mat2[5],
+ -mat2[6], mat2[7], -mat2[8]
+ };
+ mjtNum* mat_cap;
+ mjtNum pos_cap[3];
+ if (x > 0) { // top cap
+ mju_addScl3(pos_cap, pos2, axis, height);
+ mat_cap = mat2;
+ } else { // bottom cap
+ mju_addScl3(pos_cap, pos2, axis, -height);
+ mat_cap = flipmat;
+ }
+ int ncon = _PlaneSphere(con, margin, pos_cap, mat_cap, size2, pos1, mat1, size1);
+ if (ncon) {
+ // flip frame normal (because mjGEOM_PLANE < mjGEOM_SPHERE < mjGEOM_CYLINDER)
+ mju_scl3(con->frame, con->frame, -1);
+ }
+ return ncon;
+ }
+
+ // otherwise corner collision: use sphere-sphere
+ mju_scl3(p_proj, p_proj, size2[0] / mju_sqrt(p_proj_sqr)); // denominator cannot be 0
+ mju_scl3(vec, axis, x > 0 ? height : -height);
+ mju_addTo3(vec, p_proj);
+ mju_addTo3(vec, pos2);
+
+ // sphere-sphere with point sphere at the corner
+ mjtNum size_zero[1] = {0};
+ return _SphereSphere(con, margin, pos1, mat1, size1, vec, mat2, size_zero);
+}
+
+
+
// capsule : capsule
int mjc_CapsuleCapsule(const mjModel* m, const mjData* d,
mjContact* con, int g1, int g2, mjtNum margin) {
diff --git a/src/engine/engine_collision_primitive.h b/src/engine/engine_collision_primitive.h
index 1986a1d7..5eb3d5ba 100644
--- a/src/engine/engine_collision_primitive.h
+++ b/src/engine/engine_collision_primitive.h
@@ -47,6 +47,8 @@ int mjc_SphereSphere (const mjModel* m, const mjData* d,
mjContact* con, int g1, int g2, mjtNum margin);
int mjc_SphereCapsule (const mjModel* m, const mjData* d,
mjContact* con, int g1, int g2, mjtNum margin);
+int mjc_SphereCylinder (const mjModel* m, const mjData* d,
+ mjContact* con, int g1, int g2, mjtNum margin);
int mjc_CapsuleCapsule (const mjModel* m, const mjData* d,
mjContact* con, int g1, int g2, mjtNum margin);
diff --git a/test/engine/testdata/collision_primitive/sphere_cylinder.xml b/test/engine/testdata/collision_primitive/sphere_cylinder.xml
new file mode 100644
index 00000000..3023d060
--- /dev/null
+++ b/test/engine/testdata/collision_primitive/sphere_cylinder.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/engine/testdata/collision_primitive/spheres_cylinders.xml b/test/engine/testdata/collision_primitive/spheres_cylinders.xml
new file mode 100644
index 00000000..4d966455
--- /dev/null
+++ b/test/engine/testdata/collision_primitive/spheres_cylinders.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+