add networking, 3d improvements, reorganize src structure

This commit is contained in:
asrael 2026-01-17 22:52:36 -06:00
parent 39b604b333
commit 415d424057
122 changed files with 5358 additions and 721 deletions

756
src/world/pxl8_bsp.c Normal file
View file

@ -0,0 +1,756 @@
#include "pxl8_bsp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_color.h"
#include "pxl8_gfx.h"
#include "pxl8_io.h"
#include "pxl8_log.h"
#define BSP_VERSION 29
typedef enum {
CHUNK_ENTITIES = 0,
CHUNK_PLANES = 1,
CHUNK_TEXTURES = 2,
CHUNK_VERTICES = 3,
CHUNK_VISIBILITY = 4,
CHUNK_NODES = 5,
CHUNK_TEXINFO = 6,
CHUNK_FACES = 7,
CHUNK_LIGHTING = 8,
CHUNK_CLIPNODES = 9,
CHUNK_LEAFS = 10,
CHUNK_MARKSURFACES = 11,
CHUNK_EDGES = 12,
CHUNK_SURFEDGES = 13,
CHUNK_MODELS = 14,
CHUNK_COUNT = 15
} pxl8_bsp_chunk_type;
typedef struct {
u32 offset;
u32 size;
} pxl8_bsp_chunk;
typedef struct {
u32 version;
pxl8_bsp_chunk chunks[CHUNK_COUNT];
} pxl8_bsp_header;
static inline pxl8_vec3 read_vec3(pxl8_stream* stream) {
pxl8_vec3 v;
v.x = pxl8_read_f32(stream);
v.y = pxl8_read_f32(stream);
v.z = pxl8_read_f32(stream);
return v;
}
static bool validate_chunk(const pxl8_bsp_chunk* chunk, u32 element_size, size_t file_size) {
if (chunk->size == 0) return true;
if (chunk->offset >= file_size) return false;
if (chunk->offset + chunk->size > file_size) return false;
if (chunk->size % element_size != 0) return false;
return true;
}
static inline bool pxl8_bsp_get_edge_vertex(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_vert_idx) {
if (surfedge_idx >= (i32)bsp->num_surfedges) return false;
i32 edge_idx = bsp->surfedges[surfedge_idx];
u32 vertex_index;
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 0;
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) return false;
vertex_index = 1;
}
*out_vert_idx = bsp->edges[edge_idx].vertex[vertex_index];
return *out_vert_idx < bsp->num_vertices;
}
static inline bool pxl8_bsp_get_edge_vertices(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_v0_idx, u32* out_v1_idx) {
if (surfedge_idx >= (i32)bsp->num_surfedges) return false;
i32 edge_idx = bsp->surfedges[surfedge_idx];
if (edge_idx >= 0) {
if ((u32)edge_idx >= bsp->num_edges) return false;
*out_v0_idx = bsp->edges[edge_idx].vertex[0];
*out_v1_idx = bsp->edges[edge_idx].vertex[1];
} else {
edge_idx = -edge_idx;
if ((u32)edge_idx >= bsp->num_edges) return false;
*out_v0_idx = bsp->edges[edge_idx].vertex[1];
*out_v1_idx = bsp->edges[edge_idx].vertex[0];
}
return *out_v0_idx < bsp->num_vertices && *out_v1_idx < bsp->num_vertices;
}
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!path || !bsp) return PXL8_ERROR_INVALID_ARGUMENT;
memset(bsp, 0, sizeof(*bsp));
u8* file_data = NULL;
size_t file_size = 0;
pxl8_result result = pxl8_io_read_binary_file(path, &file_data, &file_size);
if (result != PXL8_OK) {
pxl8_error("Failed to load BSP file: %s", path);
return result;
}
if (file_size < sizeof(pxl8_bsp_header)) {
pxl8_error("BSP file too small: %s", path);
free(file_data);
return PXL8_ERROR_INVALID_FORMAT;
}
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
pxl8_bsp_header header;
header.version = pxl8_read_u32(&stream);
if (header.version != BSP_VERSION) {
pxl8_error("Invalid BSP version: %u (expected %d)", header.version, BSP_VERSION);
free(file_data);
return PXL8_ERROR_INVALID_FORMAT;
}
for (i32 i = 0; i < CHUNK_COUNT; i++) {
header.chunks[i].offset = pxl8_read_u32(&stream);
header.chunks[i].size = pxl8_read_u32(&stream);
}
pxl8_bsp_chunk* chunk = &header.chunks[CHUNK_VERTICES];
if (!validate_chunk(chunk, 12, file_size)) goto error_cleanup;
bsp->num_vertices = chunk->size / 12;
if (bsp->num_vertices > 0) {
bsp->vertices = calloc(bsp->num_vertices, sizeof(pxl8_bsp_vertex));
if (!bsp->vertices) goto error_cleanup;
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_vertices; i++) {
bsp->vertices[i].position = read_vec3(&stream);
}
}
chunk = &header.chunks[CHUNK_EDGES];
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_edges = chunk->size / 4;
if (bsp->num_edges > 0) {
bsp->edges = calloc(bsp->num_edges, sizeof(pxl8_bsp_edge));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_edges; i++) {
bsp->edges[i].vertex[0] = pxl8_read_u16(&stream);
bsp->edges[i].vertex[1] = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_SURFEDGES];
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_surfedges = chunk->size / 4;
if (bsp->num_surfedges > 0) {
bsp->surfedges = calloc(bsp->num_surfedges, sizeof(i32));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_surfedges; i++) {
bsp->surfedges[i] = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_PLANES];
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_planes = chunk->size / 20;
if (bsp->num_planes > 0) {
bsp->planes = calloc(bsp->num_planes, sizeof(pxl8_bsp_plane));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_planes; i++) {
bsp->planes[i].normal = read_vec3(&stream);
bsp->planes[i].dist = pxl8_read_f32(&stream);
bsp->planes[i].type = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_TEXINFO];
if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup;
bsp->num_texinfo = chunk->size / 40;
if (bsp->num_texinfo > 0) {
bsp->texinfo = calloc(bsp->num_texinfo, sizeof(pxl8_bsp_texinfo));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_texinfo; i++) {
bsp->texinfo[i].u_axis = read_vec3(&stream);
bsp->texinfo[i].u_offset = pxl8_read_f32(&stream);
bsp->texinfo[i].v_axis = read_vec3(&stream);
bsp->texinfo[i].v_offset = pxl8_read_f32(&stream);
bsp->texinfo[i].miptex = pxl8_read_u32(&stream);
}
}
chunk = &header.chunks[CHUNK_FACES];
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_faces = chunk->size / 20;
if (bsp->num_faces > 0) {
bsp->faces = calloc(bsp->num_faces, sizeof(pxl8_bsp_face));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_faces; i++) {
bsp->faces[i].plane_id = pxl8_read_u16(&stream);
bsp->faces[i].side = pxl8_read_u16(&stream);
bsp->faces[i].first_edge = pxl8_read_u32(&stream);
bsp->faces[i].num_edges = pxl8_read_u16(&stream);
bsp->faces[i].texinfo_id = pxl8_read_u16(&stream);
bsp->faces[i].styles[0] = pxl8_read_u8(&stream);
bsp->faces[i].styles[1] = pxl8_read_u8(&stream);
bsp->faces[i].styles[2] = pxl8_read_u8(&stream);
bsp->faces[i].styles[3] = pxl8_read_u8(&stream);
bsp->faces[i].lightmap_offset = pxl8_read_u32(&stream);
bsp->faces[i].aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
bsp->faces[i].aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
}
}
chunk = &header.chunks[CHUNK_NODES];
if (!validate_chunk(chunk, 24, file_size)) goto error_cleanup;
bsp->num_nodes = chunk->size / 24;
if (bsp->num_nodes > 0) {
bsp->nodes = calloc(bsp->num_nodes, sizeof(pxl8_bsp_node));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_nodes; i++) {
bsp->nodes[i].plane_id = pxl8_read_u32(&stream);
bsp->nodes[i].children[0] = pxl8_read_i16(&stream);
bsp->nodes[i].children[1] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->nodes[i].mins[j] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->nodes[i].maxs[j] = pxl8_read_i16(&stream);
bsp->nodes[i].first_face = pxl8_read_u16(&stream);
bsp->nodes[i].num_faces = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_LEAFS];
if (!validate_chunk(chunk, 28, file_size)) goto error_cleanup;
bsp->num_leafs = chunk->size / 28;
if (bsp->num_leafs > 0) {
bsp->leafs = calloc(bsp->num_leafs, sizeof(pxl8_bsp_leaf));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_leafs; i++) {
bsp->leafs[i].contents = pxl8_read_i32(&stream);
bsp->leafs[i].visofs = pxl8_read_i32(&stream);
for (u32 j = 0; j < 3; j++) bsp->leafs[i].mins[j] = pxl8_read_i16(&stream);
for (u32 j = 0; j < 3; j++) bsp->leafs[i].maxs[j] = pxl8_read_i16(&stream);
bsp->leafs[i].first_marksurface = pxl8_read_u16(&stream);
bsp->leafs[i].num_marksurfaces = pxl8_read_u16(&stream);
for (u32 j = 0; j < 4; j++) bsp->leafs[i].ambient_level[j] = pxl8_read_u8(&stream);
}
}
chunk = &header.chunks[CHUNK_MARKSURFACES];
if (!validate_chunk(chunk, 2, file_size)) goto error_cleanup;
bsp->num_marksurfaces = chunk->size / 2;
if (bsp->num_marksurfaces > 0) {
bsp->marksurfaces = calloc(bsp->num_marksurfaces, sizeof(u16));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_marksurfaces; i++) {
bsp->marksurfaces[i] = pxl8_read_u16(&stream);
}
}
chunk = &header.chunks[CHUNK_MODELS];
if (!validate_chunk(chunk, 64, file_size)) goto error_cleanup;
bsp->num_models = chunk->size / 64;
if (bsp->num_models > 0) {
bsp->models = calloc(bsp->num_models, sizeof(pxl8_bsp_model));
pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_models; i++) {
for (u32 j = 0; j < 3; j++) bsp->models[i].mins[j] = pxl8_read_f32(&stream);
for (u32 j = 0; j < 3; j++) bsp->models[i].maxs[j] = pxl8_read_f32(&stream);
bsp->models[i].origin = read_vec3(&stream);
for (u32 j = 0; j < 4; j++) bsp->models[i].headnode[j] = pxl8_read_i32(&stream);
bsp->models[i].visleafs = pxl8_read_i32(&stream);
bsp->models[i].first_face = pxl8_read_i32(&stream);
bsp->models[i].num_faces = pxl8_read_i32(&stream);
}
}
chunk = &header.chunks[CHUNK_VISIBILITY];
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->visdata_size = chunk->size;
if (bsp->visdata_size > 0) {
bsp->visdata = malloc(bsp->visdata_size);
memcpy(bsp->visdata, file_data + chunk->offset, bsp->visdata_size);
}
chunk = &header.chunks[CHUNK_LIGHTING];
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->lightdata_size = chunk->size;
if (bsp->lightdata_size > 0) {
bsp->lightdata = malloc(bsp->lightdata_size);
memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size);
}
free(file_data);
for (u32 i = 0; i < bsp->num_faces; i++) {
pxl8_bsp_face* face = &bsp->faces[i];
f32 min_x = 1e30f, min_y = 1e30f, min_z = 1e30f;
f32 max_x = -1e30f, max_y = -1e30f, max_z = -1e30f;
for (u32 j = 0; j < face->num_edges; j++) {
i32 surfedge_idx = face->first_edge + j;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) continue;
pxl8_vec3 v = bsp->vertices[vert_idx].position;
if (v.x < min_x) min_x = v.x;
if (v.x > max_x) max_x = v.x;
if (v.y < min_y) min_y = v.y;
if (v.y > max_y) max_y = v.y;
if (v.z < min_z) min_z = v.z;
if (v.z > max_z) max_z = v.z;
}
face->aabb_min = (pxl8_vec3){min_x, min_y, min_z};
face->aabb_max = (pxl8_vec3){max_x, max_y, max_z};
}
pxl8_debug("Loaded BSP: %u verts, %u faces, %u nodes, %u leafs",
bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs);
return PXL8_OK;
error_cleanup:
pxl8_error("BSP chunk validation failed: %s", path);
free(file_data);
pxl8_bsp_destroy(bsp);
return PXL8_ERROR_INVALID_FORMAT;
}
void pxl8_bsp_destroy(pxl8_bsp* bsp) {
if (!bsp) return;
free(bsp->edges);
free(bsp->faces);
free(bsp->leafs);
free(bsp->lightdata);
free(bsp->marksurfaces);
free(bsp->models);
free(bsp->nodes);
free(bsp->planes);
free(bsp->surfedges);
free(bsp->texinfo);
free(bsp->vertices);
free(bsp->visdata);
memset(bsp, 0, sizeof(*bsp));
}
i32 pxl8_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);
}
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) {
if (!bsp || !bsp->visdata || bsp->visdata_size == 0) return true;
if (leaf_from < 0 || leaf_to < 0) return true;
if ((u32)leaf_from >= bsp->num_leafs || (u32)leaf_to >= bsp->num_leafs) return true;
i32 visofs = bsp->leafs[leaf_from].visofs;
if (visofs < 0) return true;
u32 target_byte = leaf_to >> 3;
u32 target_bit = leaf_to & 7;
u32 pvs_size = (bsp->num_leafs + 7) / 8;
u32 pos = (u32)visofs;
u32 current_byte = 0;
while (current_byte < pvs_size && pos < bsp->visdata_size) {
u8 b = bsp->visdata[pos++];
if (b != 0) {
if (current_byte == target_byte) {
return (b & (1 << target_bit)) != 0;
}
current_byte++;
} else {
if (pos >= bsp->visdata_size) return false;
u32 count = bsp->visdata[pos++];
if (target_byte < current_byte + count) {
return false;
}
current_byte += count;
}
}
return false;
}
pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf) {
pxl8_bsp_pvs pvs = {0};
u32 pvs_size = (bsp->num_leafs + 7) / 8;
pvs.data = calloc(pvs_size, 1);
pvs.size = pvs_size;
if (!pvs.data) return pvs;
if (!bsp || leaf < 0 || (u32)leaf >= bsp->num_leafs) {
memset(pvs.data, 0xFF, pvs_size);
return pvs;
}
i32 visofs = bsp->leafs[leaf].visofs;
if (visofs < 0 || !bsp->visdata || bsp->visdata_size == 0) {
memset(pvs.data, 0xFF, pvs_size);
return pvs;
}
u32 pos = (u32)visofs;
u32 out = 0;
while (out < pvs_size && pos < bsp->visdata_size) {
u8 b = bsp->visdata[pos++];
if (b != 0) {
pvs.data[out++] = b;
} else {
if (pos >= bsp->visdata_size) break;
u32 count = bsp->visdata[pos++];
out += count;
}
}
return pvs;
}
void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs) {
if (pvs) {
free(pvs->data);
pvs->data = NULL;
pvs->size = 0;
}
}
bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf) {
if (!pvs || !pvs->data || leaf < 0) return false;
u32 byte_idx = leaf >> 3;
u32 bit_idx = leaf & 7;
if (byte_idx >= pvs->size) return false;
return (pvs->data[byte_idx] & (1 << bit_idx)) != 0;
}
pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b) {
return (pxl8_bsp_lightmap){
.color = {r, g, b},
.height = 0,
.offset = 0,
.width = 0,
};
}
pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset) {
return (pxl8_bsp_lightmap){
.color = {0, 0, 0},
.height = height,
.offset = offset,
.width = width,
};
}
pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v) {
pxl8_bsp_lightmap_sample white = {255, 255, 255};
if (!bsp || !bsp->lightmaps || face_idx >= bsp->num_lightmaps) {
return white;
}
const pxl8_bsp_lightmap* lm = &bsp->lightmaps[face_idx];
if (lm->width == 0) {
return (pxl8_bsp_lightmap_sample){lm->color[2], lm->color[1], lm->color[0]};
}
if (!bsp->lightdata || bsp->lightdata_size == 0) {
return white;
}
f32 w = (f32)lm->width;
f32 h = (f32)lm->height;
f32 fx = u * w;
f32 fy = v * h;
if (fx < 0) fx = 0;
if (fx > w - 1.001f) fx = w - 1.001f;
if (fy < 0) fy = 0;
if (fy > h - 1.001f) fy = h - 1.001f;
u32 x0 = (u32)fx;
u32 y0 = (u32)fy;
u32 x1 = x0 + 1;
u32 y1 = y0 + 1;
if (x1 >= lm->width) x1 = lm->width - 1;
if (y1 >= lm->height) y1 = lm->height - 1;
f32 frac_x = fx - (f32)x0;
f32 frac_y = fy - (f32)y0;
u32 stride = lm->width;
u32 base = lm->offset;
u32 idx00 = base + y0 * stride + x0;
u32 idx10 = base + y0 * stride + x1;
u32 idx01 = base + y1 * stride + x0;
u32 idx11 = base + y1 * stride + x1;
u8 r00, g00, b00, r10, g10, b10, r01, g01, b01, r11, g11, b11;
if (idx00 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx00], &r00, &g00, &b00);
else { r00 = g00 = b00 = 255; }
if (idx10 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx10], &r10, &g10, &b10);
else { r10 = g10 = b10 = 255; }
if (idx01 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx01], &r01, &g01, &b01);
else { r01 = g01 = b01 = 255; }
if (idx11 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx11], &r11, &g11, &b11);
else { r11 = g11 = b11 = 255; }
f32 inv_x = 1.0f - frac_x;
f32 inv_y = 1.0f - frac_y;
u8 r = (u8)(r00 * inv_x * inv_y + r10 * frac_x * inv_y + r01 * inv_x * frac_y + r11 * frac_x * frac_y);
u8 g = (u8)(g00 * inv_x * inv_y + g10 * frac_x * inv_y + g01 * inv_x * frac_y + g11 * frac_x * frac_y);
u8 b = (u8)(b00 * inv_x * inv_y + b10 * frac_x * inv_y + b01 * inv_x * frac_y + b11 * frac_x * frac_y);
return (pxl8_bsp_lightmap_sample){b, g, r};
}
static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_frustum* frustum) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
}
static void collect_face_to_mesh(
const pxl8_bsp* bsp,
u32 face_id,
pxl8_mesh* mesh
) {
const pxl8_bsp_face* face = &bsp->faces[face_id];
if (face->num_edges < 3) return;
pxl8_vec3 normal = {0, 1, 0};
if (face->plane_id < bsp->num_planes) {
normal = bsp->planes[face->plane_id].normal;
if (face->side) {
normal.x = -normal.x;
normal.y = -normal.y;
normal.z = -normal.z;
}
}
const pxl8_bsp_texinfo* texinfo = NULL;
f32 tex_scale = 64.0f;
if (face->texinfo_id < bsp->num_texinfo) {
texinfo = &bsp->texinfo[face->texinfo_id];
}
u16 base_idx = (u16)mesh->vertex_count;
u32 num_verts = 0;
for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) {
i32 surfedge_idx = face->first_edge + i;
u32 vert_idx;
if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) {
continue;
}
pxl8_vec3 pos = bsp->vertices[vert_idx].position;
f32 u = 0.0f, v = 0.0f;
if (texinfo) {
u = (pxl8_vec3_dot(pos, texinfo->u_axis) + texinfo->u_offset) / tex_scale;
v = (pxl8_vec3_dot(pos, texinfo->v_axis) + texinfo->v_offset) / tex_scale;
}
u8 light = 255;
if (bsp->vertex_lights && vert_idx < bsp->num_vertex_lights) {
light = (bsp->vertex_lights[vert_idx] >> 24) & 0xFF;
}
pxl8_vertex vtx = {
.position = pos,
.normal = normal,
.u = u,
.v = v,
.color = 15,
.light = light,
};
pxl8_mesh_push_vertex(mesh, vtx);
num_verts++;
}
if (num_verts < 3) return;
for (u32 i = 1; i < num_verts - 1; i++) {
pxl8_mesh_push_triangle(mesh, base_idx, base_idx + i, base_idx + i + 1);
}
}
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, u32 texture_id) {
if (!gfx || !bsp || face_id >= bsp->num_faces) return;
pxl8_mesh* mesh = pxl8_mesh_create(64, 192);
if (!mesh) return;
collect_face_to_mesh(bsp, face_id, mesh);
if (mesh->index_count > 0) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_material mat = pxl8_material_create(texture_id);
pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
}
pxl8_mesh_destroy(mesh);
}
void pxl8_bsp_render_textured(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
static int call_count = 0;
if (!gfx || !bsp || bsp->num_faces == 0) {
if (call_count++ < 5) {
pxl8_debug("bsp_render_textured: early return - gfx=%p, bsp=%p, num_faces=%u",
(void*)gfx, (void*)bsp, bsp ? bsp->num_faces : 0);
}
return;
}
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
if (!frustum) {
if (call_count++ < 5) {
pxl8_debug("bsp_render_textured: frustum is NULL!");
}
return;
}
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
static u8* rendered_faces = NULL;
static u32 rendered_faces_capacity = 0;
if (rendered_faces_capacity < bsp->num_faces) {
u8* new_buffer = realloc(rendered_faces, bsp->num_faces);
if (!new_buffer) return;
rendered_faces = new_buffer;
rendered_faces_capacity = bsp->num_faces;
}
memset(rendered_faces, 0, bsp->num_faces);
pxl8_mesh* mesh = pxl8_mesh_create(8192, 16384);
if (!mesh) return;
u32 current_texture = 0xFFFFFFFF;
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (rendered_faces[face_id]) continue;
rendered_faces[face_id] = 1;
if (!face_in_frustum(bsp, face_id, frustum)) {
continue;
}
const pxl8_bsp_face* face = &bsp->faces[face_id];
u32 texture_id = 0;
if (face->texinfo_id < bsp->num_texinfo) {
texture_id = bsp->texinfo[face->texinfo_id].miptex;
}
if (texture_id != current_texture && mesh->index_count > 0) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_material mat = pxl8_material_with_lighting(pxl8_material_with_double_sided(pxl8_material_create(current_texture)));
pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
pxl8_mesh_clear(mesh);
}
current_texture = texture_id;
collect_face_to_mesh(bsp, face_id, mesh);
}
}
if (mesh->index_count > 0) {
pxl8_mat4 identity = pxl8_mat4_identity();
pxl8_material mat = pxl8_material_with_lighting(pxl8_material_with_double_sided(pxl8_material_create(current_texture)));
pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
}
pxl8_mesh_destroy(mesh);
}
void pxl8_bsp_render_wireframe(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos, u32 color) {
if (!gfx || !bsp) return;
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
if (!frustum) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
u8 line_color = (u8)(color & 0xFF);
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i;
if (surf_idx >= bsp->num_marksurfaces) continue;
u32 face_id = bsp->marksurfaces[surf_idx];
if (face_id >= bsp->num_faces) continue;
if (!face_in_frustum(bsp, face_id, frustum)) continue;
const pxl8_bsp_face* face = &bsp->faces[face_id];
for (u32 e = 0; e < face->num_edges; e++) {
i32 surfedge_idx = face->first_edge + e;
if (surfedge_idx >= (i32)bsp->num_surfedges) continue;
u32 v0_idx, v1_idx;
if (!pxl8_bsp_get_edge_vertices(bsp, surfedge_idx, &v0_idx, &v1_idx)) continue;
pxl8_vec3 p0 = bsp->vertices[v0_idx].position;
pxl8_vec3 p1 = bsp->vertices[v1_idx].position;
pxl8_3d_draw_line(gfx, p0, p1, line_color);
}
}
}
}

164
src/world/pxl8_bsp.h Normal file
View file

@ -0,0 +1,164 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_mesh.h"
#include "pxl8_types.h"
typedef struct pxl8_bsp_edge {
u16 vertex[2];
} pxl8_bsp_edge;
typedef struct pxl8_bsp_face {
u32 first_edge;
u32 lightmap_offset;
u16 num_edges;
u16 plane_id;
u16 side;
u8 styles[4];
u16 texinfo_id;
pxl8_vec3 aabb_min;
pxl8_vec3 aabb_max;
} pxl8_bsp_face;
typedef struct pxl8_bsp_leaf {
u8 ambient_level[4];
i32 contents;
u16 first_marksurface;
i16 maxs[3];
i16 mins[3];
u16 num_marksurfaces;
i32 visofs;
} pxl8_bsp_leaf;
typedef struct pxl8_bsp_model {
i32 first_face;
i32 headnode[4];
f32 maxs[3];
f32 mins[3];
i32 num_faces;
pxl8_vec3 origin;
i32 visleafs;
} pxl8_bsp_model;
typedef struct pxl8_bsp_node {
i32 children[2];
u16 first_face;
i16 maxs[3];
i16 mins[3];
u16 num_faces;
u32 plane_id;
} pxl8_bsp_node;
typedef struct pxl8_bsp_plane {
f32 dist;
pxl8_vec3 normal;
i32 type;
} pxl8_bsp_plane;
typedef struct pxl8_bsp_texinfo {
u32 miptex;
char name[16];
f32 u_offset;
pxl8_vec3 u_axis;
f32 v_offset;
pxl8_vec3 v_axis;
} pxl8_bsp_texinfo;
typedef struct pxl8_bsp_vertex {
pxl8_vec3 position;
} pxl8_bsp_vertex;
typedef struct pxl8_bsp_lightmap {
u8 color[3];
u8 height;
u32 offset;
u8 width;
} pxl8_bsp_lightmap;
typedef struct pxl8_bsp_lightmap_sample {
u8 b;
u8 g;
u8 r;
} pxl8_bsp_lightmap_sample;
typedef struct pxl8_bsp_material_batch {
u16* face_indices;
u32 face_count;
u8 material_id;
pxl8_mesh* mesh;
} pxl8_bsp_material_batch;
typedef struct pxl8_bsp_pvs {
u8* data;
u32 size;
} pxl8_bsp_pvs;
typedef struct pxl8_bsp {
pxl8_bsp_edge* edges;
pxl8_bsp_face* faces;
pxl8_bsp_leaf* leafs;
u8* lightdata;
pxl8_bsp_lightmap* lightmaps;
u16* marksurfaces;
pxl8_bsp_material_batch* material_batches;
pxl8_bsp_model* models;
pxl8_bsp_node* nodes;
pxl8_bsp_plane* planes;
i32* surfedges;
pxl8_bsp_texinfo* texinfo;
u32* vertex_lights;
pxl8_bsp_vertex* vertices;
u8* visdata;
u32 lightdata_size;
u32 num_edges;
u32 num_faces;
u32 num_leafs;
u32 num_lightmaps;
u32 num_marksurfaces;
u32 num_material_batches;
u32 num_models;
u32 num_nodes;
u32 num_planes;
u32 num_surfedges;
u32 num_texinfo;
u32 num_vertex_lights;
u32 num_vertices;
u32 visdata_size;
} pxl8_bsp;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp);
void pxl8_bsp_destroy(pxl8_bsp* bsp);
i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos);
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to);
pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf);
void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs);
bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf);
pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b);
pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset);
pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v);
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, u32 texture_id);
void pxl8_bsp_render_textured(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos);
void pxl8_bsp_render_wireframe(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos, u32 color);
#ifdef __cplusplus
}
#endif

504
src/world/pxl8_gen.c Normal file
View file

@ -0,0 +1,504 @@
#include "pxl8_gen.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_log.h"
#include "pxl8_rng.h"
typedef struct room_grid {
u8* cells;
i32 width;
i32 height;
} room_grid;
static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
grid->width = width;
grid->height = height;
grid->cells = calloc(width * height, sizeof(u8));
return grid->cells != NULL;
}
static u8 room_grid_get(const room_grid* grid, i32 x, i32 y) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return 1;
}
return grid->cells[y * grid->width + x];
}
static void room_grid_set(room_grid* grid, i32 x, i32 y, u8 value) {
if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
return;
}
grid->cells[y * grid->width + x] = value;
}
static inline void compute_face_aabb(pxl8_bsp_face* face, const pxl8_bsp_vertex* verts, u32 vert_idx) {
face->aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
face->aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
for (u32 i = 0; i < 4; i++) {
pxl8_vec3 v = verts[vert_idx + i].position;
if (v.x < face->aabb_min.x) face->aabb_min.x = v.x;
if (v.x > face->aabb_max.x) face->aabb_max.x = v.x;
if (v.y < face->aabb_min.y) face->aabb_min.y = v.y;
if (v.y > face->aabb_max.y) face->aabb_max.y = v.y;
if (v.z < face->aabb_min.z) face->aabb_min.z = v.z;
if (v.z > face->aabb_max.z) face->aabb_max.z = v.z;
}
}
static void room_grid_fill(room_grid* grid, u8 value) {
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
room_grid_set(grid, x, y, value);
}
}
}
static pxl8_result grid_to_bsp(pxl8_bsp* bsp, const room_grid* grid) {
i32 vertex_count = 0;
i32 face_count = 0;
i32 floor_ceiling_count = 0;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
if (room_grid_get(grid, x - 1, y) == 1) face_count++;
if (room_grid_get(grid, x + 1, y) == 1) face_count++;
if (room_grid_get(grid, x, y - 1) == 1) face_count++;
if (room_grid_get(grid, x, y + 1) == 1) face_count++;
floor_ceiling_count++;
}
}
}
face_count += floor_ceiling_count * 2;
vertex_count = face_count * 4;
pxl8_debug("Cave generation: %dx%d grid -> %d faces, %d vertices",
grid->width, grid->height, face_count, vertex_count);
bsp->vertices = calloc(vertex_count, sizeof(pxl8_bsp_vertex));
bsp->faces = calloc(face_count, sizeof(pxl8_bsp_face));
bsp->planes = calloc(face_count, sizeof(pxl8_bsp_plane));
bsp->edges = calloc(vertex_count, sizeof(pxl8_bsp_edge));
bsp->surfedges = calloc(vertex_count, sizeof(i32));
if (!bsp->vertices || !bsp->faces || !bsp->planes || !bsp->edges || !bsp->surfedges) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->texinfo = NULL;
bsp->num_texinfo = 0;
i32 vert_idx = 0;
i32 face_idx = 0;
i32 edge_idx = 0;
const f32 cell_size = 64.0f;
const f32 wall_height = 128.0f;
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
if (room_grid_get(grid, x - 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){-1, 0, 0};
bsp->planes[face_idx].dist = -fx;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x + 1, y) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){1, 0, 0};
bsp->planes[face_idx].dist = fx + cell_size;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y - 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, -1};
bsp->planes[face_idx].dist = -fy;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
if (room_grid_get(grid, x, y + 1) == 1) {
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, 1};
bsp->planes[face_idx].dist = fy + cell_size;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
}
for (i32 y = 0; y < grid->height; y++) {
for (i32 x = 0; x < grid->width; x++) {
if (room_grid_get(grid, x, y) == 0) {
f32 fx = (f32)x * cell_size;
f32 fy = (f32)y * cell_size;
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, 0, fy + cell_size};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy};
bsp->planes[face_idx].normal = (pxl8_vec3){0, 1, 0};
bsp->planes[face_idx].dist = 0;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, wall_height, fy};
bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
bsp->planes[face_idx].normal = (pxl8_vec3){0, -1, 0};
bsp->planes[face_idx].dist = -wall_height;
bsp->faces[face_idx].plane_id = face_idx;
bsp->faces[face_idx].num_edges = 4;
bsp->faces[face_idx].first_edge = edge_idx;
bsp->faces[face_idx].texinfo_id = 0;
for (i32 i = 0; i < 4; i++) {
bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
bsp->surfedges[edge_idx + i] = edge_idx + i;
}
compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
vert_idx += 4;
edge_idx += 4;
face_idx++;
}
}
}
bsp->num_vertices = vertex_count;
bsp->num_faces = face_count;
bsp->num_planes = face_count;
bsp->num_edges = vertex_count;
bsp->num_surfedges = vertex_count;
bsp->leafs = calloc(1, sizeof(pxl8_bsp_leaf));
bsp->marksurfaces = calloc(face_count, sizeof(u16));
if (!bsp->leafs || !bsp->marksurfaces) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_leafs = 1;
bsp->num_marksurfaces = face_count;
bsp->leafs[0].first_marksurface = 0;
bsp->leafs[0].num_marksurfaces = face_count;
bsp->leafs[0].contents = -2;
for (i32 i = 0; i < face_count; i++) {
bsp->marksurfaces[i] = i;
}
return PXL8_OK;
}
static bool bounds_intersects(const pxl8_bounds* a, const pxl8_bounds* b) {
return !(a->x + a->w <= b->x || b->x + b->w <= a->x ||
a->y + a->h <= b->y || b->y + b->h <= a->y);
}
static void carve_corridor_h(room_grid* grid, i32 x1, i32 x2, i32 y) {
i32 start = (x1 < x2) ? x1 : x2;
i32 end = (x1 > x2) ? x1 : x2;
for (i32 x = start; x <= end; x++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x, y - 1, 0);
room_grid_set(grid, x, y + 1, 0);
}
}
static void carve_corridor_v(room_grid* grid, i32 y1, i32 y2, i32 x) {
i32 start = (y1 < y2) ? y1 : y2;
i32 end = (y1 > y2) ? y1 : y2;
for (i32 y = start; y <= end; y++) {
room_grid_set(grid, x, y, 0);
room_grid_set(grid, x - 1, y, 0);
room_grid_set(grid, x + 1, y, 0);
}
}
static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
pxl8_debug("procgen_rooms called: %dx%d, seed=%u, min=%d, max=%d, num=%d",
params->width, params->height, params->seed,
params->min_room_size, params->max_room_size, params->num_rooms);
pxl8_rng rng;
pxl8_rng_seed(&rng, params->seed);
room_grid grid;
if (!room_grid_init(&grid, params->width, params->height)) {
pxl8_error("Failed to allocate room grid");
return PXL8_ERROR_OUT_OF_MEMORY;
}
room_grid_fill(&grid, 1);
pxl8_bounds rooms[256];
i32 room_count = 0;
i32 max_attempts = params->num_rooms * 10;
for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) {
i32 w = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
i32 h = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2));
i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2));
pxl8_bounds new_room = {x, y, w, h};
bool overlaps = false;
for (i32 i = 0; i < room_count; i++) {
if (bounds_intersects(&new_room, &rooms[i])) {
overlaps = true;
break;
}
}
if (!overlaps) {
for (i32 ry = y; ry < y + h; ry++) {
for (i32 rx = x; rx < x + w; rx++) {
room_grid_set(&grid, rx, ry, 0);
}
}
if (room_count > 0) {
i32 new_cx = x + w / 2;
i32 new_cy = y + h / 2;
i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2;
i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2;
if (pxl8_rng_next(&rng) % 2 == 0) {
carve_corridor_h(&grid, prev_cx, new_cx, prev_cy);
carve_corridor_v(&grid, prev_cy, new_cy, new_cx);
} else {
carve_corridor_v(&grid, prev_cy, new_cy, prev_cx);
carve_corridor_h(&grid, prev_cx, new_cx, new_cy);
}
}
rooms[room_count++] = new_room;
}
}
pxl8_debug("Room generation: %dx%d grid -> %d rooms created",
params->width, params->height, room_count);
pxl8_result result = grid_to_bsp(bsp, &grid);
free(grid.cells);
return result;
}
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
if (!bsp || !params) {
return PXL8_ERROR_NULL_POINTER;
}
switch (params->type) {
case PXL8_PROCGEN_ROOMS:
return procgen_rooms(bsp, params);
case PXL8_PROCGEN_TERRAIN:
pxl8_error("Terrain generation not yet implemented");
return PXL8_ERROR_NOT_INITIALIZED;
default:
pxl8_error("Unknown procgen type: %d", params->type);
return PXL8_ERROR_INVALID_ARGUMENT;
}
}
static u32 hash2d(i32 x, i32 y) {
u32 h = ((u32)x * 374761393u) + ((u32)y * 668265263u);
h ^= h >> 13;
h ^= h << 17;
h ^= h >> 5;
return h;
}
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) {
if (!buffer || !params) return;
for (i32 y = 0; y < params->height; y++) {
for (i32 x = 0; x < params->width; x++) {
f32 u = (f32)x / (f32)params->width;
f32 v = (f32)y / (f32)params->height;
u8 color = params->base_color;
// Tile-based pattern (floor style)
if (params->seed == 11111) {
i32 tile_x = (i32)floorf(u * 8.0f);
i32 tile_y = (i32)floorf(v * 8.0f);
u32 h = hash2d(tile_x, tile_y);
f32 pattern = (f32)(h & 0xFF) / 255.0f;
i32 quantized = (pattern < 0.3f) ? 0 : (pattern < 0.7f) ? 1 : 2;
color = params->base_color + quantized;
// Checkerboard dither
if (((tile_x + tile_y) & 1) == 0 && (h & 0x100)) {
color = (color < 255) ? color + 1 : color;
}
}
// Large tile pattern (ceiling style)
else if (params->seed == 22222) {
i32 coarse_x = (i32)floorf(u * 2.0f);
i32 coarse_y = (i32)floorf(v * 2.0f);
u32 coarse_h = hash2d(coarse_x, coarse_y);
i32 subdivision = (coarse_h >> 8) & 0x3;
i32 tile_x, tile_y;
switch (subdivision) {
case 0: tile_x = (i32)floorf(u * 3.0f); tile_y = (i32)floorf(v * 3.0f); break;
case 1: tile_x = (i32)floorf(u * 5.0f); tile_y = (i32)floorf(v * 5.0f); break;
case 2: tile_x = (i32)floorf(u * 2.0f); tile_y = (i32)floorf(v * 4.0f); break;
default: tile_x = (i32)floorf(u * 4.0f); tile_y = (i32)floorf(v * 2.0f); break;
}
u32 h = hash2d(tile_x, tile_y);
f32 pattern = (f32)(h & 0xFF) / 255.0f;
if (pattern < 0.25f) color = params->base_color;
else if (pattern < 0.50f) color = params->base_color + 1;
else if (pattern < 0.75f) color = params->base_color + 2;
else color = params->base_color + 3;
}
// Brick pattern (wall style)
else {
f32 brick_y = floorf(v * 4.0f);
f32 offset = ((i32)brick_y & 1) ? 0.5f : 0.0f;
i32 brick_x = (i32)floorf(u * 4.0f + offset);
brick_y = (i32)brick_y;
f32 brick_u = fabsf((u * 4.0f + offset) - floorf(u * 4.0f + offset) - 0.5f);
f32 brick_v = fabsf((v * 4.0f) - floorf(v * 4.0f) - 0.5f);
u32 h = hash2d(brick_x, (i32)brick_y);
f32 noise = (f32)(h & 0xFF) / 255.0f;
// Mortar lines
if (brick_u > 0.47f || brick_v > 0.47f) {
color = params->base_color - 2;
} else {
i32 shade = (i32)(noise * 3.0f);
color = params->base_color + shade;
}
}
buffer[y * params->width + x] = color;
}
}
}

45
src/world/pxl8_gen.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
typedef enum pxl8_procgen_type {
PXL8_PROCGEN_ROOMS,
PXL8_PROCGEN_TERRAIN
} pxl8_procgen_type;
typedef struct pxl8_procgen_params {
pxl8_procgen_type type;
i32 width;
i32 height;
i32 depth;
u32 seed;
i32 min_room_size;
i32 max_room_size;
i32 num_rooms;
} pxl8_procgen_params;
typedef struct pxl8_procgen_tex_params {
char name[16];
u32 seed;
i32 width;
i32 height;
f32 scale;
f32 roughness;
u8 base_color;
u8 variation;
u8 max_color;
} pxl8_procgen_tex_params;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params);
void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params);
#ifdef __cplusplus
}
#endif

406
src/world/pxl8_world.c Normal file
View file

@ -0,0 +1,406 @@
#include "pxl8_world.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_bsp.h"
#include "pxl8_gen.h"
#include "pxl8_log.h"
#include "pxl8_math.h"
struct pxl8_world {
pxl8_bsp bsp;
bool loaded;
bool wireframe;
u32 wireframe_color;
};
pxl8_world* pxl8_world_create(void) {
pxl8_world* world = (pxl8_world*)calloc(1, sizeof(pxl8_world));
if (!world) {
pxl8_error("Failed to allocate world");
return NULL;
}
world->loaded = false;
world->wireframe_color = 15;
return world;
}
void pxl8_world_destroy(pxl8_world* world) {
if (!world) return;
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
}
free(world);
}
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params) {
if (!world || !gfx || !params) {
pxl8_error("Invalid arguments to pxl8_world_generate");
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_procgen(&world->bsp, params);
if (result != PXL8_OK) {
pxl8_error("Failed to generate world: %d", result);
pxl8_bsp_destroy(&world->bsp);
return result;
}
world->loaded = true;
return PXL8_OK;
}
pxl8_result pxl8_world_load(pxl8_world* world, const char* path) {
if (!world || !path) return PXL8_ERROR_INVALID_ARGUMENT;
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_bsp_load(path, &world->bsp);
if (result != PXL8_OK) {
pxl8_error("Failed to load world: %s", path);
return result;
}
world->loaded = true;
pxl8_info("Loaded world: %s", path);
return PXL8_OK;
}
void pxl8_world_unload(pxl8_world* world) {
if (!world || !world->loaded) return;
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count) {
if (!world || !world->loaded || !textures || count == 0) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_bsp* bsp = &world->bsp;
u32 max_texinfo = count * 6;
bsp->texinfo = calloc(max_texinfo, sizeof(pxl8_bsp_texinfo));
if (!bsp->texinfo) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_texinfo = 0;
for (u32 face_idx = 0; face_idx < bsp->num_faces; face_idx++) {
pxl8_bsp_face* face = &bsp->faces[face_idx];
pxl8_vec3 normal = bsp->planes[face->plane_id].normal;
u32 matched_texture_idx = count;
for (u32 tex_idx = 0; tex_idx < count; tex_idx++) {
if (textures[tex_idx].rule && textures[tex_idx].rule(&normal, face, bsp)) {
matched_texture_idx = tex_idx;
break;
}
}
if (matched_texture_idx >= count) {
pxl8_warn("No texture rule matched for face %u", face_idx);
continue;
}
const pxl8_world_texture* matched = &textures[matched_texture_idx];
pxl8_vec3 u_axis, v_axis;
if (fabsf(normal.y) > 0.9f) {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else if (fabsf(normal.x) > 0.7f) {
u_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
}
u32 texinfo_idx = bsp->num_texinfo;
bool found_existing = false;
for (u32 i = 0; i < bsp->num_texinfo; i++) {
if (strcmp(bsp->texinfo[i].name, matched->name) == 0 &&
bsp->texinfo[i].miptex == matched->texture_id &&
bsp->texinfo[i].u_axis.x == u_axis.x &&
bsp->texinfo[i].u_axis.y == u_axis.y &&
bsp->texinfo[i].u_axis.z == u_axis.z &&
bsp->texinfo[i].v_axis.x == v_axis.x &&
bsp->texinfo[i].v_axis.y == v_axis.y &&
bsp->texinfo[i].v_axis.z == v_axis.z) {
texinfo_idx = i;
found_existing = true;
break;
}
}
if (!found_existing) {
if (bsp->num_texinfo >= max_texinfo) {
pxl8_error("Too many unique texinfo entries");
return PXL8_ERROR_OUT_OF_MEMORY;
}
memcpy(bsp->texinfo[texinfo_idx].name, matched->name, sizeof(bsp->texinfo[texinfo_idx].name));
bsp->texinfo[texinfo_idx].name[sizeof(bsp->texinfo[texinfo_idx].name) - 1] = '\0';
bsp->texinfo[texinfo_idx].miptex = matched->texture_id;
bsp->texinfo[texinfo_idx].u_offset = 0.0f;
bsp->texinfo[texinfo_idx].v_offset = 0.0f;
bsp->texinfo[texinfo_idx].u_axis = u_axis;
bsp->texinfo[texinfo_idx].v_axis = v_axis;
bsp->num_texinfo++;
}
face->texinfo_id = texinfo_idx;
}
pxl8_info("Applied %u textures to %u faces, created %u texinfo entries",
count, bsp->num_faces, bsp->num_texinfo);
return PXL8_OK;
}
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius) {
if (!world || !world->loaded) return false;
const pxl8_bsp* bsp = &world->bsp;
for (u32 i = 0; i < bsp->num_faces; i++) {
const pxl8_bsp_face* face = &bsp->faces[i];
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) {
continue;
}
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
if (fabsf(dist) > radius) {
continue;
}
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x - radius || closest_point.x > face->aabb_max.x + radius ||
closest_point.y < face->aabb_min.y - radius || closest_point.y > face->aabb_max.y + radius ||
closest_point.z < face->aabb_min.z - radius || closest_point.z > face->aabb_max.z + radius) {
continue;
}
return true;
}
return false;
}
bool pxl8_world_is_loaded(const pxl8_world* world) {
return world && world->loaded;
}
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!world || !world->loaded) return to;
const pxl8_bsp* bsp = &world->bsp;
pxl8_vec3 pos = to;
pxl8_vec3 original_velocity = {to.x - from.x, to.y - from.y, to.z - from.z};
pxl8_vec3 clip_planes[5];
u32 num_planes = 0;
const f32 edge_epsilon = 1.2f;
const f32 radius_min = -radius + edge_epsilon;
const f32 radius_max = radius - edge_epsilon;
for (i32 iteration = 0; iteration < 4; iteration++) {
bool collided = false;
for (u32 i = 0; i < bsp->num_faces; i++) {
const pxl8_bsp_face* face = &bsp->faces[i];
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) continue;
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
f32 abs_dist = fabsf(dist);
if (abs_dist > radius) continue;
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x + radius_min || closest_point.x > face->aabb_max.x + radius_max ||
closest_point.y < face->aabb_min.y + radius_min || closest_point.y > face->aabb_max.y + radius_max ||
closest_point.z < face->aabb_min.z + radius_min || closest_point.z > face->aabb_max.z + radius_max) {
continue;
}
f32 penetration = radius - abs_dist;
if (penetration > 0.01f) {
pxl8_vec3 push_dir;
if (dist < 0) {
push_dir.x = -plane->normal.x;
push_dir.y = -plane->normal.y;
push_dir.z = -plane->normal.z;
} else {
push_dir.x = plane->normal.x;
push_dir.y = plane->normal.y;
push_dir.z = plane->normal.z;
}
bool is_new_plane = true;
for (u32 p = 0; p < num_planes; p++) {
if (pxl8_vec3_dot(push_dir, clip_planes[p]) > 0.99f) {
is_new_plane = false;
break;
}
}
if (is_new_plane && num_planes < 5) {
clip_planes[num_planes++] = push_dir;
}
pos.x += push_dir.x * penetration;
pos.y += push_dir.y * penetration;
pos.z += push_dir.z * penetration;
collided = true;
}
}
if (!collided) {
break;
}
if (num_planes >= 3) {
break;
}
}
if (num_planes == 2) {
f32 orig_vel_len_sq = original_velocity.x * original_velocity.x +
original_velocity.y * original_velocity.y +
original_velocity.z * original_velocity.z;
if (orig_vel_len_sq > 0.000001f) {
f32 vdot0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 vdot1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
f32 dot0 = fabsf(vdot0);
f32 dot1 = fabsf(vdot1);
pxl8_vec3 slide_vel;
if (dot0 < dot1) {
slide_vel.x = original_velocity.x - clip_planes[0].x * vdot0;
slide_vel.y = original_velocity.y - clip_planes[0].y * vdot0;
slide_vel.z = original_velocity.z - clip_planes[0].z * vdot0;
} else {
slide_vel.x = original_velocity.x - clip_planes[1].x * vdot1;
slide_vel.y = original_velocity.y - clip_planes[1].y * vdot1;
slide_vel.z = original_velocity.z - clip_planes[1].z * vdot1;
}
f32 slide_len = sqrtf(slide_vel.x * slide_vel.x + slide_vel.y * slide_vel.y + slide_vel.z * slide_vel.z);
if (slide_len > 0.01f) {
pos.x += slide_vel.x;
pos.y += slide_vel.y;
pos.z += slide_vel.z;
pxl8_vec3 crease_dir = pxl8_vec3_cross(clip_planes[0], clip_planes[1]);
f32 crease_len = pxl8_vec3_length(crease_dir);
if (crease_len > 0.01f && fabsf(crease_dir.y / crease_len) > 0.9f) {
f32 bias0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 bias1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
if (bias0 < 0 && bias1 < 0) {
const f32 corner_push = 0.1f;
pxl8_vec3 push_away = {
clip_planes[0].x + clip_planes[1].x,
clip_planes[0].y + clip_planes[1].y,
clip_planes[0].z + clip_planes[1].z
};
f32 push_len_sq = push_away.x * push_away.x + push_away.y * push_away.y + push_away.z * push_away.z;
if (push_len_sq > 0.000001f) {
f32 inv_push_len = corner_push / sqrtf(push_len_sq);
pos.x += push_away.x * inv_push_len;
pos.y += push_away.y * inv_push_len;
pos.z += push_away.z * inv_push_len;
}
}
}
}
}
}
f32 horizontal_movement_sq = (pos.x - from.x) * (pos.x - from.x) +
(pos.z - from.z) * (pos.z - from.z);
f32 desired_horizontal_sq = (to.x - from.x) * (to.x - from.x) +
(to.z - from.z) * (to.z - from.z);
if (desired_horizontal_sq > 0.01f && horizontal_movement_sq < desired_horizontal_sq * 0.25f) {
const f32 max_step_height = 0.4f;
pxl8_vec3 step_up = pos;
step_up.y += max_step_height;
if (!pxl8_world_check_collision(world, step_up, radius)) {
pxl8_vec3 step_forward = {
step_up.x + (to.x - pos.x),
step_up.y,
step_up.z + (to.z - pos.z)
};
if (!pxl8_world_check_collision(world, step_forward, radius)) {
pos = step_forward;
}
}
}
return pos;
}
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
if (!world || !gfx || !world->loaded) {
static int count = 0;
if (count++ < 10) {
pxl8_debug("world_render: early return - world=%p, gfx=%p, loaded=%d",
(void*)world, (void*)gfx, world ? world->loaded : -1);
}
return;
}
if (world->wireframe) {
pxl8_bsp_render_wireframe(gfx, &world->bsp, camera_pos, world->wireframe_color);
} else {
pxl8_bsp_render_textured(gfx, &world->bsp, camera_pos);
}
}

38
src/world/pxl8_world.h Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_gen.h"
#include "pxl8_gfx.h"
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef struct pxl8_world pxl8_world;
typedef bool (*pxl8_texture_rule)(const pxl8_vec3* normal, const pxl8_bsp_face* face, const pxl8_bsp* bsp);
typedef struct pxl8_world_texture {
char name[16];
u32 texture_id;
pxl8_texture_rule rule;
} pxl8_world_texture;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_world* pxl8_world_create(void);
void pxl8_world_destroy(pxl8_world* world);
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params);
pxl8_result pxl8_world_load(pxl8_world* world, const char* path);
void pxl8_world_unload(pxl8_world* world);
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count);
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius);
bool pxl8_world_is_loaded(const pxl8_world* world);
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
#ifdef __cplusplus
}
#endif