commit 8f82f467a0063221b0391f5fa2608801168e26d5 Author: Nico de Poel Date: Fri Sep 23 10:21:13 2022 +0200 First import of PS1 MDL/BSP tools project, long overdue diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..229c7aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.vs/ +Debug/ +*.raw +*.pcx +*.RoQ +*.wav +*.mkv +*.avi +*.png +*.tim +*.ps1mdl +*.tga diff --git a/N64E1M1.bsp b/N64E1M1.bsp new file mode 100644 index 0000000..5460a08 Binary files /dev/null and b/N64E1M1.bsp differ diff --git a/N64E2M2.bsp b/N64E2M2.bsp new file mode 100644 index 0000000..8e2edd3 Binary files /dev/null and b/N64E2M2.bsp differ diff --git a/PS1BSP.sln b/PS1BSP.sln new file mode 100644 index 0000000..0dc6f6e --- /dev/null +++ b/PS1BSP.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.32002.261 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PS1BSP", "PS1BSP.vcxproj", "{F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PS1MDL", "PS1MDL\PS1MDL.vcxproj", "{07BE9153-FB50-492F-9456-ADE90372E575}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Debug|x64.ActiveCfg = Debug|x64 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Debug|x64.Build.0 = Debug|x64 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Debug|x86.ActiveCfg = Debug|Win32 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Debug|x86.Build.0 = Debug|Win32 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Release|x64.ActiveCfg = Release|x64 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Release|x64.Build.0 = Release|x64 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Release|x86.ActiveCfg = Release|Win32 + {F5AD5AE7-32D3-48FB-AAE1-261E00418C1D}.Release|x86.Build.0 = Release|Win32 + {07BE9153-FB50-492F-9456-ADE90372E575}.Debug|x64.ActiveCfg = Debug|x64 + {07BE9153-FB50-492F-9456-ADE90372E575}.Debug|x64.Build.0 = Debug|x64 + {07BE9153-FB50-492F-9456-ADE90372E575}.Debug|x86.ActiveCfg = Debug|Win32 + {07BE9153-FB50-492F-9456-ADE90372E575}.Debug|x86.Build.0 = Debug|Win32 + {07BE9153-FB50-492F-9456-ADE90372E575}.Release|x64.ActiveCfg = Release|x64 + {07BE9153-FB50-492F-9456-ADE90372E575}.Release|x64.Build.0 = Release|x64 + {07BE9153-FB50-492F-9456-ADE90372E575}.Release|x86.ActiveCfg = Release|Win32 + {07BE9153-FB50-492F-9456-ADE90372E575}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B5C5C921-F256-48A3-AA40-AEA343FE0F08} + EndGlobalSection +EndGlobal diff --git a/PS1BSP.vcxproj b/PS1BSP.vcxproj new file mode 100644 index 0000000..22397b3 --- /dev/null +++ b/PS1BSP.vcxproj @@ -0,0 +1,153 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {f5ad5ae7-32d3-48fb-aae1-261e00418c1d} + PS1BSP + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/PS1BSP.vcxproj.filters b/PS1BSP.vcxproj.filters new file mode 100644 index 0000000..4c71843 --- /dev/null +++ b/PS1BSP.vcxproj.filters @@ -0,0 +1,30 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {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 + + + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/PS1MDL/PS1MDL.vcxproj b/PS1MDL/PS1MDL.vcxproj new file mode 100644 index 0000000..de5a630 --- /dev/null +++ b/PS1MDL/PS1MDL.vcxproj @@ -0,0 +1,152 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {07be9153-fb50-492f-9456-ade90372e575} + PS1MDL + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PS1MDL/PS1MDL.vcxproj.filters b/PS1MDL/PS1MDL.vcxproj.filters new file mode 100644 index 0000000..25cf4bb --- /dev/null +++ b/PS1MDL/PS1MDL.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {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 + + + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/PS1MDL/h_player.mdl b/PS1MDL/h_player.mdl new file mode 100644 index 0000000..77866f1 Binary files /dev/null and b/PS1MDL/h_player.mdl differ diff --git a/PS1MDL/main.c b/PS1MDL/main.c new file mode 100644 index 0000000..f0995fd --- /dev/null +++ b/PS1MDL/main.c @@ -0,0 +1,227 @@ +#include +#include +#include +#include "mdl.h" +#include "ps1mdl.h" + +#define PALETTE_SIZE 256 + +#define NUMVERTEXNORMALS 162 +static double anorms[NUMVERTEXNORMALS][3] = { +#include "../anorms.h" +}; + +int convertPalette(const char* lmpFile) +{ + FILE* fp; + const short palette_size = PALETTE_SIZE; + unsigned char palette[PALETTE_SIZE * 3]; + + fopen_s(&fp, lmpFile, "rb"); + if (fp == NULL) + return 0; + + fread(palette, sizeof(unsigned char) * 3, PALETTE_SIZE, fp); + fclose(fp); + + int file_size = PALETTE_SIZE * 4 + 24, tmp; + + fopen_s(&fp, "quake.pal", "wb"); + if (fp == NULL) + return 0; + + fwrite("RIFF", sizeof(char), 4, fp); + tmp = file_size - 8; + fwrite(&tmp, sizeof(int), 1, fp); + fwrite("PAL data", sizeof(char), 8, fp); + tmp = file_size - 20; + fwrite(&tmp, sizeof(int), 1, fp); + fwrite("\000\003", sizeof(char), 2, fp); + fwrite(&palette_size, sizeof(short), 1, fp); + + for (int i = 0; i < PALETTE_SIZE; ++i) + { + fwrite(palette + i * 3, 3, 1, fp); + fwrite("\000", sizeof(char), 1, fp); + } + + tmp = 1337; + fwrite(&tmp, sizeof(int), 1, fp); + fclose(fp); + + return 1; +} + +static void toFixedVector(const vec3_t inVec, int outVec[3]) +{ + const int scale = 1 << 12; + outVec[0] = (int)(inVec[0] * scale); + outVec[1] = (int)(inVec[1] * scale); + outVec[2] = (int)(inVec[2] * scale); +} + +int convertModel(const char* outFile, const mdl_header_t* inHeader, const mdl_texcoord_t* inTexCoords, const mdl_triangle_t* inTriangles, const mdl_frame_t* inFrames) +{ + FILE* fout; + fopen_s(&fout, outFile, "wb"); + if (fout == NULL) + return 0; + + // Header + ps1mdl_header_t outHeader; + outHeader.ident = inHeader->ident; + outHeader.version = inHeader->version; + toFixedVector(inHeader->scale, outHeader.scale); + toFixedVector(inHeader->translate, outHeader.translate); + outHeader.skinWidth = (unsigned short)inHeader->skinwidth; + outHeader.skinHeight = (unsigned short)inHeader->skinheight; + outHeader.vertexCount = (unsigned short)inHeader->num_verts; + outHeader.triangleCount = (unsigned short)inHeader->num_tris; + outHeader.frameCount = (unsigned short)inHeader->num_frames; + outHeader.pad = 0; + fwrite(&outHeader, sizeof(ps1mdl_header_t), 1, fout); + + // Texture coordinates + for (int i = 0; i < inHeader->num_verts; ++i) + { + ps1mdl_texcoord_t outTexCoord; + outTexCoord.onSeam = (short)(inTexCoords[i].onseam != 0); + outTexCoord.u = (short)inTexCoords[i].s; + outTexCoord.v = (short)inTexCoords[i].t; + fwrite(&outTexCoord, sizeof(ps1mdl_texcoord_t), 1, fout); + } + + // Triangles + for (int i = 0; i < inHeader->num_tris; ++i) + { + ps1mdl_triangle_t outTriangle; + outTriangle.frontFace = (short)(inTriangles[i].facesfront != 0); + outTriangle.vertexIndex[0] = (unsigned short)inTriangles[i].vertex[0]; + outTriangle.vertexIndex[1] = (unsigned short)inTriangles[i].vertex[1]; + outTriangle.vertexIndex[2] = (unsigned short)inTriangles[i].vertex[2]; + fwrite(&outTriangle, sizeof(ps1mdl_triangle_t), 1, fout); + } + + // Vertices + for (int frameIdx = 0; frameIdx < inHeader->num_frames; ++frameIdx) + { + for (int i = 0; i < inHeader->num_verts; ++i) + { + mdl_vertex_t* inVert = &inFrames[frameIdx].frame.verts[i]; + ps1mdl_vertex_t outVertex; + outVertex.position[0] = inVert->v[0]; + outVertex.position[1] = inVert->v[1]; + outVertex.position[2] = inVert->v[2]; + outVertex.normalIndex = inVert->normalIndex; + fwrite(&outVertex, sizeof(ps1mdl_vertex_t), 1, fout); + } + } + + fclose(fout); + return 1; +} + +void exportNormals() +{ + FILE* fout; + fopen_s(&fout, "ps1anorms.h", "w"); + if (fout == NULL) + return; + + for (int i = 0; i < NUMVERTEXNORMALS; ++i) + { + int x = (int)(anorms[i][0] * 4096); + int y = (int)(anorms[i][1] * 4096); + int z = (int)(anorms[i][2] * 4096); + + fprintf(fout, "{%d, %d, %d, 0},\n", x, y, z); + } + + fclose(fout); +} + +int main(int argc, char** argv) +{ + FILE* f; + mdl_header_t header; + char path[_MAX_PATH]; + + convertPalette(argv[1]); + exportNormals(); + + fopen_s(&f, argv[2], "rb"); + if (f == NULL) + return 1; + + fread(&header, sizeof(mdl_header_t), 1, f); + + printf("Header model identifier: %d, version: %d\n", header.ident, header.version); + + // Skins + printf("Reading %d skins of size: %dx%d\n", header.num_skins, header.skinwidth, header.skinheight); + + size_t skinSize = header.skinwidth * header.skinheight * sizeof(unsigned char); + unsigned char* buf = (unsigned char*)malloc(skinSize); + if (!buf) + { + fclose(f); + return 1; + } + + for (int i = 0; i < header.num_skins; ++i) + { + int group; + fread(&group, sizeof(int), 1, f); + + if (group) + { + int numPics; + fread(&numPics, sizeof(int), 1, f); + fseek(f, numPics * sizeof(float) + numPics * skinSize, SEEK_CUR); + } + else + { + fread(buf, sizeof(unsigned char), skinSize, f); + + FILE* fout; + sprintf_s(path, _MAX_PATH, "skin_%d_%dx%d.raw", i, header.skinwidth, header.skinheight); + fopen_s(&fout, path, "wb"); + if (fout == NULL) + continue; + + fwrite(buf, sizeof(unsigned char), skinSize, fout); + fclose(fout); + } + } + + free(buf); + + printf("Reading geometry data for %d vertices, %d triangles, %d frames\n", header.num_verts, header.num_tris, header.num_frames); + + // Texcoords & triangles (vertex indices) + mdl_texcoord_t* texCoords = (mdl_texcoord_t*)malloc(sizeof(mdl_texcoord_t) * header.num_verts); + mdl_triangle_t* triangles = (mdl_triangle_t*)malloc(sizeof(mdl_triangle_t) * header.num_tris); + fread(texCoords, sizeof(mdl_texcoord_t), header.num_verts, f); + fread(triangles, sizeof(mdl_triangle_t), header.num_tris, f); + + // Frames (vertex data) + mdl_frame_t* frames = (mdl_frame_t*)malloc(sizeof(mdl_frame_t) * header.num_frames); + for (int i = 0; i < header.num_frames; ++i) + { + frames[i].frame.verts = (mdl_vertex_t*)malloc(sizeof(mdl_vertex_t) * header.num_verts); + + fread(&frames[i].type, sizeof(int), 1, f); + fread(&frames[i].frame.bboxmin, sizeof(mdl_vertex_t), 1, f); + fread(&frames[i].frame.bboxmax, sizeof(mdl_vertex_t), 1, f); + fread(frames[i].frame.name, sizeof(char), 16, f); + fread(frames[i].frame.verts, sizeof(mdl_vertex_t), header.num_verts, f); + } + + fclose(f); + + // Write simplified model file for PS1 + if (!convertModel(argv[3], &header, texCoords, triangles, frames)) + return 1; + + return 0; +} diff --git a/PS1MDL/mdl.h b/PS1MDL/mdl.h new file mode 100644 index 0000000..0e973ee --- /dev/null +++ b/PS1MDL/mdl.h @@ -0,0 +1,93 @@ +#pragma once + +/* Vector */ +typedef float vec3_t[3]; + +/* MDL header */ +typedef struct +{ + int ident; /* magic number: "IDPO" */ + int version; /* version: 6 */ + + vec3_t scale; /* scale factor */ + vec3_t translate; /* translation vector */ + float boundingradius; + vec3_t eyeposition; /* eyes' position */ + + int num_skins; /* number of textures */ + int skinwidth; /* texture width */ + int skinheight; /* texture height */ + + int num_verts; /* number of vertices */ + int num_tris; /* number of triangles */ + int num_frames; /* number of frames */ + + int synctype; /* 0 = synchron, 1 = random */ + int flags; /* state flag */ + float size; +} mdl_header_t; + +/* Skin */ +typedef struct +{ + int group; /* 0 = single, 1 = group */ + unsigned char* data; /* texture data */ +} mdl_skin_t; + +/* Group of pictures */ +typedef struct +{ + int group; /* 1 = group */ + int nb; /* number of pics */ + float* time; /* time duration for each pic */ + unsigned char** data; /* texture data */ +} mdl_groupskin_t; + +/* Texture coords */ +typedef struct +{ + int onseam; + int s; + int t; +} mdl_texcoord_t; + +/* Triangle info */ +typedef struct +{ + int facesfront; /* 0 = backface, 1 = frontface */ + int vertex[3]; /* vertex indices */ +} mdl_triangle_t; + +/* Compressed vertex */ +typedef struct +{ + unsigned char v[3]; + unsigned char normalIndex; +} mdl_vertex_t; + +/* Simple frame */ +typedef struct +{ + mdl_vertex_t bboxmin; /* bouding box min */ + mdl_vertex_t bboxmax; /* bouding box max */ + char name[16]; + mdl_vertex_t* verts; /* vertex list of the frame */ +} mdl_simpleframe_t; + +/* Model frame */ +typedef struct +{ + int type; /* 0 = simple, !0 = group */ + mdl_simpleframe_t frame; /* this program can't read models + composed of group frames! */ +} mdl_frame_t; + +/* Group of simple frames */ +typedef struct +{ + int type; /* !0 = group */ + mdl_vertex_t min; /* min pos in all simple frames */ + mdl_vertex_t max; /* max pos in all simple frames */ + float* time; /* time duration for each frame */ + mdl_simpleframe_t* frames; /* simple frame list */ +} mdl_groupframe_t; diff --git a/PS1MDL/ogre.mdl b/PS1MDL/ogre.mdl new file mode 100644 index 0000000..4614ff2 Binary files /dev/null and b/PS1MDL/ogre.mdl differ diff --git a/PS1MDL/palette.lmp b/PS1MDL/palette.lmp new file mode 100644 index 0000000..7eefda1 Binary files /dev/null and b/PS1MDL/palette.lmp differ diff --git a/PS1MDL/player.mdl b/PS1MDL/player.mdl new file mode 100644 index 0000000..9e89172 Binary files /dev/null and b/PS1MDL/player.mdl differ diff --git a/PS1MDL/ps1anorms.h b/PS1MDL/ps1anorms.h new file mode 100644 index 0000000..d74cc53 --- /dev/null +++ b/PS1MDL/ps1anorms.h @@ -0,0 +1,162 @@ +{-2153, 0, 3484, 0}, +{-1813, 978, 3539, 0}, +{-1209, 0, 3913, 0}, +{-1265, 2048, 3313, 0}, +{-665, 1076, 3895, 0}, +{0, 0, 4096, 0}, +{0, 3484, 2153, 0}, +{-604, 2935, 2792, 0}, +{604, 2935, 2792, 0}, +{0, 2153, 3484, 0}, +{1265, 2048, 3313, 0}, +{2153, 0, 3484, 0}, +{1209, 0, 3913, 0}, +{1813, 978, 3539, 0}, +{665, 1076, 3895, 0}, +{-2792, 604, 2935, 0}, +{-3313, 1265, 2048, 0}, +{-2407, 1742, 2818, 0}, +{-3484, 2153, 0, 0}, +{-3539, 1813, 978, 0}, +{-2935, 2792, 604, 0}, +{-2818, 2407, 1742, 0}, +{-2048, 3313, 1265, 0}, +{-978, 3539, 1813, 0}, +{-1742, 2818, 2407, 0}, +{-2935, 2792, -604, 0}, +{-2048, 3313, -1265, 0}, +{-2153, 3484, 0, 0}, +{0, 3484, -2153, 0}, +{-978, 3539, -1813, 0}, +{0, 3913, -1209, 0}, +{-1076, 3895, -665, 0}, +{0, 4096, 0, 0}, +{0, 3913, 1209, 0}, +{-1076, 3895, 665, 0}, +{978, 3539, 1813, 0}, +{1076, 3895, 665, 0}, +{2048, 3313, 1265, 0}, +{978, 3539, -1813, 0}, +{1076, 3895, -665, 0}, +{2048, 3313, -1265, 0}, +{3484, 2153, 0, 0}, +{2935, 2792, 604, 0}, +{2935, 2792, -604, 0}, +{2153, 3484, 0, 0}, +{1742, 2818, 2407, 0}, +{3539, 1813, 978, 0}, +{2818, 2407, 1742, 0}, +{3313, 1265, 2048, 0}, +{2792, 604, 2935, 0}, +{2407, 1742, 2818, 0}, +{3913, 1209, 0, 0}, +{4096, 0, 0, 0}, +{3895, 665, 1076, 0}, +{3484, -2153, 0, 0}, +{3913, -1209, 0, 0}, +{3539, -1813, 978, 0}, +{3895, -665, 1076, 0}, +{3313, -1265, 2048, 0}, +{2792, -604, 2935, 0}, +{3484, 0, 2153, 0}, +{3539, 1813, -978, 0}, +{3313, 1265, -2048, 0}, +{3895, 665, -1076, 0}, +{2153, 0, -3484, 0}, +{2792, 604, -2935, 0}, +{2792, -604, -2935, 0}, +{3484, 0, -2153, 0}, +{3313, -1265, -2048, 0}, +{3539, -1813, -978, 0}, +{3895, -665, -1076, 0}, +{604, 2935, -2792, 0}, +{1265, 2048, -3313, 0}, +{1742, 2818, -2407, 0}, +{1813, 978, -3539, 0}, +{2407, 1742, -2818, 0}, +{2818, 2407, -1742, 0}, +{-604, 2935, -2792, 0}, +{-1265, 2048, -3313, 0}, +{0, 2153, -3484, 0}, +{-2153, 0, -3484, 0}, +{-1813, 978, -3539, 0}, +{-1209, 0, -3913, 0}, +{-665, 1076, -3895, 0}, +{0, 0, -4096, 0}, +{1209, 0, -3913, 0}, +{665, 1076, -3895, 0}, +{-1813, -978, -3539, 0}, +{-1265, -2048, -3313, 0}, +{-665, -1076, -3895, 0}, +{0, -3484, -2153, 0}, +{-604, -2935, -2792, 0}, +{604, -2935, -2792, 0}, +{0, -2153, -3484, 0}, +{1265, -2048, -3313, 0}, +{1813, -978, -3539, 0}, +{665, -1076, -3895, 0}, +{978, -3539, -1813, 0}, +{2048, -3313, -1265, 0}, +{1742, -2818, -2407, 0}, +{2935, -2792, -604, 0}, +{2818, -2407, -1742, 0}, +{2407, -1742, -2818, 0}, +{0, -3913, -1209, 0}, +{0, -4096, 0, 0}, +{1076, -3895, -665, 0}, +{0, -3484, 2153, 0}, +{0, -3913, 1209, 0}, +{978, -3539, 1813, 0}, +{1076, -3895, 665, 0}, +{2048, -3313, 1265, 0}, +{2935, -2792, 604, 0}, +{2153, -3484, 0, 0}, +{-978, -3539, -1813, 0}, +{-2048, -3313, -1265, 0}, +{-1076, -3895, -665, 0}, +{-3484, -2153, 0, 0}, +{-2935, -2792, -604, 0}, +{-2935, -2792, 604, 0}, +{-2153, -3484, 0, 0}, +{-2048, -3313, 1265, 0}, +{-978, -3539, 1813, 0}, +{-1076, -3895, 665, 0}, +{-3539, -1813, 978, 0}, +{-3313, -1265, 2048, 0}, +{-2818, -2407, 1742, 0}, +{-2792, -604, 2935, 0}, +{-1813, -978, 3539, 0}, +{-2407, -1742, 2818, 0}, +{-1265, -2048, 3313, 0}, +{-604, -2935, 2792, 0}, +{-1742, -2818, 2407, 0}, +{-665, -1076, 3895, 0}, +{1813, -978, 3539, 0}, +{665, -1076, 3895, 0}, +{1265, -2048, 3313, 0}, +{604, -2935, 2792, 0}, +{0, -2153, 3484, 0}, +{1742, -2818, 2407, 0}, +{2407, -1742, 2818, 0}, +{2818, -2407, 1742, 0}, +{-3913, 1209, 0, 0}, +{-3895, 665, 1076, 0}, +{-4096, 0, 0, 0}, +{-3484, 0, 2153, 0}, +{-3913, -1209, 0, 0}, +{-3895, -665, 1076, 0}, +{-3539, 1813, -978, 0}, +{-3895, 665, -1076, 0}, +{-3313, 1265, -2048, 0}, +{-3539, -1813, -978, 0}, +{-3895, -665, -1076, 0}, +{-3313, -1265, -2048, 0}, +{-2792, 604, -2935, 0}, +{-2792, -604, -2935, 0}, +{-3484, 0, -2153, 0}, +{-2818, 2407, -1742, 0}, +{-2407, 1742, -2818, 0}, +{-1742, 2818, -2407, 0}, +{-1742, -2818, -2407, 0}, +{-2407, -1742, -2818, 0}, +{-2818, -2407, -1742, 0}, diff --git a/PS1MDL/ps1mdl.h b/PS1MDL/ps1mdl.h new file mode 100644 index 0000000..1232e29 --- /dev/null +++ b/PS1MDL/ps1mdl.h @@ -0,0 +1,48 @@ +#ifndef __PS1MDL_H__ +#define __PS1MDL_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + int ident; + int version; + + int scale[3]; + int translate[3]; + + unsigned short skinWidth; + unsigned short skinHeight; + + unsigned short vertexCount; + unsigned short triangleCount; + unsigned short frameCount; + + unsigned short pad; +} ps1mdl_header_t; + +typedef struct +{ + short onSeam; + short u, v; +} ps1mdl_texcoord_t; + +typedef struct +{ + short frontFace; + unsigned short vertexIndex[3]; +} ps1mdl_triangle_t; + +typedef struct +{ + unsigned char position[3]; + unsigned char normalIndex; +} ps1mdl_vertex_t; + +#ifdef __cplusplus +} +#endif + +#endif // __PS1BSP_H__ diff --git a/PS1MDL/quaddama.mdl b/PS1MDL/quaddama.mdl new file mode 100644 index 0000000..01e5a2f Binary files /dev/null and b/PS1MDL/quaddama.mdl differ diff --git a/PS1MDL/quake.pal b/PS1MDL/quake.pal new file mode 100644 index 0000000..f926104 Binary files /dev/null and b/PS1MDL/quake.pal differ diff --git a/PS1MDL/shambler.mdl b/PS1MDL/shambler.mdl new file mode 100644 index 0000000..e053ea2 Binary files /dev/null and b/PS1MDL/shambler.mdl differ diff --git a/PS1MDL/soldier.mdl b/PS1MDL/soldier.mdl new file mode 100644 index 0000000..df17925 Binary files /dev/null and b/PS1MDL/soldier.mdl differ diff --git a/anorms.h b/anorms.h new file mode 100644 index 0000000..11a9007 --- /dev/null +++ b/anorms.h @@ -0,0 +1,181 @@ +/* +Copyright (C) 1996-1997 Id Software, Inc. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +{-0.525731, 0.000000, 0.850651}, +{-0.442863, 0.238856, 0.864188}, +{-0.295242, 0.000000, 0.955423}, +{-0.309017, 0.500000, 0.809017}, +{-0.162460, 0.262866, 0.951056}, +{0.000000, 0.000000, 1.000000}, +{0.000000, 0.850651, 0.525731}, +{-0.147621, 0.716567, 0.681718}, +{0.147621, 0.716567, 0.681718}, +{0.000000, 0.525731, 0.850651}, +{0.309017, 0.500000, 0.809017}, +{0.525731, 0.000000, 0.850651}, +{0.295242, 0.000000, 0.955423}, +{0.442863, 0.238856, 0.864188}, +{0.162460, 0.262866, 0.951056}, +{-0.681718, 0.147621, 0.716567}, +{-0.809017, 0.309017, 0.500000}, +{-0.587785, 0.425325, 0.688191}, +{-0.850651, 0.525731, 0.000000}, +{-0.864188, 0.442863, 0.238856}, +{-0.716567, 0.681718, 0.147621}, +{-0.688191, 0.587785, 0.425325}, +{-0.500000, 0.809017, 0.309017}, +{-0.238856, 0.864188, 0.442863}, +{-0.425325, 0.688191, 0.587785}, +{-0.716567, 0.681718, -0.147621}, +{-0.500000, 0.809017, -0.309017}, +{-0.525731, 0.850651, 0.000000}, +{0.000000, 0.850651, -0.525731}, +{-0.238856, 0.864188, -0.442863}, +{0.000000, 0.955423, -0.295242}, +{-0.262866, 0.951056, -0.162460}, +{0.000000, 1.000000, 0.000000}, +{0.000000, 0.955423, 0.295242}, +{-0.262866, 0.951056, 0.162460}, +{0.238856, 0.864188, 0.442863}, +{0.262866, 0.951056, 0.162460}, +{0.500000, 0.809017, 0.309017}, +{0.238856, 0.864188, -0.442863}, +{0.262866, 0.951056, -0.162460}, +{0.500000, 0.809017, -0.309017}, +{0.850651, 0.525731, 0.000000}, +{0.716567, 0.681718, 0.147621}, +{0.716567, 0.681718, -0.147621}, +{0.525731, 0.850651, 0.000000}, +{0.425325, 0.688191, 0.587785}, +{0.864188, 0.442863, 0.238856}, +{0.688191, 0.587785, 0.425325}, +{0.809017, 0.309017, 0.500000}, +{0.681718, 0.147621, 0.716567}, +{0.587785, 0.425325, 0.688191}, +{0.955423, 0.295242, 0.000000}, +{1.000000, 0.000000, 0.000000}, +{0.951056, 0.162460, 0.262866}, +{0.850651, -0.525731, 0.000000}, +{0.955423, -0.295242, 0.000000}, +{0.864188, -0.442863, 0.238856}, +{0.951056, -0.162460, 0.262866}, +{0.809017, -0.309017, 0.500000}, +{0.681718, -0.147621, 0.716567}, +{0.850651, 0.000000, 0.525731}, +{0.864188, 0.442863, -0.238856}, +{0.809017, 0.309017, -0.500000}, +{0.951056, 0.162460, -0.262866}, +{0.525731, 0.000000, -0.850651}, +{0.681718, 0.147621, -0.716567}, +{0.681718, -0.147621, -0.716567}, +{0.850651, 0.000000, -0.525731}, +{0.809017, -0.309017, -0.500000}, +{0.864188, -0.442863, -0.238856}, +{0.951056, -0.162460, -0.262866}, +{0.147621, 0.716567, -0.681718}, +{0.309017, 0.500000, -0.809017}, +{0.425325, 0.688191, -0.587785}, +{0.442863, 0.238856, -0.864188}, +{0.587785, 0.425325, -0.688191}, +{0.688191, 0.587785, -0.425325}, +{-0.147621, 0.716567, -0.681718}, +{-0.309017, 0.500000, -0.809017}, +{0.000000, 0.525731, -0.850651}, +{-0.525731, 0.000000, -0.850651}, +{-0.442863, 0.238856, -0.864188}, +{-0.295242, 0.000000, -0.955423}, +{-0.162460, 0.262866, -0.951056}, +{0.000000, 0.000000, -1.000000}, +{0.295242, 0.000000, -0.955423}, +{0.162460, 0.262866, -0.951056}, +{-0.442863, -0.238856, -0.864188}, +{-0.309017, -0.500000, -0.809017}, +{-0.162460, -0.262866, -0.951056}, +{0.000000, -0.850651, -0.525731}, +{-0.147621, -0.716567, -0.681718}, +{0.147621, -0.716567, -0.681718}, +{0.000000, -0.525731, -0.850651}, +{0.309017, -0.500000, -0.809017}, +{0.442863, -0.238856, -0.864188}, +{0.162460, -0.262866, -0.951056}, +{0.238856, -0.864188, -0.442863}, +{0.500000, -0.809017, -0.309017}, +{0.425325, -0.688191, -0.587785}, +{0.716567, -0.681718, -0.147621}, +{0.688191, -0.587785, -0.425325}, +{0.587785, -0.425325, -0.688191}, +{0.000000, -0.955423, -0.295242}, +{0.000000, -1.000000, 0.000000}, +{0.262866, -0.951056, -0.162460}, +{0.000000, -0.850651, 0.525731}, +{0.000000, -0.955423, 0.295242}, +{0.238856, -0.864188, 0.442863}, +{0.262866, -0.951056, 0.162460}, +{0.500000, -0.809017, 0.309017}, +{0.716567, -0.681718, 0.147621}, +{0.525731, -0.850651, 0.000000}, +{-0.238856, -0.864188, -0.442863}, +{-0.500000, -0.809017, -0.309017}, +{-0.262866, -0.951056, -0.162460}, +{-0.850651, -0.525731, 0.000000}, +{-0.716567, -0.681718, -0.147621}, +{-0.716567, -0.681718, 0.147621}, +{-0.525731, -0.850651, 0.000000}, +{-0.500000, -0.809017, 0.309017}, +{-0.238856, -0.864188, 0.442863}, +{-0.262866, -0.951056, 0.162460}, +{-0.864188, -0.442863, 0.238856}, +{-0.809017, -0.309017, 0.500000}, +{-0.688191, -0.587785, 0.425325}, +{-0.681718, -0.147621, 0.716567}, +{-0.442863, -0.238856, 0.864188}, +{-0.587785, -0.425325, 0.688191}, +{-0.309017, -0.500000, 0.809017}, +{-0.147621, -0.716567, 0.681718}, +{-0.425325, -0.688191, 0.587785}, +{-0.162460, -0.262866, 0.951056}, +{0.442863, -0.238856, 0.864188}, +{0.162460, -0.262866, 0.951056}, +{0.309017, -0.500000, 0.809017}, +{0.147621, -0.716567, 0.681718}, +{0.000000, -0.525731, 0.850651}, +{0.425325, -0.688191, 0.587785}, +{0.587785, -0.425325, 0.688191}, +{0.688191, -0.587785, 0.425325}, +{-0.955423, 0.295242, 0.000000}, +{-0.951056, 0.162460, 0.262866}, +{-1.000000, 0.000000, 0.000000}, +{-0.850651, 0.000000, 0.525731}, +{-0.955423, -0.295242, 0.000000}, +{-0.951056, -0.162460, 0.262866}, +{-0.864188, 0.442863, -0.238856}, +{-0.951056, 0.162460, -0.262866}, +{-0.809017, 0.309017, -0.500000}, +{-0.864188, -0.442863, -0.238856}, +{-0.951056, -0.162460, -0.262866}, +{-0.809017, -0.309017, -0.500000}, +{-0.681718, 0.147621, -0.716567}, +{-0.681718, -0.147621, -0.716567}, +{-0.850651, 0.000000, -0.525731}, +{-0.688191, 0.587785, -0.425325}, +{-0.587785, 0.425325, -0.688191}, +{-0.425325, 0.688191, -0.587785}, +{-0.425325, -0.688191, -0.587785}, +{-0.587785, -0.425325, -0.688191}, +{-0.688191, -0.587785, -0.425325}, diff --git a/bsp.h b/bsp.h new file mode 100644 index 0000000..1710d85 --- /dev/null +++ b/bsp.h @@ -0,0 +1,98 @@ +#pragma once + +typedef struct // A Directory entry +{ + long offset; // Offset to entry, in bytes, from start of file + long size; // Size of entry in file, in bytes +} dentry_t; + +typedef struct // The BSP file header +{ + long version; // Model version, must be 0x17 (23). + dentry_t entities; // List of Entities. + dentry_t planes; // Map Planes. + // numplanes = size/sizeof(plane_t) + dentry_t miptex; // Wall Textures. + dentry_t vertices; // Map Vertices. + // numvertices = size/sizeof(vertex_t) + dentry_t visilist; // Leaves Visibility lists. + dentry_t nodes; // BSP Nodes. + // numnodes = size/sizeof(node_t) + dentry_t texinfo; // Texture Info for faces. + // numtexinfo = size/sizeof(texinfo_t) + dentry_t faces; // Faces of each surface. + // numfaces = size/sizeof(face_t) + dentry_t lightmaps; // Wall Light Maps. + dentry_t clipnodes; // clip nodes, for Models. + // numclips = size/sizeof(clipnode_t) + dentry_t leaves; // BSP Leaves. + // numlaves = size/sizeof(leaf_t) + dentry_t lface; // List of Faces. + dentry_t edges; // Edges of faces. + // numedges = Size/sizeof(edge_t) + dentry_t ledges; // List of Edges. + dentry_t models; // List of Models. + // nummodels = Size/sizeof(model_t) +} dheader_t; + +typedef float scalar_t; // Scalar value, + +typedef struct // Vector or Position +{ + scalar_t x; // horizontal + scalar_t y; // horizontal + scalar_t z; // vertical +} vec3_t; + +typedef struct // Bounding Box, Float values +{ + vec3_t min; // minimum values of X,Y,Z + vec3_t max; // maximum values of X,Y,Z +} boundbox_t; + +typedef struct // Bounding Box, Short values +{ + short min; // minimum values of X,Y,Z + short max; // maximum values of X,Y,Z +} bboxshort_t; + +typedef struct // Mip texture list header +{ + long numtex; // Number of textures in Mip Texture list + long *offset; // Offset to each of the individual texture +} mipheader_t; // from the beginning of mipheader_t + +typedef struct // Mip Texture +{ + char name[16]; // Name of the texture. + unsigned long width; // width of picture, must be a multiple of 8 + unsigned long height; // height of picture, must be a multiple of 8 + unsigned long offset1; // offset to u_char Pix[width * height] + unsigned long offset2; // offset to u_char Pix[width/2 * height/2] + unsigned long offset4; // offset to u_char Pix[width/4 * height/4] + unsigned long offset8; // offset to u_char Pix[width/8 * height/8] +} miptex_t; + +typedef struct +{ + float X; // X,Y,Z coordinates of the vertex + float Y; // usually some integer value + float Z; // but coded in floating point +} vertex_t; + +typedef struct +{ + unsigned short plane_id; // The plane in which the face lies + // must be in [0,numplanes[ + unsigned short side; // 0 if in front of the plane, 1 if behind the plane + long ledge_id; // first edge in the List of edges + // must be in [0,numledges[ + unsigned short ledge_num; // number of edges in the List of edges + unsigned short texinfo_id; // index of the Texture info the face is part of + // must be in [0,numtexinfos[ + unsigned char typelight; // type of lighting, for the face + unsigned char baselight; // from 0xFF (dark) to 0 (bright) + unsigned char light[2]; // two additional light models + long lightmap; // Pointer inside the general light map, or -1 + // this define the start of the face light map +} face_t; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6a9309a --- /dev/null +++ b/main.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include "bsp.h" +#include "rectpack/finders_interface.h" + +int main(int argc, char** argv) +{ + FILE* f; + dheader_t header; + char path[_MAX_PATH]; + + fopen_s(&f, argv[1], "rb"); + if (f == NULL) + return 1; + + fread(&header, sizeof(dheader_t), 1, f); + + printf("Header model version: %d\n", header.version); + + // Test reading the entity string data + fseek(f, header.entities.offset, SEEK_SET); + + char* entities = (char*)malloc((header.entities.size + 1) * sizeof(char)); + if (entities == NULL) + { + fclose(f); + return 1; + } + + 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); + + // Test exporting texture data + 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) + { + fclose(f); + return 1; + } + + fread(mipheader.offset, sizeof(long), mipheader.numtex, f); + + 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 256x512 in practice, or a quarter of the PS1's VRAM allocation. + const auto discard_step = -4; + + 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) + { + unsigned long miptexOffset = header.miptex.offset + mipheader.offset[texNum]; + fseek(f, miptexOffset, SEEK_SET); + fread(&miptex, sizeof(miptex_t), 1, f); + + //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", mipheader.numtex, result_size.w, result_size.h); + unsigned char* atlas = (unsigned char*)malloc(result_size.w * result_size.h * sizeof(unsigned char)); + 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) + { + unsigned long miptexOffset = header.miptex.offset + mipheader.offset[texNum]; + fseek(f, miptexOffset, SEEK_SET); + fread(&miptex, sizeof(miptex_t), 1, f); + + 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); + + FILE* fout; + sprintf_s(path, _MAX_PATH, "textures/%s-mip%d-%dx%d.raw", outName, mipLevel, miptex.width >> mipLevel, miptex.height >> mipLevel); + fopen_s(&fout, path, "wb"); + fwrite(texBytes, sizeof(unsigned char), numBytes, fout); + fclose(fout); + + 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)); + } + } + + free(texBytes); + } + } + + FILE* fatlas; + sprintf_s(path, _MAX_PATH, "atlas-%dx%d.raw", result_size.w, result_size.h); + fopen_s(&fatlas, path, "wb"); + fwrite(atlas, sizeof(unsigned char), result_size.w * result_size.h, fatlas); + fclose(fatlas); + + free(atlas); + free(mipheader.offset); + + // Inspect vertex data + int numVertices = header.vertices.size / sizeof(vertex_t); + int numFaces = header.faces.size / sizeof(face_t); + fseek(f, header.vertices.offset, SEEK_SET); + vertex_t* vertices = (vertex_t*)malloc(header.vertices.size); + 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) + { + vertex_t* vert = &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", numVertices, 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]); + + fclose(f); + + return 0; +} diff --git a/ps1bsp.h b/ps1bsp.h new file mode 100644 index 0000000..820898d --- /dev/null +++ b/ps1bsp.h @@ -0,0 +1,60 @@ +#ifndef __PS1BSP_H__ +#define __PS1BSP_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +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 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, using the extra padding to store vertex color data. +// The full range and precision required cannot be stored in just shorts, so we make use of a floating origin stored in the BSP leafs. +// With this the higher-order bits of each vertex position are calculated into the model-view matrix, giving good precision for polygons near the camera. +typedef struct +{ + short x; + short y; + short z; + unsigned char baseLight, finalLight; // Used for gouraud shading based on static lightmap data +} ps1bsp_vertex_t; + +// Instead of edges as in the original BSP format, we store triangles for easy consumption by the PS1 +typedef struct +{ + unsigned short vertex0; + unsigned short vertex1; + unsigned short vertex2; +} ps1bsp_triangle_t; + +// Pre-parsed and encoded entity data (this runs the risk of becoming too bloated) +typedef struct +{ + unsigned short classtype; // Hash of the original classname + 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 +} ps1bsp_entity_t; + +typedef struct +{ + unsigned char length; + char message[]; +} ps1bsp_message_t; + +typedef struct +{ + // TODO: add floating origin position, so face vertices can be moved relative to the camera position +} ps1bsp_leaf_t; + +#ifdef __cplusplus +} +#endif + +#endif // __PS1BSP_H__ diff --git a/rectpack/best_bin_finder.h b/rectpack/best_bin_finder.h new file mode 100644 index 0000000..f33ef97 --- /dev/null +++ b/rectpack/best_bin_finder.h @@ -0,0 +1,286 @@ +#pragma once +#include +#include +#include "rect_structs.h" + +namespace rectpack2D { + enum class callback_result { + ABORT_PACKING, + CONTINUE_PACKING + }; + + template + auto& dereference(T& r) { + /* + This will allow us to pass orderings that consist of pointers, + as well as ones that are just plain objects in a vector. + */ + + if constexpr(std::is_pointer_v) { + return *r; + } + else { + return r; + } + }; + + /* + This function will do a binary search on viable bin sizes, + starting from the biggest one: starting_bin. + + The search stops when the bin was successfully inserted into, + AND the bin size to be tried next differs in size from the last viable one by *less* then discard_step. + + If we could not insert all input rectangles into a bin even as big as the starting_bin - the search fails. + In this case, we return the amount of space (total_area_type) inserted in total. + + If we've found a viable bin that is smaller or equal to starting_bin, the search succeeds. + In this case, we return the viable bin (rect_wh). + */ + + enum class bin_dimension { + BOTH, + WIDTH, + HEIGHT + }; + + template + std::variant best_packing_for_ordering_impl( + empty_spaces_type& root, + O ordering, + const rect_wh starting_bin, + int discard_step, + const bin_dimension tried_dimension + ) { + auto candidate_bin = starting_bin; + int tries_before_discarding = 0; + + if (discard_step <= 0) + { + tries_before_discarding = -discard_step; + discard_step = 1; + } + + //std::cout << "best_packing_for_ordering_impl dim: " << int(tried_dimension) << " w: " << starting_bin.w << " h: " << starting_bin.h << std::endl; + + int starting_step = 0; + + if (tried_dimension == bin_dimension::BOTH) { + candidate_bin.w /= 2; + candidate_bin.h /= 2; + + starting_step = candidate_bin.w / 2; + } + else if (tried_dimension == bin_dimension::WIDTH) { + candidate_bin.w /= 2; + starting_step = candidate_bin.w / 2; + } + else { + candidate_bin.h /= 2; + starting_step = candidate_bin.h / 2; + } + + for (int step = starting_step; ; step = std::max(1, step / 2)) { + //std::cout << "candidate: " << candidate_bin.w << "x" << candidate_bin.h << std::endl; + + root.reset(candidate_bin); + + int total_inserted_area = 0; + + const bool all_inserted = [&]() { + for (const auto& r : ordering) { + const auto& rect = dereference(r); + + if (root.insert(rect.get_wh())) { + total_inserted_area += rect.area(); + } + else { + return false; + } + } + + return true; + }(); + + if (all_inserted) { + /* Attempt was successful. Try with a smaller bin. */ + + if (step <= discard_step) { + if (tries_before_discarding > 0) + { + tries_before_discarding--; + } + else + { + return candidate_bin; + } + } + + if (tried_dimension == bin_dimension::BOTH) { + candidate_bin.w -= step; + candidate_bin.h -= step; + } + else if (tried_dimension == bin_dimension::WIDTH) { + candidate_bin.w -= step; + } + else { + candidate_bin.h -= step; + } + + root.reset(candidate_bin); + } + else { + /* Attempt ended with failure. Try with a bigger bin. */ + + if (tried_dimension == bin_dimension::BOTH) { + candidate_bin.w += step; + candidate_bin.h += step; + + if (candidate_bin.area() > starting_bin.area()) { + return total_inserted_area; + } + } + else if (tried_dimension == bin_dimension::WIDTH) { + candidate_bin.w += step; + + if (candidate_bin.w > starting_bin.w) { + return total_inserted_area; + } + } + else { + candidate_bin.h += step; + + if (candidate_bin.h > starting_bin.h) { + return total_inserted_area; + } + } + } + } + } + + template + std::variant best_packing_for_ordering( + empty_spaces_type& root, + O&& ordering, + const rect_wh starting_bin, + const int discard_step + ) { + const auto try_pack = [&]( + const bin_dimension tried_dimension, + const rect_wh starting_bin + ) { + return best_packing_for_ordering_impl( + root, + std::forward(ordering), + starting_bin, + discard_step, + tried_dimension + ); + }; + + const auto best_result = try_pack(bin_dimension::BOTH, starting_bin); + + if (const auto failed = std::get_if(&best_result)) { + return *failed; + } + + auto best_bin = std::get(best_result); + + auto trial = [&](const bin_dimension tried_dimension) { + const auto trial = try_pack(tried_dimension, best_bin); + + if (const auto better = std::get_if(&trial)) { + best_bin = *better; + } + }; + + trial(bin_dimension::WIDTH); + trial(bin_dimension::HEIGHT); + + return best_bin; + } + + /* + This function will try to find the best bin size among the ones generated by all provided rectangle orders. + Only the best order will have results written to. + + The function reports which of the rectangles did and did not fit in the end. + */ + + template < + class empty_spaces_type, + class OrderType, + class F, + class I + > + rect_wh find_best_packing_impl(F for_each_order, const I input) { + const auto max_bin = rect_wh(input.max_bin_side << 1, input.max_bin_side >> 1); // PS1BSP: only way I've been able to make non-square output work ¯\_(ツ)_/¯ + + OrderType* best_order = nullptr; + + int best_total_inserted = -1; + auto best_bin = max_bin; + + /* + The root node is re-used on the TLS. + It is always reset before any packing attempt. + */ + + thread_local empty_spaces_type root = rect_wh(); + root.flipping_mode = input.flipping_mode; + + for_each_order ([&](OrderType& current_order) { + const auto packing = best_packing_for_ordering( + root, + current_order, + max_bin, + input.discard_step + ); + + if (const auto total_inserted = std::get_if(&packing)) { + /* + Track which function inserts the most area in total, + just in case that all orders will fail to fit into the largest allowed bin. + */ + if (best_order == nullptr) { + if (*total_inserted > best_total_inserted) { + best_order = std::addressof(current_order); + best_total_inserted = *total_inserted; + } + } + } + else if (const auto result_bin = std::get_if(&packing)) { + /* Save the function if it performed the best. */ + if (result_bin->area() <= best_bin.area()) { + best_order = std::addressof(current_order); + best_bin = *result_bin; + } + } + }); + + { + assert(best_order != nullptr); + + root.reset(best_bin); + + for (auto& rr : *best_order) { + auto& rect = dereference(rr); + + if (const auto ret = root.insert(rect.get_wh())) { + rect = *ret; + + if (callback_result::ABORT_PACKING == input.handle_successful_insertion(rect)) { + break; + } + } + else { + if (callback_result::ABORT_PACKING == input.handle_unsuccessful_insertion(rect)) { + break; + } + } + } + + return root.get_rects_aabb(); + } + } +} diff --git a/rectpack/empty_space_allocators.h b/rectpack/empty_space_allocators.h new file mode 100644 index 0000000..217ef77 --- /dev/null +++ b/rectpack/empty_space_allocators.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include + +#include "rect_structs.h" + +namespace rectpack2D { + class default_empty_spaces { + std::vector empty_spaces; + + public: + void remove(const int i) { + empty_spaces[i] = empty_spaces.back(); + empty_spaces.pop_back(); + } + + bool add(const space_rect r) { + empty_spaces.emplace_back(r); + return true; + } + + auto get_count() const { + return empty_spaces.size(); + } + + void reset() { + empty_spaces.clear(); + } + + const auto& get(const int i) { + return empty_spaces[i]; + } + }; + + template + class static_empty_spaces { + int count_spaces = 0; + std::array empty_spaces; + + public: + void remove(const int i) { + empty_spaces[i] = empty_spaces[count_spaces - 1]; + --count_spaces; + } + + bool add(const space_rect r) { + if (count_spaces < static_cast(empty_spaces.size())) { + empty_spaces[count_spaces] = r; + ++count_spaces; + + return true; + } + + return false; + } + + auto get_count() const { + return count_spaces; + } + + void reset() { + count_spaces = 0; + } + + const auto& get(const int i) { + return empty_spaces[i]; + } + }; +} diff --git a/rectpack/empty_spaces.h b/rectpack/empty_spaces.h new file mode 100644 index 0000000..bb0ad79 --- /dev/null +++ b/rectpack/empty_spaces.h @@ -0,0 +1,149 @@ +#pragma once +#include "insert_and_split.h" + +namespace rectpack2D { + enum class flipping_option { + DISABLED, + ENABLED + }; + + class default_empty_spaces; + + template + class empty_spaces { + rect_wh current_aabb; + empty_spaces_provider spaces; + + /* MSVC fix for non-conformant if constexpr implementation */ + + static auto make_output_rect(const int x, const int y, const int w, const int h) { + return rect_xywh(x, y, w, h); + } + + static auto make_output_rect(const int x, const int y, const int w, const int h, const bool flipped) { + return rect_xywhf(x, y, w, h, flipped); + } + + public: + using output_rect_type = std::conditional_t; + + flipping_option flipping_mode = flipping_option::ENABLED; + + empty_spaces(const rect_wh& r) { + reset(r); + } + + void reset(const rect_wh& r) { + current_aabb = {}; + + spaces.reset(); + spaces.add(rect_xywh(0, 0, r.w, r.h)); + } + + template + std::optional insert(const rect_wh image_rectangle, F report_candidate_empty_space) { + for (int i = static_cast(spaces.get_count()) - 1; i >= 0; --i) { + const auto candidate_space = spaces.get(i); + + report_candidate_empty_space(candidate_space); + + auto accept_result = [this, i, image_rectangle, candidate_space]( + const created_splits& splits, + const bool flipping_necessary + ) -> std::optional { + spaces.remove(i); + + for (int s = 0; s < splits.count; ++s) { + if (!spaces.add(splits.spaces[s])) { + return std::nullopt; + } + } + + if constexpr(allow_flip) { + const auto result = make_output_rect( + candidate_space.x, + candidate_space.y, + image_rectangle.w, + image_rectangle.h, + flipping_necessary + ); + + current_aabb.expand_with(result); + return result; + } + else if constexpr(!allow_flip) { + (void)flipping_necessary; + + const auto result = make_output_rect( + candidate_space.x, + candidate_space.y, + image_rectangle.w, + image_rectangle.h + ); + + current_aabb.expand_with(result); + return result; + } + }; + + auto try_to_insert = [&](const rect_wh& img) { + return rectpack2D::insert_and_split(img, candidate_space); + }; + + if constexpr(!allow_flip) { + if (const auto normal = try_to_insert(image_rectangle)) { + return accept_result(normal, false); + } + } + else { + if (flipping_mode == flipping_option::ENABLED) { + const auto normal = try_to_insert(image_rectangle); + const auto flipped = try_to_insert(rect_wh(image_rectangle).flip()); + + /* + If both were successful, + prefer the one that generated less remainder spaces. + */ + + if (normal && flipped) { + if (flipped.better_than(normal)) { + /* Accept the flipped result if it producues less or "better" spaces. */ + + return accept_result(flipped, true); + } + + return accept_result(normal, false); + } + + if (normal) { + return accept_result(normal, false); + } + + if (flipped) { + return accept_result(flipped, true); + } + } + else { + if (const auto normal = try_to_insert(image_rectangle)) { + return accept_result(normal, false); + } + } + } + } + + return std::nullopt; + } + + decltype(auto) insert(const rect_wh& image_rectangle) { + return insert(image_rectangle, [](auto&){ }); + } + + auto get_rects_aabb() const { + return current_aabb; + } + + const auto& get_spaces() const { + return spaces; + } + }; +} diff --git a/rectpack/finders_interface.h b/rectpack/finders_interface.h new file mode 100644 index 0000000..c3ef821 --- /dev/null +++ b/rectpack/finders_interface.h @@ -0,0 +1,155 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "insert_and_split.h" +#include "empty_spaces.h" +#include "empty_space_allocators.h" + +#include "best_bin_finder.h" + +namespace rectpack2D { + template + using output_rect_t = typename empty_spaces_type::output_rect_type; + + template + struct finder_input { + const int max_bin_side; + const int discard_step; + F handle_successful_insertion; + G handle_unsuccessful_insertion; + const flipping_option flipping_mode; + }; + + template + auto make_finder_input( + const int max_bin_side, + const int discard_step, + F&& handle_successful_insertion, + G&& handle_unsuccessful_insertion, + const flipping_option flipping_mode + ) { + return finder_input { + max_bin_side, + discard_step, + std::forward(handle_successful_insertion), + std::forward(handle_unsuccessful_insertion), + flipping_mode + }; + }; + + /* + Finds the best packing for the rectangles, + just in the order that they were passed. + */ + + template + rect_wh find_best_packing_dont_sort( + std::vector>& subjects, + const finder_input& input + ) { + using order_type = std::remove_reference_t; + + return find_best_packing_impl( + [&subjects](auto callback) { callback(subjects); }, + input + ); + } + + + /* + Finds the best packing for the rectangles. + Accepts a list of predicates able to compare two input rectangles. + + The function will try to pack the rectangles in all orders generated by the predicates, + and will only write the x, y coordinates of the best packing found among the orders. + */ + + template + rect_wh find_best_packing( + std::vector>& subjects, + const finder_input& input, + + Comparator comparator, + Comparators... comparators + ) { + using rect_type = output_rect_t; + using order_type = std::vector; + + constexpr auto count_orders = 1 + sizeof...(Comparators); + thread_local std::array orders; + + { + /* order[0] will always exist since this overload requires at least one comparator */ + auto& initial_pointers = orders[0]; + initial_pointers.clear(); + + for (auto& s : subjects) { + if (s.area() > 0) { + initial_pointers.emplace_back(std::addressof(s)); + } + } + + for (std::size_t i = 1; i < count_orders; ++i) { + orders[i] = initial_pointers; + } + } + + std::size_t f = 0; + + auto& orders_ref = orders; + + auto make_order = [&f, &orders_ref](auto& predicate) { + std::sort(orders_ref[f].begin(), orders_ref[f].end(), predicate); + ++f; + }; + + make_order(comparator); + (make_order(comparators), ...); + + return find_best_packing_impl( + [&orders_ref](auto callback){ for (auto& o : orders_ref) { callback(o); } }, + input + ); + } + + /* + Finds the best packing for the rectangles. + Provides a list of several sensible comparison predicates. + */ + + template + rect_wh find_best_packing( + std::vector>& subjects, + const finder_input& input + ) { + using rect_type = output_rect_t; + + return find_best_packing( + subjects, + input, + + [](const rect_type* const a, const rect_type* const b) { + return a->area() > b->area(); + }, + [](const rect_type* const a, const rect_type* const b) { + return a->perimeter() > b->perimeter(); + }, + [](const rect_type* const a, const rect_type* const b) { + return std::max(a->w, a->h) > std::max(b->w, b->h); + }, + [](const rect_type* const a, const rect_type* const b) { + return a->w > b->w; + }, + [](const rect_type* const a, const rect_type* const b) { + return a->h > b->h; + }, + [](const rect_type* const a, const rect_type* const b) { + return a->get_wh().pathological_mult() > b->get_wh().pathological_mult(); + } + ); + } +} diff --git a/rectpack/insert_and_split.h b/rectpack/insert_and_split.h new file mode 100644 index 0000000..723582f --- /dev/null +++ b/rectpack/insert_and_split.h @@ -0,0 +1,135 @@ +#pragma once +#include +#include "rect_structs.h" + +namespace rectpack2D { + struct created_splits { + int count = 0; + std::array spaces; + + static auto failed() { + created_splits result; + result.count = -1; + return result; + } + + static auto none() { + return created_splits(); + } + + template + created_splits(Args&&... args) : spaces({ std::forward(args)... }) { + count = sizeof...(Args); + } + + bool better_than(const created_splits& b) const { + return count < b.count; + } + + explicit operator bool() const { + return count != -1; + } + }; + + inline created_splits insert_and_split( + const rect_wh& im, /* Image rectangle */ + const space_rect& sp /* Space rectangle */ + ) { + const auto free_w = sp.w - im.w; + const auto free_h = sp.h - im.h; + + if (free_w < 0 || free_h < 0) { + /* + Image is bigger than the candidate empty space. + We'll need to look further. + */ + + return created_splits::failed(); + } + + if (free_w == 0 && free_h == 0) { + /* + If the image dimensions equal the dimensions of the candidate empty space (image fits exactly), + we will just delete the space and create no splits. + */ + + return created_splits::none(); + } + + /* + If the image fits into the candidate empty space, + but exactly one of the image dimensions equals the respective dimension of the candidate empty space + (e.g. image = 20x40, candidate space = 30x40) + we delete the space and create a single split. In this case a 10x40 space. + */ + + if (free_w > 0 && free_h == 0) { + auto r = sp; + r.x += im.w; + r.w -= im.w; + return created_splits(r); + } + + if (free_w == 0 && free_h > 0) { + auto r = sp; + r.y += im.h; + r.h -= im.h; + return created_splits(r); + } + + /* + Every other option has been exhausted, + so at this point the image must be *strictly* smaller than the empty space, + that is, it is smaller in both width and height. + + Thus, free_w and free_h must be positive. + */ + + /* + Decide which way to split. + + Instead of having two normally-sized spaces, + it is better - though I have no proof of that - to have a one tiny space and a one huge space. + This creates better opportunity for insertion of future rectangles. + + This is why, if we had more of width remaining than we had of height, + we split along the vertical axis, + and if we had more of height remaining than we had of width, + we split along the horizontal axis. + */ + + if (free_w > free_h) { + const auto bigger_split = space_rect( + sp.x + im.w, + sp.y, + free_w, + sp.h + ); + + const auto lesser_split = space_rect( + sp.x, + sp.y + im.h, + im.w, + free_h + ); + + return created_splits(bigger_split, lesser_split); + } + + const auto bigger_split = space_rect( + sp.x, + sp.y + im.h, + sp.w, + free_h + ); + + const auto lesser_split = space_rect( + sp.x + im.w, + sp.y, + free_w, + im.h + ); + + return created_splits(bigger_split, lesser_split); + } +} diff --git a/rectpack/rect_structs.h b/rectpack/rect_structs.h new file mode 100644 index 0000000..8d50696 --- /dev/null +++ b/rectpack/rect_structs.h @@ -0,0 +1,78 @@ +#pragma once +#include + +namespace rectpack2D { + using total_area_type = int; + + struct rect_wh { + rect_wh() : w(0), h(0) {} + rect_wh(const int w, const int h) : w(w), h(h) {} + + int w; + int h; + + auto& flip() { + std::swap(w, h); + return *this; + } + + int max_side() const { + return h > w ? h : w; + } + + int min_side() const { + return h < w ? h : w; + } + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + float pathological_mult() const { + return float(max_side()) / min_side() * area(); + } + + template + void expand_with(const R& r) { + w = std::max(w, r.x + r.w); + h = std::max(h, r.y + r.h); + } + }; + + struct rect_xywh { + int x; + int y; + int w; + int h; + + rect_xywh() : x(0), y(0), w(0), h(0) {} + rect_xywh(const int x, const int y, const int w, const int h) : x(x), y(y), w(w), h(h) {} + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + auto get_wh() const { + return rect_wh(w, h); + } + }; + + struct rect_xywhf { + int x; + int y; + int w; + int h; + bool flipped; + + rect_xywhf() : x(0), y(0), w(0), h(0), flipped(false) {} + rect_xywhf(const int x, const int y, const int w, const int h, const bool flipped) : x(x), y(y), w(flipped ? h : w), h(flipped ? w : h), flipped(flipped) {} + rect_xywhf(const rect_xywh& b) : rect_xywhf(b.x, b.y, b.w, b.h, false) {} + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + auto get_wh() const { + return rect_wh(w, h); + } + }; + + using space_rect = rect_xywh; +} \ No newline at end of file