From 435b62f2808d83a36d92c8cfde0d814c0cf4711d Mon Sep 17 00:00:00 2001 From: Nico de Poel Date: Tue, 14 Feb 2023 12:44:59 +0100 Subject: [PATCH] Reorganized the texture packing code to allow for multiple packing steps, with repeatable textures getting packed first. --- ps1bsp.h | 1 + texture.cpp | 134 +++++++++++++++++++++++++++++++++++----------------- texture.h | 1 + 3 files changed, 92 insertions(+), 44 deletions(-) diff --git a/ps1bsp.h b/ps1bsp.h index ab0f07d..8d28549 100644 --- a/ps1bsp.h +++ b/ps1bsp.h @@ -58,6 +58,7 @@ typedef struct { unsigned short tpage; // Texture page in PS1 VRAM (precalculated when generating the texture atlas) unsigned short nextframe; // If non-zero, the texture is animated and this points to the next texture in the sequence + unsigned int twin; // Texture window for repeating textures } ps1bsp_texture_t; // This matches the SVECTOR data type; we can use the extra padding to store some more data. diff --git a/texture.cpp b/texture.cpp index 71b5cd6..e6ab4ef 100644 --- a/texture.cpp +++ b/texture.cpp @@ -25,6 +25,14 @@ Keep in mind that the coordinates will be rounded down to the next lowest textur ((((y) / 512) & 1) << 11) \ ) +#define getTexWindow(x, y, w, h) ( \ + 0xe2000000 | \ + ((~((w)-1) & 0xFF)) >> 3 | \ + (((~((h)-1) & 0xFF) >> 3) << 5) | \ + ((((x) & 0xFF) >> 3) << 10) | \ + ((((y) & 0xFF) >> 3) << 15) \ +) + // A straight conversion of the Quake palette colors comes out looking rather over-saturated. // So we desaturate the palette ahead of time to more closely match the original Quake's look. // Yoinked from: https://stackoverflow.com/questions/13328029/how-to-desaturate-a-color @@ -170,11 +178,11 @@ static void analyze_texture(unsigned char* texBytes, int numBytes, const Color p desaturate(outTexture.dominantColor.channel, outTexture.dominantColor.channel); } -bool process_textures(const world_t* world, TextureList& outTextures) -{ - using spaces_type = rectpack2D::empty_spaces; - using rect_type = rectpack2D::output_rect_t; +using spaces_type = rectpack2D::empty_spaces; +using rect_type = rectpack2D::output_rect_t; +static rectpack2D::rect_wh pack_textures(const std::map& textures, const rectpack2D::rect_wh &max_bin, std::vector& outRectangles) +{ auto report_successful = [](rect_type&) { return rectpack2D::callback_result::CONTINUE_PACKING; }; @@ -184,21 +192,11 @@ bool process_textures(const world_t* world, TextureList& outTextures) return rectpack2D::callback_result::ABORT_PACKING; }; - const auto max_bin = rectpack2D::rect_wh(1024, 256); // 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; - printf("Finding best texture packing...\n"); - - 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) + for (auto texIter = textures.cbegin(); texIter != textures.cend(); ++texIter) { - 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); + const miptex_t* miptex = texIter->second; // Shrink the larger textures, but keep smaller ones at their original size int ps1mip = miptex->width > 64 || miptex->height > 64 ? 1 : 0; @@ -220,13 +218,13 @@ bool process_textures(const world_t* world, TextureList& outTextures) ps1mip--; if (strcmp(miptex->name, "clip") && strcmp(miptex->name, "trigger")) - rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> ps1mip, miptex->height >> ps1mip)); + outRectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> ps1mip, miptex->height >> ps1mip)); else - rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> 3, miptex->width >> 3)); // Add the lowest mip level so that it at least gets included in the final texture list, and we don't mess up the texture IDs + outRectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> 3, miptex->width >> 3)); // Add the lowest mip level so that it at least gets included in the final texture list, and we don't mess up the texture IDs } - const auto result_size = rectpack2D::find_best_packing( - rectangles, + return rectpack2D::find_best_packing( + outRectangles, rectpack2D::make_finder_input( max_bin, discard_step, @@ -235,9 +233,15 @@ bool process_textures(const world_t* world, TextureList& outTextures) rectpack2D::flipping_option::DISABLED ) ); +} - printf("%d textures. Packed texture atlas size: %d x %d\n", world->mipheader.numtex, result_size.w, result_size.h); - +static void build_atlas(const std::vector& textures, const std::vector& rectangles, const rectpack2D::rect_wh& size, unsigned char* outAtlas, TextureList& outTextures) +{ +} + +bool process_textures(const world_t* world, TextureList& outTextures) +{ + // Construct palette CLUT and start building the output TIM structure Color palette[PALETTE_SIZE]; load_palette("palette.lmp", palette); @@ -249,6 +253,29 @@ bool process_textures(const world_t* world, TextureList& outTextures) outTim.clutYoffs = 0; generate_clut(palette, &outTim); + const auto max_bin = rectpack2D::rect_wh(1024, 256); // 8-bit textures take up half the horizontal space so this is 512x256 in practice, or a quarter of the PS1's VRAM allocation. + + printf("Finding best texture packing...\n"); + + std::map textures; + 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); + + textures[texNum] = miptex; + } + + const auto result_size = pack_textures(textures, max_bin, rectangles); + + printf("%d textures. Packed texture atlas size: %d x %d\n", world->mipheader.numtex, result_size.w, result_size.h); + outTim.imgWidth = result_size.w; outTim.imgHeight = result_size.h; outTim.imgData = malloc(result_size.w * result_size.h * sizeof(unsigned char)); @@ -259,8 +286,6 @@ bool process_textures(const world_t* world, TextureList& outTextures) printf("Constructing texture atlas...\n"); - std::unordered_map> animationFrames; - // Try to construct the texture atlas, see what we get for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) { @@ -295,26 +320,7 @@ bool process_textures(const world_t* world, TextureList& outTextures) tex.ps1tex.tpage = getTPage(outTim.format, 0, x, y); tex.uoffs = (u_char)((x % 64) << (2 - outTim.format)); tex.voffs = (u_char)(y & 0xFF); - - if (miptex->name[0] == '+') - { - // Animated texture - int frameNum = miptex->name[1] - '0'; - if (frameNum >= 0 && frameNum <= 9) - { - std::string animName = &miptex->name[2]; - animationFrames[animName][frameNum] = texNum; - } - else - { - frameNum = miptex->name[1] - 'a'; - if (frameNum >= 0 && frameNum <= 9) - { - std::string animName = std::string(&miptex->name[2]) + "_alt"; - animationFrames[animName][frameNum] = texNum; - } - } - } + tex.ps1tex.twin = getTexWindow(tex.uoffs, tex.voffs, tex.w, tex.h); // TODO: figure out the right offsets that are multiples of w and h } } @@ -324,6 +330,32 @@ bool process_textures(const world_t* world, TextureList& outTextures) outTextures.push_back(tex); } + // Identify animated textures + std::unordered_map> animationFrames; + for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum) + { + miptex_t* miptex = &world->miptexes[texNum]; + if (miptex->name[0] == '+') + { + // Animated texture + int frameNum = miptex->name[1] - '0'; + if (frameNum >= 0 && frameNum <= 9) + { + std::string animName = &miptex->name[2]; + animationFrames[animName][frameNum] = texNum; + } + else + { + frameNum = miptex->name[1] - 'a'; + if (frameNum >= 0 && frameNum <= 9) + { + std::string animName = std::string(&miptex->name[2]) + "_alt"; + animationFrames[animName][frameNum] = texNum; + } + } + } + } + // Link animated texture frames together for (auto animIter = animationFrames.cbegin(); animIter != animationFrames.cend(); ++animIter) { @@ -349,3 +381,17 @@ bool process_textures(const world_t* world, TextureList& outTextures) free(outTim.clutData); return true; } + +bool texture_isRepeatable(miptex_t* miptex) +{ + // Check if the texture is square + if (miptex->width != miptex->height) + return false; + + // Check if width and height are powers of two (and a multiple of 8) + unsigned int pot = 8; + while (pot < miptex->width) + pot <<= 1; + + return pot == miptex->width; +} diff --git a/texture.h b/texture.h index dce9fd1..809b28a 100644 --- a/texture.h +++ b/texture.h @@ -25,3 +25,4 @@ struct TextureDescriptor typedef std::vector TextureList; bool process_textures(const world_t* world, TextureList& outTextures); +bool texture_isRepeatable(miptex_t* miptex);