Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract spell icon backgrounds at load time #7196

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 118 additions & 14 deletions Source/panels/spell_icons.cpp
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
#include "panels/spell_icons.hpp"

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <optional>

#include "engine.h"
#include "engine/load_cel.hpp"
#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;

Expand Down Expand Up @@ -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<unsigned>(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<size_t>(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<size_t>(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<size_t>((borders.top + innerHeight) * pitch)],
TransparentColor, static_cast<size_t>(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<int>(width), static_cast<int>(height * numSprites)) };
auto *pixels = static_cast<uint8_t *>(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<int>(height);
}
}
src = std::nullopt;

background.emplace(SurfaceToClx(
tmp.subregionY(static_cast<int>(EmptySpriteIndex * height), static_cast<int>(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<size_t>(width) * height);
foreground.emplace(SurfaceToClx(tmp, numSprites, /*transparentColor=*/TransparentColor));
}
#endif

} // namespace

void LoadLargeSpellIcons()
Expand All @@ -91,24 +191,30 @@ 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);
}

void FreeLargeSpellIcons()
{
#ifdef UNPACKED_MPQS
LargeSpellIconsBackground = std::nullopt;
#endif
LargeSpellIcons = std::nullopt;
}

Expand All @@ -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;
}

Expand All @@ -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);
}

Expand Down
Loading