diff --git a/PS1BSP.vcxproj b/PS1BSP.vcxproj index 22397b3..daae418 100644 --- a/PS1BSP.vcxproj +++ b/PS1BSP.vcxproj @@ -146,6 +146,12 @@ + + + + + + diff --git a/PS1BSP.vcxproj.filters b/PS1BSP.vcxproj.filters index 4c71843..613a5ff 100644 --- a/PS1BSP.vcxproj.filters +++ b/PS1BSP.vcxproj.filters @@ -13,6 +13,9 @@ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + {4da4a411-1acb-4601-bb08-8a5edfacf240} + @@ -26,5 +29,23 @@ Header Files + + Header Files\rectpack2D + + + Header Files\rectpack2D + + + Header Files\rectpack2D + + + Header Files\rectpack2D + + + Header Files\rectpack2D + + + Header Files\rectpack2D + \ No newline at end of file diff --git a/bsp.h b/bsp.h index 1710d85..bacce60 100644 --- a/bsp.h +++ b/bsp.h @@ -52,8 +52,8 @@ typedef struct // Bounding Box, Float values typedef struct // Bounding Box, Short values { - short min; // minimum values of X,Y,Z - short max; // maximum values of X,Y,Z + short min[3]; // minimum values of X,Y,Z + short max[3]; // maximum values of X,Y,Z } bboxshort_t; typedef struct // Mip texture list header @@ -80,6 +80,14 @@ typedef struct float Z; // but coded in floating point } vertex_t; +typedef struct +{ + unsigned short vertex0; // index of the start vertex + // must be in [0,numvertices[ + unsigned short vertex1; // index of the end vertex + // must be in [0,numvertices[ +} edge_t; + typedef struct { unsigned short plane_id; // The plane in which the face lies @@ -96,3 +104,66 @@ typedef struct long lightmap; // Pointer inside the general light map, or -1 // this define the start of the face light map } face_t; + +typedef struct +{ + long plane_id; // The plane that splits the node + // must be in [0,numplanes[ + unsigned short front; // If bit15==0, index of Front child node + // If bit15==1, ~front = index of child leaf + unsigned short back; // If bit15==0, id of Back child node + // If bit15==1, ~back = id of child leaf + bboxshort_t box; // Bounding box of node and all childs + unsigned short face_id; // Index of first Polygons in the node + unsigned short face_num; // Number of faces in the node +} node_t; + +typedef struct +{ + long type; // Special type of leaf + long vislist; // Beginning of visibility lists + // must be -1 or in [0,numvislist[ + bboxshort_t bound; // Bounding box of the leaf + unsigned short lface_id; // First item of the list of faces + // must be in [0,numlfaces[ + unsigned short lface_num; // Number of faces in the leaf + unsigned char sndwater; // level of the four ambient sounds: + unsigned char sndsky; // 0 is no sound + unsigned char sndslime; // 0xFF is maximum volume + unsigned char sndlava; // +} dleaf_t; + +typedef struct +{ + const char* name; + dheader_t header; + + mipheader_t mipheader; + miptex_t* miptexes; + unsigned char** textures; + + int numVertices; + vertex_t* vertices; + + int numEdges; + edge_t* edges; + + int edgeListLength; + unsigned short* edgeList; + + int numFaces; + face_t* faces; + + int faceListLength; + unsigned short* faceList; + + int numNodes; + node_t* nodes; + + int numLeaves; + dleaf_t* leaves; + + int entitiesLength; + char* entities; + +} world_t; diff --git a/main.cpp b/main.cpp index 3efe848..c3a66f2 100644 --- a/main.cpp +++ b/main.cpp @@ -1,38 +1,22 @@ #include #include #include +#include +#include #include "bsp.h" #include "rectpack/finders_interface.h" +#include "ps1bsp.h" static char path[_MAX_PATH]; -int process_entities(dheader_t* header, FILE* f) +int process_entities(const world_t *world) { - fseek(f, header->entities.offset, SEEK_SET); - - char* entities = (char*)malloc((header->entities.size + 1) * sizeof(char)); - if (entities == NULL) - return 0; - - memset(entities, 0, (header->entities.size + 1) * sizeof(char)); - fread(entities, sizeof(char), header->entities.size, f); - - printf("Entities list:\n%s\n", entities); - free(entities); + printf("Entities list:\n%s\n", world->entities); return 1; } -int process_textures(const char* bspname, dheader_t* header, FILE* f) +int process_textures(const world_t* world) { - fseek(f, header->miptex.offset, SEEK_SET); - mipheader_t mipheader; - fread(&mipheader.numtex, sizeof(long), 1, f); - mipheader.offset = (long*)malloc(mipheader.numtex * sizeof(long)); - if (mipheader.offset == NULL) - return 0; - - fread(mipheader.offset, sizeof(long), mipheader.numtex, f); - using spaces_type = rectpack2D::empty_spaces; using rect_type = rectpack2D::output_rect_t; @@ -49,22 +33,18 @@ int process_textures(const char* bspname, dheader_t* header, FILE* f) std::vector rectangles; - miptex_t miptex; - // Try some texture packing and see if we fit inside the PS1's VRAM - for (int texNum = 0; texNum < mipheader.numtex; ++texNum) + for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) { - unsigned long miptexOffset = header->miptex.offset + mipheader.offset[texNum]; - fseek(f, miptexOffset, SEEK_SET); - fread(&miptex, sizeof(miptex_t), 1, f); + miptex_t *miptex = &world->miptexes[texNum]; //printf("Texture %d (%dx%d): %.16s\n", texNum, miptex.width, miptex.height, miptex.name); // Shrink the larger textures, but keep smaller ones at their original size - int ps1mip = miptex.width > 64 || miptex.height > 64 ? 1 : 0; + int ps1mip = miptex->width > 64 || miptex->height > 64 ? 1 : 0; - if (strcmp(miptex.name, "clip") && strcmp(miptex.name, "trigger")) - rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex.width >> ps1mip, miptex.height >> ps1mip)); + if (strcmp(miptex->name, "clip") && strcmp(miptex->name, "trigger")) + rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> ps1mip, miptex->height >> ps1mip)); else rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, 0, 0)); } @@ -81,7 +61,7 @@ int process_textures(const char* bspname, dheader_t* header, FILE* f) ) ); - printf("%d textures. Packed texture atlas size: %d x %d\n", mipheader.numtex, result_size.w, result_size.h); + printf("%d textures. Packed texture atlas size: %d x %d\n", world->mipheader.numtex, result_size.w, result_size.h); unsigned char* atlas = (unsigned char*)malloc(result_size.w * result_size.h * sizeof(unsigned char)); if (atlas == NULL) return 0; @@ -89,36 +69,30 @@ int process_textures(const char* bspname, dheader_t* header, FILE* f) memset(atlas, 0, result_size.w * result_size.h * sizeof(unsigned char)); // Try to construct the texture atlas, see what we get - for (int texNum = 0; texNum < mipheader.numtex; ++texNum) + for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) { - unsigned long miptexOffset = header->miptex.offset + mipheader.offset[texNum]; - fseek(f, miptexOffset, SEEK_SET); - fread(&miptex, sizeof(miptex_t), 1, f); + miptex_t* miptex = &world->miptexes[texNum]; - char* outName = miptex.name; + char* outName = miptex->name; if (*outName == '*' || *outName == '+') outName++; for (int mipLevel = 0; mipLevel < 4; ++mipLevel) { - unsigned long mipOffset = *(&miptex.offset1 + mipLevel); - fseek(f, miptexOffset + mipOffset, SEEK_SET); - - size_t numBytes = (miptex.width * miptex.height) >> mipLevel; - unsigned char* texBytes = (unsigned char*)malloc(sizeof(unsigned char) * numBytes); - fread(texBytes, sizeof(unsigned char), numBytes, f); + unsigned char* texBytes = world->textures[texNum * 4 + mipLevel]; - FILE* fout; - sprintf_s(path, _MAX_PATH, "textures/%s-%s-mip%d-%dx%d.raw", bspname, outName, mipLevel, miptex.width >> mipLevel, miptex.height >> mipLevel); - fopen_s(&fout, path, "wb"); - if (fout != NULL) + FILE* fraw; + sprintf_s(path, _MAX_PATH, "textures/%s-%s-mip%d-%dx%d.raw", world->name, outName, mipLevel, miptex->width >> mipLevel, miptex->height >> mipLevel); + fopen_s(&fraw, path, "wb"); + if (fraw != NULL) { - fwrite(texBytes, sizeof(unsigned char), numBytes, fout); - fclose(fout); + size_t numBytes = (miptex->width * miptex->height) >> mipLevel; + fwrite(texBytes, sizeof(unsigned char), numBytes, fraw); + fclose(fraw); } const auto& rectangle = rectangles[texNum]; - if (miptex.width >> mipLevel == rectangle.w) // This is the mip level we've previously decided we want for our PS1 atlas + if (miptex->width >> mipLevel == rectangle.w) // This is the mip level we've previously decided we want for our PS1 atlas { //printf("Writing texture %s mip %d to position: (%d, %d) w = %d, h = %d\n", miptex.name, mipLevel, rectangle.x, rectangle.y, rectangle.w, rectangle.h); for (int y = 0; y < rectangle.h; ++y) @@ -126,13 +100,11 @@ int process_textures(const char* bspname, dheader_t* header, FILE* f) memcpy_s(atlas + ((rectangle.y + y) * result_size.w + rectangle.x), rectangle.w * sizeof(unsigned char), texBytes + (y * rectangle.w), rectangle.w * sizeof(unsigned char)); } } - - free(texBytes); } } FILE* fatlas; - sprintf_s(path, _MAX_PATH, "%s-atlas-%dx%d.raw", bspname, result_size.w, result_size.h); + sprintf_s(path, _MAX_PATH, "%s-atlas-%dx%d.raw", world->name, result_size.w, result_size.h); fopen_s(&fatlas, path, "wb"); if (fatlas != NULL) { @@ -141,26 +113,15 @@ int process_textures(const char* bspname, dheader_t* header, FILE* f) } free(atlas); - free(mipheader.offset); return 1; } -int process_vertices(dheader_t* header, FILE* f) +int process_vertices(const world_t* world) { - int numVertices = header->vertices.size / sizeof(vertex_t); - int numFaces = header->faces.size / sizeof(face_t); - - vertex_t* vertices = (vertex_t*)malloc(header->vertices.size); - if (vertices == NULL) - return 0; - - fseek(f, header->vertices.offset, SEEK_SET); - fread(vertices, sizeof(vertex_t), numVertices, f); - vec3_t min = { FLT_MAX, FLT_MAX, FLT_MAX }, max = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; - for (int vertIdx = 0; vertIdx < numVertices; ++vertIdx) + for (int vertIdx = 0; vertIdx < world->numVertices; ++vertIdx) { - vertex_t* vert = &vertices[vertIdx]; + vertex_t* vert = &world->vertices[vertIdx]; if (vert->X > max.x) max.x = vert->X; if (vert->Y > max.y) max.y = vert->Y; if (vert->Z > max.z) max.z = vert->Z; @@ -169,54 +130,269 @@ int process_vertices(dheader_t* header, FILE* f) if (vert->Z < min.z) min.z = vert->Z; } - printf("%d vertices, %d faces, min = (%f, %f, %f), max = (%f, %f, %f)\n", numVertices, numFaces, min.x, min.y, min.z, max.x, max.y, max.z); + printf("%d vertices, %d faces, min = (%f, %f, %f), max = (%f, %f, %f)\n", world->numVertices, world->numFaces, min.x, min.y, min.z, max.x, max.y, max.z); const int fixedScale = 1 << 14; int fixedMin[3] = { (int)(min.x * fixedScale), (int)(min.y * fixedScale), (int)(min.z * fixedScale) }; int fixedMax[3] = { (int)(max.x * fixedScale), (int)(max.y * fixedScale), (int)(max.z * fixedScale) }; printf("Fixed point min = (%d, %d, %d), max = (%d, %d, %d)\n", fixedMin[0], fixedMin[1], fixedMin[2], fixedMax[0], fixedMax[1], fixedMax[2]); + return 1; } -int process_bsp(const char* bspname) +typedef struct { - FILE* f; - dheader_t header; + vertex_t* vertex; + unsigned short index; +} vertexref_t; - sprintf_s(path, _MAX_PATH, "%s.bsp", bspname); - fopen_s(&f, path, "rb"); - if (f == NULL) +int process_faces(const world_t* world) +{ + // Write some data to a file + FILE* fbsp; + fopen_s(&fbsp, "test.ps1bsp", "wb"); + if (!fbsp) return 0; - fread(&header, sizeof(dheader_t), 1, f); - printf("Header model version: %d\n", header.version); + ps1bsp_header_t outHeader = { 0 }; // Write an empty placeholder header first + fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp); + + // TODO: convert vertices and group them by material properties (texture, lightmap) and floating origin zone (based on leaf data), duplicate where necessary + + // Organize vertex indices so we can shuffle them around and duplicate them, while keeping track of which vertex is where + std::unordered_map vertexRefs; + for (unsigned short i = 0; i < world->header.vertices.size / sizeof(vertex_t); ++i) + { + vertex_t* v = &world->vertices[i]; + vertexRefs[v] = vertexref_t{ v, i }; + } + + // Write vertex data to a file (no vertex splitting yet) + for (unsigned short i = 0; i < world->header.vertices.size / sizeof(vertex_t); ++i) + { + // TODO: we should respect the ordering from vertexRef->index here but meh, problem for later + vertex_t* inVertex = &world->vertices[i]; + ps1bsp_vertex_t outVertex = { 0 }; + if (inVertex->X > -8192 && inVertex->X < 8192 && inVertex->Y > -8192 && inVertex->Y < 8192 && inVertex->Z > -8192 && inVertex->Z < 8192) + { + outVertex.x = (short)(inVertex->X * 4); + outVertex.y = (short)(inVertex->Y * 4); + outVertex.z = (short)(inVertex->Z * 4); + } + outVertex.baseLight = 128; + outVertex.finalLight = 128; + + fwrite(&outVertex, sizeof(ps1bsp_vertex_t), 1, fbsp); + } + outHeader.numVertices = (unsigned short)(world->header.vertices.size / sizeof(vertex_t)); + + std::vector faceVerts; + for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) + { + face_t* face = &world->faces[faceIdx]; + + for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx) + { + short edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; + + int vertIndex = edgeIdx > 0 ? + world->edges[edgeIdx].vertex0 : + world->edges[-edgeIdx].vertex1; + + vertex_t* v = &world->vertices[vertIndex]; + faceVerts.push_back(&vertexRefs[v]); + } + + printf("Face %d: %d vertices\n", faceIdx, faceVerts.size()); + + // Triangulate face into polygons (triangle fan, the naive method) + + // Write polygon and face data to a file + faceVerts.clear(); + } + + // Write final header + fseek(fbsp, 0, SEEK_SET); + fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp); + fclose(fbsp); + + return 1; +} + +int process_bsp(const world_t *world) +{ // Test reading the entity string data - if (!process_entities(&header, f)) + if (!process_entities(world)) { - fclose(f); return 0; } // Test exporting texture data - if (!process_textures(bspname, &header, f)) + if (!process_textures(world)) { - fclose(f); return 0; } // Inspect vertex data - if (!process_vertices(&header, f)) + if (!process_vertices(world)) { - fclose(f); return 0; } + // Inspect faces/edges data + if (!process_faces(world)) + { + return 0; + } + + return 1; +} + +int load_bsp(const char* bspname, world_t* world) +{ + FILE* f; + dheader_t* header = &world->header; + + world->name = bspname; + + sprintf_s(path, _MAX_PATH, "%s.bsp", bspname); + fopen_s(&f, path, "rb"); + if (f == NULL) + return 0; + + fread(header, sizeof(dheader_t), 1, f); + printf("Header model version: %d\n", header->version); + + // Load entities + fseek(f, header->entities.offset, SEEK_SET); + world->entitiesLength = header->entities.size + 1; + world->entities = (char*)malloc(world->entitiesLength * sizeof(char)); + if (world->entities == NULL) + return 0; + + memset(world->entities, 0, world->entitiesLength * sizeof(char)); + fread(world->entities, sizeof(char), world->entitiesLength, f); + + // Load textures + mipheader_t* mipheader = &world->mipheader; + fseek(f, header->miptex.offset, SEEK_SET); + fread(&mipheader->numtex, sizeof(long), 1, f); + mipheader->offset = (long*)malloc(mipheader->numtex * sizeof(long)); + if (mipheader->offset == NULL) + return 0; + + fread(mipheader->offset, sizeof(long), mipheader->numtex, f); + + world->miptexes = (miptex_t*)malloc(mipheader->numtex * sizeof(miptex_t)); + if (world->miptexes == NULL) + return 0; + + const int numMipLevels = 4; + world->textures = (unsigned char**)malloc(mipheader->numtex * numMipLevels * sizeof(unsigned char*)); + if (world->textures == NULL) + return 0; + + memset(world->textures, 0, mipheader->numtex * numMipLevels * sizeof(unsigned char*)); + + for (int texNum = 0; texNum < mipheader->numtex; ++texNum) + { + miptex_t* miptex = &world->miptexes[texNum]; + + unsigned long miptexOffset = header->miptex.offset + mipheader->offset[texNum]; + fseek(f, miptexOffset, SEEK_SET); + fread(miptex, sizeof(miptex_t), 1, f); + + for (int mipLevel = 0; mipLevel < numMipLevels; ++mipLevel) + { + unsigned long mipOffset = *(&miptex->offset1 + mipLevel); + fseek(f, miptexOffset + mipOffset, SEEK_SET); + + size_t numBytes = (miptex->width * miptex->height) >> mipLevel; + unsigned char* texBytes = (unsigned char*)malloc(sizeof(unsigned char) * numBytes); + if (texBytes == NULL) + return 0; + + fread(texBytes, sizeof(unsigned char), numBytes, f); + world->textures[texNum * numMipLevels + mipLevel] = texBytes; + } + } + + // Load vertices + world->numVertices = header->vertices.size / sizeof(vertex_t); + world->vertices = (vertex_t*)malloc(header->vertices.size); + if (world->vertices == NULL) + return 0; + + fseek(f, header->vertices.offset, SEEK_SET); + fread(world->vertices, sizeof(vertex_t), world->numVertices, f); + + // Load edges + world->numEdges = header->edges.size / sizeof(edge_t); + world->edges = (edge_t*)malloc(header->edges.size); + if (world->edges == NULL) + return 0; + + fseek(f, header->edges.offset, SEEK_SET); + fread(world->edges, sizeof(edge_t), world->numEdges, f); + + world->edgeListLength = header->ledges.size / sizeof(unsigned short); + world->edgeList = (unsigned short*)malloc(header->ledges.size); + if (world->edgeList == NULL) + return 0; + + fseek(f, header->ledges.offset, SEEK_SET); + fread(world->edgeList, sizeof(unsigned short), world->edgeListLength, f); + + // Load faces + world->numFaces = header->faces.size / sizeof(face_t); + world->faces = (face_t*)malloc(header->faces.size); + if (world->faces == NULL) + return 0; + + fseek(f, header->faces.offset, SEEK_SET); + fread(world->faces, sizeof(face_t), world->numFaces, f); + + world->faceListLength = header->lface.size / sizeof(unsigned short); + world->faceList = (unsigned short*)malloc(header->lface.size); + if (world->faceList == NULL) + return 0; + + fseek(f, header->lface.offset, SEEK_SET); + fread(world->faceList, sizeof(unsigned short), world->faceListLength, f); + fclose(f); return 1; } +void free_bsp(world_t* world) +{ + free(world->faces); + free(world->faceList); + free(world->edges); + free(world->edgeList); + free(world->vertices); + + for (int i = 0; i < world->mipheader.numtex; ++i) + { + free(world->textures[i]); + } + + free(world->textures); + free(world->miptexes); + free(world->mipheader.offset); + + free(world->entities); +} + int main(int argc, char** argv) { - return !process_bsp(argv[1]); + world_t world = { 0 }; + if (!load_bsp(argv[1], &world)) + return 1; + + int result = process_bsp(&world); + + free_bsp(&world); + return !result; } diff --git a/ps1bsp.h b/ps1bsp.h index 7868b66..d3086cb 100644 --- a/ps1bsp.h +++ b/ps1bsp.h @@ -5,6 +5,27 @@ extern "C" { #endif +/* +Probable rendering process: +- Determine visible leaves based on PVS and frustum culling, the usual. +- Chain together faces/polygons to be drawn. Possibly grouped by texture ID? + Texture chaining might improve performance by making better use of texture cache, but we'll still be separating polygons based on depth anyway, so the advantage is questionable. + Texture chaining should probably be the last optimization we try. +- Tessellate polygons close to the camera by recursively cutting edges in two, up to X times based on camera distance/screen size. Possibly GTE can aid with averaging coordinates? => look at GPF/GPL (general purpose interpolation) +- Collect all vertices that need to be transformed, put them through GTE, update lighting values if needed, and cache the results. + (It may not be worth it to collect and precalculate vertices, as keeping track of all the administration also comes at a considerable cost.) +- Draw all the (tessellated) polygons using the precalculated vertex positions. Use GTE to calculate average depth and order polygons. + Note: we may not have to calculate average depth for BSP polygons, as the leafs already provide an ordering, and leafs are convex so there is no need to sort the polygons within. + We do however need some kind of depth value per leaf to insert alias models at the correct positions in the ordering table. +*/ + +typedef struct +{ + unsigned short numVertices; + unsigned short numTriangles; + unsigned short numFaces; +} ps1bsp_header_t; + typedef struct { unsigned char w, h; // These may be necessary for scaling UVs, especially since we use a mix of mip0 and mip1 textures @@ -22,9 +43,21 @@ typedef struct short y; short z; unsigned char baseLight, finalLight; // Used for gouraud shading based on static lightmap data + + // Sampled color value from the face texture, for untextured gouraud shaded drawing + unsigned char a : 1; // 0 = opaque, 1 = semi-transparent + unsigned char r : 5; + unsigned char g : 5; + unsigned char b : 5; + +#ifdef _WIN32 + // Extra fields used for administration during the conversion process + unsigned short index; +#endif } ps1bsp_vertex_t; // Instead of edges as in the original BSP format, we store triangles for easy consumption by the PS1 +// Note: it may actually be more efficient to render quads for faces with 4+ vertices typedef struct { unsigned short vertex0; @@ -32,6 +65,15 @@ typedef struct unsigned short vertex2; } ps1bsp_triangle_t; +typedef struct +{ + unsigned short firstTriangleId; + unsigned short numTriangle; + + unsigned short firstQuadId; // For if/when we decide to add quads to the mix + unsigned short numQuads; +} ps1bsp_face_t; + // Pre-parsed and encoded entity data (this runs the risk of becoming too bloated) typedef struct { @@ -39,7 +81,7 @@ typedef struct short angle[3]; // Can store both mangle (all axes) and just angle (Z-axis rotation only) int origin[3]; // In 12-bit fixed point coordinates unsigned int spawnflags; - unsigned short message_id; // Index into a pool of pre-defined messages + unsigned short messageId; // Index into a pool of pre-defined messages } ps1bsp_entity_t; typedef struct