diff --git a/src/CoreLib/Image.cpp b/src/CoreLib/Image.cpp index 5087564..d0bb6ff 100644 --- a/src/CoreLib/Image.cpp +++ b/src/CoreLib/Image.cpp @@ -7,7 +7,7 @@ #include "MernelPlatform/ByteOrderStream.hpp" -#include +#include #include namespace D2ModGen::Image { @@ -67,158 +67,6 @@ uint8_t s_defaultPaletteGBR[768] = { // clang-format on }; -class StoneTextureGenerator { -private: - int p[512]; - - // Standard Fade function for smooth transitions - double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); } - - // Linear interpolation - double lerp(double t, double a, double b) { return a + t * (b - a); } - - // Gradient calculation - double grad(int hash, double x, double y) - { - int h = hash & 7; - double u = h < 4 ? x : y; - double v = h < 4 ? y : x; - return ((h & 1) ? -u : u) + ((h & 2) ? -2.0 * v : 2.0 * v); - } - -public: - StoneTextureGenerator(unsigned int seed = 42) - { - // Initialize permutation table with a standard shuffle sequence - int base_p[256]; - std::iota(std::begin(base_p), std::end(base_p), 0); - - // Simple LCG deterministic shuffle using the seed - for (int i = 255; i > 0; --i) { - seed = seed * 1103515245 + 12345; - int j = (seed / 65536) % (i + 1); - std::swap(base_p[i], base_p[j]); - } - - // Duplicate the array to avoid buffer overflows during interpolation - for (int i = 0; i < 256; i++) { - p[i] = base_p[i]; - p[i + 256] = base_p[i]; - } - } - - // 2D Perlin Noise implementation returning range [-1.0, 1.0] - double noise(double x, double y) - { - int X = (int) std::floor(x) & 255; - int Y = (int) std::floor(y) & 255; - - x -= std::floor(x); - y -= std::floor(y); - - double u = fade(x); - double v = fade(y); - - int A = p[X] + Y; - int B = p[X + 1] + Y; - - return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)), lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1))); - } - - // Generates a 32-bit RGBA color buffer matching the UI element dimensions - void generateStoneTexture(Bitmap& bitmap, double scale = 0.03) - { - int width = bitmap.size.width; - int height = bitmap.size.height; - - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - double sampleX = x * scale; - double sampleY = y * scale; - - double totalHeight = 0.0; - double slopeX = 0.0; - double slopeY = 0.0; - - double amplitude = 1.0; - double frequency = 1.0; - double maxValue = 0.0; - - // 3 octaves for high speed while preserving micro-cracks - for (int octave = 0; octave < 3; ++octave) { - double nx = sampleX * frequency; - double ny = sampleY * frequency; - - // --- FAST ANALYTICAL INLINE DERIVATIVE --- - int X = (int) std::floor(nx) & 255; - int Y = (int) std::floor(ny) & 255; - double fx = nx - std::floor(nx); - double fy = ny - std::floor(ny); - - // Inline fade curves and their exact derivatives - double u = fx * fx * fx * (fx * (fx * 6 - 15) + 10); - double v = fy * fy * fy * (fy * (fy * 6 - 15) + 10); - double du = 30.0 * fx * fx * (fx * (fx - 2.0) + 1.0); - double dv = 30.0 * fy * fy * (fy * (fy - 2.0) + 1.0); - - int A = p[X] + Y; - int B = p[X + 1] + Y; - double g00 = grad(p[A], fx, fy); - double g10 = grad(p[B], fx - 1, fy); - double g01 = grad(p[A + 1], fx, fy - 1); - double g11 = grad(p[B + 1], fx - 1, fy - 1); - - // Calculate smooth height - double r = lerp(v, lerp(u, g00, g10), lerp(u, g01, g11)); - - // Transform to Ridged/Craggy profile mathematically - double crag = 1.0 - std::abs(r); - double cragSq = crag * crag; - - totalHeight += cragSq * amplitude; - maxValue += amplitude; - - // Chain rule to extract matching slopes instantly without extra noise calls - double dNdx = lerp(v, g10 - g00, g11 - g01) * du; - double dNdy = (lerp(u, g01, g11) - lerp(u, g00, g10)) * dv; - - // Sign correction based on the absolute value flip - double sign = (r < 0.0) ? 1.0 : -1.0; - slopeX += 2.0 * crag * sign * dNdx * amplitude * frequency; - slopeY += 2.0 * crag * sign * dNdy * amplitude * frequency; - - amplitude *= 0.45; - frequency *= 2.2; - } - - totalHeight /= maxValue; - - // --- FAKE LIGHTING CALCULATIONS --- - double lightX = -0.5; - double lightY = -0.5; - double lightZ = 0.8; - double rsq = slopeX * slopeX + slopeY * slopeY + 1.0; - double inv_len = 1.0 / std::sqrt(rsq); - - double light = (slopeX * inv_len) * lightX + (slopeY * inv_len) * lightY + inv_len * lightZ; - light = std::max(0.0, light); - - double rockPattern = std::pow(totalHeight, 2.5); - double finalShade = rockPattern * (0.3 + 0.7 * light); - - // --- COMPATIBILITY WITH YOUR BITMAP API --- - Color c; - c.r = static_cast(12 + finalShade * 40); // Range ~12 to 52 (Dark Charcoal) - c.g = static_cast(14 + finalShade * 42); - c.b = static_cast(16 + finalShade * 45); - c.a = 255; - - bitmap.putDot({ x, y }, c); - } - } - } -}; - void makeBorderImage(Bitmap& dest, const Bitmap& src, Size cornerSize) { const int srcW = src.size.width; @@ -301,6 +149,14 @@ Point Point::operator+(const Point& s) return result; } +Point Point::operator-(const Point& s) +{ + Point result = *this; + result.x -= s.x; + result.y -= s.y; + return result; +} + Point& Point::operator+=(const Point& r) { x += r.x; @@ -308,6 +164,13 @@ Point& Point::operator+=(const Point& r) return *this; } +Point& Point::operator-=(const Point& r) +{ + x -= r.x; + y -= r.y; + return *this; +} + Point& Point::operator/=(int downscale) { x /= downscale; @@ -606,12 +469,6 @@ void Bitmap::fill() } } -void Bitmap::fillWithStone() -{ - StoneTextureGenerator gen; - gen.generateStoneTexture(*this); -} - void Bitmap::copyBorders(const Bitmap& src, Size cornerSize) { makeBorderImage(*this, src, cornerSize); @@ -626,16 +483,63 @@ void Bitmap::draw(Point destPoint, const Bitmap& src, Point srcPoint, Size size) } } -void Bitmap::tileWith(const Bitmap& tile) +void Bitmap::drawBlended(Point destPoint, const Bitmap& src, Point srcPoint, Size size) { - const int w = tile.size.width - 1; - const int h = tile.size.height - 1; - for (int x = 0; x < size.width; x++) { - const int srcX = x % w; - for (int y = 0; y < size.height; y++) { - const int srcY = y % h; + for (int y = 0; y < size.height; ++y) { + for (int x = 0; x < size.width; ++x) { + auto* destPixel = getPixel(destPoint + Point{ x, y }); + const auto* srcPixel = src.getPixel(srcPoint + Point{ x, y }); + if (!destPixel || !srcPixel) + continue; + + if (destPixel[3] == 0) { + memcpy(destPixel, srcPixel, 4); + continue; + } - drawDot({ x, y }, tile, { srcX, srcY }); + const int alpha = srcPixel[3]; + for (int channel = 0; channel < 3; ++channel) + destPixel[channel] = uint8_t((srcPixel[channel] * alpha + destPixel[channel] * (255 - alpha)) / 255); + if (alpha > destPixel[3]) + destPixel[3] = uint8_t(alpha); + } + } +} + +void Bitmap::drawScaled(Rect destRect, const Bitmap& src, Rect srcRect) +{ + if (destRect.size.width <= 0 || destRect.size.height <= 0 || srcRect.size.width <= 0 || srcRect.size.height <= 0) + return; + + const double scaleX = double(srcRect.size.width) / destRect.size.width; + const double scaleY = double(srcRect.size.height) / destRect.size.height; + const int maxX = srcRect.size.width - 1; + const int maxY = srcRect.size.height - 1; + + for (int y = 0; y < destRect.size.height; ++y) { + const double sy = std::clamp((y + 0.5) * scaleY - 0.5, 0.0, double(maxY)); + const int y0 = int(sy); + const int y1 = std::min(y0 + 1, maxY); + const double fy = sy - y0; + for (int x = 0; x < destRect.size.width; ++x) { + const double sx = std::clamp((x + 0.5) * scaleX - 0.5, 0.0, double(maxX)); + const int x0 = int(sx); + const int x1 = std::min(x0 + 1, maxX); + const double fx = sx - x0; + + auto* destPixel = getPixel(destRect.topLeft + Point{ x, y }); + const uint8_t* p00 = src.getPixel(srcRect.topLeft + Point{ x0, y0 }); + const uint8_t* p10 = src.getPixel(srcRect.topLeft + Point{ x1, y0 }); + const uint8_t* p01 = src.getPixel(srcRect.topLeft + Point{ x0, y1 }); + const uint8_t* p11 = src.getPixel(srcRect.topLeft + Point{ x1, y1 }); + if (!destPixel || !p00 || !p10 || !p01 || !p11) + continue; + + for (int channel = 0; channel < 4; ++channel) { + const double top = p00[channel] + (p10[channel] - p00[channel]) * fx; + const double bottom = p01[channel] + (p11[channel] - p01[channel]) * fx; + destPixel[channel] = uint8_t(std::lround(top + (bottom - top) * fy)); + } } } } diff --git a/src/CoreLib/Image.hpp b/src/CoreLib/Image.hpp index 08d58d9..aa4678b 100644 --- a/src/CoreLib/Image.hpp +++ b/src/CoreLib/Image.hpp @@ -20,8 +20,10 @@ struct Point { void read(const Mernel::PropertyTree& data); void write(Mernel::PropertyTree& data) const; Point operator+(const Point& s); + Point operator-(const Point& s); Point& operator+=(const Point& r); + Point& operator-=(const Point& r); Point& operator/=(int downscale); std::string toString() const; }; @@ -150,15 +152,15 @@ struct MODGENCORE_EXPORT Bitmap { void paintLineV(Point point, int height); void fill(); - void fillWithStone(); void copyBorders(const Bitmap& src, Size cornerSize); void fillRect(Rect rect); void linedRect(Rect rect); void draw(Point destPoint, const Bitmap& src, Point srcPoint, Size size); + void drawBlended(Point destPoint, const Bitmap& src, Point srcPoint, Size size); - void tileWith(const Bitmap& tile); + void drawScaled(Rect destRect, const Bitmap& src, Rect srcRect); // bilinear void drawDot(Point destPoint, const Bitmap& src, Point srcPoint); }; diff --git a/src/CoreLib/Modules/ModuleExpandedInventory.cpp b/src/CoreLib/Modules/ModuleExpandedInventory.cpp index 0e4c7b0..5940797 100644 --- a/src/CoreLib/Modules/ModuleExpandedInventory.cpp +++ b/src/CoreLib/Modules/ModuleExpandedInventory.cpp @@ -114,6 +114,19 @@ const auto s_rectsCorrectionMKBD = std::map{ const Point ctrl1{ -99, -60 }; const Point ctrl2{ 123, -62 }; +// Controller backgrounds are anchored at the panel center, so their sprites do not start at the panel origin. +const Point s_ctrlPanelCenter{ 1408 / 2, 1486 / 2 }; +const Point s_ctrlBackgroundRectOrig{ -508, -700 }; +const Point s_ctrlBackgroundRectExpanded{ -789, -860 }; +const Point s_ctrlSpriteOriginOrig{ s_ctrlPanelCenter.x + s_ctrlBackgroundRectOrig.x, s_ctrlPanelCenter.y + s_ctrlBackgroundRectOrig.y }; +const Point s_ctrlSpriteOriginExpanded{ s_ctrlPanelCenter.x + s_ctrlBackgroundRectExpanded.x, s_ctrlPanelCenter.y + s_ctrlBackgroundRectExpanded.y }; + +// Stone area of the biggest panel in controller mainpanelbg.sprite, without its frame and corner ornaments. +const Rect s_panelBgStoneRect{ .topLeft = { 645, 202 }, .size = { 1590, 1366 } }; + +const int s_ctrlFrameMargin = 12; // controller frames extend past the widget rect +const Size s_ctrlBeltSize{ 505, 118 }; + const auto s_rectsCorrectionCTRL = std::map{ { "slot_right_arm", { .x = -99, .y = -60 } }, @@ -129,9 +142,9 @@ const auto s_rectsCorrectionCTRL = std::map{ { "slot_right_hand", { .x = 389, .y = -417 } }, { "slot_left_hand", { .x = 126, .y = -417 } }, - { "Belt", { .x = +15, .y = 595 } }, - { "gold_amount", { .x = -464, .y = 20 } }, - { "gold_button", { .x = -464, .y = 20 } }, + { "Belt", { .x = +223, .y = 595 } }, + { "gold_amount", { .x = -256, .y = 20 } }, + { "gold_button", { .x = -256, .y = 20 } }, { "background_right_arm", ctrl1 }, { "background_right_arm_selected", ctrl1 }, @@ -287,8 +300,7 @@ void ModuleExpandedInventory::generate() const int growWidth = growCellX * cellWidth; const int growHeight = growCellY * cellHeight; - const auto gridOffset = Point{ ((cellCountXmax - cellCountX) / 2) * cellWidth, ((cellCountYmax - cellCountY) / 2) * cellHeight }; - const auto widgetsOffset = Point{ 100, 0 }; + const auto gridOffset = Point{ (cellCountXmax - cellCountX) * cellWidth / 2, (cellCountYmax - cellCountY) * cellHeight / 2 }; DebugOverlayList debugOverlays; BlitDescList blitList; @@ -304,6 +316,11 @@ void ModuleExpandedInventory::generate() bd.expansion = desc.expansion; bd.controller = desc.controller; + const Point sourceOrigin = desc.controller ? s_ctrlSpriteOriginOrig : Point{}; + const Point targetOrigin = desc.controller ? s_ctrlSpriteOriginExpanded : Point{}; + + const Point widgetsOffset = desc.controller ? Point{ -15, 0 } : Point{ 100, 0 }; + if (!desc.expansion && !desc.controller) json["fields"]["rect"] = Mernel::PropertyTreeScalar("$RightPanelRect_ExpandedInventory"); @@ -319,12 +336,20 @@ void ModuleExpandedInventory::generate() if (!d.read(child)) continue; + if (desc.controller && d.name == "Belt") + d.rect.size = s_ctrlBeltSize; // the layout only stores its position + bd.rect = d.rect; d.rect.topLeft += correction; d.rect.topLeft += widgetsOffset; // @todo: future uncollapsing - bd.moveTo = d.rect.topLeft; - bd.rect.expand(10); - bd.moveTo += Point{ -10, -10 }; // extra borders + bd.moveTo = d.rect.topLeft - targetOrigin; + + if (bd.rect.size.width > 0) { + const int margin = desc.controller ? s_ctrlFrameMargin : 10; // extra borders + bd.rect.expand(margin); + bd.moveTo -= Point{ margin, margin }; + } + bd.rect.topLeft -= sourceOrigin; d.write(child); blitList.push_back(bd); @@ -332,17 +357,18 @@ void ModuleExpandedInventory::generate() //Logger() << d.name << " " << bd.rect.toString() << " -> " << bd.moveTo.toString(); } else if (d.name == "grid") { - Point correction = desc.controller ? Point{ -132, -344 } : Point{ -37, -229 }; + Point correction = desc.controller ? Point{ -247, -344 } : Point{ -37, -229 }; if (!d.read(child)) continue; bd.rect = d.rect; + bd.rect.topLeft -= sourceOrigin; d.rect.topLeft += correction; d.rect.topLeft += gridOffset; bd.rect.size.width = cellWidth; bd.rect.size.height = cellHeight; - bd.moveTo = d.rect.topLeft; + bd.moveTo = d.rect.topLeft - targetOrigin; bd.repeatX = cellCountX; bd.repeatY = cellCountY; @@ -368,8 +394,8 @@ void ModuleExpandedInventory::generate() child["fields"]["filename"] = Mernel::PropertyTreeScalar("Controller/Panel/InventoryPanel/V2/InventoryBG_Expanded"); if (!desc.expansion) { child["fields"]["filename"] = Mernel::PropertyTreeScalar("Controller/Panel/InventoryPanel/V2/InventoryBG_Classic_Expanded"); - child["fields"]["rect"]["x"] = Mernel::PropertyTreeScalar(-674); - child["fields"]["rect"]["y"] = Mernel::PropertyTreeScalar(-860); + child["fields"]["rect"]["x"] = Mernel::PropertyTreeScalar(s_ctrlBackgroundRectExpanded.x); + child["fields"]["rect"]["y"] = Mernel::PropertyTreeScalar(s_ctrlBackgroundRectExpanded.y); } } else { child["fields"]["filename"] = Mernel::PropertyTreeScalar("PANEL\\Inventory\\Background_Expanded"); @@ -390,9 +416,9 @@ void ModuleExpandedInventory::generate() } } - Bitmap stone; - stone.resize({ 1600, 800 }); - stone.fillWithStone(); + Bitmap panelBgHighre, panelBgLowend; + panelBgHighre.readFromSprite(output.blobs[BlobId::OrignCtrlMainPanelBgSpriteHighre]); + panelBgLowend.readFromSprite(output.blobs[BlobId::OrignCtrlMainPanelBgSpriteLowend]); for (const auto& desc : s_spriteDescs) { const auto& origSpriteBlob = output.blobs[desc.orig]; @@ -411,9 +437,12 @@ void ModuleExpandedInventory::generate() cornerSize /= downscale; bmp.resize(newSize); bmp.frameWidth = newSize.width; - if (!desc.controller) { - // controller use transparent back - bmp.tileWith(stone); + if (desc.controller) { + bmp.rgba.assign(bmp.rgba.size(), 0); // the game draws its own panel behind the controller background + } else { + Rect stoneRect = s_panelBgStoneRect; + stoneRect /= downscale; + bmp.drawScaled({ .topLeft = {}, .size = newSize }, desc.lowres ? panelBgLowend : panelBgHighre, stoneRect); bmp.copyBorders(origBmp, cornerSize); } @@ -425,7 +454,10 @@ void ModuleExpandedInventory::generate() bd /= downscale; for (int x = 0, offsetX = 0; x < bd.repeatX; x++, offsetX += bd.rect.size.width) { for (int y = 0, offsetY = 0; y < bd.repeatY; y++, offsetY += bd.rect.size.height) { - bmp.draw(bd.moveTo + Point(offsetX, offsetY), origBmp, bd.rect.topLeft, bd.rect.size); + if (bd.controller) + bmp.drawBlended(bd.moveTo + Point(offsetX, offsetY), origBmp, bd.rect.topLeft, bd.rect.size); + else + bmp.draw(bd.moveTo + Point(offsetX, offsetY), origBmp, bd.rect.topLeft, bd.rect.size); } } } @@ -438,6 +470,8 @@ void ModuleExpandedInventory::generate() if (!desc.expansion && overlay.expansion) continue; + if (overlay.controller) + overlay.rect.topLeft -= s_ctrlSpriteOriginExpanded; overlay /= desc.lowres ? 2 : 1; overlay.paintTo(bmp); } diff --git a/src/CoreLib/Storage/IStorage.cpp b/src/CoreLib/Storage/IStorage.cpp index e1f3d2a..ead5ef8 100644 --- a/src/CoreLib/Storage/IStorage.cpp +++ b/src/CoreLib/Storage/IStorage.cpp @@ -96,6 +96,9 @@ const std::array s_blobpaths{ "data\\hd\\global\\ui\\controller\\panel\\inventorypanel\\v2\\inventorybg.sprite", "data\\hd\\global\\ui\\controller\\panel\\inventorypanel\\v2\\inventorybg.lowend.sprite", + "data\\hd\\global\\ui\\controller\\panel\\mainpanelbg.sprite", + "data\\hd\\global\\ui\\controller\\panel\\mainpanelbg.lowend.sprite", + "data\\hd\\global\\ui\\spells\\amazon\\amskillicon.sprite", "data\\hd\\global\\ui\\spells\\amazon\\amskillicon.lowend.sprite", "data\\hd\\global\\ui\\spells\\sorceress\\soskillicon.sprite", diff --git a/src/CoreLib/Storage/IStorage.hpp b/src/CoreLib/Storage/IStorage.hpp index eca0af1..0f87ece 100644 --- a/src/CoreLib/Storage/IStorage.hpp +++ b/src/CoreLib/Storage/IStorage.hpp @@ -114,6 +114,10 @@ enum class BlobId OrignCtrlInventoryExpansnSpriteHighre, OrignCtrlInventoryExpansnSpriteLowend, + // fullscreen controller panel background, used as base for all expanded inventory backgrounds + OrignCtrlMainPanelBgSpriteHighre, + OrignCtrlMainPanelBgSpriteLowend, + IconAmaHighre, IconAmaLowres, IconSorHighre, @@ -151,7 +155,7 @@ enum class BlobId ResurrectedLast = IconWarLowres, SkipWriteFirst = OrignMnkbInventoryClassicSpriteHighre, - SkipWriteLast = OrignCtrlInventoryExpansnSpriteLowend, + SkipWriteLast = OrignCtrlMainPanelBgSpriteLowend, }; inline auto getBlobIdsLegacy()