add support for ordon sword recoloring

This commit is contained in:
gymnast86
2026-08-09 08:18:28 -07:00
parent e9918cc8ed
commit 16dd1118d1
5 changed files with 246 additions and 13 deletions
@@ -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<uint8_t>((2 * base * blend) / 255);
}
return static_cast<uint8_t>(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;
}
@@ -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);
+4
View File
@@ -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)
+2
View File
@@ -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;
+227 -13
View File
@@ -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<BE<uint16_t>*>(JSUConvertOffsetToPtr<u8>(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<uint8_t>((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<uint8_t>((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<uint16_t>((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<uint8_t>(c0_565 >> 8);
dst[1] = static_cast<uint8_t>(c0_565 & 0xFF);
dst[2] = static_cast<uint8_t>(c1_565 >> 8);
dst[3] = static_cast<uint8_t>(c1_565 & 0xFF);
dst[4] = static_cast<uint8_t>(indices >> 24);
dst[5] = static_cast<uint8_t>((indices >> 16) & 0xFF);
dst[6] = static_cast<uint8_t>((indices >> 8) & 0xFF);
dst[7] = static_cast<uint8_t>(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<uint8_t>(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);
}
}
}