summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEblo <7004497+Eblo@users.noreply.github.com>2025-07-09 15:09:16 -0400
committerGitHub <noreply@github.com>2025-07-09 15:09:16 -0400
commit7d23c7fe154ba92570aca5d4ee97ec88bb0033dd (patch)
tree7fe6a9e4316709f973dee7a679c8f93c8ff0403c
parentc30597249ff82d75b98edcd3bdf382ca99a13e61 (diff)
Tweak cosmetic editor to handle Zora colors, add missed pieces (#1198)
* Tweak cosmetic editor to handle Zora colors Apply custom colors to Goron roll Apply custom colors to Deku in flower Apply custom colors to Zora boomerang and shield * Max alpha should be 31, not 32
-rw-r--r--mm/2s2h/BenGui/CosmeticEditor.cpp207
1 files changed, 147 insertions, 60 deletions
diff --git a/mm/2s2h/BenGui/CosmeticEditor.cpp b/mm/2s2h/BenGui/CosmeticEditor.cpp
index caa6702c6..605780694 100644
--- a/mm/2s2h/BenGui/CosmeticEditor.cpp
+++ b/mm/2s2h/BenGui/CosmeticEditor.cpp
@@ -117,9 +117,9 @@ Gfx backToWhite[] = {
};
// First, it pulls all colors from the palette. Then it finds the average color across the range, and calculates the
-// difference between the average color and the target color. It then colors the range a bright shade of off-white,
+// difference between the average color and the target color. It then colors the range according to newBase,
// and shades it lighter or darker based on the difference between the average color and the target color.
-void ShadePaletteWhite(const char* path, uint32_t begin, uint32_t end, SHADE_MODE mode) {
+void ShadePaletteNewBase(const char* path, uint32_t begin, uint32_t end, Color_RGBA8 newBase, SHADE_MODE mode) {
auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path);
auto data = (uint8_t*)res->GetRawPointer();
@@ -166,14 +166,93 @@ void ShadePaletteWhite(const char* path, uint32_t begin, uint32_t end, SHADE_MOD
diff = MAX(MAX(diffR, diffG), diffB);
}
- r = MIN(MAX(31 - diff, 0), 31);
- g = MIN(MAX(31 - diff, 0), 31);
- b = MIN(MAX(31 - diff, 0), 31);
+ diff = MIN(MAX(31 - diff, 0), 31);
+
+ r = (diff * newBase.r) / 255;
+ g = (diff * newBase.g) / 255;
+ b = (diff * newBase.b) / 255;
PatchPalette(path, i, r, g, b);
}
}
+static const Color_RGBA8 whiteBase = { 255, 255, 255, 255 };
+
+void ShadePaletteWhite(const char* path, uint32_t begin, uint32_t end, SHADE_MODE mode) {
+ ShadePaletteNewBase(path, begin, end, whiteBase, mode);
+}
+
+void ShadePaletteRevert(const char* path, uint32_t begin, uint32_t end) {
+ ShadePaletteNewBase(path, begin, end, whiteBase, MODE_REVERT);
+}
+
+/*
+ * Given the existing base and target color, map the current color to a new blend between the new base and target. The
+ * resulting color's channels are in (0-31) format.
+ */
+Color_RGBA8 mapNewBaseColorToGradient(Color_RGBA8 currentColor, Color_RGBA8 oldBase, Color_RGBA8 newBase,
+ Color_RGBA8 targetEnd) {
+ double gradientRed = targetEnd.r - oldBase.r;
+ double gradientGreen = targetEnd.g - oldBase.g;
+ double gradientBlue = targetEnd.b - oldBase.b;
+
+ double deltaRed = currentColor.r - oldBase.r;
+ double deltaGreen = currentColor.g - oldBase.g;
+ double deltaBlue = currentColor.b - oldBase.b;
+
+ double gradient = gradientRed * gradientRed + gradientGreen * gradientGreen + gradientBlue * gradientBlue;
+ double projection = gradientRed * deltaRed + gradientGreen * deltaGreen + gradientBlue * deltaBlue;
+
+ // This condition exists in the event that oldBase and targetEnd are equal, leading to a division by zero.
+ // Normally shouldn't happen, but just in case.
+ double position = 0.0;
+ if (gradient != 0.0) {
+ position = projection / gradient;
+ }
+
+ return { uint8_t(newBase.r + position * (targetEnd.r - newBase.r)),
+ uint8_t(newBase.g + position * (targetEnd.g - newBase.g)),
+ uint8_t(newBase.b + position * (targetEnd.b - newBase.b)), 31 };
+}
+
+/*
+ * Recolors textures that gradually transition from one color to another. Given oldBase (e.g. green parts of Zora) and
+ * targetEnd (e.g. bluish skin of Zora), recolor so that the resulting texture instead fades from newBase (e.g. custom
+ * tunic color) to targetEnd.
+ */
+void ShadePaletteGradient(const char* path, uint32_t begin, uint32_t end, Color_RGBA8 oldBase, Color_RGBA8 newBase,
+ Color_RGBA8 targetEnd) {
+ ShadePaletteRevert(path, begin, end);
+
+ // Convert 0-255 range to 0-31 range
+ newBase.r >>= 3;
+ newBase.g >>= 3;
+ newBase.b >>= 3;
+ newBase.a >>= 3;
+ targetEnd.r >>= 3;
+ targetEnd.g >>= 3;
+ targetEnd.b >>= 3;
+ targetEnd.a >>= 3;
+ oldBase.r >>= 3;
+ oldBase.g >>= 3;
+ oldBase.b >>= 3;
+ oldBase.a >>= 3;
+
+ auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path);
+ auto data = (uint8_t*)res->GetRawPointer();
+ for (int i = begin; i <= end; i++) {
+ uint16_t col16 = (data[i * 2] << 8) | data[i * 2 + 1];
+ uint8_t a = col16 & 1;
+ uint8_t r = col16 >> 11;
+ uint8_t g = (col16 >> 6) & 0x1f;
+ uint8_t b = (col16 >> 1) & 0x1f;
+
+ Color_RGBA8 currentColor = { r, g, b, a };
+ Color_RGBA8 newColor = mapNewBaseColorToGradient(currentColor, oldBase, newBase, targetEnd);
+ PatchPalette(path, i, newColor.r, newColor.g, newColor.b);
+ }
+}
+
void CopyFloatArray(CosmeticEditorElementID id, float currentColor[4], bool isChanged) {
if (isChanged) {
Color_RGBA8 changedColor = CVarGetColor(cosmeticEditorElements[id].colorCvar, {});
@@ -457,7 +536,7 @@ static RegisterShipInitFunc humanHairPatch(
ResourceMgr_UnpatchGfxByName("objects/object_link_child/gLinkHumanHeadDL", "setPrim1");
ResourceMgr_UnpatchGfxByName("objects/object_link_child/gLinkHumanHeadDL", "setPrim2");
- ShadePaletteWhite("objects/object_link_child/object_link_child_Tex_005400", 0, 127, MODE_REVERT);
+ ShadePaletteRevert("objects/object_link_child/object_link_child_Tex_005400", 0, 127);
}
gfx_texture_cache_clear();
},
@@ -497,7 +576,7 @@ static RegisterShipInitFunc dekuTunicPatch(
ResourceMgr_UnpatchGfxByName("objects/object_link_nuts/gLinkDekuHeadDL", "setPrim2");
ResourceMgr_UnpatchGfxByName("objects/object_link_nuts/gLinkDekuHatDL", "setPrim");
- ShadePaletteWhite("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 243, 254, MODE_REVERT);
+ ShadePaletteRevert("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 243, 254);
}
gfx_texture_cache_clear();
},
@@ -525,15 +604,18 @@ static RegisterShipInitFunc dekuHairPatch(
gsSPDisplayList(dekuHair));
ResourceMgr_PatchGfxByName("objects/object_link_nuts/gLinkDekuHeadDL", "setPrim4", 42,
gsSPDisplayList(backToWhite));
+ ResourceMgr_PatchGfxByName("objects/object_link_nuts/object_link_nuts_DL_009C48", "setPrim", 22,
+ gsSPDisplayList(dekuHair));
ShadePaletteWhite("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 109, 122, MODE_MAX);
ShadePaletteWhite("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 124, 242, MODE_MAX);
} else {
ResourceMgr_UnpatchGfxByName("objects/object_link_nuts/gLinkDekuHeadDL", "setPrim3");
ResourceMgr_UnpatchGfxByName("objects/object_link_nuts/gLinkDekuHeadDL", "setPrim4");
+ ResourceMgr_UnpatchGfxByName("objects/object_link_nuts/object_link_nuts_DL_009C48", "setPrim");
- ShadePaletteWhite("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 109, 122, MODE_REVERT);
- ShadePaletteWhite("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 124, 242, MODE_REVERT);
+ ShadePaletteRevert("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 109, 122);
+ ShadePaletteRevert("objects/object_link_nuts/object_link_nuts_TLUT_003EB0", 124, 242);
}
gfx_texture_cache_clear();
},
@@ -574,8 +656,8 @@ static RegisterShipInitFunc kafeiHairPatch(
ResourceMgr_UnpatchGfxByName("objects/object_test3/gKafeiHeadDL", "setPrim3");
ResourceMgr_UnpatchGfxByName("objects/object_test3/gKafeiHeadDL", "setPrim4");
- ShadePaletteWhite("objects/object_test3/gKafeiBody2TLUT", 1, 3, MODE_REVERT);
- ShadePaletteWhite("objects/object_test3/gKafeiBody2TLUT", 8, 255, MODE_REVERT);
+ ShadePaletteRevert("objects/object_test3/gKafeiBody2TLUT", 1, 3);
+ ShadePaletteRevert("objects/object_test3/gKafeiBody2TLUT", 8, 255);
}
gfx_texture_cache_clear();
},
@@ -605,11 +687,20 @@ static RegisterShipInitFunc goronTunicPatch(
gsSPDisplayList(goronTunic));
ShadePaletteWhite("objects/object_link_goron/object_link_goron_Tex_002780", 0, 127, MODE_MAX);
+ /*
+ * gLinkGoronCurledDL contains information for applying the hat texture when Link is Goron rolling, but it
+ * does not seem to obey color for anything but the necklace beads. Instead, directly set the color of the
+ * texture.
+ */
+ Color_RGBA8 changedColor = CVarGetColor(cosmeticEditorElements[COSMETIC_ELEMENT_GORON_TUNIC].colorCvar, {});
+ ShadePaletteNewBase("objects/object_link_goron/object_link_goron_Tex_00CEB8", 0, 127, changedColor,
+ MODE_MAX);
} else {
ResourceMgr_UnpatchGfxByName("objects/object_link_goron/gLinkGoronWaistDL", "setPrim");
ResourceMgr_UnpatchGfxByName("objects/object_link_goron/gLinkGoronHatDL", "setPrim");
- ShadePaletteWhite("objects/object_link_goron/object_link_goron_Tex_002780", 0, 127, MODE_REVERT);
+ ShadePaletteRevert("objects/object_link_goron/object_link_goron_Tex_002780", 0, 127);
+ ShadePaletteRevert("objects/object_link_goron/object_link_goron_Tex_00CEB8", 0, 127);
}
gfx_texture_cache_clear();
},
@@ -623,60 +714,56 @@ static RegisterShipInitFunc goronTunicColor(
{ cosmeticEditorElements[COSMETIC_ELEMENT_GORON_TUNIC].colorCvar });
// COSMETIC_ELEMENT_ZORA_TUNIC
-Gfx zoraTunic[] = {
- gsDPSetPrimColor(0, 0, 0, 0, 0, 0),
- gsDPPipeSync(),
- gsSPEndDisplayList(),
-};
+static const Color_RGBA8 zoraSkinColor = { 197, 247, 247, 255 };
+static const Color_RGBA8 zoraTunicBaseColor = { 0, 74, 16, 255 };
static RegisterShipInitFunc zoraTunicPatch(
[]() {
if (CVarGetInteger(cosmeticEditorElements[COSMETIC_ELEMENT_ZORA_TUNIC].colorChangedCvar, 0)) {
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraHatDL", "setPrim", 28,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraWaistDL", "setPrim", 46,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim1", 31,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim2", 85,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim3", 97,
- gsSPDisplayList(backToWhite));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim1", 31,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim2", 85,
- gsSPDisplayList(zoraTunic));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim3", 97,
- gsSPDisplayList(backToWhite));
- ResourceMgr_PatchGfxByName("objects/object_link_zora/object_link_zora_DL_00CC38", "setPrim", 43,
- gsSPDisplayList(zoraTunic)); // left arm
- ResourceMgr_PatchGfxByName("objects/object_link_zora/object_link_zora_DL_00CDA0", "setPrim", 43,
- gsSPDisplayList(zoraTunic)); // right arm
-
- ShadePaletteWhite("objects/object_link_zora/object_link_zora_TLUT_00C578", 151, 177, MODE_MIN);
- ShadePaletteWhite("objects/object_link_zora/object_link_zora_TLUT_005000", 151, 177, MODE_MIN);
+ /*
+ * Zora works differently from the other color changes. Other forms apply a grayscale to the green tunic
+ * textures and then alter the Gfx commands to set the color. That works because those textures are one
+ * basic color. Zora, however, gradually transitions from green to the bluish Zora skin. A further
+ * complication is that relevant colors in the TLUTs are not contiguous, so the brightness calculation will
+ * not work as intended. Instead of using the palette approach, here we directly apply the custom color to
+ * the textures and TLUTs.
+ */
+ Color_RGBA8 changedColor = CVarGetColor(cosmeticEditorElements[COSMETIC_ELEMENT_ZORA_TUNIC].colorCvar, {});
+ // Arms
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_00C578", 151, 177, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_00C578", 179, 180, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_00C578", 183, 183, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+
+ // Hat/head and pants
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_005000", 151, 177, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_005000", 179, 180, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_TLUT_005000", 183, 183, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+
+ // Shield
+ ShadePaletteGradient("objects/object_link_zora/object_link_zora_Tex_010228", 80, 511, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
+
+ // Boomerangs
+ ShadePaletteGradient("objects/gameplay_keep/gameplay_keep_Tex_0700B0", 80, 511, zoraTunicBaseColor,
+ changedColor, zoraSkinColor);
} else {
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraHatDL", "setPrim");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraWaistDL", "setPrim");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim1");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim2");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraRightThighDL", "setPrim3");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim1");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim2");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/gLinkZoraLeftThighDL", "setPrim3");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/object_link_zora_DL_00CC38", "setPrim");
- ResourceMgr_UnpatchGfxByName("objects/object_link_zora/object_link_zora_DL_00CDA0", "setPrim");
-
- ShadePaletteWhite("objects/object_link_zora/object_link_zora_TLUT_00C578", 151, 177, MODE_REVERT);
- ShadePaletteWhite("objects/object_link_zora/object_link_zora_TLUT_005000", 151, 177, MODE_REVERT);
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_00C578", 151, 177);
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_00C578", 179, 180);
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_00C578", 183, 183);
+
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_005000", 151, 177);
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_005000", 179, 180);
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_TLUT_005000", 183, 183);
+
+ ShadePaletteRevert("objects/object_link_zora/object_link_zora_Tex_010228", 80, 511);
+ ShadePaletteRevert("objects/gameplay_keep/gameplay_keep_Tex_0700B0", 80, 511);
}
gfx_texture_cache_clear();
},
{ cosmeticEditorElements[COSMETIC_ELEMENT_ZORA_TUNIC].colorChangedCvar });
-
-static RegisterShipInitFunc zoraTunicColor(
- []() {
- Color_RGBA8 changedColor = CVarGetColor(cosmeticEditorElements[COSMETIC_ELEMENT_ZORA_TUNIC].colorCvar, {});
- zoraTunic[0] = gsDPSetPrimColor(0, 0, changedColor.r, changedColor.g, changedColor.b, 255);
- },
- { cosmeticEditorElements[COSMETIC_ELEMENT_ZORA_TUNIC].colorCvar });