diff --git a/PS1BSP.vcxproj b/PS1BSP.vcxproj index e5c9747..b0f337a 100644 --- a/PS1BSP.vcxproj +++ b/PS1BSP.vcxproj @@ -143,6 +143,7 @@ + @@ -156,6 +157,7 @@ + diff --git a/PS1BSP.vcxproj.filters b/PS1BSP.vcxproj.filters index 9dac1b5..18876d9 100644 --- a/PS1BSP.vcxproj.filters +++ b/PS1BSP.vcxproj.filters @@ -24,6 +24,9 @@ Header Files + + Header Files + @@ -59,5 +62,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/bsp.h b/bsp.h index 1cd6978..0d85329 100644 --- a/bsp.h +++ b/bsp.h @@ -127,6 +127,17 @@ typedef struct // must be in [0,numvertices[ } edge_t; +typedef struct +{ + vec3_t vectorS; // S vector, horizontal in texture space) + scalar_t distS; // horizontal offset in texture space + vec3_t vectorT; // T vector, vertical in texture space + scalar_t distT; // vertical offset in texture space + unsigned long texture_id; // Index of Mip Texture + // must be in [0,numtex[ + unsigned long animated; // 0 for ordinary textures, 1 for water +} texinfo_t; + typedef struct { unsigned short plane_id; // The plane in which the face lies @@ -193,6 +204,9 @@ typedef struct World int edgeListLength; int* edgeList; + int numTexInfos; + texinfo_t* texInfos; + int numFaces; face_t* faces; diff --git a/main.cpp b/main.cpp index abdca5a..028d457 100644 --- a/main.cpp +++ b/main.cpp @@ -1,167 +1,12 @@ #include "common.h" #include "bsp.h" -#include "rectpack/finders_interface.h" #include "ps1types.h" #include "ps1bsp.h" #include "lighting.h" +#include "texture.h" static char path[_MAX_PATH]; -int process_entities(const world_t *world) -{ - printf("Entities list:\n%s\n", world->entities); - return 1; -} - -int process_textures(const world_t* world) -{ - using spaces_type = rectpack2D::empty_spaces; - using rect_type = rectpack2D::output_rect_t; - - auto report_successful = [](rect_type&) { - return rectpack2D::callback_result::CONTINUE_PACKING; - }; - - auto report_unsuccessful = [](rect_type&) { - return rectpack2D::callback_result::ABORT_PACKING; - }; - - const auto max_side = 512; // Max height of PS1 VRAM. 8-bit textures take up half the horizontal space so this is 512x256 in practice, or a quarter of the PS1's VRAM allocation. - const auto discard_step = -4; - - std::vector rectangles; - - // Try some texture packing and see if we fit inside the PS1's VRAM - for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) - { - miptex_t *miptex = &world->miptexes[texNum]; - if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps? - miptex->width = miptex->height = 0; - - //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; - - 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)); - } - - // Automatic atlas packing. Nice but it tries to make a square atlas which is not what we want. (This is solved by hacking the header itself) - const auto result_size = rectpack2D::find_best_packing( - rectangles, - rectpack2D::make_finder_input( - max_side, - discard_step, - report_successful, - report_unsuccessful, - rectpack2D::flipping_option::DISABLED - ) - ); - - 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; - - 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 < world->mipheader.numtex; ++texNum) - { - miptex_t* miptex = &world->miptexes[texNum]; - if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps? - continue; - - char* outName = miptex->name; - if (*outName == '*' || *outName == '+') - outName++; - - for (int mipLevel = 0; mipLevel < 4; ++mipLevel) - { - unsigned char* texBytes = world->textures[texNum * 4 + mipLevel]; - - 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) - { - 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 - { - //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) - { - 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)); - } - } - } - } - - FILE* fatlas; - 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) - { - fwrite(atlas, sizeof(unsigned char), result_size.w * result_size.h, fatlas); - fclose(fatlas); - } - - free(atlas); - return 1; -} - -int process_vertices(const world_t* world) -{ - vec3_t min = { FLT_MAX, FLT_MAX, FLT_MAX }, max = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; - for (int vertIdx = 0; vertIdx < world->numVertices; ++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; - if (vert->X < min.x) min.x = vert->X; - if (vert->Y < min.y) min.y = vert->Y; - if (vert->Z < min.z) min.z = vert->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; -} - -typedef struct -{ - vertex_t* vertex; - unsigned short index; -} vertexref_t; - -// Determines a floating origin for the given leaf -static void leaf_zone(const dleaf_t* leaf, short zone[3]) -{ - const unsigned short mask = 0xFE00; // Zero out the 9 least significant bits - - short midX = (leaf->bound.min[0] + leaf->bound.max[0]) / 2; - short midY = (leaf->bound.min[1] + leaf->bound.max[1]) / 2; - short midZ = (leaf->bound.min[2] + leaf->bound.max[2]) / 2; - - zone[0] = midX & mask; - zone[1] = midY & mask; - zone[2] = midZ & mask; -} - template size_t writeMapData(const std::vector& data, ps1bsp_dentry_t& dentry, FILE* f) { dentry.offset = (unsigned int)ftell(f); @@ -225,7 +70,7 @@ static float computeFaceArea(const world_t* world, const face_t* face) return extents.x * extents.y; } -int process_faces(const world_t* world) +int process_faces(const world_t* world, const std::vector& textures) { // Write some data to a file FILE* fbsp; @@ -271,11 +116,15 @@ int process_faces(const world_t* world) for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) { face_t* face = &world->faces[faceIdx]; + const texinfo_t* texinfo = &world->texInfos[face->texinfo_id]; + const miptex_t* miptex = &world->miptexes[texinfo->texture_id]; + const ps1bsp_texture_t& ps1tex = textures[texinfo->texture_id]; ps1bsp_face_t outFace = { 0 }; outFace.planeId = face->plane_id; outFace.side = face->side; outFace.firstFaceVertex = (unsigned short)outFaceVertices.size(); + outFace.textureId = (unsigned char)face->texinfo_id; // Traverse the list of face edges to collect all of the face's vertices Vec3 vertexSum; @@ -288,14 +137,24 @@ int process_faces(const world_t* world) world->edges[edgeIdx].vertex0 : world->edges[-edgeIdx].vertex1; + const vertex_t* vertex = &world->vertices[vertIndex]; + Vec3 vertexPoint = vertex->toVec(); + ps1bsp_facevertex_t faceVertex; faceVertex.index = vertIndex; faceVertex.light = 0; outFaceVertices.push_back(faceVertex); + // Calculate texture UVs + float s = (vertexPoint.dotProduct(texinfo->vectorS) + texinfo->distS) / miptex->width; + float t = (vertexPoint.dotProduct(texinfo->vectorT) + texinfo->distT) / miptex->height; + while (s > 1) s -= 1; while (s < 0) s += 1; // TODO: this is a nasty fudge to deal with the lack of texture tiling on PS1 hardware + while (t > 1) t -= 1; while (t < 0) t += 1; // We'll need to break up the faces and manually tile I guess... + // Rescale the UVs to the dimensions of the mipmap we've selected for our texture atlas + faceVertex.u = (unsigned char)(s * ps1tex.w); + faceVertex.v = (unsigned char)(t * ps1tex.h); + // Calculate bounding box of this face - const vertex_t* vertex = &world->vertices[vertIndex]; - Vec3 vertexPoint = vertex->toVec(); if (edgeListIdx == 0) bounds.init(vertexPoint); else @@ -317,13 +176,15 @@ int process_faces(const world_t* world) //if (face->ledge_num >= 10) // export_lightmap(world, face, bounds, faceIdx); - outFace.numFaceVertices = (unsigned short)(outFaceVertices.size() - outFace.firstFaceVertex); + outFace.numFaceVertices = (unsigned char)(outFaceVertices.size() - outFace.firstFaceVertex); outFace.center = convertWorldPosition(vertexSum / outFace.numFaceVertices); float area = computeFaceArea(world, face); outFace.center.pad = (short)(sqrt(area)); outFaces.push_back(outFace); } + SurfaceList surfaces = group_surfaces(world, vertexFaces); + // Iterate over all faces again; now that we know the bounds of each face, we can calculate lighting for all of them for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) { @@ -391,6 +252,7 @@ int process_faces(const world_t* world) std::vector outVisData(world->visList, world->visList + world->visListLength); // Write collected data to file and update header info + writeMapData(textures, outHeader.textures, fbsp); writeMapData(outVertices, outHeader.vertices, fbsp); writeMapData(outFaces, outHeader.faces, fbsp); writeMapData(outFaceVertices, outHeader.faceVertices, fbsp); @@ -414,26 +276,15 @@ int process_faces(const world_t* world) int process_bsp(const world_t *world) { - // Test reading the entity string data - if (!process_entities(world)) - { - return 0; - } - // Test exporting texture data - if (!process_textures(world)) - { - return 0; - } - - // Inspect vertex data - if (!process_vertices(world)) + std::vector textures; + if (!process_textures(world, textures)) { return 0; } // Inspect faces/edges data - if (!process_faces(world)) + if (!process_faces(world, textures)) { return 0; } @@ -545,6 +396,15 @@ int load_bsp(const char* bspname, world_t* world) fseek(f, header->ledges.offset, SEEK_SET); fread(world->edgeList, sizeof(int), world->edgeListLength, f); + // Load texture info + world->numTexInfos = header->texinfo.size / sizeof(texinfo_t); + world->texInfos = (texinfo_t*)malloc(header->texinfo.size); + if (world->texInfos == NULL) + return 0; + + fseek(f, header->texinfo.offset, SEEK_SET); + fread(world->texInfos, sizeof(texinfo_t), world->numTexInfos, f); + // Load faces world->numFaces = header->faces.size / sizeof(face_t); world->faces = (face_t*)malloc(header->faces.size); @@ -604,10 +464,12 @@ int load_bsp(const char* bspname, world_t* world) void free_bsp(world_t* world) { + free(world->lightmap); free(world->leaves); free(world->nodes); free(world->visList); + free(world->texInfos); free(world->faces); free(world->faceList); free(world->edges); diff --git a/ps1bsp.h b/ps1bsp.h index 4e3ec83..ee293d7 100644 --- a/ps1bsp.h +++ b/ps1bsp.h @@ -28,6 +28,7 @@ typedef struct { u_short version; + ps1bsp_dentry_t textures; ps1bsp_dentry_t vertices; ps1bsp_dentry_t faces; ps1bsp_dentry_t faceVertices; @@ -41,17 +42,15 @@ typedef struct typedef struct { unsigned char w, h; // These may be necessary for scaling UVs, especially since we use a mix of mip0 and mip1 textures - int tpage; // Texture page in PS1 VRAM (precalculated when generating the texture atlas) - short uoffs, voffs; // Texture coordinate offset within the texture page + unsigned short tpage; // Texture page in PS1 VRAM (precalculated when generating the texture atlas) + unsigned char uoffs, voffs; // Texture coordinate offset within the texture page unsigned short nextframe; // If non-zero, the texture is animated and this points to the next texture in the sequence } ps1bsp_texture_t; // This matches the SVECTOR data type; we can use the extra padding to store some more data. typedef struct { - short x; - short y; - short z; + short x, y, z; short pad; } ps1bsp_vertex_t; @@ -60,8 +59,13 @@ typedef struct unsigned short index; unsigned short light; - // TODO: add texture uv's - // TODO: add sampled texture color * light, for untextured gouraud shaded drawing at range + unsigned char u, v; // TODO: make into unsigned short, clamp/mask/modulo to u_char at run-time. So we can build tiling polygons later. + + // Sampled texture color * light, for untextured gouraud shaded drawing at range + unsigned char a : 1; + unsigned char r : 5; + unsigned char g : 5; + unsigned char b : 5; } ps1bsp_facevertex_t; typedef struct @@ -70,7 +74,9 @@ typedef struct unsigned short side; unsigned short firstFaceVertex; - unsigned short numFaceVertices; + unsigned char numFaceVertices; + + unsigned char textureId; SVECTOR center; @@ -120,7 +126,7 @@ typedef struct typedef struct { unsigned short length; - char message[]; + char message[1]; } ps1bsp_message_t; #ifdef __cplusplus diff --git a/texture.cpp b/texture.cpp new file mode 100644 index 0000000..dd784bd --- /dev/null +++ b/texture.cpp @@ -0,0 +1,150 @@ +#include "common.h" +#include "bsp.h" +#include "ps1types.h" +#include "ps1bsp.h" +#include "texture.h" + +#include "rectpack/finders_interface.h" + +static char path[_MAX_PATH]; + +#define getTPage(tp, abr, x, y) ( \ + (((x) / 64) & 15) | \ + ((((y) / 256) & 1) << 4) | \ + (((abr) & 3) << 5) | \ + (((tp) & 3) << 7) | \ + ((((y) / 512) & 1) << 11) \ +) + +bool process_textures(const world_t* world, std::vector& outTextures) +{ + using spaces_type = rectpack2D::empty_spaces; + using rect_type = rectpack2D::output_rect_t; + + auto report_successful = [](rect_type&) { + return rectpack2D::callback_result::CONTINUE_PACKING; + }; + + auto report_unsuccessful = [](rect_type&) { + return rectpack2D::callback_result::ABORT_PACKING; + }; + + const auto max_side = 512; // Max height of PS1 VRAM. 8-bit textures take up half the horizontal space so this is 512x256 in practice, or a quarter of the PS1's VRAM allocation. + const auto discard_step = -4; + + std::vector rectangles; + + // Try some texture packing and see if we fit inside the PS1's VRAM + for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) + { + miptex_t* miptex = &world->miptexes[texNum]; + if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps? + miptex->width = miptex->height = 0; + + //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; + + 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)); + } + + // Automatic atlas packing. Nice but it tries to make a square atlas which is not what we want. (This is solved by hacking the header itself) + const auto result_size = rectpack2D::find_best_packing( + rectangles, + rectpack2D::make_finder_input( + max_side, + discard_step, + report_successful, + report_unsuccessful, + rectpack2D::flipping_option::DISABLED + ) + ); + + 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 false; + + 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 < world->mipheader.numtex; ++texNum) + { + miptex_t* miptex = &world->miptexes[texNum]; + if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps? + { + outTextures.push_back(ps1bsp_texture_t{ 0 }); // We have to add something, otherwise the texture IDs get messed up + continue; + } + + char* outName = miptex->name; + if (*outName == '*' || *outName == '+') + outName++; + + for (int mipLevel = 0; mipLevel < 4; ++mipLevel) + { + unsigned char* texBytes = world->textures[texNum * 4 + mipLevel]; + + // Dump each individual texture + //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) + //{ + // 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 + { + //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) + { + 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)); + } + + ps1bsp_texture_t ps1tex = { 0 }; + ps1tex.w = (u_char)miptex->width; + ps1tex.h = (u_char)miptex->height; + + u_short x = rectangle.x + 512; + u_short y = rectangle.y + 256; + + // prect is derived from the texture's position inside the atlas (rectangle.x/y) and the planned position of the atlas in VRAM (512, 256) + // mode is always 1 (8-bit palletized) + //texture->uoffs = (texture->prect.x % 64) << (2 - (texture->mode & 0x3)); + //texture->voffs = (texture->prect.y & 0xFF); + + /* + tp specifies the color depth for the texture page in the range of 0 to 2 (0:4-bit, 1:8-bit, 2:16-bit). + abr specifies the blend operator for both non-textured and textured semi-transparent primitives which can be ignored for now and lastly, + x,y specifies the X,Y coordinates of the VRAM in 16-bit pixel units. + Keep in mind that the coordinates will be rounded down to the next lowest texture page. */ + + ps1tex.tpage = getTPage(1, 0, x, y); + ps1tex.uoffs = (u_char)((x % 64) << 1); + ps1tex.voffs = (u_char)(y & 0xFF); + // TODO: animated textures + outTextures.push_back(ps1tex); + } + } + } + + FILE* fatlas; + 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) + { + fwrite(atlas, sizeof(unsigned char), result_size.w * result_size.h, fatlas); + fclose(fatlas); + } + + free(atlas); + return true; +} diff --git a/texture.h b/texture.h new file mode 100644 index 0000000..6356d8c --- /dev/null +++ b/texture.h @@ -0,0 +1,3 @@ +#pragma once + +bool process_textures(const world_t* world, std::vector& outTextures);