commit 0c1d771d0aac601d906647dee11e80b5c0c5b9d4
Author: amrfti <andrew@kloet.net>
Date: Sun, 28 Dec 2025 11:48:54 -0500
initial commit
Diffstat:
| A | Makefile | | | 24 | ++++++++++++++++++++++++ |
| A | assets/level.glb | | | 0 | |
| A | assets/model.glb | | | 0 | |
| A | config.h | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
| A | main.c | | | 184 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | physics.c | | | 232 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | physics.h | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
7 files changed, 516 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,24 @@
+CC = cc
+CFLAGS = -Wall -Wextra -pedantic -std=c99 -O3
+LDFLAGS = -lraylib -lm
+
+SRC = main.c physics.c
+OBJ = $(SRC:.c=.o)
+
+EXE = main
+
+all: $(EXE)
+
+$(EXE): $(OBJ)
+ $(CC) $(OBJ) -o $(EXE) $(LDFLAGS)
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+ rm -f $(OBJ) $(EXE)
+
+run: $(EXE)
+ ./$(EXE)
+
+.PHONY: all clean run
diff --git a/assets/level.glb b/assets/level.glb
Binary files differ.
diff --git a/assets/model.glb b/assets/model.glb
Binary files differ.
diff --git a/config.h b/config.h
@@ -0,0 +1,38 @@
+#pragma once
+
+// window
+#define WIDTH 1000
+#define HEIGHT 800
+#define TARGET_FPS 144
+
+// physics
+#define MAX_SLIDES 4
+#define SKIN 0.001f
+#define GRAVITY 45.0f
+#define MAX_GROUND_ANGLE_DEG 50.0f
+#define GROUND_NORMAL_Y (cosf(MAX_GROUND_ANGLE_DEG * DEG2RAD))
+#define DOWN (Vector3){0, -1, 0}
+#define UP (Vector3){0, 1, 0}
+#define GROUND_SNAP_DIST 0.5f
+#define COYOTE_TIME 0.1f
+
+// movement
+#define STEP_HEIGHT 0.4f
+#define STEP_ITERATIONS 2
+#define PLAYER_RADIUS 0.3f
+#define WALK_SPEED 4.0f
+#define SPRINT_SPEED 7.0f
+#define JUMP_FORCE 10.0f
+#define ACCELERATION 10.0f
+#define FRICTION 10.0f
+#define AIR_ACCELERATION ACCELERATION // could change control in air
+
+// camera
+#define BASE_FOV 70.0f
+#define SPRINT_FOV 80.0f
+#define MAX_PITCH 1.5f
+#define FOV_LERP_SPEED 30.0f
+#define ROLL_FACTOR 5.0f
+#define ROLL_LERP_SPEED 20.0f
+#define MOUSE_SENSITIVITY 0.003f
+#define SPRINT_FOV_LERP_SPEED 10.0f
diff --git a/main.c b/main.c
@@ -0,0 +1,184 @@
+#include <float.h>
+#include <math.h>
+#include <raylib.h>
+#include <raymath.h>
+#include <stdlib.h>
+
+#include "config.h"
+#include "physics.h"
+
+#define UNGROUND_VELOCITY_EPSILON 0.5f
+
+float camYaw = 0.0f;
+float camPitch = 0.0f;
+float camRoll = 0.0f;
+
+Triangle *BuildTriangleCache(Model *model, int *outCount) {
+ int total = 0;
+ for (int m = 0; m < model->meshCount; m++)
+ total += model->meshes[m].triangleCount;
+
+ Triangle *tris = malloc(sizeof(Triangle) * total);
+ int t = 0;
+
+ for (int m = 0; m < model->meshCount; m++) {
+ Mesh mesh = model->meshes[m];
+ float *v = mesh.vertices;
+ unsigned short *idx = mesh.indices;
+
+ for (int i = 0; i < mesh.triangleCount; i++) {
+ Vector3 a = {v[idx[i * 3 + 0] * 3 + 0], v[idx[i * 3 + 0] * 3 + 1],
+ v[idx[i * 3 + 0] * 3 + 2]};
+ Vector3 b = {v[idx[i * 3 + 1] * 3 + 0], v[idx[i * 3 + 1] * 3 + 1],
+ v[idx[i * 3 + 1] * 3 + 2]};
+ Vector3 c = {v[idx[i * 3 + 2] * 3 + 0], v[idx[i * 3 + 2] * 3 + 1],
+ v[idx[i * 3 + 2] * 3 + 2]};
+
+ Vector3 normal = Vector3Normalize(
+ Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(c, a)));
+
+ Vector3 min = {fminf(a.x, fminf(b.x, c.x)), fminf(a.y, fminf(b.y, c.y)),
+ fminf(a.z, fminf(b.z, c.z))};
+ Vector3 max = {fmaxf(a.x, fmaxf(b.x, c.x)), fmaxf(a.y, fmaxf(b.y, c.y)),
+ fmaxf(a.z, fmaxf(b.z, c.z))};
+
+ tris[t++] = (Triangle){a, b, c, normal, min, max};
+ }
+ }
+
+ *outCount = total;
+ return tris;
+}
+
+int main(void) {
+ InitWindow(WIDTH, HEIGHT, "Sphere Character Controller");
+ DisableCursor();
+ SetTargetFPS(TARGET_FPS);
+ SetConfigFlags(FLAG_VSYNC_HINT);
+
+ Camera3D camera = {.position = {0, 0, 0},
+ .target = {0, 0, 1},
+ .up = UP,
+ .fovy = BASE_FOV,
+ .projection = CAMERA_PERSPECTIVE};
+
+ Model model = LoadModel("assets/level.glb");
+
+ int triCount = 0;
+ Triangle *triangles = BuildTriangleCache(&model, &triCount);
+
+ Vector3 playerPos = {0, 0, 0};
+ Vector3 velocity = {0};
+ float radius = PLAYER_RADIUS;
+ bool grounded = false;
+
+ while (!WindowShouldClose()) {
+ float dt = GetFrameTime();
+
+ Vector2 mouseDelta = GetMouseDelta();
+ camYaw -= mouseDelta.x * MOUSE_SENSITIVITY;
+ camPitch -= mouseDelta.y * MOUSE_SENSITIVITY;
+ camPitch = Clamp(camPitch, -MAX_PITCH, MAX_PITCH);
+
+ Vector3 forward = {cosf(camPitch) * sinf(camYaw), sinf(camPitch),
+ cosf(camPitch) * cosf(camYaw)};
+
+ Vector3 right = {sinf(camYaw - PI / 2.0f), 0.0f, cosf(camYaw - PI / 2.0f)};
+
+ Vector3 input = {0};
+ if (IsKeyDown(KEY_W))
+ input.z += 1;
+ if (IsKeyDown(KEY_S))
+ input.z -= 1;
+ if (IsKeyDown(KEY_D))
+ input.x += 1;
+ if (IsKeyDown(KEY_A))
+ input.x -= 1;
+
+ bool sprinting = IsKeyDown(KEY_LEFT_SHIFT);
+ float speed = sprinting ? SPRINT_SPEED : WALK_SPEED;
+
+ Vector3 targetVel = {0};
+
+ if (Vector3Length(input) > 0) {
+ input = Vector3Normalize(input);
+ Vector3 wishDir = Vector3Add(Vector3Scale(forward, input.z),
+ Vector3Scale(right, input.x));
+ wishDir.y = 0;
+ wishDir = Vector3Normalize(wishDir);
+
+ targetVel.x = wishDir.x * speed;
+ targetVel.z = wishDir.z * speed;
+ }
+
+ velocity.x = targetVel.x;
+ velocity.z = targetVel.z;
+
+ if (grounded && IsKeyPressed(KEY_SPACE))
+ velocity.y = JUMP_FORCE;
+
+ if (IsKeyPressed(KEY_R)) {
+ playerPos = (Vector3){0, 0, 0};
+ velocity = (Vector3){0};
+ }
+
+ Physics_MoveCharacter(&playerPos, &velocity, dt, radius, triangles,
+ triCount, &grounded);
+
+ Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, radius * 2.0f, 0});
+
+ camera.position = eyePos;
+ camera.target = Vector3Add(eyePos, forward);
+
+ Vector3 forwardDir =
+ Vector3Normalize(Vector3Subtract(camera.target, camera.position));
+ Vector3 rightDir =
+ Vector3Normalize(Vector3CrossProduct(forwardDir, (Vector3){0, 1, 0}));
+
+ float lateralSpeed = Vector3DotProduct(velocity, rightDir);
+ float targetRoll = lateralSpeed * ROLL_FACTOR / SPRINT_SPEED;
+ camRoll = Lerp(camRoll, targetRoll, dt * ROLL_LERP_SPEED);
+
+ camera.fovy =
+ Lerp(camera.fovy,
+ sprinting && Vector3Length(input) > 0 ? SPRINT_FOV : BASE_FOV,
+ dt * SPRINT_FOV_LERP_SPEED);
+
+ float rad = camRoll * DEG2RAD;
+
+ Vector3 rotatedUp = {
+ UP.x * cosf(rad) +
+ (forwardDir.y * UP.z - forwardDir.z * UP.y) * sinf(rad),
+ UP.y * cosf(rad) +
+ (forwardDir.z * UP.x - forwardDir.x * UP.z) * sinf(rad),
+ UP.z * cosf(rad) +
+ (forwardDir.x * UP.y - forwardDir.y * UP.x) * sinf(rad)};
+
+ camera.up = Vector3Normalize(rotatedUp);
+
+ BeginDrawing();
+ ClearBackground(RAYWHITE);
+ BeginMode3D(camera);
+
+ DrawModel(model, Vector3Zero(), 1.0f, WHITE);
+
+ EndMode3D();
+
+ DrawText(TextFormat("Pos: %.2f %.2f %.2f", playerPos.x, playerPos.y,
+ playerPos.z),
+ 10, 10, 20, BLACK);
+
+ DrawText(
+ TextFormat("Vel: %.4f %.4f %.4f", velocity.x, velocity.y, velocity.z),
+ 10, 30, 20, BLACK);
+
+ DrawText(TextFormat("Grounded: %d", grounded), 10, 50, 20, BLACK);
+
+ EndDrawing();
+ }
+
+ UnloadModel(model);
+ free(triangles);
+ CloseWindow();
+ return 0;
+}
diff --git a/physics.c b/physics.c
@@ -0,0 +1,232 @@
+#include "physics.h"
+#include "config.h"
+#include <float.h>
+#include <raymath.h>
+
+#define SQR(x) ((x) * (x))
+
+static inline bool TriangleLikelyContains(Vector3 p, float r,
+ const Triangle *t) {
+ float r2 = r + SKIN;
+
+ if (p.x + r2 < fminf(fminf(t->a.x, t->b.x), t->c.x))
+ 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))
+ return false;
+ if (p.y + r2 < fminf(fminf(t->a.y, t->b.y), t->c.y))
+ return false;
+ if (p.y - r2 > fmaxf(fmaxf(t->a.y, t->b.y), t->c.y))
+ return false;
+
+ return true;
+}
+
+Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) {
+ Vector3 ab = Vector3Subtract(b, a);
+ Vector3 ac = Vector3Subtract(c, a);
+ Vector3 ap = Vector3Subtract(p, a);
+ float d1 = Vector3DotProduct(ab, ap);
+ float d2 = Vector3DotProduct(ac, ap);
+ if (d1 <= 0.0f && d2 <= 0.0f)
+ return a;
+
+ Vector3 bp = Vector3Subtract(p, b);
+ float d3 = Vector3DotProduct(ab, bp);
+ float d4 = Vector3DotProduct(ac, bp);
+ if (d3 >= 0.0f && d4 <= d3)
+ return b;
+
+ float vc = d1 * d4 - d3 * d2;
+ if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f)
+ return Vector3Add(a, Vector3Scale(ab, d1 / (d1 - d3)));
+
+ Vector3 cp = Vector3Subtract(p, c);
+ float d5 = Vector3DotProduct(ab, cp);
+ float d6 = Vector3DotProduct(ac, cp);
+ if (d6 >= 0.0f && d5 <= d6)
+ return c;
+
+ float vb = d5 * d2 - d1 * d6;
+ if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f)
+ return Vector3Add(a, Vector3Scale(ac, d2 / (d2 - d6)));
+
+ float va = d3 * d6 - d5 * d4;
+ if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f)
+ return Vector3Add(b, Vector3Scale(Vector3Subtract(c, b),
+ (d4 - d3) / ((d4 - d3) + (d5 - d6))));
+
+ float denom = 1.0f / (va + vb + vc);
+ return Vector3Add(a, Vector3Add(Vector3Scale(ab, vb * denom),
+ Vector3Scale(ac, vc * denom)));
+}
+
+bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist,
+ const Triangle *tri, HitInfo *hit) {
+ Vector3 n = tri->normal;
+
+ // 1. Intersect with the infinite plane
+ float denom = Vector3DotProduct(n, dir);
+
+ if (denom >= -EPSILON)
+ return false;
+
+ float distToPlane = Vector3DotProduct(n, Vector3Subtract(tri->a, pos));
+ float tPlane = (distToPlane + radius) / denom;
+
+ if (tPlane > maxDist)
+ return false;
+
+ // 2. Determine where the sphere touches the plane
+ Vector3 planeIntersectionPoint = Vector3Add(pos, Vector3Scale(dir, tPlane));
+ Vector3 centerOnPlane =
+ Vector3Subtract(planeIntersectionPoint, Vector3Scale(n, radius));
+
+ // 3. Find closest point on the actual triangle
+ Vector3 closestPt =
+ ClosestPointOnTriangle(centerOnPlane, tri->a, tri->b, tri->c);
+
+ // 4. Check distance
+ float distSq = Vector3DistanceSqr(centerOnPlane, closestPt);
+
+ // 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
+ Vector3 L = Vector3Subtract(pos, closestPt);
+ float a = 1.0f;
+ float b = 2.0f * Vector3DotProduct(L, dir);
+ float c = Vector3DotProduct(L, L) - SQR(radius);
+
+ float delta = b * b - 4 * a * c;
+
+ if (delta >= 0.0f) {
+ float sqrtDelta = sqrtf(delta);
+ float tEdge = (-b - sqrtDelta) / 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;
+}
+
+bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius,
+ Triangle *tris, int triCount, HitInfo *outHit) {
+ HitInfo best = {0};
+ best.distance = dist;
+ bool found = false;
+
+ for (int i = 0; i < triCount; i++) {
+ HitInfo h;
+ if (SphereCastTriangle(pos, dir, radius, best.distance, &tris[i], &h)) {
+ best = h;
+ found = true;
+ }
+ }
+
+ if (!found)
+ return false;
+
+ *outHit = best;
+ return true;
+}
+
+void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount,
+ bool *hitFloor) {
+ float rSq = SQR(radius);
+
+ for (int iter = 0; iter < 2; iter++) {
+ bool collisionFound = false;
+
+ for (int i = 0; i < triCount; i++) {
+ Triangle *t = &tris[i];
+
+ if (!TriangleLikelyContains(*pos, radius, t))
+ continue;
+
+ Vector3 closest = ClosestPointOnTriangle(*pos, t->a, t->b, t->c);
+ Vector3 pushVec = Vector3Subtract(*pos, closest);
+ float d2 = Vector3LengthSqr(pushVec);
+
+ // allow for floating point error at the exact surface boundary
+ if (d2 > 0 && d2 < rSq + EPSILON) {
+ float d = sqrtf(d2);
+ Vector3 n = Vector3Scale(pushVec, 1.0f / d);
+
+ // push out
+ float penetration = radius - d + SKIN;
+ *pos = Vector3Add(*pos, Vector3Scale(n, penetration));
+
+ if (n.y > GROUND_NORMAL_Y)
+ *hitFloor = true;
+ else if (t->normal.y > GROUND_NORMAL_Y && n.y > EPSILON)
+ *hitFloor = true;
+
+ collisionFound = true;
+ }
+ }
+
+ if (!collisionFound)
+ break;
+ }
+}
+
+void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
+ bool *touchingGround) {
+ 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) {
+ 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);
+
+ if (effectivelyGrounded && velocity->y <= 0) {
+ SnapToGround(pos, radius, tris, triCount, &touchingGround);
+ velocity->y = 0;
+ } else
+ velocity->y -= GRAVITY * dt;
+
+ if (touchingGround)
+ groundedTimer = COYOTE_TIME;
+
+ *groundedOut = effectivelyGrounded;
+}
diff --git a/physics.h b/physics.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <raymath.h>
+#include <stdbool.h>
+#define MAX_ITERS 3
+
+typedef struct {
+ bool hit;
+ float distance;
+ Vector3 normal;
+} HitInfo;
+
+typedef struct {
+ Vector3 a, b, c;
+ Vector3 normal;
+ Vector3 min;
+ Vector3 max;
+} Triangle;
+
+Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c);
+
+bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float dist,
+ const Triangle *tri, HitInfo *hit);
+
+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);
+
+bool CheckGrounded(Vector3 pos, float radius, Triangle *tris, int triCount);
+
+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);