spherecast

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

commit cfcbdd934672703ce6e2106610cb45a22a108496
parent 2a172e5f13f6c512a989033eadf080139e738ab2
Author: amrfti <andrew@kloet.net>
Date:   Sun, 28 Dec 2025 16:44:57 -0500

change player controller from sphere to capsule

Diffstat:
MMakefile | 1+
Mconfig.h | 10++++------
Mmain.c | 7+++----
Mphysics.c | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mphysics.h | 8++++----
5 files changed, 97 insertions(+), 54 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,5 +1,6 @@ CC = cc CFLAGS = -Wall -Wextra -pedantic -std=c99 -O3 +CFLAGS += -D_POSIX_C_SOURCE=200809L LDFLAGS = -lraylib -lm SRC = main.c physics.c diff --git a/config.h b/config.h @@ -8,7 +8,7 @@ // physics #define MAX_SLIDES 4 #define SKIN 0.001f -#define GRAVITY 45.0f +#define GRAVITY 40.0f #define MAX_GROUND_ANGLE_DEG 50.0f #define GROUND_NORMAL_Y (cosf(MAX_GROUND_ANGLE_DEG * DEG2RAD)) #define DOWN (Vector3){0, -1, 0} @@ -17,14 +17,12 @@ #define COYOTE_TIME 0.1f // movement -#define STEP_HEIGHT 0.4f -#define STEP_ITERATIONS 2 #define PLAYER_RADIUS 0.3f +#define PLAYER_HEIGHT 0.8f #define WALK_SPEED 4.0f -#define SPRINT_SPEED 7.0f +#define SPRINT_SPEED 6.0f #define JUMP_FORCE 10.0f -#define ACCELERATION 10.0f -#define FRICTION 10.0f +#define ACCELERATION 8.0f #define AIR_ACCELERATION ACCELERATION // could change control in air // camera diff --git a/main.c b/main.c @@ -69,7 +69,6 @@ int main(void) { Vector3 playerPos = {0, 0, 0}; Vector3 velocity = {0}; - float radius = PLAYER_RADIUS; bool grounded = false; while (!WindowShouldClose()) { @@ -123,10 +122,10 @@ int main(void) { velocity = (Vector3){0}; } - Physics_MoveCharacter(&playerPos, &velocity, dt, radius, triangles, - triCount, &grounded); + Physics_MoveCharacter(&playerPos, &velocity, dt, PLAYER_RADIUS, + PLAYER_HEIGHT, triangles, triCount, &grounded); - Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, radius * 2.0f, 0}); + Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, PLAYER_HEIGHT, 0}); camera.position = eyePos; camera.target = Vector3Add(eyePos, forward); diff --git a/physics.c b/physics.c @@ -6,23 +6,53 @@ #define SQR(x) ((x) * (x)) /* - * Check for if point p is within the bounds of triangle t with radius r + * Find closest point on a line segment [a, b] to point p */ -static inline bool TriangleLikelyContains(Vector3 p, float r, +Vector3 ClosestPointOnSegment(Vector3 p, Vector3 a, Vector3 b) { + Vector3 ab = Vector3Subtract(b, a); + float t = Vector3DotProduct(Vector3Subtract(p, a), ab); + float lenSq = Vector3LengthSqr(ab); + + /* Validate length to avoid division by zero */ + if (lenSq < EPSILON) + return a; + + t = t / lenSq; + + /* Clamp t to [0, 1] */ + t = fmaxf(0.0f, fminf(1.0f, t)); + + return Vector3Add(a, Vector3Scale(ab, t)); +} + +/* + * Check for if capsule is within bounds of triangle t + */ +static inline bool TriangleLikelyContains(Vector3 pos, float height, float r, const Triangle *t) { float r2 = r + SKIN; - if (p.x + r2 < fminf(fminf(t->a.x, t->b.x), t->c.x)) + /* + * AABB check: Triangle must overlap the AABB of the capsule + * Capsule Y Range: [pos.y - r, pos.y + height + r] + */ + float minX = fminf(fminf(t->a.x, t->b.x), t->c.x); + float maxX = fmaxf(fmaxf(t->a.x, t->b.x), t->c.x); + if (pos.x + r2 < minX || pos.x - r2 > maxX) return false; - if (p.x - r2 > fmaxf(fmaxf(t->a.x, t->b.x), t->c.x)) - return false; - if (p.z + r2 < fminf(fminf(t->a.z, t->b.z), t->c.z)) - return false; - if (p.z - r2 > fmaxf(fmaxf(t->a.z, t->b.z), t->c.z)) + + float minZ = fminf(fminf(t->a.z, t->b.z), t->c.z); + float maxZ = fmaxf(fmaxf(t->a.z, t->b.z), t->c.z); + if (pos.z + r2 < minZ || pos.z - r2 > maxZ) return false; - if (p.y + r2 < fminf(fminf(t->a.y, t->b.y), t->c.y)) + + float minY = fminf(fminf(t->a.y, t->b.y), t->c.y); + float maxY = fmaxf(fmaxf(t->a.y, t->b.y), t->c.y); + + /* Check capsule bottom vs tri top and capsule top vs tri bottom */ + if ((pos.y + height + r2) < minY) return false; - if (p.y - r2 > fmaxf(fmaxf(t->a.y, t->b.y), t->c.y)) + if ((pos.y - r2) > maxY) return false; return true; @@ -70,12 +100,9 @@ Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) { bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist, const Triangle *tri, HitInfo *hit) { Vector3 n = tri->normal; - - // intersect with the infinite plane float denom = Vector3DotProduct(n, dir); - if (denom >= -EPSILON) - return false; + return false; /* Backface culling */ float distToPlane = Vector3DotProduct(n, Vector3Subtract(tri->a, pos)); float tPlane = (distToPlane + radius) / denom; @@ -83,31 +110,24 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist, if (tPlane > maxDist) return false; - // determine where the sphere touches the plane Vector3 planeIntersectionPoint = Vector3Add(pos, Vector3Scale(dir, tPlane)); Vector3 centerOnPlane = Vector3Subtract(planeIntersectionPoint, Vector3Scale(n, radius)); - - // find closest point on the actual triangle Vector3 closestPt = ClosestPointOnTriangle(centerOnPlane, tri->a, tri->b, tri->c); - - // check distance float distSq = Vector3DistanceSqr(centerOnPlane, closestPt); - // CASE A: Hit the FACE + /* CASE A: Hit the FACE */ if (distSq < EPSILON) { if (tPlane < 0) return false; - hit->hit = true; hit->distance = tPlane; hit->normal = n; return true; } - // CASE B: Sweep against Edge/Vertex - // Equation: |(pos + dir * t) - closestPt|^2 = radius^2 + /* CASE B: Hit Edge/Vertex */ Vector3 L = Vector3Subtract(pos, closestPt); float a = 1.0f; float b = 2.0f * Vector3DotProduct(L, dir); @@ -116,19 +136,15 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist, float delta = b * b - 4 * a * c; if (delta >= 0.0f) { - float sqrtDelta = sqrtf(delta); - float tEdge = (-b - sqrtDelta) / 2.0f; - + float tEdge = (-b - sqrtf(delta)) / 2.0f; if (tEdge >= 0 && tEdge < maxDist) { hit->hit = true; hit->distance = tEdge; - Vector3 hitPos = Vector3Add(pos, Vector3Scale(dir, tEdge)); hit->normal = Vector3Normalize(Vector3Subtract(hitPos, closestPt)); return true; } } - return false; } @@ -148,18 +164,26 @@ bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius, if (!found) return false; - *outHit = best; return true; } -void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount, - bool *hitFloor) { +/* + * Resolves capsule overlaps. Pos is the center of the BOTTOM sphere. + */ +void ResolveOverlaps(Vector3 *pos, float height, float radius, Triangle *tris, + int triCount, bool *hitFloor) { float rSq = SQR(radius); + /* Define capsule axis relative to pos */ + Vector3 capsTopOffset = {0, height, 0}; for (int iter = 0; iter < 2; iter++) { bool collisionFound = false; + /* Calculate current capsule world positions */ + Vector3 pBottom = *pos; + Vector3 pTop = Vector3Add(pBottom, capsTopOffset); + for (int i = 0; i < triCount; i++) { Triangle *t = &tris[i]; @@ -168,11 +192,29 @@ void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount, * within the bounding box of triangle t then skip it * TODO: replace with O(1) BVH */ - if (!TriangleLikelyContains(*pos, radius, t)) + if (!TriangleLikelyContains(*pos, height, radius, t)) continue; - Vector3 closest = ClosestPointOnTriangle(*pos, t->a, t->b, t->c); - Vector3 pushVec = Vector3Subtract(*pos, closest); + /* + * Find the point on the triangle closest to the capsule's central + * segment Heuristic: Check closest point from the capsule's true center + */ + Vector3 midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f); + Vector3 closestOnTri = ClosestPointOnTriangle(midPoint, t->a, t->b, t->c); + + /* Find the point on the capsule segment closest to that triangle point */ + Vector3 closestOnSegment = + ClosestPointOnSegment(closestOnTri, pBottom, pTop); + + /* + * Re-refine the point on the triangle based on the specific segment + * point This helps when the capsule is tall and the triangle is near the + * feet or head + */ + closestOnTri = ClosestPointOnTriangle(closestOnSegment, t->a, t->b, t->c); + + /* Calculate push */ + Vector3 pushVec = Vector3Subtract(closestOnSegment, closestOnTri); float d2 = Vector3LengthSqr(pushVec); if (d2 <= 0 || d2 >= rSq + EPSILON) @@ -192,8 +234,11 @@ void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount, if (n.y > GROUND_NORMAL_Y) { pos->y += penetration / n.y; *hitFloor = true; - } else + } else { *pos = Vector3Add(*pos, Vector3Scale(n, penetration)); + pBottom = *pos; + pTop = Vector3Add(pBottom, capsTopOffset); + } collisionFound = true; } @@ -212,29 +257,29 @@ void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount, HitInfo hit; if (!SphereCast(*pos, DOWN, GROUND_SNAP_DIST, radius, tris, triCount, &hit)) return; + if (hit.normal.y < GROUND_NORMAL_Y) return; + *pos = Vector3Add(*pos, Vector3Scale(DOWN, hit.distance)); *touchingGround = true; } void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt, - float radius, Triangle *tris, int triCount, - bool *groundedOut) { + float radius, float height, Triangle *tris, + int triCount, bool *groundedOut) { static float groundedTimer = 0.0f; Vector3 moveStep = Vector3Scale(*velocity, dt); *pos = Vector3Add(*pos, moveStep); groundedTimer -= dt; - if (groundedTimer < 0) groundedTimer = 0; - bool effectivelyGrounded = (groundedTimer > 0); bool touchingGround = false; - ResolveOverlaps(pos, radius, tris, triCount, &touchingGround); + ResolveOverlaps(pos, height, radius, tris, triCount, &touchingGround); if (effectivelyGrounded && velocity->y <= 0) { SnapToGround(pos, radius, tris, triCount, &touchingGround); diff --git a/physics.h b/physics.h @@ -25,8 +25,8 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float dist, bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius, Triangle *tris, int triCount, HitInfo *outHit); -void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount, - bool *hitFloor); +void ResolveOverlaps(Vector3 *pos, float radius, float height, Triangle *tris, + int triCount, bool *hitFloor); bool CheckGrounded(Vector3 pos, float radius, Triangle *tris, int triCount); @@ -34,5 +34,5 @@ void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount, bool *hitFloor); void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt, - float radius, Triangle *tris, int triCount, - bool *groundedOut); + float radius, float height, Triangle *tris, + int triCount, bool *groundedOut);