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 }