spherecast

wip first person shooter engine
Download | Log | Files | Refs

physics.c (7997B)


      1 #include <float.h>
      2 #include <raylib.h>
      3 
      4 #include "config.h"
      5 #include "physics.h"
      6 #include "player.h"
      7 
      8 #define SQR(x) ((x) * (x))
      9 
     10 /*
     11  * Find closest point on a line segment [a, b] to point p
     12  */
     13 Vector3 ClosestPointOnSegment(Vector3 p, Vector3 a, Vector3 b) {
     14   Vector3 ab = Vector3Subtract(b, a);
     15   float t = Vector3DotProduct(Vector3Subtract(p, a), ab);
     16   float lenSq = Vector3LengthSqr(ab);
     17 
     18   /* Validate length to avoid division by zero */
     19   if (lenSq < EPSILON)
     20     return a;
     21 
     22   t = t / lenSq;
     23 
     24   /* Clamp t to [0, 1] */
     25   t = fmaxf(0.0f, fminf(1.0f, t));
     26 
     27   return Vector3Add(a, Vector3Scale(ab, t));
     28 }
     29 
     30 /*
     31  * Check for if capsule is within bounds of triangle t
     32  */
     33 static inline bool TriangleLikelyContains(Vector3 pos, float height, float r,
     34                                           const Triangle *t) {
     35   float r2 = r + SKIN;
     36 
     37   /*
     38    * AABB check: Triangle must overlap the AABB of the capsule
     39    * Capsule Y Range: [pos.y - r, pos.y + height + r]
     40    */
     41   float minX = fminf(fminf(t->a.x, t->b.x), t->c.x);
     42   float maxX = fmaxf(fmaxf(t->a.x, t->b.x), t->c.x);
     43   if (pos.x + r2 < minX || pos.x - r2 > maxX)
     44     return false;
     45 
     46   float minZ = fminf(fminf(t->a.z, t->b.z), t->c.z);
     47   float maxZ = fmaxf(fmaxf(t->a.z, t->b.z), t->c.z);
     48   if (pos.z + r2 < minZ || pos.z - r2 > maxZ)
     49     return false;
     50 
     51   float minY = fminf(fminf(t->a.y, t->b.y), t->c.y);
     52   float maxY = fmaxf(fmaxf(t->a.y, t->b.y), t->c.y);
     53 
     54   /* Check capsule bottom vs tri top and capsule top vs tri bottom */
     55   if ((pos.y + height + r2) < minY)
     56     return false;
     57   if ((pos.y - r2) > maxY)
     58     return false;
     59 
     60   return true;
     61 }
     62 
     63 Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) {
     64   Vector3 ab = Vector3Subtract(b, a);
     65   Vector3 ac = Vector3Subtract(c, a);
     66   Vector3 ap = Vector3Subtract(p, a);
     67   float d1 = Vector3DotProduct(ab, ap);
     68   float d2 = Vector3DotProduct(ac, ap);
     69   if (d1 <= 0.0f && d2 <= 0.0f)
     70     return a;
     71 
     72   Vector3 bp = Vector3Subtract(p, b);
     73   float d3 = Vector3DotProduct(ab, bp);
     74   float d4 = Vector3DotProduct(ac, bp);
     75   if (d3 >= 0.0f && d4 <= d3)
     76     return b;
     77 
     78   float vc = d1 * d4 - d3 * d2;
     79   if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f)
     80     return Vector3Add(a, Vector3Scale(ab, d1 / (d1 - d3)));
     81 
     82   Vector3 cp = Vector3Subtract(p, c);
     83   float d5 = Vector3DotProduct(ab, cp);
     84   float d6 = Vector3DotProduct(ac, cp);
     85   if (d6 >= 0.0f && d5 <= d6)
     86     return c;
     87 
     88   float vb = d5 * d2 - d1 * d6;
     89   if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f)
     90     return Vector3Add(a, Vector3Scale(ac, d2 / (d2 - d6)));
     91 
     92   float va = d3 * d6 - d5 * d4;
     93   if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f)
     94     return Vector3Add(b, Vector3Scale(Vector3Subtract(c, b),
     95                                       (d4 - d3) / ((d4 - d3) + (d5 - d6))));
     96 
     97   float denom = 1.0f / (va + vb + vc);
     98   return Vector3Add(a, Vector3Add(Vector3Scale(ab, vb * denom),
     99                                   Vector3Scale(ac, vc * denom)));
    100 }
    101 
    102 bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist,
    103                         const Triangle *tri, HitInfo *hit) {
    104   Vector3 n = tri->normal;
    105   float denom = Vector3DotProduct(n, dir);
    106   if (denom >= -EPSILON)
    107     return false; /* Backface culling */
    108 
    109   float distToPlane = Vector3DotProduct(n, Vector3Subtract(tri->a, pos));
    110   float tPlane = (distToPlane + radius) / denom;
    111 
    112   if (tPlane > maxDist)
    113     return false;
    114 
    115   Vector3 planeIntersectionPoint = Vector3Add(pos, Vector3Scale(dir, tPlane));
    116   Vector3 centerOnPlane =
    117       Vector3Subtract(planeIntersectionPoint, Vector3Scale(n, radius));
    118   Vector3 closestPt =
    119       ClosestPointOnTriangle(centerOnPlane, tri->a, tri->b, tri->c);
    120   float distSq = Vector3DistanceSqr(centerOnPlane, closestPt);
    121 
    122   /* CASE A: Hit the FACE */
    123   if (distSq < EPSILON) {
    124     if (tPlane < 0)
    125       return false;
    126     hit->hit = true;
    127     hit->distance = tPlane;
    128     hit->normal = n;
    129     return true;
    130   }
    131 
    132   /* CASE B: Hit Edge/Vertex */
    133   Vector3 L = Vector3Subtract(pos, closestPt);
    134   float a = 1.0f;
    135   float b = 2.0f * Vector3DotProduct(L, dir);
    136   float c = Vector3DotProduct(L, L) - SQR(radius);
    137 
    138   float delta = b * b - 4 * a * c;
    139 
    140   if (delta >= 0.0f) {
    141     float tEdge = (-b - sqrtf(delta)) / 2.0f;
    142     if (tEdge >= 0 && tEdge < maxDist) {
    143       hit->hit = true;
    144       hit->distance = tEdge;
    145       Vector3 hitPos = Vector3Add(pos, Vector3Scale(dir, tEdge));
    146       hit->normal = Vector3Normalize(Vector3Subtract(hitPos, closestPt));
    147       return true;
    148     }
    149   }
    150   return false;
    151 }
    152 
    153 bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius,
    154                 Triangle *tris, int triCount, HitInfo *outHit) {
    155   HitInfo best = {0};
    156   best.distance = dist;
    157   bool found = false;
    158 
    159   for (int i = 0; i < triCount; i++) {
    160     HitInfo h;
    161     if (SphereCastTriangle(pos, dir, radius, best.distance, &tris[i], &h)) {
    162       best = h;
    163       found = true;
    164     }
    165   }
    166 
    167   if (!found)
    168     return false;
    169   *outHit = best;
    170   return true;
    171 }
    172 
    173 /*
    174  * Resolves capsule overlaps. Pos is the center of the BOTTOM sphere.
    175  */
    176 void ResolveCollisions(Player *p, Triangle *tris, int count) {
    177   float rSq = SQR(p->radius);
    178   Vector3 capsTopOffset = {0, p->height, 0};
    179 
    180   HitInfo hit;
    181   if (p->mode != MOVE_SLIDE && p->vel.y <= 0 &&
    182       SphereCast(p->pos, DOWN, GROUND_SNAP_DIST, p->radius, tris, count,
    183                  &hit)) {
    184     p->pos = Vector3Add(p->pos, Vector3Scale(DOWN, hit.distance));
    185     p->grounded = true;
    186   }
    187 
    188   /* Four iterations should be max: ceil, floor, wall1, wall2 */
    189   for (int iter = 0; iter < 4; iter++) {
    190     bool collisionFound = false;
    191 
    192     Vector3 pBottom = p->pos;
    193     Vector3 pTop = Vector3Add(pBottom, capsTopOffset);
    194     Vector3 midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f);
    195 
    196     for (int i = 0; i < count; i++) {
    197       Triangle *t = &tris[i];
    198 
    199       Vector3 closestOnTri = ClosestPointOnTriangle(midPoint, t->a, t->b, t->c);
    200       Vector3 closestOnSegment =
    201           ClosestPointOnSegment(closestOnTri, pBottom, pTop);
    202 
    203       // Re-refine
    204       closestOnTri = ClosestPointOnTriangle(closestOnSegment, t->a, t->b, t->c);
    205 
    206       Vector3 pushVec = Vector3Subtract(closestOnSegment, closestOnTri);
    207       float d2 = Vector3LengthSqr(pushVec);
    208 
    209       /* Early exit if no collision or invalid math */
    210       if (d2 <= EPSILON || d2 >= rSq)
    211         continue;
    212 
    213       float d = sqrtf(d2);
    214       Vector3 n = Vector3Scale(pushVec, 1.0f / d);
    215 
    216       /* Add small skin width to prevent floating point sinking */
    217       float penetration = p->radius - d + SKIN;
    218 
    219       /* Check if the surface is "Walkable" (Flat floor or manageable slope) */
    220       if (n.y > GROUND_NORMAL_Y) {
    221         /*
    222          * If we are on walkable ground, we want to push the player STRICTLY UP.
    223          * hypotenuse = adjacent / cos(theta)
    224          * vertical_move = penetration / n.y
    225          */
    226 
    227         p->grounded = true;
    228         float verticalPush = penetration / n.y;
    229         p->pos.y += verticalPush;
    230 
    231       } else {
    232         /*
    233          * If it is a steep slope, wall, or ceiling, push along the normal
    234          * to prevent the player from walking up walls.
    235          */
    236         p->pos = Vector3Add(p->pos, Vector3Scale(n, penetration));
    237       }
    238 
    239       pBottom = p->pos;
    240       pTop = Vector3Add(pBottom, capsTopOffset);
    241       midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f);
    242 
    243       float vn = Vector3DotProduct(p->vel, n);
    244 
    245       /* Only act if checking velocity moving INTO the wall/floor */
    246       if (vn < 0.0f) {
    247 
    248         if (n.y > GROUND_NORMAL_Y && p->mode != MOVE_SLIDE) {
    249           p->groundedTimer = COYOTE_TIME;
    250 
    251           /*
    252            * To prevent sliding on slopes, we zero out the Y velocity.
    253            * This stops gravity from accumulating and sliding you down.
    254            */
    255           p->vel.y = 0.0f;
    256           p->grounded = true;
    257         } else {
    258           /* Remove the velocity moving into the wall (Slide along wall) */
    259           p->vel = Vector3Subtract(p->vel, Vector3Scale(n, vn));
    260         }
    261       }
    262 
    263       collisionFound = true;
    264     }
    265 
    266     if (!collisionFound)
    267       break;
    268   }
    269 }