-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[FEATURE] Add analytical collisions for capsule-capsule and capsule-sphere. #2363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 17 commits
a2d3de8
3d9e064
1dad77e
cdaecc1
e8f6685
df35315
44fa993
79bca62
9770c4d
08b3f16
79bc4a9
72239c1
198dd45
7b8aae5
36228e6
bc3ad55
5859afa
99568ba
b512f20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| import gstaichi as ti | ||
|
|
||
| import genesis as gs | ||
| import genesis.utils.geom as gu | ||
| import genesis.utils.array_class as array_class | ||
|
|
||
|
|
||
| @ti.func | ||
| def func_closest_points_on_segments( | ||
| seg_a_p1: ti.types.vector(3, gs.ti_float), | ||
| seg_a_p2: ti.types.vector(3, gs.ti_float), | ||
| seg_b_p1: ti.types.vector(3, gs.ti_float), | ||
| seg_b_p2: ti.types.vector(3, gs.ti_float), | ||
| EPS: gs.ti_float, | ||
| ): | ||
| """ | ||
| Compute closest points on two line segments using analytical solution. | ||
|
|
||
| References | ||
| ---------- | ||
| Real-Time Collision Detection by Christer Ericson, Chapter 5.1.9 | ||
| """ | ||
| segment_a_dir = seg_a_p2 - seg_a_p1 | ||
| segment_b_dir = seg_b_p2 - seg_b_p1 | ||
| vec_between_segment_origins = seg_a_p1 - seg_b_p1 | ||
|
|
||
| a_squared_len = segment_a_dir.dot(segment_a_dir) | ||
| dot_product_dir = segment_a_dir.dot(segment_b_dir) | ||
| b_squared_len = segment_b_dir.dot(segment_b_dir) | ||
| d = segment_a_dir.dot(vec_between_segment_origins) | ||
| e = segment_b_dir.dot(vec_between_segment_origins) | ||
|
|
||
| denom = a_squared_len * b_squared_len - dot_product_dir * dot_product_dir | ||
|
|
||
| s = gs.ti_float(0.0) | ||
| t = gs.ti_float(0.0) | ||
|
|
||
| if denom < EPS: | ||
| # Segments are parallel or one/both are degenerate | ||
| s = 0.0 | ||
| if b_squared_len > EPS: | ||
| t = ti.math.clamp(e / b_squared_len, 0.0, 1.0) | ||
| else: | ||
| t = 0.0 | ||
| else: | ||
| # General case: solve for optimal parameters | ||
| s = (dot_product_dir * e - b_squared_len * d) / denom | ||
| t = (a_squared_len * e - dot_product_dir * d) / denom | ||
|
|
||
| s = ti.math.clamp(s, 0.0, 1.0) | ||
|
|
||
| # Recompute t for clamped s | ||
| t = ti.math.clamp((dot_product_dir * s + e) / b_squared_len if b_squared_len > EPS else 0.0, 0.0, 1.0) | ||
|
|
||
| # Recompute s for clamped t (ensures we're on segment boundaries) | ||
| s_new = ti.math.clamp((dot_product_dir * t - d) / a_squared_len if a_squared_len > EPS else 0.0, 0.0, 1.0) | ||
|
|
||
| # Use refined s if it improves the solution | ||
| if a_squared_len > EPS: | ||
| s = s_new | ||
|
|
||
| seg_a_closest = seg_a_p1 + s * segment_a_dir | ||
| seg_b_closest = seg_b_p1 + t * segment_b_dir | ||
|
|
||
| return seg_a_closest, seg_b_closest | ||
|
|
||
|
|
||
| @ti.func | ||
| def func_capsule_capsule_contact( | ||
| i_ga, | ||
| i_gb, | ||
| i_b, | ||
| geoms_state: array_class.GeomsState, | ||
| geoms_info: array_class.GeomsInfo, | ||
| rigid_global_info: array_class.RigidGlobalInfo, | ||
| collider_state: array_class.ColliderState, | ||
| collider_info: array_class.ColliderInfo, | ||
| errno: array_class.V_ANNOTATION, | ||
| ): | ||
| """ | ||
| Analytical capsule-capsule collision detection. | ||
|
|
||
| A capsule is defined as a line segment plus a radius (swept sphere). | ||
| Collision between two capsules reduces to: | ||
| 1. Find closest points on the two line segments (analytical) | ||
| 2. Check if distance < sum of radii | ||
| 3. Compute contact point and normal | ||
| """ | ||
| EPS = rigid_global_info.EPS[None] | ||
| is_col = False | ||
| normal = ti.Vector.zero(gs.ti_float, 3) | ||
| contact_pos = ti.Vector.zero(gs.ti_float, 3) | ||
| penetration = gs.ti_float(0.0) | ||
|
|
||
| # Get capsule A parameters | ||
| pos_a = geoms_state.pos[i_ga, i_b] | ||
| quat_a = geoms_state.quat[i_ga, i_b] | ||
| radius_a = geoms_info.data[i_ga][0] | ||
| halflength_a = gs.ti_float(0.5) * geoms_info.data[i_ga][1] | ||
|
|
||
| # Get capsule B parameters | ||
| pos_b = geoms_state.pos[i_gb, i_b] | ||
| quat_b = geoms_state.quat[i_gb, i_b] | ||
| radius_b = geoms_info.data[i_gb][0] | ||
| halflength_b = gs.ti_float(0.5) * geoms_info.data[i_gb][1] | ||
|
|
||
| # Capsules are aligned along local Z-axis by convention | ||
| local_z = ti.Vector([0.0, 0.0, 1.0], dt=gs.ti_float) | ||
|
|
||
| # Get segment axes in world space | ||
| axis_a = gu.ti_transform_by_quat(local_z, quat_a) | ||
| axis_b = gu.ti_transform_by_quat(local_z, quat_b) | ||
|
|
||
| # Compute segment endpoints in world space | ||
| P1 = pos_a - halflength_a * axis_a | ||
| P2 = pos_a + halflength_a * axis_a | ||
| Q1 = pos_b - halflength_b * axis_b | ||
| Q2 = pos_b + halflength_b * axis_b | ||
|
|
||
| Pa, Pb = func_closest_points_on_segments(P1, P2, Q1, Q2, EPS) | ||
|
|
||
| diff = Pb - Pa | ||
| dist_sq = diff.dot(diff) | ||
| combined_radius = radius_a + radius_b | ||
| combined_radius_sq = combined_radius * combined_radius | ||
|
|
||
| if dist_sq < combined_radius_sq: | ||
| is_col = True | ||
| dist = ti.sqrt(dist_sq) | ||
|
|
||
| # Compute contact normal (from B to A, pointing into geom A) | ||
| if dist > EPS: | ||
| # Negative because func_add_contact expects normal from B to A | ||
| normal = -diff / dist | ||
| else: | ||
| # Segments are coincident, use arbitrary perpendicular direction | ||
| # Try cross product with axis_a first | ||
| temp_normal = axis_a.cross(axis_b) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if temp_normal.dot(temp_normal) < EPS: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be more efficient to store |
||
| # Axes are parallel, use any perpendicular | ||
| if ti.abs(axis_a[0]) < 0.9: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this threshold in particular?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question 🤔
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts on what we should do here? |
||
| temp_normal = ti.Vector([1.0, 0.0, 0.0], dt=gs.ti_float).cross(axis_a) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. I wonder if it would be worth it to use the reduce formula. At least in this case it is very simple with no math involved, so I would recommend it here, with the original formula in comment: temp_normal = ti.Vector([0.0, -axis_a[2], axis_a[1]], dt=gs.ti_float) |
||
| else: | ||
| temp_normal = ti.Vector([0.0, 1.0, 0.0], dt=gs.ti_float).cross(axis_a) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same: temp_normal = ti.Vector([axis_a[2], 0.0, -axis_a[0]], dt=gs.ti_float) |
||
| # For coincident case, the sign doesn't matter much, but keep consistent | ||
| normal = -gu.ti_normalize(temp_normal, EPS) | ||
|
|
||
| penetration = combined_radius - dist | ||
| contact_pos = Pa - radius_a * normal | ||
|
|
||
| return is_col, normal, contact_pos, penetration | ||
|
|
||
|
|
||
| @ti.func | ||
| def func_sphere_capsule_contact( | ||
| i_ga, | ||
| i_gb, | ||
| i_b, | ||
| geoms_state: array_class.GeomsState, | ||
| geoms_info: array_class.GeomsInfo, | ||
| rigid_global_info: array_class.RigidGlobalInfo, | ||
| collider_state: array_class.ColliderState, | ||
| collider_info: array_class.ColliderInfo, | ||
| errno: array_class.V_ANNOTATION, | ||
| ): | ||
| """ | ||
| Analytical sphere-capsule collision detection. | ||
|
|
||
| A sphere-capsule collision reduces to: | ||
| 1. Find closest point on the capsule's line segment to sphere center | ||
| 2. Check if distance < sum of radii | ||
| 3. Compute contact point and normal | ||
|
|
||
| """ | ||
| EPS = rigid_global_info.EPS[None] | ||
| is_col = False | ||
| normal = ti.Vector.zero(gs.ti_float, 3) | ||
| contact_pos = ti.Vector.zero(gs.ti_float, 3) | ||
| penetration = gs.ti_float(0.0) | ||
|
|
||
| sphere_center = geoms_state.pos[i_ga, i_b] | ||
| sphere_radius = geoms_info.data[i_ga][0] | ||
|
|
||
| capsule_center = geoms_state.pos[i_gb, i_b] | ||
| capsule_quat = geoms_state.quat[i_gb, i_b] | ||
| capsule_radius = geoms_info.data[i_gb][0] | ||
| capsule_halflength = gs.ti_float(0.5) * geoms_info.data[i_gb][1] | ||
|
|
||
| # Capsule is aligned along local Z-axis | ||
| local_z = ti.Vector([0.0, 0.0, 1.0], dt=gs.ti_float) | ||
| capsule_axis = gu.ti_transform_by_quat(local_z, capsule_quat) | ||
|
Comment on lines
+196
to
+197
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. Not sure if we should use the reduce formula here. |
||
|
|
||
| # Compute capsule segment endpoints | ||
| P1 = capsule_center - capsule_halflength * capsule_axis | ||
| P2 = capsule_center + capsule_halflength * capsule_axis | ||
|
|
||
| # Find closest point on capsule segment to sphere center | ||
| # Using parametric form: P(t) = P1 + t*(P2-P1), t ∈ [0,1] | ||
| segment_vec = P2 - P1 | ||
| segment_length_sq = segment_vec.dot(segment_vec) | ||
|
|
||
| # Project sphere center onto segment | ||
| # Default for degenerate case | ||
| t = gs.ti_float(0.5) | ||
| if segment_length_sq > EPS: | ||
| t = (sphere_center - P1).dot(segment_vec) / segment_length_sq | ||
| t = ti.math.clamp(t, 0.0, 1.0) | ||
|
|
||
| closest_point = P1 + t * segment_vec | ||
|
|
||
| # Compute distance from sphere center to closest point | ||
| diff = sphere_center - closest_point | ||
| dist_sq = diff.dot(diff) | ||
| combined_radius = sphere_radius + capsule_radius | ||
| combined_radius_sq = combined_radius * combined_radius | ||
|
|
||
| if dist_sq < combined_radius_sq: | ||
| is_col = True | ||
| dist = ti.sqrt(dist_sq) | ||
|
|
||
| # Compute contact normal (from capsule to sphere, i.e., B to A) | ||
| if dist > EPS: | ||
| normal = diff / dist | ||
| else: | ||
| # Sphere center is exactly on capsule axis | ||
| # Use any perpendicular direction to the capsule axis | ||
| if ti.abs(capsule_axis[0]) < 0.9: | ||
| normal = ti.Vector([1.0, 0.0, 0.0], dt=gs.ti_float).cross(capsule_axis) | ||
| else: | ||
| normal = ti.Vector([0.0, 1.0, 0.0], dt=gs.ti_float).cross(capsule_axis) | ||
|
Comment on lines
+233
to
+236
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. Using the reduce formula here sounds appropriate. |
||
| normal = gu.ti_normalize(normal, EPS) | ||
|
|
||
| penetration = combined_radius - dist | ||
| contact_pos = sphere_center - sphere_radius * normal | ||
|
|
||
| return is_col, normal, contact_pos, penetration | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if using the simplified formula for normalised quaternion + local_z == world_z is improve performance:
I guess it won't.