#include #include #include "pxl8_bsp.h" #include "pxl8_gfx.h" #include "pxl8_io.h" #include "pxl8_macros.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; } pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) { if (!path || !bsp) return PXL8_ERROR_INVALID_ARGUMENT; memset(bsp, 0, sizeof(*bsp)); size_t file_size; u8* file_data = (u8*)SDL_LoadFile(path, &file_size); if (!file_data) { pxl8_error("Failed to load BSP file: %s", path); return PXL8_ERROR_FILE_NOT_FOUND; } if (file_size < sizeof(pxl8_bsp_header)) { pxl8_error("BSP file too small: %s", path); SDL_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); SDL_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 = SDL_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 = SDL_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 = SDL_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 = SDL_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 = SDL_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 = SDL_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); } } 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 = SDL_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 = SDL_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 = SDL_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 = SDL_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 = SDL_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 = SDL_malloc(bsp->lightdata_size); memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size); } SDL_free(file_data); 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); SDL_free(file_data); pxl8_bsp_destroy(bsp); return PXL8_ERROR_INVALID_FORMAT; } void pxl8_bsp_destroy(pxl8_bsp* bsp) { if (!bsp) return; SDL_free(bsp->edges); SDL_free(bsp->faces); SDL_free(bsp->leafs); SDL_free(bsp->lightdata); SDL_free(bsp->marksurfaces); SDL_free(bsp->models); SDL_free(bsp->nodes); SDL_free(bsp->planes); SDL_free(bsp->surfedges); SDL_free(bsp->texinfo); SDL_free(bsp->vertices); SDL_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 || 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; i32 byte_idx = leaf_to >> 3; i32 bit_idx = leaf_to & 7; if ((u32)visofs + byte_idx >= bsp->visdata_size) return true; return (bsp->visdata[visofs + byte_idx] & (1 << bit_idx)) != 0; } void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, u32 color) { if (!gfx || !bsp || face_id >= bsp->num_faces) return; const pxl8_bsp_face* face = &bsp->faces[face_id]; if (face->num_edges < 3) return; pxl8_vec3 verts[64]; u32 num_verts = 0; for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) { i32 surfedge_idx = face->first_edge + i; if (surfedge_idx >= (i32)bsp->num_surfedges) continue; i32 edge_idx = bsp->surfedges[surfedge_idx]; u32 vert_idx; if (edge_idx >= 0) { if ((u32)edge_idx >= bsp->num_edges) continue; vert_idx = bsp->edges[edge_idx].vertex[0]; } else { edge_idx = -edge_idx; if ((u32)edge_idx >= bsp->num_edges) continue; vert_idx = bsp->edges[edge_idx].vertex[1]; } if (vert_idx >= bsp->num_vertices) continue; verts[num_verts++] = bsp->vertices[vert_idx].position; } if (num_verts < 3) return; for (u32 i = 1; i < num_verts - 1; i++) { pxl8_3d_draw_triangle_raw(gfx, verts[0], verts[i], verts[i + 1], color); } } void pxl8_bsp_render_wireframe( pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos, u32 color ) { if (!gfx || !bsp) return; i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos); 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; 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; i32 next_surfedge_idx = face->first_edge + ((e + 1) % face->num_edges); if (surfedge_idx >= (i32)bsp->num_surfedges || next_surfedge_idx >= (i32)bsp->num_surfedges) continue; i32 edge_idx = bsp->surfedges[surfedge_idx]; u32 v0_idx, v1_idx; if (edge_idx >= 0) { if ((u32)edge_idx >= bsp->num_edges) continue; v0_idx = bsp->edges[edge_idx].vertex[0]; v1_idx = bsp->edges[edge_idx].vertex[1]; } else { edge_idx = -edge_idx; if ((u32)edge_idx >= bsp->num_edges) continue; v0_idx = bsp->edges[edge_idx].vertex[1]; v1_idx = bsp->edges[edge_idx].vertex[0]; } if (v0_idx >= bsp->num_vertices || v1_idx >= bsp->num_vertices) continue; pxl8_vec3 p0 = bsp->vertices[v0_idx].position; pxl8_vec3 p1 = bsp->vertices[v1_idx].position; pxl8_3d_draw_line_3d(gfx, p0, p1, color); } } } }