diff --git a/mods/basic_cosmetics_mod/src/color_utils.cpp b/mods/basic_cosmetics_mod/src/color_utils.cpp index d555186907..b522bce7e0 100644 --- a/mods/basic_cosmetics_mod/src/color_utils.cpp +++ b/mods/basic_cosmetics_mod/src/color_utils.cpp @@ -49,6 +49,16 @@ uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color) return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3); } +// Helper function to perform overlay blending on a single 8-bit color channel +uint8_t blend_overlay_channel(uint8_t base, uint8_t blend) +{ + if (base < 128) { + return static_cast((2 * base * blend) / 255); + } + + return static_cast(255 - (2 * (255 - base) * (255 - blend)) / 255); +} + bool is_valid_hex_color_str(std::string_view hexStr) { return hexStr.find_first_not_of("0123456789ABCDEFabcdef") == std::string_view::npos && hexStr.length() == 6; } diff --git a/mods/basic_cosmetics_mod/src/color_utils.hpp b/mods/basic_cosmetics_mod/src/color_utils.hpp index eef8b438d7..a8ebd3e334 100644 --- a/mods/basic_cosmetics_mod/src/color_utils.hpp +++ b/mods/basic_cosmetics_mod/src/color_utils.hpp @@ -18,6 +18,9 @@ uint8_t desaturate_rgb_565(uint16_t rgb565Val); // array of {r,g,b}. Returns the result as an RGB565. uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color); +// Perform overlay blending on a single 8-bit color channel +uint8_t blend_overlay_channel(uint8_t base, uint8_t blend); + bool is_valid_hex_color_str(std::string_view hexStr); GXColor hex_color_str_to_gx_color(const std::string& hexColorStr); diff --git a/mods/basic_cosmetics_mod/src/mod.cpp b/mods/basic_cosmetics_mod/src/mod.cpp index e82d42c49e..fcadb3ea03 100644 --- a/mods/basic_cosmetics_mod/src/mod.cpp +++ b/mods/basic_cosmetics_mod/src/mod.cpp @@ -251,6 +251,8 @@ ModResult build_equipment_colors_tab( add_cosmetic_option(left, g_cvars.zoraArmorFlippersColor, "Zora Armor Flippers Color", kDefaultHexColorsExplanation); add_cosmetic_option_with_rainbow(left, g_cvars.lanternGlowColor, "Lantern Glow Color", kGlowColorsExplanation); add_cosmetic_option(left, g_cvars.woodenSwordColor, "Wooden Sword Color", kDefaultHexColorsExplanation); + add_cosmetic_option(left, g_cvars.ordonSwordBladeColor, "Ordon Sword Blade Color", kDefaultHexColorsExplanation); + add_cosmetic_option(left, g_cvars.ordonSwordHandleColor, "Ordon Sword Handle Color", kDefaultHexColorsExplanation); add_cosmetic_option(left, g_cvars.msBladeColor, "Master Sword Blade Color", kMasterSwordColorsExplanation); add_cosmetic_option(left, g_cvars.msHandleColor, "Master Sword Handle Color", kMasterSwordColorsExplanation); add_cosmetic_option_with_rainbow(left, g_cvars.lightSwordGlowColor, "Light Sword Glow Color", kGlowColorsExplanation); @@ -345,6 +347,8 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { REGISTER_COSMETIC_OPTION(zoraArmorFlippersColor) REGISTER_COSMETIC_OPTION(lanternGlowColor) REGISTER_COSMETIC_OPTION(woodenSwordColor) + REGISTER_COSMETIC_OPTION(ordonSwordBladeColor) + REGISTER_COSMETIC_OPTION(ordonSwordHandleColor) REGISTER_COSMETIC_OPTION(msBladeColor) REGISTER_COSMETIC_OPTION(msHandleColor) REGISTER_COSMETIC_OPTION(lightSwordGlowColor) diff --git a/mods/basic_cosmetics_mod/src/mod.hpp b/mods/basic_cosmetics_mod/src/mod.hpp index 113364e4d2..936b5097af 100644 --- a/mods/basic_cosmetics_mod/src/mod.hpp +++ b/mods/basic_cosmetics_mod/src/mod.hpp @@ -15,6 +15,8 @@ struct cvars { ConfigVarHandle zoraArmorFlippersColor = 0; ConfigVarHandle lanternGlowColor = 0; ConfigVarHandle woodenSwordColor = 0; + ConfigVarHandle ordonSwordBladeColor = 0; + ConfigVarHandle ordonSwordHandleColor = 0; ConfigVarHandle msBladeColor = 0; ConfigVarHandle msHandleColor = 0; ConfigVarHandle lightSwordGlowColor = 0; diff --git a/mods/basic_cosmetics_mod/src/texture_utils.cpp b/mods/basic_cosmetics_mod/src/texture_utils.cpp index 86674da456..f9ea00d9a3 100644 --- a/mods/basic_cosmetics_mod/src/texture_utils.cpp +++ b/mods/basic_cosmetics_mod/src/texture_utils.cpp @@ -36,6 +36,73 @@ ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* return nullptr; } +void recolor_rgb5a3_texture(ResTIMG* texHeaderPtr, GXColor color) +{ + // Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes + uint16_t recolors_rgb555[0x100]; + uint16_t recolors_rgb444[0x100]; + + for (int32_t i = 0; i < 0x100; i++) { + const uint8_t r = blend_overlay_channel(i, color.r); + const uint8_t g = blend_overlay_channel(i, color.g); + const uint8_t b = blend_overlay_channel(i, color.b); + + // Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B + recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3); + + // Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0) + recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4); + } + + constexpr int32_t blockWidth = 4; + constexpr int32_t blockHeight = 4; + + const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth); + const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight); + + const int32_t totalPixels = roundedWidth * roundedHeight; + + auto* pixelPtr = reinterpret_cast*>(JSUConvertOffsetToPtr(texHeaderPtr, texHeaderPtr->imageOffset)); + + for (int32_t i = 0; i < totalPixels; i++) { + const uint16_t rawPixel = pixelPtr[i]; + + // MSB determines if pixel is opaque or translucent + if (rawPixel & 0x8000) { + // Pixel is opaque + const uint8_t r5 = (rawPixel >> 10) & 0x1F; + const uint8_t g5 = (rawPixel >> 5) & 0x1F; + const uint8_t b5 = rawPixel & 0x1F; + + // Expand 5-bit to 8-bit + const uint8_t r8 = (r5 << 3) | (r5 >> 2); + const uint8_t g8 = (g5 << 3) | (g5 >> 2); + const uint8_t b8 = (b5 << 3) | (b5 >> 2); + + const uint8_t grayVal = static_cast((r8 * 77 + g8 * 150 + b8 * 29) >> 8); + + pixelPtr[i] = recolors_rgb555[grayVal]; + } else { + // Pixel is translucent + const uint16_t alpha3 = rawPixel & 0x7000; + + const uint8_t r4 = (rawPixel >> 8) & 0x0F; + const uint8_t g4 = (rawPixel >> 4) & 0x0F; + const uint8_t b4 = rawPixel & 0x0F; + + // Expand 4-bit to 8-bit + const uint8_t r8 = (r4 << 4) | r4; + const uint8_t g8 = (g4 << 4) | g4; + const uint8_t b8 = (b4 << 4) | b4; + + const uint8_t grayVal = static_cast((r8 * 77 + g8 * 150 + b8 * 29) >> 8); + + // Combine original alpha with recolored RGB444 + pixelPtr[i] = alpha3 | recolors_rgb444[grayVal]; + } + } +} + // When left is greater than right // 0b00 points to the left color // 0b01 points to the right color @@ -73,18 +140,8 @@ uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) { return bits ^ mask; } -void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) +void recolor_cmpr_texture(ResTIMG* texHeaderPtr, GXColor color) { - ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName); - if (texHeaderPtr == nullptr) { - return; - } - - if (texHeaderPtr->format != GX_VA_TEX1) { - // Texture is not CMPR - return; - } - uint16_t recolors[0x100]; for (int32_t i = 0; i < 0x100; i++) { recolors[i] = blend_overlay_rgb_565(i, color); @@ -159,6 +216,158 @@ void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXC } } +// Function to encode a single 4x4 sub-block (16 pixels) into an 8-byte CMPR block +static void encode_cmpr_sub_block(uint8_t* dst, const uint8_t pixels[16]) { + uint8_t min_val = 255; + uint8_t max_val = 0; + + for (int i = 0; i < 16; ++i) { + if (pixels[i] < min_val) min_val = pixels[i]; + if (pixels[i] > max_val) max_val = pixels[i]; + } + + auto intensity_to_rgb565 = [](uint8_t val) -> uint16_t { + uint16_t r5 = val >> 3; + uint16_t g6 = val >> 2; + uint16_t b5 = val >> 3; + return static_cast((r5 << 11) | (g6 << 5) | b5); + }; + + uint16_t c0_565 = intensity_to_rgb565(max_val); + uint16_t c1_565 = intensity_to_rgb565(min_val); + uint32_t indices = 0; + + if (max_val > min_val) { + // Enforce c0_565 > c1_565 in unsigned 16-bit representation to use 4-color mode + if (c0_565 == c1_565) { + if ((c0_565 & 0x001F) < 0x001F) { + c0_565 += 1; + } else { + c1_565 -= 1; + } + } + + // Interpolated 8-bit intensity values for quantization + const int c0 = max_val; + const int c1 = min_val; + const int c2 = (2 * max_val + min_val) / 3; + const int c3 = (max_val + 2 * min_val) / 3; + + // Map each pixel to the nearest palette entry + for (int i = 0; i < 16; ++i) { + const int p = pixels[i]; + const int d0 = std::abs(p - c0); + const int d1 = std::abs(p - c1); + const int d2 = std::abs(p - c2); + const int d3 = std::abs(p - c3); + + uint32_t best_idx = 0; + int min_d = d0; + + if (d1 < min_d) { min_d = d1; best_idx = 1; } + if (d2 < min_d) { min_d = d2; best_idx = 2; } + if (d3 < min_d) { min_d = d3; best_idx = 3; } + + indices |= (best_idx << (30 - (2 * i))); + } + } + + // Account for big endian data expectation + dst[0] = static_cast(c0_565 >> 8); + dst[1] = static_cast(c0_565 & 0xFF); + dst[2] = static_cast(c1_565 >> 8); + dst[3] = static_cast(c1_565 & 0xFF); + dst[4] = static_cast(indices >> 24); + dst[5] = static_cast((indices >> 16) & 0xFF); + dst[6] = static_cast((indices >> 8) & 0xFF); + dst[7] = static_cast(indices & 0xFF); +} + +bool convert_i8_to_cmpr(ResTIMG* texHeaderPtr) { + + const uint16_t width = texHeaderPtr->width; + const uint16_t height = texHeaderPtr->height; + + if (width % 8 != 0 || height % 8 != 0) { + return false; + } + + const uint32_t tilesX = width / 8; + const uint32_t blocksY_CMPR = height / 8; + + auto* imageData = JSUConvertOffsetToPtr(texHeaderPtr, texHeaderPtr->imageOffset); + uint8_t* writePtr = imageData; + + // Process image in 8x8 blocks + for (uint32_t by = 0; by < blocksY_CMPR; ++by) { + for (uint32_t bx = 0; bx < tilesX; ++bx) { + // Locate the two stacked 8x4 I8 tiles (32 bytes each) making up the 8x8 block + const uint32_t topTileIdx = (2 * by) * tilesX + bx; + const uint32_t bottomTileIdx = (2 * by + 1) * tilesX + bx; + + const uint8_t* topTileData = imageData + (topTileIdx * 32); + const uint8_t* bottomTileData = imageData + (bottomTileIdx * 32); + + uint8_t subBlockPixels[4][16]; + + // Extract Sub-block 0 (Top-Left) and Sub-block 1 (Top-Right) + for (int row = 0; row < 4; ++row) { + for (int col = 0; col < 4; ++col) { + subBlockPixels[0][row * 4 + col] = topTileData[row * 8 + col]; + subBlockPixels[1][row * 4 + col] = topTileData[row * 8 + col + 4]; + } + } + + // Extract Sub-block 2 (Bottom-Left) and Sub-block 3 (Bottom-Right) + for (int row = 0; row < 4; ++row) { + for (int col = 0; col < 4; ++col) { + subBlockPixels[2][row * 4 + col] = bottomTileData[row * 8 + col]; + subBlockPixels[3][row * 4 + col] = bottomTileData[row * 8 + col + 4]; + } + } + + // Encode the 4 sub-blocks in CMPR order (32 bytes per 8x8 block) + for (const auto& subBlockPixel : subBlockPixels) { + encode_cmpr_sub_block(writePtr, subBlockPixel); + writePtr += 8; + } + } + } + + // Update ResTIMG header metadata + texHeaderPtr->format = GX_TF_CMPR; // 0x0E + texHeaderPtr->colorFormat = 0; + texHeaderPtr->numColors = 0; + texHeaderPtr->paletteOffset = 0; + + return true; +} + +void recolor_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) { + ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName); + if (texHeaderPtr == nullptr) { + return; + } + + switch (texHeaderPtr->format) { + case GX_TF_CMPR: + recolor_cmpr_texture(texHeaderPtr, color); + break; + case GX_TF_RGB5A3: + recolor_rgb5a3_texture(texHeaderPtr, color); + break; + case GX_TF_I8: + if (convert_i8_to_cmpr(texHeaderPtr)) { + recolor_cmpr_texture(texHeaderPtr, color); + } else { + mods::log::debug("Could not convert {} from i8 to cmpr", textureName); + } + break; + default: + break; + } +} + J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr) { if (bmdPtr == nullptr) { @@ -246,6 +455,11 @@ auto& get_cosmetic_overrides() { cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_swb.bmd"] = { {.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor}, }; + // Ordon Sword Colors + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwr/al_swa.bmd"] = { + {.textures = {"al_SWA", "hilight01"}, .hexColor = g_cvars.ordonSwordBladeColor}, + {.textures = {"al_SWgripA"}, .hexColor = g_cvars.ordonSwordHandleColor}, + }; // Master Sword Colors cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwe/al_swm.bmd"] = { {.textures = {"al_SWM"}, .hexColor = g_cvars.msBladeColor}, @@ -265,7 +479,7 @@ auto& get_cosmetic_overrides() { }; // Wolf Link Color cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = { - {.textures = {"wl_body"}, .hexColor = g_cvars.wolfLinkColor}, + {.textures = {"wl_body", "wl_eye.1", "wl_eye.2", "wl_eye.3", "wl_eye.4", "wl_eye.5"}, .hexColor = g_cvars.wolfLinkColor}, }; // Wooden Sword Item Model cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_SWB.arc")]["bmdr/o_gd_al_swb.bmd"] = { @@ -327,7 +541,7 @@ void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive) { auto color = hex_color_str_to_gx_color(hexColorStr); if (tex1Addr) { for (const auto& textureName : textures) { - recolor_cmpr_texture(tex1Addr, textureName.data(), color); + recolor_texture(tex1Addr, textureName.data(), color); } } }