From 7e0027b7a02a46c729823d06f68fcb596f9c6fc4 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sun, 21 Jul 2024 16:25:43 +0100 Subject: [PATCH] Extract spell icon backgrounds at load time This allows for consistency between the regular version and the `UNPACKED_MPQS` one. The icons with extracted background have a slightly less blurry drop shadow (only noticeable in a side-by-side comparison). --- Source/panels/spell_icons.cpp | 132 ++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/Source/panels/spell_icons.cpp b/Source/panels/spell_icons.cpp index b1b0e9dea2a..6fee98e7577 100644 --- a/Source/panels/spell_icons.cpp +++ b/Source/panels/spell_icons.cpp @@ -1,6 +1,8 @@ #include "panels/spell_icons.hpp" +#include #include +#include #include #include "engine.h" @@ -8,17 +10,16 @@ #include "engine/load_clx.hpp" #include "engine/palette.h" #include "engine/render/clx_render.hpp" +#include "engine/surface.hpp" #include "init.h" +#include "utils/surface_to_clx.hpp" namespace devilution { namespace { -#ifdef UNPACKED_MPQS OptionalOwnedClxSpriteList LargeSpellIconsBackground; OptionalOwnedClxSpriteList SmallSpellIconsBackground; -#endif - OptionalOwnedClxSpriteList SmallSpellIcons; OptionalOwnedClxSpriteList LargeSpellIcons; @@ -82,6 +83,105 @@ const SpellIcon SpellITbl[] = { // clang-format on }; +#ifndef UNPACKED_MPQS +constexpr uint8_t TransparentColor = 255; + +constexpr uint8_t MinBgColor = 192; +constexpr uint8_t MaxBgColor = 206; + +struct Borders { + uint8_t top; + uint8_t right; + uint8_t bottom; + uint8_t left; +}; + +constexpr bool ShouldRemove(uint8_t fg, uint8_t bg) +{ + if (fg == 144) { + // A bright yellow pixel on the foreground is never + // part of the icon. + return true; + } + if (fg < MinBgColor || fg > MaxBgColor) { + // Out-of-range of background pixels. + return false; + } + + // Generated by `gen_extract_spell_icons_color_distances_main.cpp` + // from https://github.com/diasurgical/devilutionx-mpq-tools: + const auto [a, b] = std::minmax({ fg, bg }); + const auto d = static_cast(b - a); + if (d <= 5) return true; + if (d >= 7 && d <= 14) return false; + if ((a == 196 && b == 202) || (a == 199 && b == 205)) return true; + return false; +} + +void RemoveBackground(uint8_t *pixels, unsigned pitch, unsigned width, unsigned height, + Borders borders, const uint8_t *bg) +{ + // Remove top border: + std::memset(pixels, TransparentColor, static_cast(pitch) * borders.top); + + const unsigned innerWidth = width - borders.left - borders.right; + const unsigned innerHeight = height - borders.bottom - borders.top; + + // First round: remove borders, diff against the background, + // remove confidently transparent colors. + // Unfortunately, this alone is not enough because the backgrounds + // are all slightly different (looks like noise). + for (unsigned y = borders.top, yEnd = borders.top + innerHeight; y < yEnd; ++y) { + // Remove left border: + std::memset(&pixels[static_cast(y * pitch)], + TransparentColor, borders.left); + for (unsigned x = borders.left, xEnd = borders.left + innerWidth; x < xEnd; ++x) { + uint8_t &pixel = pixels[y * pitch + x]; + if (ShouldRemove(pixel, bg[y * pitch + x])) + pixel = TransparentColor; + } + // Remove right border: + std::memset(&pixels[y * pitch + borders.left + innerWidth], + TransparentColor, borders.right); + } + // Remove bottom border: + std::memset(&pixels[static_cast((borders.top + innerHeight) * pitch)], + TransparentColor, static_cast(pitch) * borders.bottom); +} + +void ExtractSpellIconsBackground(const char *celPath, uint16_t celWidth, unsigned numUnusedSprites, + Borders borders, OptionalOwnedClxSpriteList &foreground, OptionalOwnedClxSpriteList &background) +{ + OptionalOwnedClxSpriteList src = LoadCel(celPath, celWidth); + const unsigned width = (*src)[0].width(); + const unsigned height = (*src)[0].height(); + const unsigned numSprites = (*src).numSprites() - numUnusedSprites; + constexpr size_t EmptySpriteIndex = 26; + const OwnedSurface tmp { Size(static_cast(width), static_cast(height * numSprites)) }; + auto *pixels = static_cast(tmp.surface->pixels); + const unsigned pitch = tmp.surface->pitch; + { + Point pos { 0, 0 }; + for (uint_fast32_t i = 0; i < numSprites; ++i) { + RenderClxSprite(tmp, (*src)[i], pos); + pos.y += static_cast(height); + } + } + src = std::nullopt; + + background.emplace(SurfaceToClx( + tmp.subregionY(static_cast(EmptySpriteIndex * height), static_cast(height)))); + for (uint_fast32_t i = 0; i < numSprites; ++i) { + if (i == EmptySpriteIndex) continue; + RemoveBackground(&pixels[i * pitch * height], pitch, + width, height, borders, + &pixels[EmptySpriteIndex * pitch * height]); + } + std::memset(&pixels[EmptySpriteIndex * width * height], TransparentColor, static_cast(width) * height); + foreground.emplace(SurfaceToClx(tmp, numSprites, /*transparentColor=*/TransparentColor)); +} +#endif + } // namespace void LoadLargeSpellIcons() @@ -91,14 +191,22 @@ void LoadLargeSpellIcons() LargeSpellIcons = LoadClx("ctrlpan\\spelicon_fg.clx"); LargeSpellIconsBackground = LoadClx("ctrlpan\\spelicon_bg.clx"); #else - LargeSpellIcons = LoadCel("ctrlpan\\spelicon", SPLICONLENGTH); + // The last 9 sprites are overlays, unused in DevilutionX. + ExtractSpellIconsBackground( + "ctrlpan\\spelicon", /*celWidth=*/SPLICONLENGTH, /*numUnusedSprites=*/9, + Borders { .top = 5, .right = 5, .bottom = 4, .left = 4 }, + LargeSpellIcons, LargeSpellIconsBackground); #endif } else { #ifdef UNPACKED_MPQS LargeSpellIcons = LoadClx("data\\spelicon_fg.clx"); LargeSpellIconsBackground = LoadClx("data\\spelicon_bg.clx"); #else - LargeSpellIcons = LoadCel("data\\spelicon", SPLICONLENGTH); + // The last 9 sprites are overlays, unused in DevilutionX. + ExtractSpellIconsBackground( + "data\\spelicon", /*celWidth=*/SPLICONLENGTH, /*numUnusedSprites=*/9, + Borders { .top = 5, .right = 5, .bottom = 4, .left = 4 }, + LargeSpellIcons, LargeSpellIconsBackground); #endif } SetSpellTrans(SpellType::Skill); @@ -106,9 +214,7 @@ void LoadLargeSpellIcons() void FreeLargeSpellIcons() { -#ifdef UNPACKED_MPQS LargeSpellIconsBackground = std::nullopt; -#endif LargeSpellIcons = std::nullopt; } @@ -118,15 +224,17 @@ void LoadSmallSpellIcons() SmallSpellIcons = LoadClx("data\\spelli2_fg.clx"); SmallSpellIconsBackground = LoadClx("data\\spelli2_bg.clx"); #else - SmallSpellIcons = LoadCel("data\\spelli2", 37); + // The last sprite is unused + ExtractSpellIconsBackground( + "data\\spelli2", /*celWidth=*/37, /*numUnusedSprites=*/1, + Borders { .top = 2, .right = 2, .bottom = 1, .left = 1 }, + SmallSpellIcons, SmallSpellIconsBackground); #endif } void FreeSmallSpellIcons() { -#ifdef UNPACKED_MPQS SmallSpellIconsBackground = std::nullopt; -#endif SmallSpellIcons = std::nullopt; } @@ -137,17 +245,13 @@ uint8_t GetSpellIconFrame(SpellID spell) void DrawLargeSpellIcon(const Surface &out, Point position, SpellID spell) { -#ifdef UNPACKED_MPQS ClxDrawTRN(out, position, (*LargeSpellIconsBackground)[0], SplTransTbl); -#endif ClxDrawTRN(out, position, (*LargeSpellIcons)[GetSpellIconFrame(spell)], SplTransTbl); } void DrawSmallSpellIcon(const Surface &out, Point position, SpellID spell) { -#ifdef UNPACKED_MPQS ClxDrawTRN(out, position, (*SmallSpellIconsBackground)[0], SplTransTbl); -#endif ClxDrawTRN(out, position, (*SmallSpellIcons)[GetSpellIconFrame(spell)], SplTransTbl); }