#include "pxl8_bsp.h" #include #include #include #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; static int face_debug = 0; bool debug_this = (face_debug++ < 3); 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)) { if (debug_this) pxl8_debug("face %u: edge %u failed to get vertex (surfedge_idx=%d)", face_id, i, surfedge_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 (debug_this) { pxl8_debug("face %u: num_edges=%u, collected %u verts, texinfo_id=%u", face_id, face->num_edges, num_verts, face->texinfo_id); } 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_material mat = pxl8_material_new(texture_id); pxl8_3d_draw_mesh(gfx, mesh, pxl8_mat4_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; static int debug_cull = 0; u32 faces_checked = 0, faces_culled = 0, faces_passed = 0; 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; faces_checked++; if (!face_in_frustum(bsp, face_id, frustum)) { faces_culled++; continue; } faces_passed++; 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_material mat = pxl8_material_with_double_sided(pxl8_material_new(current_texture)); pxl8_3d_draw_mesh(gfx, mesh, pxl8_mat4_identity(), mat); pxl8_mesh_clear(mesh); } current_texture = texture_id; u32 before = mesh->index_count; collect_face_to_mesh(bsp, face_id, mesh); if (debug_cull < 3 && mesh->index_count > before) { pxl8_debug("Added face %u: mesh now has %u indices (was %u)", face_id, mesh->index_count, before); } } } static int draw_count = 0; if (debug_cull < 3) { pxl8_debug("Final mesh: %u indices, %u vertices", mesh->index_count, mesh->vertex_count); } if (mesh->index_count > 0) { pxl8_material mat = pxl8_material_with_double_sided(pxl8_material_new(current_texture)); pxl8_3d_draw_mesh(gfx, mesh, pxl8_mat4_identity(), mat); if (draw_count++ < 5) { pxl8_debug("bsp_render_textured: drew mesh with %u indices, camera_leaf=%d", mesh->index_count, camera_leaf); } } else if (draw_count < 5) { pxl8_debug("bsp_render_textured: mesh is empty, camera_leaf=%d, num_leafs=%u", camera_leaf, bsp->num_leafs); draw_count++; } if (debug_cull++ < 5) { pxl8_debug("bsp_render: checked=%u, culled=%u, passed=%u", faces_checked, faces_culled, faces_passed); } 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); } } } }