pxl8/src/sim/pxl8_sim.c

449 lines
14 KiB
C
Raw Normal View History

2026-01-31 09:31:17 -06:00
#include "pxl8_sim.h"
#include <math.h>
#define DIST_EPSILON 0.03125f
typedef struct {
f32 fraction;
pxl8_vec3 normal;
bool all_solid;
bool start_solid;
} trace_result;
2026-01-31 09:31:17 -06:00
static i32 bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
if (!bsp || bsp->num_nodes == 0) return -1;
i32 node_id = 0;
while (node_id >= 0) {
const pxl8_bsp_node* node = &bsp->nodes[node_id];
const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];
f32 dist = pxl8_vec3_dot(pos, plane->normal) - plane->dist;
node_id = node->children[dist < 0 ? 1 : 0];
}
return -(node_id + 1);
}
static i32 bsp_contents_from(const pxl8_bsp* bsp, i32 node_id, pxl8_vec3 pos) {
while (node_id >= 0) {
const pxl8_bsp_node* node = &bsp->nodes[node_id];
const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];
f32 d = pxl8_vec3_dot(pos, plane->normal) - plane->dist;
node_id = node->children[d < 0 ? 1 : 0];
}
i32 leaf_idx = -(node_id + 1);
if (leaf_idx < 0 || (u32)leaf_idx >= bsp->num_leafs) return -1;
return bsp->leafs[leaf_idx].contents;
}
static bool bsp_recursive_trace(const pxl8_bsp* bsp, i32 node_id,
f32 p1f, f32 p2f,
pxl8_vec3 p1, pxl8_vec3 p2,
trace_result* tr) {
if (node_id < 0) {
i32 leaf_idx = -(node_id + 1);
if (leaf_idx >= 0 && (u32)leaf_idx < bsp->num_leafs &&
bsp->leafs[leaf_idx].contents == -1) {
tr->start_solid = true;
} else {
tr->all_solid = false;
}
return true;
}
const pxl8_bsp_node* node = &bsp->nodes[node_id];
const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];
f32 t1 = pxl8_vec3_dot(p1, plane->normal) - plane->dist;
f32 t2 = pxl8_vec3_dot(p2, plane->normal) - plane->dist;
if (t1 >= 0 && t2 >= 0)
return bsp_recursive_trace(bsp, node->children[0], p1f, p2f, p1, p2, tr);
if (t1 < 0 && t2 < 0)
return bsp_recursive_trace(bsp, node->children[1], p1f, p2f, p1, p2, tr);
i32 side;
f32 frac;
if (t1 < 0) {
frac = (t1 + DIST_EPSILON) / (t1 - t2);
side = 1;
} else {
frac = (t1 - DIST_EPSILON) / (t1 - t2);
side = 0;
}
if (frac < 0) frac = 0;
if (frac > 1) frac = 1;
f32 midf = p1f + (p2f - p1f) * frac;
pxl8_vec3 mid = {
p1.x + frac * (p2.x - p1.x),
p1.y + frac * (p2.y - p1.y),
p1.z + frac * (p2.z - p1.z),
};
if (!bsp_recursive_trace(bsp, node->children[side], p1f, midf, p1, mid, tr))
return false;
if (bsp_contents_from(bsp, node->children[side ^ 1], mid) != -1)
return bsp_recursive_trace(bsp, node->children[side ^ 1], midf, p2f, mid, p2, tr);
if (tr->all_solid)
return false;
if (midf < tr->fraction) {
tr->fraction = midf;
if (side == 0) {
tr->normal = plane->normal;
} else {
tr->normal.x = -plane->normal.x;
tr->normal.y = -plane->normal.y;
tr->normal.z = -plane->normal.z;
}
}
return false;
}
static trace_result bsp_trace_line(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to) {
trace_result tr = { .fraction = 1.0f, .all_solid = true };
if (!bsp || bsp->num_nodes == 0) {
tr.all_solid = false;
return tr;
}
bsp_recursive_trace(bsp, 0, 0.0f, 1.0f, from, to, &tr);
if (tr.all_solid) {
tr.fraction = 0.0f;
tr.start_solid = true;
}
return tr;
}
2026-01-31 09:31:17 -06:00
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos) {
if (!bsp) return false;
if (bsp->bounds_max_x > bsp->bounds_min_x &&
(pos.x < bsp->bounds_min_x || pos.x >= bsp->bounds_max_x ||
pos.z < bsp->bounds_min_z || pos.z >= bsp->bounds_max_z))
return false;
2026-01-31 09:31:17 -06:00
i32 leaf = bsp_find_leaf(bsp, pos);
if (leaf < 0 || (u32)leaf >= bsp->num_leafs) return true;
return bsp->leafs[leaf].contents == -1;
}
static void trace_offsets(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to,
f32 radius, f32* out_frac, pxl8_vec3* out_normal) {
f32 d = radius * 0.7071f;
pxl8_vec3 offsets[9] = {
{0, 0, 0},
{radius, 0, 0}, {-radius, 0, 0},
{0, 0, radius}, {0, 0, -radius},
{d, 0, d}, {d, 0, -d},
{-d, 0, d}, {-d, 0, -d},
};
*out_frac = 1.0f;
*out_normal = (pxl8_vec3){0, 0, 0};
for (i32 i = 0; i < 9; i++) {
pxl8_vec3 s = { from.x + offsets[i].x, from.y + offsets[i].y, from.z + offsets[i].z };
pxl8_vec3 e = { to.x + offsets[i].x, to.y + offsets[i].y, to.z + offsets[i].z };
trace_result tr = bsp_trace_line(bsp, s, e);
if (tr.fraction < *out_frac && !tr.start_solid) {
*out_frac = tr.fraction;
*out_normal = tr.normal;
}
}
2026-01-31 09:31:17 -06:00
}
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!bsp || bsp->num_nodes == 0) return to;
f32 frac;
pxl8_vec3 normal;
trace_offsets(bsp, from, to, radius, &frac, &normal);
if (frac >= 1.0f) return to;
pxl8_vec3 delta = { to.x - from.x, to.y - from.y, to.z - from.z };
pxl8_vec3 hit = {
from.x + delta.x * frac,
from.y + delta.y * frac,
from.z + delta.z * frac,
};
f32 remaining = 1.0f - frac;
if (remaining <= 0.0f) return hit;
2026-01-31 09:31:17 -06:00
f32 backoff = pxl8_vec3_dot(delta, normal) * remaining;
pxl8_vec3 slide_target = {
hit.x + delta.x * remaining - normal.x * backoff,
hit.y + delta.y * remaining - normal.y * backoff,
hit.z + delta.z * remaining - normal.z * backoff,
};
f32 slide_frac;
pxl8_vec3 slide_normal;
trace_offsets(bsp, hit, slide_target, radius, &slide_frac, &slide_normal);
if (slide_frac >= 1.0f) return slide_target;
pxl8_vec3 slide_delta = {
slide_target.x - hit.x,
slide_target.y - hit.y,
slide_target.z - hit.z,
};
return (pxl8_vec3){
hit.x + slide_delta.x * slide_frac,
hit.y + slide_delta.y * slide_frac,
hit.z + slide_delta.z * slide_frac,
};
}
2026-01-31 09:31:17 -06:00
static const pxl8_bsp* sim_bsp_at(const pxl8_sim_world* world, f32 x, f32 z) {
i32 cx = (i32)floorf(x / world->chunk_size);
i32 cz = (i32)floorf(z / world->chunk_size);
i32 dx = cx - world->center_cx + 1;
i32 dz = cz - world->center_cz + 1;
if (dx < 0 || dx > 2 || dz < 0 || dz > 2) return NULL;
return world->chunks[dz * 3 + dx];
}
2026-01-31 09:31:17 -06:00
static f32 bsp_terrain_height(const pxl8_bsp* bsp, f32 x, f32 z) {
if (!bsp || !bsp->heightfield || bsp->heightfield_cell_size <= 0) return -1e9f;
f32 lx = (x - bsp->heightfield_ox) / bsp->heightfield_cell_size;
f32 lz = (z - bsp->heightfield_oz) / bsp->heightfield_cell_size;
i32 ix = (i32)floorf(lx);
i32 iz = (i32)floorf(lz);
if (ix < 0 || ix >= bsp->heightfield_w - 1 || iz < 0 || iz >= bsp->heightfield_h - 1) return -1e9f;
f32 fx = lx - ix;
f32 fz = lz - iz;
i32 w = bsp->heightfield_w;
f32 h00 = bsp->heightfield[iz * w + ix];
f32 h10 = bsp->heightfield[iz * w + ix + 1];
f32 h01 = bsp->heightfield[(iz + 1) * w + ix];
f32 h11 = bsp->heightfield[(iz + 1) * w + ix + 1];
f32 h0 = h00 + (h10 - h00) * fx;
f32 h1 = h01 + (h11 - h01) * fx;
return h0 + (h1 - h0) * fz;
}
static f32 sim_terrain_height(const pxl8_sim_world* world, f32 x, f32 z) {
const pxl8_bsp* bsp = sim_bsp_at(world, x, z);
return bsp_terrain_height(bsp, x, z);
}
static void sim_trace_offsets(const pxl8_sim_world* world, pxl8_vec3 from, pxl8_vec3 to,
f32 radius, f32* out_frac, pxl8_vec3* out_normal) {
f32 d = radius * 0.7071f;
pxl8_vec3 offsets[9] = {
{0, 0, 0},
{radius, 0, 0}, {-radius, 0, 0},
{0, 0, radius}, {0, 0, -radius},
{d, 0, d}, {d, 0, -d},
{-d, 0, d}, {-d, 0, -d},
};
*out_frac = 1.0f;
*out_normal = (pxl8_vec3){0, 0, 0};
for (i32 i = 0; i < 9; i++) {
pxl8_vec3 s = { from.x + offsets[i].x, from.y + offsets[i].y, from.z + offsets[i].z };
pxl8_vec3 e = { to.x + offsets[i].x, to.y + offsets[i].y, to.z + offsets[i].z };
const pxl8_bsp* bsp_s = sim_bsp_at(world, s.x, s.z);
const pxl8_bsp* bsp_e = sim_bsp_at(world, e.x, e.z);
if (bsp_s) {
trace_result tr = bsp_trace_line(bsp_s, s, e);
if (tr.fraction < *out_frac && !tr.start_solid) {
*out_frac = tr.fraction;
*out_normal = tr.normal;
}
}
if (bsp_e && bsp_e != bsp_s) {
bool inside_bounds = !(bsp_e->bounds_max_x > bsp_e->bounds_min_x) ||
(s.x >= bsp_e->bounds_min_x && s.x < bsp_e->bounds_max_x &&
s.z >= bsp_e->bounds_min_z && s.z < bsp_e->bounds_max_z);
if (inside_bounds) {
trace_result tr = bsp_trace_line(bsp_e, s, e);
if (tr.fraction < *out_frac && !tr.start_solid) {
*out_frac = tr.fraction;
*out_normal = tr.normal;
}
}
}
}
2026-01-31 09:31:17 -06:00
}
pxl8_vec3 pxl8_sim_trace(const pxl8_sim_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius, f32 height) {
(void)height;
if (!world || world->chunk_size <= 0) return to;
f32 frac;
pxl8_vec3 normal;
sim_trace_offsets(world, from, to, radius, &frac, &normal);
if (frac >= 1.0f) return to;
pxl8_vec3 delta = { to.x - from.x, to.y - from.y, to.z - from.z };
pxl8_vec3 hit = {
from.x + delta.x * frac,
from.y + delta.y * frac,
from.z + delta.z * frac,
};
2026-01-31 09:31:17 -06:00
f32 remaining = 1.0f - frac;
if (remaining <= 0.0f) return hit;
f32 backoff = pxl8_vec3_dot(delta, normal) * remaining;
pxl8_vec3 slide_target = {
hit.x + delta.x * remaining - normal.x * backoff,
hit.y + delta.y * remaining - normal.y * backoff,
hit.z + delta.z * remaining - normal.z * backoff,
};
2026-01-31 09:31:17 -06:00
f32 slide_frac;
pxl8_vec3 slide_normal;
sim_trace_offsets(world, hit, slide_target, radius, &slide_frac, &slide_normal);
if (slide_frac >= 1.0f) return slide_target;
pxl8_vec3 slide_delta = {
slide_target.x - hit.x,
slide_target.y - hit.y,
slide_target.z - hit.z,
};
return (pxl8_vec3){
hit.x + slide_delta.x * slide_frac,
hit.y + slide_delta.y * slide_frac,
hit.z + slide_delta.z * slide_frac,
};
2026-01-31 09:31:17 -06:00
}
bool pxl8_sim_check_ground(const pxl8_sim_world* world, pxl8_vec3 pos, f32 radius) {
if (!world || world->chunk_size <= 0) return true;
f32 th = sim_terrain_height(world, pos.x, pos.z);
if (th > -1e8f && pos.y - th < 2.0f) return true;
2026-01-31 09:31:17 -06:00
pxl8_vec3 down = {pos.x, pos.y - 2.0f, pos.z};
pxl8_vec3 result = pxl8_sim_trace(world, pos, down, radius, 0.0f);
2026-01-31 09:31:17 -06:00
return result.y > down.y;
}
void pxl8_sim_move_player(pxl8_sim_entity* ent, const pxl8_input_msg* input,
const pxl8_sim_world* world, const pxl8_sim_config* cfg, f32 dt) {
if (!ent || !input || !cfg) return;
2026-01-31 09:31:17 -06:00
if (!(ent->flags & PXL8_SIM_FLAG_ALIVE)) return;
ent->yaw -= input->look_dx * 0.008f;
f32 new_pitch = ent->pitch - input->look_dy * 0.008f;
if (new_pitch > cfg->max_pitch) new_pitch = cfg->max_pitch;
if (new_pitch < -cfg->max_pitch) new_pitch = -cfg->max_pitch;
2026-01-31 09:31:17 -06:00
ent->pitch = new_pitch;
f32 sin_yaw = sinf(ent->yaw);
f32 cos_yaw = cosf(ent->yaw);
f32 input_len = sqrtf(input->move_x * input->move_x + input->move_y * input->move_y);
pxl8_vec3 move_dir = {0, 0, 0};
f32 target_speed = 0.0f;
if (input_len > 0.0f) {
f32 nx = input->move_x / input_len;
f32 ny = input->move_y / input_len;
move_dir.x = cos_yaw * nx - sin_yaw * ny;
move_dir.z = -sin_yaw * nx - cos_yaw * ny;
target_speed = cfg->move_speed;
2026-01-31 09:31:17 -06:00
}
ent->vel.x = move_dir.x * target_speed;
ent->vel.z = move_dir.z * target_speed;
bool grounded = (ent->flags & PXL8_SIM_FLAG_GROUNDED) != 0;
if (grounded && (input->buttons & 1)) {
ent->vel.y = cfg->jump_velocity;
2026-01-31 09:31:17 -06:00
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
grounded = false;
}
if (!grounded) {
ent->vel.y -= cfg->gravity * dt;
2026-01-31 09:31:17 -06:00
}
pxl8_vec3 target = {
ent->pos.x + ent->vel.x * dt,
ent->pos.y + ent->vel.y * dt,
ent->pos.z + ent->vel.z * dt
};
if (grounded) {
f32 th_dest = sim_terrain_height(world, target.x, target.z);
if (th_dest > -1e8f) target.y = th_dest;
}
pxl8_vec3 new_pos = pxl8_sim_trace(world, ent->pos, target, cfg->player_radius, cfg->player_height);
f32 th = sim_terrain_height(world, new_pos.x, new_pos.z);
if (th > -1e8f && new_pos.y < th) {
new_pos.y = th;
if (ent->vel.y < 0) ent->vel.y = 0;
}
2026-01-31 09:31:17 -06:00
ent->pos = new_pos;
if (pxl8_sim_check_ground(world, ent->pos, cfg->player_radius)) {
2026-01-31 09:31:17 -06:00
ent->flags |= PXL8_SIM_FLAG_GROUNDED;
if (ent->vel.y < 0) ent->vel.y = 0;
} else {
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
}
}
void pxl8_sim_integrate(pxl8_sim_entity* ent, const pxl8_sim_world* world,
const pxl8_sim_config* cfg, f32 dt) {
if (!ent || !cfg) return;
2026-01-31 09:31:17 -06:00
if (!(ent->flags & PXL8_SIM_FLAG_ALIVE)) return;
if (ent->flags & PXL8_SIM_FLAG_PLAYER) return;
bool grounded = (ent->flags & PXL8_SIM_FLAG_GROUNDED) != 0;
ent->vel.y -= cfg->gravity * dt;
2026-01-31 09:31:17 -06:00
if (grounded) {
f32 speed = sqrtf(ent->vel.x * ent->vel.x + ent->vel.z * ent->vel.z);
if (speed > 0.0f) {
f32 drop = speed * cfg->friction * dt;
2026-01-31 09:31:17 -06:00
f32 new_speed = speed - drop;
if (new_speed < 0.0f) new_speed = 0.0f;
f32 scale = new_speed / speed;
ent->vel.x *= scale;
ent->vel.z *= scale;
}
}
pxl8_vec3 target = {
ent->pos.x + ent->vel.x * dt,
ent->pos.y + ent->vel.y * dt,
ent->pos.z + ent->vel.z * dt
};
if (grounded) {
f32 th_dest = sim_terrain_height(world, target.x, target.z);
if (th_dest > -1e8f) target.y = th_dest;
}
pxl8_vec3 new_pos = pxl8_sim_trace(world, ent->pos, target, cfg->player_radius, cfg->player_height);
f32 th2 = sim_terrain_height(world, new_pos.x, new_pos.z);
if (th2 > -1e8f && new_pos.y < th2) {
new_pos.y = th2;
if (ent->vel.y < 0.0f) ent->vel.y = 0.0f;
}
2026-01-31 09:31:17 -06:00
ent->pos = new_pos;
if (pxl8_sim_check_ground(world, ent->pos, cfg->player_radius)) {
2026-01-31 09:31:17 -06:00
ent->flags |= PXL8_SIM_FLAG_GROUNDED;
if (ent->vel.y < 0.0f) ent->vel.y = 0.0f;
} else {
ent->flags &= ~PXL8_SIM_FLAG_GROUNDED;
}
}