LCOV - code coverage report
Current view: top level - rbc/analytic - BoxBox.hpp (source / functions) Coverage Total Hit
Test: coverage_clean.info Lines: 85.4 % 178 152
Test Date: 2026-03-30 03:13:12 Functions: 100.0 % 6 6

            Line data    Source code
       1              : #pragma once
       2              : #include "rbc/Dispatcher.hpp"
       3              : #include <algorithm>
       4              : #include <array>
       5              : #include <limits>
       6              : 
       7              : // ============================================================================
       8              : //  Box vs Box — SAT + Sutherland-Hodgman contact manifold
       9              : //
      10              : //  Pipeline:
      11              : //   1. SAT over 15 axes → find minimum-overlap axis (the contact normal).
      12              : //   2. Identify the REFERENCE face (on A, most aligned with normal) and the
      13              : //      INCIDENT face (on B, most anti-aligned with normal).
      14              : //   3. Clip the incident face polygon against the 4 side planes of the
      15              : //      reference face (Sutherland-Hodgman).
      16              : //   4. Keep clipped points that are below the reference face plane (penetrating).
      17              : //   5. Reduce to at most 4 contacts by keeping the extremal points.
      18              : //
      19              : //  Result: ContactManifold with 1-4 points, all physically meaningful.
      20              : // ============================================================================
      21              : 
      22              : namespace rbc
      23              : {
      24              : 
      25              : namespace detail
      26              : {
      27              : 
      28              : // ---------------------------------------------------------------------------
      29              : //  Low-level geometry helpers
      30              : // ---------------------------------------------------------------------------
      31              : 
      32              : // Project OBB onto a world-space axis → half-extent (support radius).
      33          172 : inline m3d::scalar obb_radius(const Box     &box,
      34              :                                const m3d::tf &tf,
      35              :                                const m3d::vec3 &axis)
      36              : {
      37          172 :     const m3d::vec3 ax = tf.rotate_vector(m3d::vec3(1, 0, 0));
      38          172 :     const m3d::vec3 ay = tf.rotate_vector(m3d::vec3(0, 1, 0));
      39          172 :     const m3d::vec3 az = tf.rotate_vector(m3d::vec3(0, 0, 1));
      40          172 :     return m3d::abs(m3d::dot(ax, axis)) * box.half_extents.x
      41          172 :          + m3d::abs(m3d::dot(ay, axis)) * box.half_extents.y
      42          172 :          + m3d::abs(m3d::dot(az, axis)) * box.half_extents.z;
      43              : }
      44              : 
      45              : // Test one SAT axis. Updates best_depth / best_normal if this is the
      46              : // smallest penetration seen so far. Returns false → separating axis found.
      47          107 : inline bool test_axis(const m3d::vec3 &axis,
      48              :                       const Box &a, const m3d::tf &tf_a,
      49              :                       const Box &b, const m3d::tf &tf_b,
      50              :                       const m3d::vec3 &centre_diff,
      51              :                       m3d::scalar &best_depth,
      52              :                       m3d::vec3   &best_normal,
      53              :                       int          feature_id,       // which axis index
      54              :                       int         &best_feature)
      55              : {
      56          107 :     const m3d::scalar len_sq = m3d::length_sq(axis);
      57          107 :     if (len_sq < m3d::EPSILON * m3d::EPSILON)
      58           21 :         return true; // degenerate (parallel edges) → skip but don't separate
      59              : 
      60           86 :     const m3d::vec3   norm_axis = axis / m3d::sqrt(len_sq);
      61           86 :     const m3d::scalar rA   = obb_radius(a, tf_a, norm_axis);
      62           86 :     const m3d::scalar rB   = obb_radius(b, tf_b, norm_axis);
      63           86 :     const m3d::scalar dist = m3d::abs(m3d::dot(centre_diff, norm_axis));
      64           86 :     const m3d::scalar overlap = rA + rB - dist;
      65              : 
      66           86 :     if (overlap < 0.0)
      67            2 :         return false;
      68              : 
      69           84 :     if (overlap < best_depth)
      70              :     {
      71            9 :         best_depth   = overlap;
      72            9 :         best_normal  = (m3d::dot(centre_diff, norm_axis) >= 0.0)
      73            9 :                          ? norm_axis : -norm_axis;
      74            9 :         best_feature = feature_id;
      75              :     }
      76           84 :     return true;
      77              : }
      78              : 
      79              : // ---------------------------------------------------------------------------
      80              : //  Face helpers
      81              : // ---------------------------------------------------------------------------
      82              : 
      83              : // Return the 4 corners of the face on `box` (in world space) that is most
      84              : // aligned with `dir` (i.e. the face whose outward normal is closest to dir).
      85           14 : inline void get_best_face(const Box     &box,
      86              :                            const m3d::tf &tf,
      87              :                            const m3d::vec3 &dir,
      88              :                            m3d::vec3 out_corners[4],
      89              :                            m3d::vec3 &out_normal)
      90              : {
      91              :     // Local axes in world space
      92              :     const m3d::vec3 axes[3] = {
      93           14 :         tf.rotate_vector(m3d::vec3(1, 0, 0)),
      94           14 :         tf.rotate_vector(m3d::vec3(0, 1, 0)),
      95           14 :         tf.rotate_vector(m3d::vec3(0, 0, 1)),
      96           42 :     };
      97              :     const m3d::scalar half[3] = {
      98           14 :         box.half_extents.x,
      99           14 :         box.half_extents.y,
     100           14 :         box.half_extents.z,
     101           14 :     };
     102              : 
     103              :     // Find which face axis is most aligned with dir
     104           14 :     int   best_axis  = 0;
     105           14 :     m3d::scalar best_dot = m3d::abs(m3d::dot(axes[0], dir));
     106           42 :     for (int i = 1; i < 3; ++i)
     107              :     {
     108           28 :         m3d::scalar d = m3d::abs(m3d::dot(axes[i], dir));
     109           28 :         if (d > best_dot) { best_dot = d; best_axis = i; }
     110              :     }
     111              : 
     112              :     // Face normal (sign chosen so it points along dir)
     113           14 :     const m3d::scalar sign = (m3d::dot(axes[best_axis], dir) >= 0.0) ? 1.0 : -1.0;
     114           14 :     out_normal = axes[best_axis] * sign;
     115              : 
     116              :     // Face centre
     117           14 :     const m3d::vec3 face_centre = tf.pos + out_normal * half[best_axis];
     118              : 
     119              :     // The other two axes span the face
     120           14 :     const int u = (best_axis + 1) % 3;
     121           14 :     const int v = (best_axis + 2) % 3;
     122              : 
     123           14 :     out_corners[0] = face_centre + axes[u] * half[u] + axes[v] * half[v];
     124           14 :     out_corners[1] = face_centre - axes[u] * half[u] + axes[v] * half[v];
     125           14 :     out_corners[2] = face_centre - axes[u] * half[u] - axes[v] * half[v];
     126           14 :     out_corners[3] = face_centre + axes[u] * half[u] - axes[v] * half[v];
     127           14 : }
     128              : 
     129              : // ---------------------------------------------------------------------------
     130              : //  Sutherland-Hodgman clipping
     131              : // ---------------------------------------------------------------------------
     132              : 
     133              : // Clip polygon `poly` (size `n`) against a half-space defined by plane
     134              : // normal `plane_n` and a point on the plane `plane_p`.
     135              : // Points on the positive side of the plane are KEPT.
     136              : // Returns the number of output vertices (written into `out`).
     137           28 : inline int clip_polygon_by_plane(const m3d::vec3 *poly, int n,
     138              :                                   const m3d::vec3 &plane_n,
     139              :                                   const m3d::vec3 &plane_p,
     140              :                                   m3d::vec3       *out)
     141              : {
     142           28 :     int out_n = 0;
     143          200 :     for (int i = 0; i < n; ++i)
     144              :     {
     145          172 :         const m3d::vec3 &A = poly[i];
     146          172 :         const m3d::vec3 &B = poly[(i + 1) % n];
     147              : 
     148          172 :         const m3d::scalar dA = m3d::dot(A - plane_p, plane_n);
     149          172 :         const m3d::scalar dB = m3d::dot(B - plane_p, plane_n);
     150              : 
     151          172 :         if (dA >= 0.0)                      // A inside
     152          168 :             out[out_n++] = A;
     153              : 
     154          172 :         if ((dA > 0.0) != (dB > 0.0))      // edge crosses plane
     155              :         {
     156           44 :             const m3d::scalar t = dA / (dA - dB);
     157           44 :             out[out_n++] = A + (B - A) * t;
     158              :         }
     159              :     }
     160           28 :     return out_n;
     161              : }
     162              : 
     163              : // ---------------------------------------------------------------------------
     164              : //  Manifold point reduction
     165              : // ---------------------------------------------------------------------------
     166              : // Keep at most 4 contact points from `pts[n]`:
     167              : //   1. Deepest penetration point.
     168              : //   2. Farthest from point 1.
     169              : //   3. Farthest from line 1-2.
     170              : //   4. Farthest from triangle 1-2-3 (area maximisation).
     171              : // This gives a stable support polygon for the solver.
     172            7 : inline int reduce_to_4(const m3d::vec3      *pts,
     173              :                         const m3d::scalar    *depths,
     174              :                         int                   n,
     175              :                         m3d::vec3            *out_pts,
     176              :                         m3d::scalar          *out_depths)
     177              : {
     178            7 :     if (n <= 4)
     179              :     {
     180           10 :         for (int i = 0; i < n; ++i) { out_pts[i] = pts[i]; out_depths[i] = depths[i]; }
     181            2 :         return n;
     182              :     }
     183              : 
     184              :     // 1. Deepest point
     185            5 :     int i0 = 0;
     186           60 :     for (int i = 1; i < n; ++i)
     187           55 :         if (depths[i] > depths[i0]) i0 = i;
     188              : 
     189              :     // 2. Farthest from i0
     190            5 :     int i1 = (i0 == 0) ? 1 : 0;
     191            5 :     m3d::scalar best = m3d::length_sq(pts[i1] - pts[i0]);
     192           65 :     for (int i = 0; i < n; ++i)
     193              :     {
     194           60 :         if (i == i0) continue;
     195           55 :         m3d::scalar d = m3d::length_sq(pts[i] - pts[i0]);
     196           55 :         if (d > best) { best = d; i1 = i; }
     197              :     }
     198              : 
     199              :     // 3. Farthest from segment i0-i1
     200            5 :     int i2 = -1;
     201            5 :     best = 0.0;
     202            5 :     const m3d::vec3 seg = pts[i1] - pts[i0];
     203           65 :     for (int i = 0; i < n; ++i)
     204              :     {
     205           60 :         if (i == i0 || i == i1) continue;
     206           50 :         m3d::scalar d = m3d::length_sq(m3d::cross(pts[i] - pts[i0], seg));
     207           50 :         if (d > best) { best = d; i2 = i; }
     208              :     }
     209            5 :     if (i2 < 0) { i2 = 0; if (i2 == i0 || i2 == i1) i2 = 1; if (i2 == i0 || i2 == i1) i2 = 2; }
     210              : 
     211              :     // 4. Farthest from triangle i0-i1-i2 (signed area)
     212            5 :     int i3 = -1;
     213            5 :     best = 0.0;
     214            5 :     const m3d::vec3 tri_n = m3d::cross(pts[i1] - pts[i0], pts[i2] - pts[i0]);
     215           65 :     for (int i = 0; i < n; ++i)
     216              :     {
     217           60 :         if (i == i0 || i == i1 || i == i2) continue;
     218           45 :         m3d::scalar d = m3d::abs(m3d::dot(pts[i] - pts[i0], tri_n));
     219           45 :         if (d > best) { best = d; i3 = i; }
     220              :     }
     221           10 :     if (i3 < 0) { for (int i = 0; i < n; ++i) if (i!=i0&&i!=i1&&i!=i2){i3=i;break;} }
     222              : 
     223            5 :     out_pts[0] = pts[i0]; out_depths[0] = depths[i0];
     224            5 :     out_pts[1] = pts[i1]; out_depths[1] = depths[i1];
     225            5 :     out_pts[2] = pts[i2]; out_depths[2] = depths[i2];
     226            5 :     if (i3 >= 0) { out_pts[3] = pts[i3]; out_depths[3] = depths[i3]; return 4; }
     227            0 :     return 3;
     228              : }
     229              : 
     230              : } // namespace detail
     231              : 
     232              : // ===========================================================================
     233              : //  CollisionAlgorithm<Box, Box>
     234              : // ===========================================================================
     235              : template <>
     236              : struct CollisionAlgorithm<Box, Box>
     237              : {
     238            9 :     static bool test(const Box     &a, const m3d::tf &tf_a,
     239              :                      const Box     &b, const m3d::tf &tf_b,
     240              :                      ContactManifold &manifold)
     241              :     {
     242            9 :         const m3d::vec3 centre_diff = tf_b.pos - tf_a.pos;
     243              : 
     244              :         // World-space local axes of A and B
     245              :         const m3d::vec3 ax[3] = {
     246            9 :             tf_a.rotate_vector(m3d::vec3(1,0,0)),
     247            9 :             tf_a.rotate_vector(m3d::vec3(0,1,0)),
     248            9 :             tf_a.rotate_vector(m3d::vec3(0,0,1)),
     249           27 :         };
     250              :         const m3d::vec3 bx[3] = {
     251            9 :             tf_b.rotate_vector(m3d::vec3(1,0,0)),
     252            9 :             tf_b.rotate_vector(m3d::vec3(0,1,0)),
     253            9 :             tf_b.rotate_vector(m3d::vec3(0,0,1)),
     254           27 :         };
     255              : 
     256            9 :         m3d::scalar best_depth   = std::numeric_limits<m3d::scalar>::max();
     257            9 :         m3d::vec3   best_normal  = m3d::vec3(1, 0, 0);
     258            9 :         int         best_feature = 0; // 0-5 = face axes, 6-14 = edge-edge axes
     259              : 
     260            9 :         int feat = 0;
     261              : 
     262              :         // 3 face axes of A
     263           30 :         for (int i = 0; i < 3; ++i, ++feat)
     264           23 :             if (!detail::test_axis(ax[i], a, tf_a, b, tf_b,
     265              :                                    centre_diff, best_depth, best_normal,
     266              :                                    feat, best_feature))
     267            2 :                 return false;
     268              : 
     269              :         // 3 face axes of B
     270           28 :         for (int i = 0; i < 3; ++i, ++feat)
     271           21 :             if (!detail::test_axis(bx[i], a, tf_a, b, tf_b,
     272              :                                    centre_diff, best_depth, best_normal,
     273              :                                    feat, best_feature))
     274            0 :                 return false;
     275              : 
     276              :         // 9 edge cross-product axes
     277           28 :         for (int i = 0; i < 3; ++i)
     278           84 :             for (int j = 0; j < 3; ++j, ++feat)
     279           63 :                 if (!detail::test_axis(m3d::cross(ax[i], bx[j]),
     280              :                                        a, tf_a, b, tf_b,
     281              :                                        centre_diff, best_depth, best_normal,
     282              :                                        feat, best_feature))
     283            0 :                     return false;
     284              : 
     285              :         // ── All 15 axes passed — we have a collision ─────────────────────────
     286              : 
     287            7 :         manifold.normal     = best_normal;
     288            7 :         manifold.num_points = 0;
     289              : 
     290              :         // ── Face-face contact (features 0-5): use Sutherland-Hodgman clipping
     291            7 :         if (best_feature < 6)
     292              :         {
     293              :             // Reference face: on A, most aligned with the contact normal
     294              :             // Incident face: on B, most anti-aligned with the contact normal
     295           35 :             m3d::vec3 ref_corners[4], ref_normal;
     296           35 :             m3d::vec3 inc_corners[4], inc_normal;
     297              : 
     298            7 :             detail::get_best_face(a, tf_a,  best_normal, ref_corners, ref_normal);
     299            7 :             detail::get_best_face(b, tf_b, -best_normal, inc_corners, inc_normal);
     300              : 
     301              :             // Sutherland-Hodgman: clip incident face against 4 side planes of reference face
     302              :             // The 4 side planes are perpendicular to the reference face and pass through its edges.
     303              :             // side normal for edge (ref_corners[i] → ref_corners[(i+1)%4]) is
     304              :             //   cross(ref_normal, edge_dir)  (pointing inward)
     305              : 
     306              :             // Working buffers (max 8 vertices after each clip)
     307          231 :             m3d::vec3 buf0[16], buf1[16];
     308            7 :             int       cnt = 4;
     309           35 :             for (int i = 0; i < 4; ++i) buf0[i] = inc_corners[i];
     310              : 
     311           35 :             for (int i = 0; i < 4; ++i)
     312              :             {
     313           28 :                 const m3d::vec3 edge   = ref_corners[(i+1)%4] - ref_corners[i];
     314           28 :                 const m3d::vec3 side_n = m3d::normalize(m3d::cross(ref_normal, edge)); // inward
     315           28 :                 cnt = detail::clip_polygon_by_plane(buf0, cnt, side_n, ref_corners[i], buf1);
     316           28 :                 if (cnt == 0) break;
     317          240 :                 for (int k = 0; k < cnt; ++k) buf0[k] = buf1[k];
     318              :             }
     319              : 
     320            7 :             if (cnt == 0) return false; // clipped away entirely
     321              : 
     322              :             // Keep only points that penetrate the reference face plane
     323              :             // (i.e. on the negative side of ref_normal through ref_corners[0])
     324            7 :             const m3d::scalar ref_d = m3d::dot(ref_corners[0], ref_normal);
     325              : 
     326          119 :             m3d::vec3   keep_pts[16];
     327              :             m3d::scalar keep_dep[16];
     328            7 :             int         keep_n = 0;
     329              : 
     330           75 :             for (int i = 0; i < cnt; ++i)
     331              :             {
     332           68 :                 const m3d::scalar signed_dist = m3d::dot(buf0[i], ref_normal) - ref_d;
     333           68 :                 if (signed_dist <= m3d::EPSILON) // on or below the reference face
     334              :                 {
     335           68 :                     keep_pts[keep_n] = buf0[i];
     336           68 :                     keep_dep[keep_n] = -signed_dist + best_depth * 0.0; // penetration ≈ depth of this point
     337              :                     // More precise per-point depth: project along normal onto box surfaces
     338           68 :                     keep_dep[keep_n] = m3d::abs(signed_dist) > 0.0
     339           68 :                                          ? m3d::abs(signed_dist)
     340              :                                          : best_depth;
     341           68 :                     ++keep_n;
     342              :                 }
     343              :             }
     344              : 
     345            7 :             if (keep_n == 0)
     346              :             {
     347              :                 // Fall back: single point at the box-contact surface midpoint
     348            0 :                 manifold.num_points             = 1;
     349            0 :                 manifold.points[0].penetration_depth = best_depth;
     350              :                 manifold.points[0].position     = tf_a.pos
     351            0 :                     + tf_a.rotate_vector(m3d::vec3(0,0,0)) // placeholder
     352            0 :                     - best_normal * (best_depth * 0.5);
     353            0 :                 return true;
     354              :             }
     355              : 
     356              :             // Reduce to ≤4 contact points
     357           35 :             m3d::vec3   red_pts[4];
     358              :             m3d::scalar red_dep[4];
     359            7 :             int red_n = detail::reduce_to_4(keep_pts, keep_dep, keep_n, red_pts, red_dep);
     360              : 
     361            7 :             manifold.num_points = red_n;
     362           35 :             for (int i = 0; i < red_n; ++i)
     363              :             {
     364           28 :                 manifold.points[i].position          = red_pts[i];
     365           28 :                 manifold.points[i].penetration_depth = best_depth; // uniform for face-face
     366              :             }
     367              :         }
     368              :         else
     369              :         {
     370              :             // ── Edge-edge contact (features 6-14): single contact point ──────
     371              :             // For edge-edge the closest points on both edges form the contact.
     372              :             // We compute them via the standard segment-closest-point formula.
     373              : 
     374            0 :             const int ei = (best_feature - 6) / 3; // edge axis index on A (0,1,2)
     375            0 :             const int ej = (best_feature - 6) % 3; // edge axis index on B (0,1,2)
     376              : 
     377              :             // Support points on each box along the edge direction
     378            0 :             const m3d::vec3 pa = tf_a.pos + tf_a.rotate_vector(
     379            0 :                 m3d::vec3(
     380            0 :                     (m3d::dot(ax[0], best_normal) >= 0 ? 1.0 : -1.0) * (ei==0 ? 0.0 : a.half_extents.x),
     381            0 :                     (m3d::dot(ax[1], best_normal) >= 0 ? 1.0 : -1.0) * (ei==1 ? 0.0 : a.half_extents.y),
     382            0 :                     (m3d::dot(ax[2], best_normal) >= 0 ? 1.0 : -1.0) * (ei==2 ? 0.0 : a.half_extents.z)
     383            0 :                 ));
     384            0 :             const m3d::vec3 pb = tf_b.pos + tf_b.rotate_vector(
     385            0 :                 m3d::vec3(
     386            0 :                     (m3d::dot(bx[0],-best_normal) >= 0 ? 1.0 : -1.0) * (ej==0 ? 0.0 : b.half_extents.x),
     387            0 :                     (m3d::dot(bx[1],-best_normal) >= 0 ? 1.0 : -1.0) * (ej==1 ? 0.0 : b.half_extents.y),
     388            0 :                     (m3d::dot(bx[2],-best_normal) >= 0 ? 1.0 : -1.0) * (ej==2 ? 0.0 : b.half_extents.z)
     389            0 :                 ));
     390              : 
     391              :             // Closest point between the two edges (midpoint approximation)
     392            0 :             const m3d::vec3 contact_pt = (pa + pb) * 0.5;
     393              : 
     394            0 :             manifold.num_points                  = 1;
     395            0 :             manifold.points[0].position          = contact_pt;
     396            0 :             manifold.points[0].penetration_depth = best_depth;
     397              :         }
     398              : 
     399            7 :         return true;
     400              :     }
     401              : };
     402              : 
     403              : } // namespace rbc
        

Generated by: LCOV version 2.0-1