diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index e44f6ec5e..f1679d045 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -88,6 +88,7 @@ set(vcmiclientcommon_SRCS render/CBitmapHandler.cpp render/CDefFile.cpp render/Canvas.cpp + render/CanvasImage.cpp render/ColorFilter.cpp render/Colors.cpp render/Graphics.cpp @@ -99,10 +100,11 @@ set(vcmiclientcommon_SRCS renderSDL/CursorHardware.cpp renderSDL/CursorSoftware.cpp renderSDL/FontChain.cpp - renderSDL/ImageScaled.cpp + renderSDL/ScalableImage.cpp renderSDL/RenderHandler.cpp renderSDL/SDLImage.cpp renderSDL/SDLImageLoader.cpp + renderSDL/SDLImageScaler.cpp renderSDL/SDLRWwrapper.cpp renderSDL/ScreenHandler.cpp renderSDL/SDL_Extensions.cpp @@ -290,6 +292,7 @@ set(vcmiclientcommon_HEADERS render/CBitmapHandler.h render/CDefFile.h render/Canvas.h + render/CanvasImage.h render/ColorFilter.h render/Colors.h render/EFont.h @@ -307,10 +310,11 @@ set(vcmiclientcommon_HEADERS renderSDL/CursorHardware.h renderSDL/CursorSoftware.h renderSDL/FontChain.h - renderSDL/ImageScaled.h + renderSDL/ScalableImage.h renderSDL/RenderHandler.h renderSDL/SDLImage.h renderSDL/SDLImageLoader.h + renderSDL/SDLImageScaler.h renderSDL/SDLRWwrapper.h renderSDL/ScreenHandler.h renderSDL/SDL_Extensions.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 39fc84306..81d6a8f67 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1171,7 +1171,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component if(t) { auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(35, 23)); + image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); images.push_back(image); } } diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 67a0174ce..909b712aa 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -22,7 +22,6 @@ #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/Graphics.h" -#include "../renderSDL/SDL_Extensions.h" #include "../windows/InfoWindows.h" #include "../../CCallback.h" @@ -178,7 +177,7 @@ void CMinimap::mouseDragged(const Point & cursorPosition, const Point & lastUpda void CMinimap::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), aiShield->pos); + CanvasClipRectGuard guard(to, aiShield->pos); CIntObject::showAll(to); if(minimap) diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 6e6ad3731..673dc9763 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -26,7 +26,6 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" #include "../render/IRenderHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" @@ -857,7 +856,7 @@ void BattleFieldController::tick(uint32_t msPassed) void BattleFieldController::show(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CanvasClipRectGuard guard(to, pos); renderBattlefield(to); diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 3c2cccf84..3f7617d47 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -24,7 +24,6 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" #include "../eventsSDL/InputHandler.h" #include "../../CCallback.h" @@ -76,7 +75,7 @@ void BasicMapView::tick(uint32_t msPassed) void BasicMapView::show(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CanvasClipRectGuard guard(to, pos); render(to, false); controller->afterRender(); @@ -84,7 +83,7 @@ void BasicMapView::show(Canvas & to) void BasicMapView::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CanvasClipRectGuard guard(to, pos); render(to, true); } diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 84edc6943..cd2eef606 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -381,12 +381,12 @@ Point CVideoInstance::size() return dimensions / GH.screenHandler().getScalingFactor(); } -void CVideoInstance::show(const Point & position, Canvas & canvas) +void CVideoInstance::show(const Point & position, SDL_Surface * to) { if(sws == nullptr) throw std::runtime_error("No video to show!"); - CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position * GH.screenHandler().getScalingFactor()); + CSDL_Ext::blitSurface(surface, to, position * GH.screenHandler().getScalingFactor()); } double FFMpegStream::getCurrentFrameEndTime() const diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index a4229074b..77848aec0 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -98,7 +98,7 @@ public: bool videoEnded() final; Point size() final; - void show(const Point & position, Canvas & canvas) final; + void show(const Point & position, SDL_Surface * to) final; void tick(uint32_t msPassed) final; void activate() final; void deactivate() final; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 35f385bc1..fcfd11e20 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -11,7 +11,7 @@ #include "../lib/filesystem/ResourcePath.h" -class Canvas; +struct SDL_Surface; VCMI_LIB_NAMESPACE_BEGIN class Point; @@ -30,7 +30,7 @@ public: virtual Point size() = 0; /// Displays current frame at specified position - virtual void show(const Point & position, Canvas & canvas) = 0; + virtual void show(const Point & position, SDL_Surface * to) = 0; /// Advances video playback by specified duration virtual void tick(uint32_t msPassed) = 0; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 56945f922..20882069d 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -14,6 +14,7 @@ #include "../render/IImage.h" #include "../render/IImageLoader.h" #include "../render/Canvas.h" +#include "../render/CanvasImage.h" #include "../render/ColorFilter.h" #include "../render/IRenderHandler.h" #include "../render/CAnimation.h" @@ -58,12 +59,13 @@ void AssetGenerator::createAdventureOptionsCleanBackground() return; ResourcePath savePath(filename, EResType::IMAGE); - auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK")); - locator.scalingFactor = 1; + auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE); - std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + std::shared_ptr img = GH.renderHandler().loadImage(locator); + + auto image = GH.renderHandler().createImage(Point(575, 585), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); - Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE); canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585)); canvas.draw(img, Point(54, 121), Rect(54, 123, 335, 1)); canvas.draw(img, Point(158, 84), Rect(156, 84, 2, 37)); @@ -72,8 +74,6 @@ void AssetGenerator::createAdventureOptionsCleanBackground() canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3)); canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47)); - std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); - image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); } @@ -88,11 +88,11 @@ void AssetGenerator::createBigSpellBook() return; ResourcePath savePath(filename, EResType::IMAGE); - auto locator = ImageLocator(ImagePath::builtin("SpelBack")); - locator.scalingFactor = 1; + auto locator = ImageLocator(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE); - std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); - Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE); + std::shared_ptr img = GH.renderHandler().loadImage(locator); + auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); // edges canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); @@ -135,8 +135,6 @@ void AssetGenerator::createBigSpellBook() canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45)); canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47)); - std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); - image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); } @@ -152,10 +150,9 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player) ResourcePath savePath(filename, EResType::IMAGE); - auto locator = ImageLocator(ImagePath::builtin("DiBoxBck")); - locator.scalingFactor = 1; + auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE); - std::shared_ptr texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + std::shared_ptr texture = GH.renderHandler().loadImage(locator); // transform to make color of brown DIBOX.PCX texture match color of specified player auto filterSettings = VLC->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"]; @@ -199,10 +196,10 @@ void AssetGenerator::createCombatUnitNumberWindow() !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png")) return; - auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN")); - locator.scalingFactor = 1; + auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"), EImageBlitMode::OPAQUE); + locator.layer = EImageBlitMode::OPAQUE; - std::shared_ptr texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + std::shared_ptr texture = GH.renderHandler().loadImage(locator); static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); @@ -233,12 +230,12 @@ void AssetGenerator::createCampaignBackground() return; ResourcePath savePath(filename, EResType::IMAGE); - auto locator = ImageLocator(ImagePath::builtin("CAMPBACK")); - locator.scalingFactor = 1; + auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE); + + std::shared_ptr img = GH.renderHandler().loadImage(locator); + auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); - std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); - Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE); - canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600)); // left image @@ -263,13 +260,10 @@ void AssetGenerator::createCampaignBackground() canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114)); // skull - auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC")); - locatorSkull.scalingFactor = 1; - std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE); + auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"), EImageBlitMode::OPAQUE); + std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull); canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19)); - std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); - image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); } @@ -290,11 +284,11 @@ void AssetGenerator::createChroniclesCampaignImages() continue; ResourcePath savePath(filename, EResType::IMAGE); - auto locator = ImageLocator(imgPathBg); - locator.scalingFactor = 1; + auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE); - std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); - Canvas canvas = Canvas(Point(200, 116), CanvasScalingPolicy::IGNORE); + std::shared_ptr img = GH.renderHandler().loadImage(locator); + auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); switch (i) { @@ -323,9 +317,8 @@ void AssetGenerator::createChroniclesCampaignImages() canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116)); //skull - auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1")); - locatorSkull.scalingFactor = 1; - std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE); + auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE); + std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull); canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22)); canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4)); canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4)); @@ -334,8 +327,6 @@ void AssetGenerator::createChroniclesCampaignImages() break; } - std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); - image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); } } @@ -403,8 +394,7 @@ void AssetGenerator::createPaletteShiftedSprites() return; auto imgLoc = anim->getImageLocator(j, 0); - imgLoc.scalingFactor = 1; - auto img = GH.renderHandler().loadImage(imgLoc, EImageBlitMode::COLORKEY); + auto img = GH.renderHandler().loadImage(imgLoc); for(int k = 0; k < paletteAnimations[i].size(); k++) { auto element = paletteAnimations[i][k]; @@ -420,9 +410,9 @@ void AssetGenerator::createPaletteShiftedSprites() } } - Canvas canvas = Canvas(Point(32, 32), CanvasScalingPolicy::IGNORE); + auto image = GH.renderHandler().createImage(Point(32, 32), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2)); - std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); JsonNode node(JsonMap{ diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 63439bf1c..d045e73a8 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -30,7 +30,7 @@ bool CAnimation::loadFrame(size_t frame, size_t group, bool verbose) if(auto image = getImageImpl(frame, group, false)) return true; - std::shared_ptr image = GH.renderHandler().loadImage(getImageLocator(frame, group), mode); + std::shared_ptr image = GH.renderHandler().loadImage(getImageLocator(frame, group)); if(image) { @@ -224,5 +224,5 @@ ImageLocator CAnimation::getImageLocator(size_t frame, size_t group) const throw std::runtime_error("Frame " + std::to_string(frame) + " of group " + std::to_string(group) + " is missing from animation " + name.getOriginalName() ); } - return ImageLocator(name, frame, group); + return ImageLocator(name, frame, group, mode); } diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index a89e1a6f5..45300be38 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -11,6 +11,7 @@ #include "Canvas.h" #include "../gui/CGuiHandler.h" +#include "../media/IVideoPlayer.h" #include "../render/IRenderHandler.h" #include "../render/IScreenHandler.h" #include "../renderSDL/SDL_Extensions.h" @@ -102,11 +103,21 @@ Canvas::~Canvas() SDL_FreeSurface(surface); } +void Canvas::draw(IVideoInstance & video, const Point & pos) +{ + video.show(pos, surface); +} + +void Canvas::draw(const IImage& image, const Point & pos) +{ + image.draw(surface, transformPos(pos), nullptr, getScalingFactor()); +} + void Canvas::draw(const std::shared_ptr& image, const Point & pos) { assert(image); if (image) - image->draw(surface, transformPos(pos)); + image->draw(surface, transformPos(pos), nullptr, getScalingFactor()); } void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) @@ -114,7 +125,7 @@ void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect realSourceRect = sourceRect * getScalingFactor(); assert(image); if (image) - image->draw(surface, transformPos(pos), &realSourceRect); + image->draw(surface, transformPos(pos), &realSourceRect, getScalingFactor()); } void Canvas::draw(const Canvas & image, const Point & pos) @@ -218,16 +229,22 @@ void Canvas::fillTexture(const std::shared_ptr& image) for (int y=0; y < surface->h; y+= imageArea.h) { for (int x=0; x < surface->w; x+= imageArea.w) - image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor())); + image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor()), nullptr, getScalingFactor()); } } -SDL_Surface * Canvas::getInternalSurface() -{ - return surface; -} - Rect Canvas::getRenderArea() const { return renderArea; } + +CanvasClipRectGuard::CanvasClipRectGuard(Canvas & canvas, const Rect & rect): surf(canvas.surface) +{ + CSDL_Ext::getClipRect(surf, oldRect); + CSDL_Ext::setClipRect(surf, rect * GH.screenHandler().getScalingFactor()); +} + +CanvasClipRectGuard::~CanvasClipRectGuard() +{ + CSDL_Ext::setClipRect(surf, oldRect); +} diff --git a/client/render/Canvas.h b/client/render/Canvas.h index a421ab232..1e41e06c7 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -15,6 +15,7 @@ struct SDL_Surface; class IImage; +class IVideoInstance; enum EFonts : int8_t; enum class CanvasScalingPolicy @@ -27,6 +28,8 @@ enum class CanvasScalingPolicy /// Class that represents surface for drawing on class Canvas { + friend class CanvasClipRectGuard; + /// Upscaler awareness. Must be first member for initialization CanvasScalingPolicy scalingPolicy; @@ -72,6 +75,9 @@ public: /// renders image onto this canvas at specified position void draw(const std::shared_ptr& image, const Point & pos); + void draw(const IImage& image, const Point & pos); + + void draw(IVideoInstance & video, const Point & pos); /// renders section of image bounded by sourceRect at specified position void draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect); @@ -114,9 +120,16 @@ public: int getScalingFactor() const; - /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. - SDL_Surface * getInternalSurface(); - /// get the render area Rect getRenderArea() const; }; + +class CanvasClipRectGuard : boost::noncopyable +{ + SDL_Surface * surf; + Rect oldRect; + +public: + CanvasClipRectGuard(Canvas & canvas, const Rect & rect); + ~CanvasClipRectGuard(); +}; diff --git a/client/render/CanvasImage.cpp b/client/render/CanvasImage.cpp new file mode 100644 index 000000000..07e9b3474 --- /dev/null +++ b/client/render/CanvasImage.cpp @@ -0,0 +1,63 @@ +/* + * CanvasImage.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CanvasImage.h" + +#include "../gui/CGuiHandler.h" +#include "../render/IScreenHandler.h" +#include "../renderSDL/SDL_Extensions.h" +#include "../renderSDL/SDLImageScaler.h" + +#include +#include + +CanvasImage::CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy) + : surface(CSDL_Ext::newSurface(scalingPolicy == CanvasScalingPolicy::IGNORE ? size : (size * GH.screenHandler().getScalingFactor()))) + , scalingPolicy(scalingPolicy) +{ +} + +void CanvasImage::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const +{ + if(src) + CSDL_Ext::blitSurface(surface, *src, where, pos); + else + CSDL_Ext::blitSurface(surface, where, pos); +} + +void CanvasImage::scaleTo(const Point & size, EScalingAlgorithm algorithm) +{ + Point scaledSize = size * GH.screenHandler().getScalingFactor(); + + SDLImageScaler scaler(surface); + scaler.scaleSurface(scaledSize, algorithm); + SDL_FreeSurface(surface); + surface = scaler.acquireResultSurface(); +} + +void CanvasImage::exportBitmap(const boost::filesystem::path & path) const +{ + IMG_SavePNG(surface, path.string().c_str()); +} + +Canvas CanvasImage::getCanvas() +{ + return Canvas::createFromSurface(surface, scalingPolicy); +} + +Rect CanvasImage::contentRect() const +{ + return Rect(Point(0, 0), dimensions()); +} + +Point CanvasImage::dimensions() const +{ + return {surface->w, surface->h}; +} diff --git a/client/render/CanvasImage.h b/client/render/CanvasImage.h new file mode 100644 index 000000000..18c342832 --- /dev/null +++ b/client/render/CanvasImage.h @@ -0,0 +1,41 @@ +/* + * CanvasImage.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "IImage.h" +#include "Canvas.h" + +class CanvasImage : public IImage +{ +public: + CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy); + + Canvas getCanvas(); + + void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override; + void scaleTo(const Point & size, EScalingAlgorithm algorithm) override; + void exportBitmap(const boost::filesystem::path & path) const override; + Rect contentRect() const override; + Point dimensions() const override; + + //no-op methods + + bool isTransparent(const Point & coords) const override{ return false;}; + void setAlpha(uint8_t value) override{}; + void playerColored(const PlayerColor & player) override{}; + void setOverlayColor(const ColorRGBA & color) override{}; + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override{}; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override{}; + +private: + SDL_Surface * surface; + CanvasScalingPolicy scalingPolicy; +}; + diff --git a/client/render/IImage.h b/client/render/IImage.h index 51f0e24e2..ec27276a6 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -62,21 +62,28 @@ enum class EImageBlitMode : uint8_t ONLY_OVERLAY, }; +enum class EScalingAlgorithm : int8_t +{ + NEAREST, + BILINEAR, + XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image. Only for integer scaling + XBRZ_ALPHA // xbrz, image edges are considered to be transparent. Only for integer scaling +}; + /// Base class for images for use in client code. /// This class represents current state of image, with potential transformations applied, such as player coloring class IImage { public: //draws image on surface "where" at position - virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src = nullptr) const = 0; + virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const = 0; - virtual void scaleTo(const Point & size) = 0; - virtual void scaleInteger(int factor) = 0; + virtual void scaleTo(const Point & size, EScalingAlgorithm algorithm) = 0; virtual void exportBitmap(const boost::filesystem::path & path) const = 0; //Change palette to specific player - virtual void playerColored(PlayerColor player) = 0; + virtual void playerColored(const PlayerColor & player) = 0; //test transparency of specific pixel virtual bool isTransparent(const Point & coords) const = 0; @@ -92,13 +99,10 @@ public: virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0; virtual void setAlpha(uint8_t value) = 0; - virtual void setBlitMode(EImageBlitMode mode) = 0; //only indexed bitmaps with 7 special colors virtual void setOverlayColor(const ColorRGBA & color) = 0; - virtual std::shared_ptr getSharedImage() const = 0; - virtual ~IImage() = default; }; @@ -112,15 +116,20 @@ public: virtual void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const = 0; virtual bool isTransparent(const Point & coords) const = 0; virtual Rect contentRect() const = 0; + + virtual void scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaling, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0; virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0; - [[nodiscard]] virtual std::shared_ptr createImageReference(EImageBlitMode mode) const = 0; + /// Returns true if this image is still loading and can't be used + virtual bool isLoading() const = 0; + + virtual ~ISharedImage() = default; + + virtual const SDL_Palette * getPalette() const = 0; [[nodiscard]] virtual std::shared_ptr horizontalFlip() const = 0; [[nodiscard]] virtual std::shared_ptr verticalFlip() const = 0; [[nodiscard]] virtual std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0; [[nodiscard]] virtual std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const = 0; - - virtual ~ISharedImage() = default; }; diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h index b3b0deb19..cefd02890 100644 --- a/client/render/IRenderHandler.h +++ b/client/render/IRenderHandler.h @@ -20,7 +20,10 @@ struct SDL_Surface; class IFont; class IImage; class CAnimation; +class CanvasImage; +class SDLImageShared; enum class EImageBlitMode : uint8_t; +enum class CanvasScalingPolicy; enum EFonts : int8_t; class IRenderHandler : public boost::noncopyable @@ -32,13 +35,15 @@ public: virtual void onLibraryLoadingFinished(const Services * services) = 0; /// Loads image using given path - virtual std::shared_ptr loadImage(const ImageLocator & locator, EImageBlitMode mode) = 0; + virtual std::shared_ptr loadImage(const ImageLocator & locator) = 0; virtual std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) = 0; virtual std::shared_ptr loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) = 0; - /// temporary compatibility method. Creates IImage from existing SDL_Surface - /// Surface will be shared, caller must still free it with SDL_FreeSurface - virtual std::shared_ptr createImage(SDL_Surface * source) = 0; + /// Loads single upscaled image without auto-scaling support + virtual std::shared_ptr loadScaledImage(const ImageLocator & locator) = 0; + + /// Creates image which can be used as target for drawing on + virtual std::shared_ptr createImage(const Point & size, CanvasScalingPolicy scalingPolicy) = 0; /// Loads animation using given path virtual std::shared_ptr loadAnimation(const AnimationPath & path, EImageBlitMode mode) = 0; diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp index 515e767a0..ddc10e49f 100644 --- a/client/render/ImageLocator.cpp +++ b/client/render/ImageLocator.cpp @@ -15,11 +15,10 @@ #include "../../lib/json/JsonNode.h" -ImageLocator::ImageLocator(const JsonNode & config) +SharedImageLocator::SharedImageLocator(const JsonNode & config, EImageBlitMode mode) : defFrame(config["defFrame"].Integer()) , defGroup(config["defGroup"].Integer()) - , verticalFlip(config["verticalFlip"].Bool()) - , horizontalFlip(config["horizontalFlip"].Bool()) + , layer(mode) { if(!config["file"].isNull()) image = ImagePath::fromJson(config["file"]); @@ -28,19 +27,28 @@ ImageLocator::ImageLocator(const JsonNode & config) defFile = AnimationPath::fromJson(config["defFile"]); } -ImageLocator::ImageLocator(const ImagePath & path) +SharedImageLocator::SharedImageLocator(const ImagePath & path, EImageBlitMode mode) : image(path) + , layer(mode) { } -ImageLocator::ImageLocator(const AnimationPath & path, int frame, int group) +SharedImageLocator::SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode mode) : defFile(path) , defFrame(frame) , defGroup(group) + , layer(mode) { } -bool ImageLocator::operator<(const ImageLocator & other) const +ImageLocator::ImageLocator(const JsonNode & config, EImageBlitMode mode) + : SharedImageLocator(config, mode) + , verticalFlip(config["verticalFlip"].Bool()) + , horizontalFlip(config["horizontalFlip"].Bool()) +{ +} + +bool SharedImageLocator::operator < (const SharedImageLocator & other) const { if(image != other.image) return image < other.image; @@ -50,14 +58,6 @@ bool ImageLocator::operator<(const ImageLocator & other) const return defGroup < other.defGroup; if(defFrame != other.defFrame) return defFrame < other.defFrame; - if(verticalFlip != other.verticalFlip) - return verticalFlip < other.verticalFlip; - if(horizontalFlip != other.horizontalFlip) - return horizontalFlip < other.horizontalFlip; - if(scalingFactor != other.scalingFactor) - return scalingFactor < other.scalingFactor; - if(playerColored != other.playerColored) - return playerColored < other.playerColored; if(layer != other.layer) return layer < other.layer; @@ -68,70 +68,3 @@ bool ImageLocator::empty() const { return !image.has_value() && !defFile.has_value(); } - -ImageLocator ImageLocator::copyFile() const -{ - ImageLocator result; - result.scalingFactor = 1; - result.preScaledFactor = preScaledFactor; - result.image = image; - result.defFile = defFile; - result.defFrame = defFrame; - result.defGroup = defGroup; - return result; -} - -ImageLocator ImageLocator::copyFileTransform() const -{ - ImageLocator result = copyFile(); - result.horizontalFlip = horizontalFlip; - result.verticalFlip = verticalFlip; - return result; -} - -ImageLocator ImageLocator::copyFileTransformScale() const -{ - return *this; // full copy -} - -std::string ImageLocator::toString() const -{ - std::string result; - if (empty()) - return "invalid"; - - if (image) - { - result += image->getOriginalName(); - assert(!result.empty()); - } - - if (defFile) - { - result += defFile->getOriginalName(); - assert(!result.empty()); - result += "-" + std::to_string(defGroup); - result += "-" + std::to_string(defFrame); - } - - if (verticalFlip) - result += "-vflip"; - - if (horizontalFlip) - result += "-hflip"; - - if (scalingFactor > 1) - result += "-scale" + std::to_string(scalingFactor); - - if (playerColored.isValidPlayer()) - result += "-player" + playerColored.toString(); - - if (layer == EImageBlitMode::ONLY_OVERLAY) - result += "-overlay"; - - if (layer == EImageBlitMode::ONLY_SHADOW) - result += "-shadow"; - - - return result; -} diff --git a/client/render/ImageLocator.h b/client/render/ImageLocator.h index 1868caaf2..59754b2b6 100644 --- a/client/render/ImageLocator.h +++ b/client/render/ImageLocator.h @@ -14,35 +14,32 @@ #include "../../lib/filesystem/ResourcePath.h" #include "../../lib/constants/EntityIdentifiers.h" -struct ImageLocator +struct SharedImageLocator { std::optional image; std::optional defFile; int defFrame = -1; int defGroup = -1; + EImageBlitMode layer = EImageBlitMode::OPAQUE; - PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading? + SharedImageLocator() = default; + SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer); + SharedImageLocator(const JsonNode & config, EImageBlitMode layer); + SharedImageLocator(const ImagePath & path, EImageBlitMode layer); + + bool operator < (const SharedImageLocator & other) const; +}; + +struct ImageLocator : SharedImageLocator +{ + PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; bool verticalFlip = false; bool horizontalFlip = false; int8_t scalingFactor = 0; // 0 = auto / use default scaling - int8_t preScaledFactor = 1; - EImageBlitMode layer = EImageBlitMode::OPAQUE; - ImageLocator() = default; - ImageLocator(const AnimationPath & path, int frame, int group); - explicit ImageLocator(const JsonNode & config); - explicit ImageLocator(const ImagePath & path); + using SharedImageLocator::SharedImageLocator; + ImageLocator(const JsonNode & config, EImageBlitMode layer); - bool operator < (const ImageLocator & other) const; bool empty() const; - - ImageLocator copyFile() const; - ImageLocator copyFileTransform() const; - ImageLocator copyFileTransformScale() const; - - // generates string representation of this image locator - // guaranteed to be a valid file path with no extension - // but may contain '/' if source file is in directory - std::string toString() const; }; diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 4287221e7..13cdfb344 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -11,9 +11,12 @@ #include "CBitmapFont.h" #include "SDL_Extensions.h" +#include "SDLImageScaler.h" + #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" #include "../render/Colors.h" +#include "../render/IImage.h" #include "../render/IScreenHandler.h" #include "../../lib/CConfigHandler.h" @@ -206,9 +209,10 @@ CBitmapFont::CBitmapFont(const std::string & filename): auto filterName = settings["video"]["fontUpscalingFilter"].String(); EScalingAlgorithm algorithm = filterNameToEnum.at(filterName); - auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor(), algorithm); + SDLImageScaler scaler(atlasImage); + scaler.scaleSurfaceIntegerFactor(GH.screenHandler().getScalingFactor(), algorithm); SDL_FreeSurface(atlasImage); - atlasImage = scaledSurface; + atlasImage = scaler.acquireResultSurface(); } logGlobal->debug("Loaded BMP font: '%s', height %d, ascent %d", diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index 7c9daea4d..569ce7399 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -12,6 +12,7 @@ #include "CursorHardware.h" #include "SDL_Extensions.h" +#include "SDLImageScaler.h" #include "../gui/CGuiHandler.h" #include "../render/IScreenHandler.h" @@ -59,8 +60,11 @@ void CursorHardware::setImage(std::shared_ptr image, const Point & pivot CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); - image->draw(cursorSurface, Point(0,0)); - auto cursorSurfaceScaled = CSDL_Ext::scaleSurface(cursorSurface, cursorDimensionsScaled.x, cursorDimensionsScaled.y ); + image->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor()); + + SDLImageScaler scaler(cursorSurface); + scaler.scaleSurface(cursorDimensionsScaled, EScalingAlgorithm::BILINEAR); + SDL_Surface * cursorSurfaceScaled = scaler.acquireResultSurface(); auto oldCursor = cursor; cursor = SDL_CreateColorCursor(cursorSurfaceScaled, pivotOffsetScaled.x, pivotOffsetScaled.y); diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index e5d5a9c09..1e84ecd06 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -65,7 +65,7 @@ void CursorSoftware::updateTexture() CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); - cursorImage->draw(cursorSurface, Point(0,0)); + cursorImage->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor()); SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch); needUpdate = false; } diff --git a/client/renderSDL/ImageScaled.cpp b/client/renderSDL/ImageScaled.cpp deleted file mode 100644 index 55ed44cd1..000000000 --- a/client/renderSDL/ImageScaled.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * ImageScaled.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "ImageScaled.h" - -#include "SDLImage.h" -#include "SDL_Extensions.h" - -#include "../gui/CGuiHandler.h" -#include "../render/IScreenHandler.h" -#include "../render/Colors.h" - -#include "../../lib/constants/EntityIdentifiers.h" - -#include - -ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr & source, EImageBlitMode mode) - : source(source) - , locator(inputLocator) - , colorMultiplier(Colors::WHITE_TRUE) - , alphaValue(SDL_ALPHA_OPAQUE) - , blitMode(mode) -{ - prepareImages(); -} - -std::shared_ptr ImageScaled::getSharedImage() const -{ - return body; -} - -void ImageScaled::scaleInteger(int factor) -{ - assert(0); -} - -void ImageScaled::scaleTo(const Point & size) -{ - if (source) - source = source->scaleTo(size, nullptr); - - if (body) - body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr); -} - -void ImageScaled::exportBitmap(const boost::filesystem::path &path) const -{ - source->exportBitmap(path, nullptr); -} - -bool ImageScaled::isTransparent(const Point &coords) const -{ - return source->isTransparent(coords); -} - -Rect ImageScaled::contentRect() const -{ - return source->contentRect(); -} - -Point ImageScaled::dimensions() const -{ - return source->dimensions(); -} - -void ImageScaled::setAlpha(uint8_t value) -{ - alphaValue = value; -} - -void ImageScaled::setBlitMode(EImageBlitMode mode) -{ - blitMode = mode; -} - -void ImageScaled::draw(SDL_Surface *where, const Point &pos, const Rect *src) const -{ - if (shadow) - shadow->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); - if (body) - body->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); - if (overlay) - overlay->draw(where, nullptr, pos, src, colorMultiplier, colorMultiplier.a * alphaValue / 255, blitMode); -} - -void ImageScaled::setOverlayColor(const ColorRGBA & color) -{ - colorMultiplier = color; -} - -void ImageScaled::playerColored(PlayerColor player) -{ - playerColor = player; - prepareImages(); -} - -void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) -{ - // TODO: implement -} - -void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSkipMask) -{ - // TODO: implement -} - -void ImageScaled::prepareImages() -{ - switch(blitMode) - { - case EImageBlitMode::OPAQUE: - case EImageBlitMode::COLORKEY: - case EImageBlitMode::SIMPLE: - locator.layer = blitMode; - locator.playerColored = playerColor; - body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - break; - - case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: - case EImageBlitMode::ONLY_BODY: - locator.layer = EImageBlitMode::ONLY_BODY; - locator.playerColored = playerColor; - body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - break; - - case EImageBlitMode::WITH_SHADOW: - case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: - locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY; - locator.playerColored = playerColor; - body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - break; - - case EImageBlitMode::ONLY_SHADOW: - case EImageBlitMode::ONLY_OVERLAY: - body = nullptr; - break; - } - - switch(blitMode) - { - case EImageBlitMode::WITH_SHADOW: - case EImageBlitMode::ONLY_SHADOW: - case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: - locator.layer = EImageBlitMode::ONLY_SHADOW; - locator.playerColored = PlayerColor::CANNOT_DETERMINE; - shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - break; - default: - shadow = nullptr; - break; - } - - switch(blitMode) - { - case EImageBlitMode::ONLY_OVERLAY: - case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: - locator.layer = EImageBlitMode::ONLY_OVERLAY; - locator.playerColored = PlayerColor::CANNOT_DETERMINE; - overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - break; - default: - overlay = nullptr; - break; - } -} diff --git a/client/renderSDL/ImageScaled.h b/client/renderSDL/ImageScaled.h deleted file mode 100644 index 0d719b88d..000000000 --- a/client/renderSDL/ImageScaled.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * ImageScaled.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" - -#include "../../lib/Color.h" -#include "../../lib/constants/EntityIdentifiers.h" - -struct SDL_Palette; - -class SDLImageShared; - -// Upscaled image with several mechanisms to emulate H3 palette effects -class ImageScaled final : public IImage -{ -private: - - /// Original unscaled image - std::shared_ptr source; - - /// Upscaled shadow of our image, may be null - std::shared_ptr shadow; - - /// Upscaled main part of our image, may be null - std::shared_ptr body; - - /// Upscaled overlay (player color, selection highlight) of our image, may be null - std::shared_ptr overlay; - - ImageLocator locator; - - ColorRGBA colorMultiplier; - PlayerColor playerColor = PlayerColor::CANNOT_DETERMINE; - - uint8_t alphaValue; - EImageBlitMode blitMode; - - void prepareImages(); -public: - ImageScaled(const ImageLocator & locator, const std::shared_ptr & source, EImageBlitMode mode); - - void scaleInteger(int factor) override; - void scaleTo(const Point & size) override; - void exportBitmap(const boost::filesystem::path & path) const override; - bool isTransparent(const Point & coords) const override; - Rect contentRect() const override; - Point dimensions() const override; - void setAlpha(uint8_t value) override; - void setBlitMode(EImageBlitMode mode) override; - void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; - void setOverlayColor(const ColorRGBA & color) override; - void playerColored(PlayerColor player) override; - void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - - std::shared_ptr getSharedImage() const override; -}; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 551473b7b..7d7f2e8a9 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -11,19 +11,22 @@ #include "RenderHandler.h" #include "SDLImage.h" -#include "ImageScaled.h" +#include "ScalableImage.h" #include "FontChain.h" #include "../gui/CGuiHandler.h" #include "../render/CAnimation.h" +#include "../render/CanvasImage.h" #include "../render/CDefFile.h" #include "../render/Colors.h" #include "../render/ColorFilter.h" #include "../render/IScreenHandler.h" #include "../../lib/json/JsonUtils.h" +#include "../../lib/CThreadHelper.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/VCMIDirs.h" +#include "../../lib/constants/StringConstants.h" #include #include @@ -55,60 +58,7 @@ std::shared_ptr RenderHandler::getAnimationFile(const AnimationPath & return result; } -std::optional RenderHandler::getPathForScaleFactor(const ResourcePath & path, const std::string & factor) -{ - if(path.getType() == EResType::IMAGE) - { - auto p = ImagePath::builtin(path.getName()); - if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/"))) - return std::optional(p.addPrefix("SPRITES" + factor + "X/")); - if(CResourceHandler::get()->existsResource(p.addPrefix("DATA" + factor + "X/"))) - return std::optional(p.addPrefix("DATA" + factor + "X/")); - } - else - { - auto p = AnimationPath::builtin(path.getName()); - auto pJson = p.toType(); - if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/"))) - return std::optional(p.addPrefix("SPRITES" + factor + "X/")); - if(CResourceHandler::get()->existsResource(pJson)) - return std::optional(p); - if(CResourceHandler::get()->existsResource(pJson.addPrefix("SPRITES" + factor + "X/"))) - return std::optional(p.addPrefix("SPRITES" + factor + "X/")); - } - - return std::nullopt; -} - -std::pair RenderHandler::getScalePath(const ResourcePath & p) -{ - auto path = p; - int scaleFactor = 1; - if(getScalingFactor() > 1) - { - std::vector factorsToCheck = {getScalingFactor(), 4, 3, 2}; - for(auto factorToCheck : factorsToCheck) - { - std::string name = boost::algorithm::to_upper_copy(p.getName()); - boost::replace_all(name, "SPRITES/", std::string("SPRITES") + std::to_string(factorToCheck) + std::string("X/")); - boost::replace_all(name, "DATA/", std::string("DATA") + std::to_string(factorToCheck) + std::string("X/")); - ResourcePath scaledPath = ImagePath::builtin(name); - if(p.getType() != EResType::IMAGE) - scaledPath = AnimationPath::builtin(name); - auto tmpPath = getPathForScaleFactor(scaledPath, std::to_string(factorToCheck)); - if(tmpPath) - { - path = *tmpPath; - scaleFactor = factorToCheck; - break; - } - } - } - - return std::pair(path, scaleFactor); -}; - -void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config) +void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config, EImageBlitMode mode) { std::string basepath; basepath = config["basepath"].String(); @@ -128,7 +78,7 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c JsonNode toAdd = frame; JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + frame.String(); - source[groupID].emplace_back(toAdd); + source[groupID].emplace_back(toAdd, mode); } } @@ -149,15 +99,26 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c if (toAdd.Struct().count("defFile")) toAdd["defFile"].String() = basepath + node["defFile"].String(); - source[group][frame] = ImageLocator(toAdd); + source[group][frame] = ImageLocator(toAdd, mode); } } -RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path) +RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode) { - auto tmp = getScalePath(path); - auto animPath = AnimationPath::builtin(tmp.first.getName()); - AnimationPath actualPath = boost::starts_with(animPath.getName(), "SPRITES") ? animPath : animPath.addPrefix("SPRITES/"); + static constexpr std::array scaledSpritesPath = { + "", // 0x + "SPRITES/", + "SPRITES2X/", + "SPRITES3X/", + "SPRITES4X/", + }; + + std::string pathString = path.getName(); + + if (boost::starts_with(pathString, "SPRITES/")) + pathString = pathString.substr(std::string("SPRITES/").length()); + + AnimationPath actualPath = AnimationPath::builtin(scaledSpritesPath.at(scalingFactor) + pathString); auto it = animationLayouts.find(actualPath); @@ -184,15 +145,11 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); - const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), animPath.getOriginalName()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), path.getOriginalName()); - initFromJson(result, config); + initFromJson(result, config, mode); } - for(auto & g : result) - for(auto & i : g.second) - i.preScaledFactor = tmp.second; - animationLayouts[actualPath] = result; return animationLayouts[actualPath]; } @@ -202,209 +159,174 @@ int RenderHandler::getScalingFactor() const return GH.screenHandler().getScalingFactor(); } -ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group) +ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, int scaling, EImageBlitMode mode) { - const auto & layout = getAnimationLayout(path); + const auto & layout = getAnimationLayout(path, scaling, mode); if (!layout.count(group)) - return ImageLocator(ImagePath::builtin("DEFAULT")); + return ImageLocator(); if (frame >= layout.at(group).size()) - return ImageLocator(ImagePath::builtin("DEFAULT")); + return ImageLocator(); const auto & locator = layout.at(group).at(frame); if (locator.image || locator.defFile) return locator; - return ImageLocator(path, frame, group); + return ImageLocator(path, frame, group, mode); } -std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & locator) { auto it = imageFiles.find(locator); if (it != imageFiles.end()) return it->second; - // TODO: order should be different: - // 1) try to find correctly scaled image - // 2) if fails -> try to find correctly transformed - // 3) if also fails -> try to find image from correct file - // 4) load missing part of the sequence - // TODO: check whether (load -> transform -> scale) or (load -> scale -> transform) order should be used for proper loading of pre-scaled data - auto imageFromFile = loadImageFromFile(locator.copyFile()); - auto transformedImage = transformImage(locator.copyFileTransform(), imageFromFile); - auto scaledImage = scaleImage(locator.copyFileTransformScale(), transformedImage); + auto sdlImage = loadImageFromFileUncached(locator); + auto scaledImage = std::make_shared(locator, sdlImage); + storeCachedImage(locator, scaledImage); return scaledImage; } -std::shared_ptr RenderHandler::loadImageFromFileUncached(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadImageFromFileUncached(const ImageLocator & locator) { if(locator.image) { // TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load - return std::make_shared(*locator.image, locator.preScaledFactor); + return std::make_shared(*locator.image); } if(locator.defFile) { auto defFile = getAnimationFile(*locator.defFile); - int preScaledFactor = locator.preScaledFactor; - if(!defFile) // no prescale for this frame - { - auto tmpPath = (*locator.defFile).getName(); - boost::algorithm::replace_all(tmpPath, "SPRITES2X/", "SPRITES/"); - boost::algorithm::replace_all(tmpPath, "SPRITES3X/", "SPRITES/"); - boost::algorithm::replace_all(tmpPath, "SPRITES4X/", "SPRITES/"); - preScaledFactor = 1; - defFile = getAnimationFile(AnimationPath::builtin(tmpPath)); - } if(defFile->hasFrame(locator.defFrame, locator.defGroup)) - return std::make_shared(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor); + return std::make_shared(defFile.get(), locator.defFrame, locator.defGroup); else { logGlobal->error("Frame %d in group %d not found in file: %s", locator.defFrame, locator.defGroup, locator.defFile->getName().c_str()); - return std::make_shared(ImagePath::builtin("DEFAULT"), locator.preScaledFactor); + return std::make_shared(ImagePath::builtin("DEFAULT")); } } throw std::runtime_error("Invalid image locator received!"); } -void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr image) +void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr image) { imageFiles[locator] = image; - -#if 0 - const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "imageCache" / (locator.toString() + ".png"); - boost::filesystem::path outDir = outPath; - outDir.remove_filename(); - boost::filesystem::create_directories(outDir); - image->exportBitmap(outPath , nullptr); -#endif } -std::shared_ptr RenderHandler::loadImageFromFile(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadScaledImage(const ImageLocator & locator) { - if (imageFiles.count(locator)) - return imageFiles.at(locator); + static constexpr std::array scaledDataPath = { + "", // 0x + "DATA/", + "DATA2X/", + "DATA3X/", + "DATA4X/", + }; - auto result = loadImageFromFileUncached(locator); - storeCachedImage(locator, result); - return result; + static constexpr std::array scaledSpritesPath = { + "", // 0x + "SPRITES/", + "SPRITES2X/", + "SPRITES3X/", + "SPRITES4X/", + }; + + ImagePath pathToLoad; + + if(locator.defFile) + { + auto remappedLocator = getLocatorForAnimationFrame(*locator.defFile, locator.defFrame, locator.defGroup, locator.scalingFactor, locator.layer); + // we expect that .def's are only used for 1x data, upscaled assets should use standalone images + if (!remappedLocator.image) + return nullptr; + + pathToLoad = *remappedLocator.image; + } + + if(locator.image) + pathToLoad = *locator.image; + + if (pathToLoad.empty()) + return nullptr; + + std::string imagePathString = pathToLoad.getName(); + + if(locator.layer == EImageBlitMode::ONLY_OVERLAY) + imagePathString += "-OVERLAY"; + if(locator.layer == EImageBlitMode::ONLY_SHADOW) + imagePathString += "-SHADOW"; + if(locator.playerColored.isValidPlayer()) + imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]); + if(locator.playerColored == PlayerColor::NEUTRAL) + imagePathString += "-NEUTRAL"; + + auto imagePath = ImagePath::builtin(imagePathString); + auto imagePathSprites = ImagePath::builtin(imagePathString).addPrefix(scaledSpritesPath.at(locator.scalingFactor)); + auto imagePathData = ImagePath::builtin(imagePathString).addPrefix(scaledDataPath.at(locator.scalingFactor)); + + if(CResourceHandler::get()->existsResource(imagePathSprites)) + return std::make_shared(imagePathSprites); + + if(CResourceHandler::get()->existsResource(imagePathData)) + return std::make_shared(imagePathData); + + if(CResourceHandler::get()->existsResource(imagePath)) + return std::make_shared(imagePath); + + return nullptr; } -std::shared_ptr RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr image) -{ - if (imageFiles.count(locator)) - return imageFiles.at(locator); - - auto result = image; - - if (locator.verticalFlip) - result = result->verticalFlip(); - - if (locator.horizontalFlip) - result = result->horizontalFlip(); - - storeCachedImage(locator, result); - return result; -} - -std::shared_ptr RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr image) -{ - if (imageFiles.count(locator)) - return imageFiles.at(locator); - - auto handle = image->createImageReference(locator.layer); - - assert(locator.scalingFactor != 1); // should be filtered-out before - if (locator.playerColored != PlayerColor::CANNOT_DETERMINE) - handle->playerColored(locator.playerColored); - - handle->scaleInteger(locator.scalingFactor); - - auto result = handle->getSharedImage(); - storeCachedImage(locator, result); - return result; -} - -std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode) +std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator) { ImageLocator adjustedLocator = locator; - if(adjustedLocator.image) - { - std::string imgPath = (*adjustedLocator.image).getName(); - if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY) - imgPath += "-OVERLAY"; - if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW) - imgPath += "-SHADOW"; - - if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) || - CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("DATA/")) || - CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("SPRITES/"))) - adjustedLocator.image = ImagePath::builtin(imgPath); - } - - if(adjustedLocator.defFile && adjustedLocator.scalingFactor == 0) - { - auto tmp = getScalePath(*adjustedLocator.defFile); - adjustedLocator.defFile = AnimationPath::builtin(tmp.first.getName()); - adjustedLocator.preScaledFactor = tmp.second; - } - if(adjustedLocator.image && adjustedLocator.scalingFactor == 0) - { - auto tmp = getScalePath(*adjustedLocator.image); - adjustedLocator.image = ImagePath::builtin(tmp.first.getName()); - adjustedLocator.preScaledFactor = tmp.second; - } - - if (adjustedLocator.scalingFactor == 0 && getScalingFactor() != 1 ) - { - auto unscaledLocator = adjustedLocator; - auto scaledLocator = adjustedLocator; - - unscaledLocator.scalingFactor = 1; - scaledLocator.scalingFactor = getScalingFactor(); - auto unscaledImage = loadImageImpl(unscaledLocator); - - return std::make_shared(scaledLocator, unscaledImage, mode); - } + std::shared_ptr result; if (adjustedLocator.scalingFactor == 0) { auto scaledLocator = adjustedLocator; scaledLocator.scalingFactor = getScalingFactor(); - return loadImageImpl(scaledLocator)->createImageReference(mode); + result = loadImageImpl(scaledLocator)->createImageReference(); } else - return loadImageImpl(adjustedLocator)->createImageReference(mode); + result = loadImageImpl(adjustedLocator)->createImageReference(); + + if (locator.horizontalFlip) + result->horizontalFlip(); + if (locator.verticalFlip) + result->verticalFlip(); + + return result; } std::shared_ptr RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) { - auto tmp = getScalePath(path); - ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group); - locator.preScaledFactor = tmp.second; - return loadImage(locator, mode); + ImageLocator locator = getLocatorForAnimationFrame(path, frame, group, 1, mode); + if (!locator.empty()) + return loadImage(locator); + else + return loadImage(ImageLocator(ImagePath::builtin("DEFAULT"), mode)); } std::shared_ptr RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode) { - ImageLocator locator(path); - return loadImage(locator, mode); + ImageLocator locator(path, mode); + return loadImage(locator); } -std::shared_ptr RenderHandler::createImage(SDL_Surface * source) +std::shared_ptr RenderHandler::createImage(const Point & size, CanvasScalingPolicy scalingPolicy) { - return std::make_shared(source)->createImageReference(EImageBlitMode::SIMPLE); + return std::make_shared(size, scalingPolicy); } std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode) { - return std::make_shared(path, getAnimationLayout(path), mode); + return std::make_shared(path, getAnimationLayout(path, 1, mode), mode); } void RenderHandler::addImageListEntries(const EntityService * service) @@ -416,7 +338,7 @@ void RenderHandler::addImageListEntries(const EntityService * service) if (imageName.empty()) return; - auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName)); + auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::SIMPLE); JsonNode entry; entry["file"].String() = imageName; @@ -424,7 +346,7 @@ void RenderHandler::addImageListEntries(const EntityService * service) if (index >= layout[group].size()) layout[group].resize(index + 1); - layout[group][index] = ImageLocator(entry); + layout[group][index] = ImageLocator(entry, EImageBlitMode::SIMPLE); }); }); } diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index 43df617a1..4a7e73c6a 100644 --- a/client/renderSDL/RenderHandler.h +++ b/client/renderSDL/RenderHandler.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_END class CDefFile; class SDLImageShared; -class ISharedImage; +class ScalableImageShared; class RenderHandler : public IRenderHandler { @@ -25,28 +25,22 @@ class RenderHandler : public IRenderHandler std::map> animationFiles; std::map animationLayouts; - std::map> imageFiles; + std::map> imageFiles; std::map> fonts; std::shared_ptr getAnimationFile(const AnimationPath & path); - std::optional getPathForScaleFactor(const ResourcePath & path, const std::string & factor); - std::pair getScalePath(const ResourcePath & p); - AnimationLayoutMap & getAnimationLayout(const AnimationPath & path); - void initFromJson(AnimationLayoutMap & layout, const JsonNode & config); + AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode); + void initFromJson(AnimationLayoutMap & layout, const JsonNode & config, EImageBlitMode mode); void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName); void addImageListEntries(const EntityService * service); - void storeCachedImage(const ImageLocator & locator, std::shared_ptr image); + void storeCachedImage(const ImageLocator & locator, std::shared_ptr image); - std::shared_ptr loadImageImpl(const ImageLocator & config); + std::shared_ptr loadImageImpl(const ImageLocator & config); - std::shared_ptr loadImageFromFileUncached(const ImageLocator & locator); - std::shared_ptr loadImageFromFile(const ImageLocator & locator); + std::shared_ptr loadImageFromFileUncached(const ImageLocator & locator); - std::shared_ptr transformImage(const ImageLocator & locator, std::shared_ptr image); - std::shared_ptr scaleImage(const ImageLocator & locator, std::shared_ptr image); - - ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group); + ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, int scaling, EImageBlitMode mode); int getScalingFactor() const; @@ -55,13 +49,15 @@ public: // IRenderHandler implementation void onLibraryLoadingFinished(const Services * services) override; - std::shared_ptr loadImage(const ImageLocator & locator, EImageBlitMode mode) override; + std::shared_ptr loadImage(const ImageLocator & locator) override; std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) override; std::shared_ptr loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) override; + std::shared_ptr loadScaledImage(const ImageLocator & locator) override; + std::shared_ptr loadAnimation(const AnimationPath & path, EImageBlitMode mode) override; - std::shared_ptr createImage(SDL_Surface * source) override; + std::shared_ptr createImage(const Point & size, CanvasScalingPolicy scalingPolicy) override; /// Returns font with specified identifer std::shared_ptr loadFont(EFonts font) override; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 7ce9f2d91..d7633358c 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -11,76 +11,24 @@ #include "SDLImage.h" #include "SDLImageLoader.h" +#include "SDLImageScaler.h" #include "SDL_Extensions.h" #include "../render/ColorFilter.h" -#include "../render/Colors.h" #include "../render/CBitmapHandler.h" #include "../render/CDefFile.h" -#include "../render/Graphics.h" -#include "../xBRZ/xbrz.h" #include "../gui/CGuiHandler.h" #include "../render/IScreenHandler.h" #include -#include +#include + #include +#include +#include class SDLImageLoader; -//First 8 colors in def palette used for transparency -static constexpr std::array sourcePalette = {{ - {0, 255, 255, SDL_ALPHA_OPAQUE}, - {255, 150, 255, SDL_ALPHA_OPAQUE}, - {255, 100, 255, SDL_ALPHA_OPAQUE}, - {255, 50, 255, SDL_ALPHA_OPAQUE}, - {255, 0, 255, SDL_ALPHA_OPAQUE}, - {255, 255, 0, SDL_ALPHA_OPAQUE}, - {180, 0, 255, SDL_ALPHA_OPAQUE}, - {0, 255, 0, SDL_ALPHA_OPAQUE} -}}; - -static constexpr std::array targetPalette = {{ - {0, 0, 0, 0 }, // 0 - transparency ( used in most images ) - {0, 0, 0, 64 }, // 1 - shadow border ( used in battle, adventure map def's ) - {0, 0, 0, 64 }, // 2 - shadow border ( used in fog-of-war def's ) - {0, 0, 0, 128}, // 3 - shadow body ( used in fog-of-war def's ) - {0, 0, 0, 128}, // 4 - shadow body ( used in battle, adventure map def's ) - {0, 0, 0, 0 }, // 5 - selection / owner flag ( used in battle, adventure map def's ) - {0, 0, 0, 128}, // 6 - shadow body below selection ( used in battle def's ) - {0, 0, 0, 64 } // 7 - shadow border below selection ( used in battle def's ) -}}; - -static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) -{ - return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; -} - -static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) -{ - return ColorRGBA( - mixChannels(over.r, base.r, over.a, base.a), - mixChannels(over.g, base.g, over.a, base.a), - mixChannels(over.b, base.b, over.a, base.a), - static_cast(over.a + base.a * (255 - over.a) / 256) - ); -} - -static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs) -{ - // it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow - // exact logic is not clear and requires extensive testing with image editing - // potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component - static const int threshold = 8; - - int diffR = static_cast(lhs.r) - rhs.r; - int diffG = static_cast(lhs.g) - rhs.g; - int diffB = static_cast(lhs.b) - rhs.b; - int diffA = static_cast(lhs.a) - rhs.a; - - return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; -} - int IImage::width() const { return dimensions().x; @@ -91,12 +39,11 @@ int IImage::height() const return dimensions().y; } -SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group, int preScaleFactor) +SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr), - preScaleFactor(preScaleFactor) + originalPalette(nullptr) { SDLImageLoader loader(this); data->loadFrame(frame, group, loader); @@ -104,12 +51,11 @@ SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group savePalette(); } -SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor) +SDLImageShared::SDLImageShared(SDL_Surface * from) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr), - preScaleFactor(preScaleFactor) + originalPalette(nullptr) { surf = from; if (surf == nullptr) @@ -122,12 +68,11 @@ SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor) +SDLImageShared::SDLImageShared(const ImagePath & filename) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr), - preScaleFactor(preScaleFactor) + originalPalette(nullptr) { surf = BitmapHandler::loadBitmap(filename); @@ -146,9 +91,70 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor) } } +void SDLImageShared::scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaleTo, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const +{ + assert(upscalingInProgress == false); + if (!surf) + return; + + Rect sourceRect(0, 0, surf->w, surf->h); + Point destShift(0, 0); + Point destScale = Point(surf->w, surf->h) * scaleTo / dimensions(); + Point marginsScaled = margins * scaleTo / dimensions(); + + if(src) + { + Rect srcUnscaled(Point(src->topLeft() * dimensions() / scaleTo), Point(src->dimensions() * dimensions() / scaleTo)); + + if(srcUnscaled.x < margins.x) + destShift.x += marginsScaled.x - src->x; + + if(srcUnscaled.y < margins.y) + destShift.y += marginsScaled.y - src->y; + + sourceRect = Rect(srcUnscaled).intersect(Rect(margins.x, margins.y, surf->w, surf->h)); + + destScale.x = std::min(destScale.x, sourceRect.w * scaleTo.x / dimensions().x); + destScale.y = std::min(destScale.y, sourceRect.h * scaleTo.y / dimensions().y); + + sourceRect -= margins; + } + else + destShift = marginsScaled; + + destShift += dest; + + SDL_SetSurfaceColorMod(surf, colorMultiplier.r, colorMultiplier.g, colorMultiplier.b); + SDL_SetSurfaceAlphaMod(surf, alpha); + + if (alpha != SDL_ALPHA_OPAQUE || (mode != EImageBlitMode::OPAQUE && surf->format->Amask != 0)) + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); + else + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); + + if (palette && surf->format->palette) + SDL_SetSurfacePalette(surf, palette); + + SDL_Rect srcRect = CSDL_Ext::toSDL(sourceRect); + SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(destShift, destScale)); + + if (sourceRect.dimensions() * scaleTo / dimensions() != destScale) + logGlobal->info("???"); + + SDL_Surface * tempSurface = SDL_ConvertSurface(surf, where->format, 0); + int result = SDL_BlitScaled(tempSurface, &srcRect, where, &dstRect); + + SDL_FreeSurface(tempSurface); + if (result != 0) + logGlobal->error("SDL_BlitScaled failed! %s", SDL_GetError()); + + if (surf->format->palette) + SDL_SetSurfacePalette(surf, originalPalette); +} void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const { + assert(upscalingInProgress == false); if (!surf) return; @@ -199,89 +205,23 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin void SDLImageShared::optimizeSurface() { + assert(upscalingInProgress == false); if (!surf) return; - int left = surf->w; - int top = surf->h; - int right = 0; - int bottom = 0; + SDLImageOptimizer optimizer(surf, Rect(margins, fullSize)); - // locate fully-transparent area around image - // H3 hadles this on format level, but mods or images scaled in runtime do not - if (surf->format->palette) - { - for (int y = 0; y < surf->h; ++y) - { - const uint8_t * row = static_cast(surf->pixels) + y * surf->pitch; - for (int x = 0; x < surf->w; ++x) - { - if (row[x] != 0) - { - // opaque or can be opaque (e.g. disabled shadow) - top = std::min(top, y); - left = std::min(left, x); - right = std::max(right, x); - bottom = std::max(bottom, y); - } - } - } - } - else - { - for (int y = 0; y < surf->h; ++y) - { - for (int x = 0; x < surf->w; ++x) - { - ColorRGBA color; - SDL_GetRGBA(CSDL_Ext::getPixel(surf, x, y), surf->format, &color.r, &color.g, &color.b, &color.a); + optimizer.optimizeSurface(surf); + SDL_FreeSurface(surf); - if (color.a != SDL_ALPHA_TRANSPARENT) - { - // opaque - top = std::min(top, y); - left = std::min(left, x); - right = std::max(right, x); - bottom = std::max(bottom, y); - } - } - } - } - - if (left == surf->w) - { - // empty image - simply delete it - SDL_FreeSurface(surf); - surf = nullptr; - return; - } - - if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1) - { - // non-zero border found - Rect newDimensions(left, top, right - left + 1, bottom - top + 1); - SDL_Rect rectSDL = CSDL_Ext::toSDL(newDimensions); - auto newSurface = CSDL_Ext::newSurface(newDimensions.dimensions(), surf); - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); - SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr); - - if (SDL_HasColorKey(surf)) - { - uint32_t colorKey; - SDL_GetColorKey(surf, &colorKey); - SDL_SetColorKey(newSurface, SDL_TRUE, colorKey); - } - - SDL_FreeSurface(surf); - surf = newSurface; - - margins.x += left; - margins.y += top; - } + surf = optimizer.acquireResultSurface(); + margins = optimizer.getResultDimensions().topLeft(); + fullSize = optimizer.getResultDimensions().dimensions(); } std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const { + assert(upscalingInProgress == false); if (factor <= 0) throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor)); @@ -291,47 +231,58 @@ std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); - SDL_Surface * scaled = nullptr; - if(preScaleFactor == factor) - return shared_from_this(); - else if(preScaleFactor == 1) - { - // dump heuristics to differentiate tileable UI elements from map object / combat assets - if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE) - scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE); - else - scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA); - } + // simple heuristics to differentiate tileable UI elements from map object / combat assets + EScalingAlgorithm algorithm; + if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE) + algorithm = EScalingAlgorithm::XBRZ_OPAQUE; else - scaled = CSDL_Ext::scaleSurface(surf, (int) round((float)surf->w * factor / preScaleFactor), (int) round((float)surf->h * factor / preScaleFactor)); + algorithm = EScalingAlgorithm::XBRZ_ALPHA; - auto ret = std::make_shared(scaled, preScaleFactor); - - ret->fullSize.x = fullSize.x * factor; - ret->fullSize.y = fullSize.y * factor; - - ret->margins.x = (int) round((float)margins.x * factor / preScaleFactor); - ret->margins.y = (int) round((float)margins.y * factor / preScaleFactor); - ret->optimizeSurface(); - - // erase our own reference - SDL_FreeSurface(scaled); + auto result = std::make_shared(this, factor, algorithm); if (surf->format->palette) SDL_SetSurfacePalette(surf, originalPalette); - return ret; + return result; +} + +SDLImageShared::SDLImageShared(const SDLImageShared * from, int integerScaleFactor, EScalingAlgorithm algorithm) +{ + static tbb::task_arena upscalingArena; + + upscalingInProgress = true; + + auto scaler = std::make_shared(from->surf, Rect(from->margins, from->fullSize)); + + const auto & scalingTask = [this, algorithm, scaler]() + { + scaler->scaleSurfaceIntegerFactor(GH.screenHandler().getScalingFactor(), algorithm); + surf = scaler->acquireResultSurface(); + fullSize = scaler->getResultDimensions().dimensions(); + margins = scaler->getResultDimensions().topLeft(); + + upscalingInProgress = false; + }; + + upscalingArena.enqueue(scalingTask); +} + +bool SDLImageShared::isLoading() const +{ + return upscalingInProgress; } std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const { - float scaleX = static_cast(size.x) / fullSize.x; - float scaleY = static_cast(size.y) / fullSize.y; - + assert(upscalingInProgress == false); if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); - auto scaled = CSDL_Ext::scaleSurface(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY)); + SDLImageScaler scaler(surf, Rect(margins, fullSize)); + + scaler.scaleSurface(size, EScalingAlgorithm::XBRZ_ALPHA); + + auto scaled = scaler.acquireResultSurface(); if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]); @@ -340,13 +291,9 @@ std::shared_ptr SDLImageShared::scaleTo(const Point & size, else CSDL_Ext::setDefaultColorKey(scaled);//just in case - auto ret = std::make_shared(scaled, preScaleFactor); - - ret->fullSize.x = (int) round((float)fullSize.x * scaleX); - ret->fullSize.y = (int) round((float)fullSize.y * scaleY); - - ret->margins.x = (int) round((float)margins.x * scaleX); - ret->margins.y = (int) round((float)margins.y * scaleY); + auto ret = std::make_shared(scaled); + ret->fullSize = scaler.getResultDimensions().dimensions(); + ret->margins = scaler.getResultDimensions().topLeft(); // erase our own reference SDL_FreeSurface(scaled); @@ -359,6 +306,7 @@ std::shared_ptr SDLImageShared::scaleTo(const Point & size, void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palette * palette) const { + assert(upscalingInProgress == false); if (!surf) return; @@ -369,13 +317,9 @@ void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palet SDL_SetSurfacePalette(surf, originalPalette); } -void SDLImageIndexed::playerColored(PlayerColor player) -{ - graphics->setPlayerPalette(currentPalette, player); -} - bool SDLImageShared::isTransparent(const Point & coords) const { + assert(upscalingInProgress == false); if (surf) return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y - margins.y); else @@ -384,31 +328,34 @@ bool SDLImageShared::isTransparent(const Point & coords) const Rect SDLImageShared::contentRect() const { - auto tmpMargins = margins / preScaleFactor; - auto tmpSize = Point(surf->w, surf->h) / preScaleFactor; + assert(upscalingInProgress == false); + auto tmpMargins = margins; + auto tmpSize = Point(surf->w, surf->h); return Rect(tmpMargins, tmpSize); } +const SDL_Palette * SDLImageShared::getPalette() const +{ + assert(upscalingInProgress == false); + if (!surf) + return nullptr; + return surf->format->palette; +} + Point SDLImageShared::dimensions() const { - return fullSize / preScaleFactor; -} - -std::shared_ptr SDLImageShared::createImageReference(EImageBlitMode mode) const -{ - if (surf && surf->format->palette) - return std::make_shared(shared_from_this(), originalPalette, mode); - else - return std::make_shared(shared_from_this(), mode); + assert(upscalingInProgress == false); + return fullSize; } std::shared_ptr SDLImageShared::horizontalFlip() const { + assert(upscalingInProgress == false); if (!surf) return shared_from_this(); SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); - auto ret = std::make_shared(flipped, preScaleFactor); + auto ret = std::make_shared(flipped); ret->fullSize = fullSize; ret->margins.x = margins.x; ret->margins.y = fullSize.y - surf->h - margins.y; @@ -419,11 +366,12 @@ std::shared_ptr SDLImageShared::horizontalFlip() const std::shared_ptr SDLImageShared::verticalFlip() const { + assert(upscalingInProgress == false); if (!surf) return shared_from_this(); SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); - auto ret = std::make_shared(flipped, preScaleFactor); + auto ret = std::make_shared(flipped); ret->fullSize = fullSize; ret->margins.x = fullSize.x - surf->w - margins.x; ret->margins.y = margins.y; @@ -435,6 +383,7 @@ std::shared_ptr SDLImageShared::verticalFlip() const // Keep the original palette, in order to do color switching operation void SDLImageShared::savePalette() { + assert(upscalingInProgress == false); // For some images that don't have palette, skip this if(surf->format->palette == nullptr) return; @@ -445,219 +394,8 @@ void SDLImageShared::savePalette() SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, surf->format->palette->ncolors); } -void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) -{ - std::vector shifterColors(colorsToMove); - - for(uint32_t i=0; icolors[firstColorID + i]; - - SDL_SetPaletteColors(currentPalette, shifterColors.data(), firstColorID, colorsToMove); -} - -void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) -{ - // If shadow is enabled, following colors must be skipped unconditionally - if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY) - colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4); - - // Note: here we skip first colors in the palette that are predefined in H3 images - for(int i = 0; i < currentPalette->ncolors; i++) - { - if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i])) - continue; - - if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - continue; - - currentPalette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); - } -} - -SDLImageIndexed::SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * originalPalette, EImageBlitMode mode) - :SDLImageBase::SDLImageBase(image, mode) - ,originalPalette(originalPalette) -{ - currentPalette = SDL_AllocPalette(originalPalette->ncolors); - SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors); - - preparePalette(); -} - -SDLImageIndexed::~SDLImageIndexed() -{ - SDL_FreePalette(currentPalette); -} - -void SDLImageIndexed::setShadowTransparency(float factor) -{ - ColorRGBA shadow50(0, 0, 0, 128 * factor); - ColorRGBA shadow25(0, 0, 0, 64 * factor); - - std::array colorsSDL = { - originalPalette->colors[0], - originalPalette->colors[1], - originalPalette->colors[2], - originalPalette->colors[3], - originalPalette->colors[4] - }; - - // seems to be used unconditionally - colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY); - colorsSDL[1] = CSDL_Ext::toSDL(shadow25); - colorsSDL[4] = CSDL_Ext::toSDL(shadow50); - - // seems to be used only if color matches - if (colorsSimilar(originalPalette->colors[2], sourcePalette[2])) - colorsSDL[2] = CSDL_Ext::toSDL(shadow25); - - if (colorsSimilar(originalPalette->colors[3], sourcePalette[3])) - colorsSDL[3] = CSDL_Ext::toSDL(shadow50); - - SDL_SetPaletteColors(currentPalette, colorsSDL.data(), 0, colorsSDL.size()); -} - -void SDLImageIndexed::setOverlayColor(const ColorRGBA & color) -{ - currentPalette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color)); - - for (int i : {6,7}) - { - if (colorsSimilar(originalPalette->colors[i], sourcePalette[i])) - currentPalette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color)); - } -} - -void SDLImageIndexed::preparePalette() -{ - switch(blitMode) - { - case EImageBlitMode::ONLY_SHADOW: - case EImageBlitMode::ONLY_OVERLAY: - adjustPalette(ColorFilter::genAlphaShifter(0), 0); - break; - } - - switch(blitMode) - { - case EImageBlitMode::SIMPLE: - case EImageBlitMode::WITH_SHADOW: - case EImageBlitMode::ONLY_SHADOW: - case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: - setShadowTransparency(1.0); - break; - case EImageBlitMode::ONLY_BODY: - case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: - case EImageBlitMode::ONLY_OVERLAY: - setShadowTransparency(0.0); - break; - } - - switch(blitMode) - { - case EImageBlitMode::ONLY_OVERLAY: - case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: - setOverlayColor(Colors::WHITE_TRUE); - break; - case EImageBlitMode::ONLY_SHADOW: - case EImageBlitMode::ONLY_BODY: - setOverlayColor(Colors::TRANSPARENCY); - break; - } -} - SDLImageShared::~SDLImageShared() { SDL_FreeSurface(surf); SDL_FreePalette(originalPalette); } - -SDLImageBase::SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode) - :image(image) - , alphaValue(SDL_ALPHA_OPAQUE) - , blitMode(mode) -{} - -std::shared_ptr SDLImageBase::getSharedImage() const -{ - return image; -} - -void SDLImageRGB::draw(SDL_Surface * where, const Point & pos, const Rect * src) const -{ - image->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); -} - -void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect * src) const -{ - image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); -} - -void SDLImageIndexed::exportBitmap(const boost::filesystem::path & path) const -{ - image->exportBitmap(path, currentPalette); -} - -void SDLImageIndexed::scaleTo(const Point & size) -{ - image = image->scaleTo(size, currentPalette); -} - -void SDLImageRGB::scaleTo(const Point & size) -{ - image = image->scaleTo(size, nullptr); -} - -void SDLImageIndexed::scaleInteger(int factor) -{ - image = image->scaleInteger(factor, currentPalette, blitMode); -} - -void SDLImageRGB::scaleInteger(int factor) -{ - image = image->scaleInteger(factor, nullptr, blitMode); -} - -void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const -{ - image->exportBitmap(path, nullptr); -} - -bool SDLImageBase::isTransparent(const Point & coords) const -{ - return image->isTransparent(coords); -} - -Rect SDLImageBase::contentRect() const -{ - return image->contentRect(); -} - -Point SDLImageBase::dimensions() const -{ - return image->dimensions(); -} - -void SDLImageBase::setAlpha(uint8_t value) -{ - alphaValue = value; -} - -void SDLImageBase::setBlitMode(EImageBlitMode mode) -{ - blitMode = mode; -} - -void SDLImageRGB::setOverlayColor(const ColorRGBA & color) -{} - -void SDLImageRGB::playerColored(PlayerColor player) -{} - -void SDLImageRGB::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) -{} - -void SDLImageRGB::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) -{} - - diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index b4a8038d3..2ad465c67 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -27,16 +27,15 @@ struct SDL_Palette; class SDLImageShared final : public ISharedImage, public std::enable_shared_from_this, boost::noncopyable { //Surface without empty borders - SDL_Surface * surf; + SDL_Surface * surf = nullptr; - SDL_Palette * originalPalette; + SDL_Palette * originalPalette = nullptr; //size of left and top borders Point margins; //total size including borders Point fullSize; - //pre scaled image - int preScaleFactor; + std::atomic_bool upscalingInProgress = false; // Keep the original palette, in order to do color switching operation void savePalette(); @@ -45,20 +44,27 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from public: //Load image from def file - SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1); + SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImageShared(const ImagePath & filename, int preScaleFactor=1); + SDLImageShared(const ImagePath & filename); //Create using existing surface, extraRef will increase refcount on SDL_Surface - SDLImageShared(SDL_Surface * from, int preScaleFactor=1); + SDLImageShared(SDL_Surface * from); + /// Creates image at specified scaling factor from source image + SDLImageShared(const SDLImageShared * from, int integerScaleFactor, EScalingAlgorithm algorithm); ~SDLImageShared(); + void scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaling, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override; void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override; void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override; Point dimensions() const override; bool isTransparent(const Point & coords) const override; Rect contentRect() const override; - [[nodiscard]] std::shared_ptr createImageReference(EImageBlitMode mode) const override; + + bool isLoading() const override; + + const SDL_Palette * getPalette() const override; + [[nodiscard]] std::shared_ptr horizontalFlip() const override; [[nodiscard]] std::shared_ptr verticalFlip() const override; [[nodiscard]] std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override; @@ -66,58 +72,3 @@ public: friend class SDLImageLoader; }; - -class SDLImageBase : public IImage, boost::noncopyable -{ -protected: - std::shared_ptr image; - - uint8_t alphaValue; - EImageBlitMode blitMode; - -public: - SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode); - - bool isTransparent(const Point & coords) const override; - Rect contentRect() const override; - Point dimensions() const override; - void setAlpha(uint8_t value) override; - void setBlitMode(EImageBlitMode mode) override; - std::shared_ptr getSharedImage() const override; -}; - -class SDLImageIndexed final : public SDLImageBase -{ - SDL_Palette * currentPalette = nullptr; - SDL_Palette * originalPalette = nullptr; - - void setShadowTransparency(float factor); - void preparePalette(); -public: - SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * palette, EImageBlitMode mode); - ~SDLImageIndexed(); - - void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; - void setOverlayColor(const ColorRGBA & color) override; - void playerColored(PlayerColor player) override; - void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - void scaleInteger(int factor) override; - void scaleTo(const Point & size) override; - void exportBitmap(const boost::filesystem::path & path) const override; -}; - -class SDLImageRGB final : public SDLImageBase -{ -public: - using SDLImageBase::SDLImageBase; - - void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; - void setOverlayColor(const ColorRGBA & color) override; - void playerColored(PlayerColor player) override; - void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - void scaleInteger(int factor) override; - void scaleTo(const Point & size) override; - void exportBitmap(const boost::filesystem::path & path) const override; -}; diff --git a/client/renderSDL/SDLImageScaler.cpp b/client/renderSDL/SDLImageScaler.cpp new file mode 100644 index 000000000..e3b5451e5 --- /dev/null +++ b/client/renderSDL/SDLImageScaler.cpp @@ -0,0 +1,233 @@ +/* + * SDLImageScaler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "SDLImageScaler.h" + +#include "SDL_Extensions.h" + +#include "../CMT.h" +#include "../xBRZ/xbrz.h" + +#include +#include + +SDLImageOptimizer::SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions) + : surf(surf) + , virtualDimensions(virtualDimensions) +{ +} + +void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface) +{ + if (!surf) + return; + + int left = surf->w; + int top = surf->h; + int right = 0; + int bottom = 0; + + // locate fully-transparent area around image + // H3 hadles this on format level, but mods or images scaled in runtime do not + if (surf->format->palette) + { + for (int y = 0; y < surf->h; ++y) + { + const uint8_t * row = static_cast(surf->pixels) + y * surf->pitch; + for (int x = 0; x < surf->w; ++x) + { + if (row[x] != 0) + { + // opaque or can be opaque (e.g. disabled shadow) + top = std::min(top, y); + left = std::min(left, x); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + } + } + } + else + { + for (int y = 0; y < surf->h; ++y) + { + for (int x = 0; x < surf->w; ++x) + { + ColorRGBA color; + SDL_GetRGBA(CSDL_Ext::getPixel(surf, x, y), surf->format, &color.r, &color.g, &color.b, &color.a); + + if (color.a != SDL_ALPHA_TRANSPARENT) + { + // opaque + top = std::min(top, y); + left = std::min(left, x); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + } + } + } + + // empty image + if (left == surf->w) + return; + + if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1) + { + // non-zero border found + Rect newDimensions(left, top, right - left + 1, bottom - top + 1); + SDL_Rect rectSDL = CSDL_Ext::toSDL(newDimensions); + auto newSurface = CSDL_Ext::newSurface(newDimensions.dimensions(), formatSourceSurface); + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); + SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr); + + if (SDL_HasColorKey(surf)) + { + uint32_t colorKey; + SDL_GetColorKey(surf, &colorKey); + SDL_SetColorKey(newSurface, SDL_TRUE, colorKey); + } + output = newSurface; + + virtualDimensions.x += left; + virtualDimensions.y += top; + } + else + { + output = surf; + output->refcount += 1; + } +} + +SDL_Surface * SDLImageOptimizer::acquireResultSurface() +{ + SDL_Surface * result = output; + output = nullptr; + return result; +} + +const Rect & SDLImageOptimizer::getResultDimensions() const +{ + return virtualDimensions; +} + +void SDLImageScaler::scaleSurface(Point targetDimensions, EScalingAlgorithm algorithm) +{ + if(!targetDimensions.x || !targetDimensions.y) + throw std::runtime_error("invalid scaling dimensions!"); + + Point inputSurfaceSize(intermediate->w, intermediate->h); + Point outputSurfaceSize = targetDimensions * inputSurfaceSize / virtualDimensionsInput.dimensions(); + Point outputMargins = targetDimensions * virtualDimensionsInput.topLeft() / virtualDimensionsInput.dimensions(); + + // TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%? + // Need to investigate which is optimal for performance and for visuals + ret = CSDL_Ext::newSurface(Point(outputSurfaceSize.x, outputSurfaceSize.y), intermediate); + + virtualDimensionsOutput = Rect(outputMargins, targetDimensions); // TODO: account for input virtual size + + const uint32_t * srcPixels = static_cast(intermediate->pixels); + uint32_t * dstPixels = static_cast(ret->pixels); + + if (algorithm == EScalingAlgorithm::NEAREST) + xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); + else + xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); +} + +void SDLImageScaler::scaleSurfaceIntegerFactor(int factor, EScalingAlgorithm algorithm) +{ + if(factor == 0) + throw std::runtime_error("invalid scaling factor!"); + + int newWidth = intermediate->w * factor; + int newHight = intermediate->h * factor; + + virtualDimensionsOutput = virtualDimensionsInput * factor; + + ret = CSDL_Ext::newSurface(Point(newWidth, newHight), intermediate); + + assert(intermediate->pitch == intermediate->w * 4); + assert(ret->pitch == ret->w * 4); + + const uint32_t * srcPixels = static_cast(intermediate->pixels); + uint32_t * dstPixels = static_cast(ret->pixels); + + switch (algorithm) + { + case EScalingAlgorithm::NEAREST: + xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); + break; + case EScalingAlgorithm::BILINEAR: + xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); + break; + case EScalingAlgorithm::XBRZ_ALPHA: + case EScalingAlgorithm::XBRZ_OPAQUE: + { + auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB; + + if(intermediate->h < 32) + { + // for tiny images tbb incurs too high overhead + xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}); + } + else + { + // xbrz recommends granulation of 16, but according to tests, for smaller images granulation of 4 is actually the best option + const int granulation = intermediate->h > 400 ? 16 : 4; + tbb::parallel_for(tbb::blocked_range(0, intermediate->h, granulation), [this, factor, srcPixels, dstPixels, format](const tbb::blocked_range & r) + { + xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end()); + }); + } + + break; + } + default: + throw std::runtime_error("invalid scaling algorithm!"); + } +} + +SDLImageScaler::SDLImageScaler(SDL_Surface * surf) + :SDLImageScaler(surf, Rect(0,0,surf->w, surf->h)) +{ +} + +SDLImageScaler::SDLImageScaler(SDL_Surface * surf, const Rect & virtualDimensions) +{ + SDLImageOptimizer optimizer(surf, virtualDimensions); + optimizer.optimizeSurface(screen); + intermediate = optimizer.acquireResultSurface(); + virtualDimensionsInput = optimizer.getResultDimensions(); + + if (intermediate == surf) + { + SDL_FreeSurface(intermediate); + intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0); + } +} + +SDLImageScaler::~SDLImageScaler() +{ + SDL_FreeSurface(intermediate); + SDL_FreeSurface(ret); +} + +SDL_Surface * SDLImageScaler::acquireResultSurface() +{ + SDL_Surface * result = ret; + ret = nullptr; + return result; +} + +const Rect & SDLImageScaler::getResultDimensions() const +{ + return virtualDimensionsOutput; +} diff --git a/client/renderSDL/SDLImageScaler.h b/client/renderSDL/SDLImageScaler.h new file mode 100644 index 000000000..b3f94534e --- /dev/null +++ b/client/renderSDL/SDLImageScaler.h @@ -0,0 +1,63 @@ +/* + * SDLImageScaler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "../render/IImage.h" +#include "../../lib/Rect.h" + +class SDLImageOptimizer : boost::noncopyable +{ + SDL_Surface * surf = nullptr; + SDL_Surface * output = nullptr; + Rect virtualDimensions = Rect(0,0,0,0); +public: + SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions); + + void optimizeSurface(SDL_Surface * formatSourceSurface); + + /// Aquires resulting surface and transfers surface ownership to the caller + /// May return nullptr if input image was empty + SDL_Surface * acquireResultSurface(); + + /// Returns adjusted virtual dimensions of resulting surface + const Rect & getResultDimensions() const; +}; + +/// Class that performs scaling of SDL surfaces +/// Object construction MUST be performed while holding UI lock +/// but task execution can be performed asynchronously if needed +class SDLImageScaler : boost::noncopyable +{ + SDL_Surface * intermediate = nullptr; + SDL_Surface * ret = nullptr; + Rect virtualDimensionsInput = Rect(0,0,0,0); + Rect virtualDimensionsOutput = Rect(0,0,0,0); + +public: + SDLImageScaler(SDL_Surface * surf); + SDLImageScaler(SDL_Surface * surf, const Rect & virtualDimensions); + ~SDLImageScaler(); + + /// Performs upscaling or downscaling to a requested dimensions + /// Aspect ratio is NOT maintained. + /// xbrz algorithm is not supported in this mode + void scaleSurface(Point dimensions, EScalingAlgorithm algorithm); + + /// Performs upscaling by specified integral factor, potentially using xbrz algorithm if requested + void scaleSurfaceIntegerFactor(int factor, EScalingAlgorithm algorithm); + + /// Aquires resulting surface and transfers surface ownership to the caller + /// May return nullptr if input image was empty + SDL_Surface * acquireResultSurface(); + + /// Returns adjusted virtual dimensions of resulting surface + const Rect & getResultDimensions() const; +}; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 14671c2db..38818999f 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -14,6 +14,7 @@ #include "../gui/CGuiHandler.h" #include "../render/Graphics.h" +#include "../render/IImage.h" #include "../render/IScreenHandler.h" #include "../render/Colors.h" #include "../CMT.h" @@ -630,86 +631,6 @@ void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect ) } } -// scaling via bilinear interpolation algorithm. -// NOTE: best results are for scaling in range 50%...200%. -// And upscaling looks awful right now - should be fixed somehow -SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height) -{ - if(!surf || !width || !height) - return nullptr; - - // TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%? - // Need to investigate which is optimal for performance and for visuals - - SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0); - SDL_Surface * ret = newSurface(Point(width, height), intermediate); - -#if SDL_VERSION_ATLEAST(2,0,16) - SDL_SoftStretchLinear(intermediate, nullptr, ret, nullptr); -#else - SDL_SoftStretch(intermediate, nullptr, ret, nullptr); -#endif - SDL_FreeSurface(intermediate); - - return ret; -} - -SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm algorithm) -{ - if(surf == nullptr || factor == 0) - return nullptr; - - int newWidth = surf->w * factor; - int newHight = surf->h * factor; - - SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0); - SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate); - - assert(intermediate->pitch == intermediate->w * 4); - assert(ret->pitch == ret->w * 4); - - const uint32_t * srcPixels = static_cast(intermediate->pixels); - uint32_t * dstPixels = static_cast(ret->pixels); - - switch (algorithm) - { - case EScalingAlgorithm::NEAREST: - xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); - break; - case EScalingAlgorithm::BILINEAR: - xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); - break; - case EScalingAlgorithm::XBRZ_ALPHA: - case EScalingAlgorithm::XBRZ_OPAQUE: - { - auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB; - - if(intermediate->h < 32) - { - // for tiny images tbb incurs too high overhead - xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}); - } - else - { - // xbrz recommends granulation of 16, but according to tests, for smaller images granulation of 4 is actually the best option - const int granulation = intermediate->h > 400 ? 16 : 4; - tbb::parallel_for(tbb::blocked_range(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate, format](const tbb::blocked_range & r) - { - xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end()); - }); - } - - break; - } - default: - throw std::runtime_error("invalid scaling algorithm!"); - } - - SDL_FreeSurface(intermediate); - - return ret; -} - void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint) { SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput); @@ -799,10 +720,5 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } -int CSDL_Ext::CClipRectGuard::getScalingFactor() const -{ - return GH.screenHandler().getScalingFactor(); -} - template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index acdb0dc5a..6d01d2520 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -27,14 +27,6 @@ class Point; VCMI_LIB_NAMESPACE_END -enum class EScalingAlgorithm : int8_t -{ - NEAREST, - BILINEAR, - XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image - XBRZ_ALPHA // xbrz, image edges are considered to be transparent -}; - namespace CSDL_Ext { @@ -98,10 +90,6 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, template SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value - // bilinear filtering. Always returns rgba surface - SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); - SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm scaler); - template void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); void convertToGrayscale(SDL_Surface * surf, const Rect & rect); diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp new file mode 100644 index 000000000..93ec8d803 --- /dev/null +++ b/client/renderSDL/ScalableImage.cpp @@ -0,0 +1,532 @@ +/* + * ScalableImage.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ScalableImage.h" + +#include "SDLImage.h" +#include "SDL_Extensions.h" + +#include "../gui/CGuiHandler.h" + +#include "../render/ColorFilter.h" +#include "../render/Colors.h" +#include "../render/Graphics.h" +#include "../render/IRenderHandler.h" +#include "../render/IScreenHandler.h" +#include "../render/CanvasImage.h" + +#include "../../lib/constants/EntityIdentifiers.h" + +#include + +//First 8 colors in def palette used for transparency +static constexpr std::array sourcePalette = {{ + {0, 255, 255, SDL_ALPHA_OPAQUE}, + {255, 150, 255, SDL_ALPHA_OPAQUE}, + {255, 100, 255, SDL_ALPHA_OPAQUE}, + {255, 50, 255, SDL_ALPHA_OPAQUE}, + {255, 0, 255, SDL_ALPHA_OPAQUE}, + {255, 255, 0, SDL_ALPHA_OPAQUE}, + {180, 0, 255, SDL_ALPHA_OPAQUE}, + {0, 255, 0, SDL_ALPHA_OPAQUE} +}}; + +static constexpr std::array targetPalette = {{ + {0, 0, 0, 0 }, // 0 - transparency ( used in most images ) + {0, 0, 0, 64 }, // 1 - shadow border ( used in battle, adventure map def's ) + {0, 0, 0, 64 }, // 2 - shadow border ( used in fog-of-war def's ) + {0, 0, 0, 128}, // 3 - shadow body ( used in fog-of-war def's ) + {0, 0, 0, 128}, // 4 - shadow body ( used in battle, adventure map def's ) + {0, 0, 0, 0 }, // 5 - selection / owner flag ( used in battle, adventure map def's ) + {0, 0, 0, 128}, // 6 - shadow body below selection ( used in battle def's ) + {0, 0, 0, 64 } // 7 - shadow border below selection ( used in battle def's ) +}}; + +static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) +{ + return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; +} + +static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) +{ + return ColorRGBA( + mixChannels(over.r, base.r, over.a, base.a), + mixChannels(over.g, base.g, over.a, base.a), + mixChannels(over.b, base.b, over.a, base.a), + static_cast(over.a + base.a * (255 - over.a) / 256) + ); +} +static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs) +{ + // it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow + // exact logic is not clear and requires extensive testing with image editing + // potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component + static const int threshold = 8; + + int diffR = static_cast(lhs.r) - rhs.r; + int diffG = static_cast(lhs.g) - rhs.g; + int diffB = static_cast(lhs.b) - rhs.b; + int diffA = static_cast(lhs.a) - rhs.a; + + return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; +} + +ScalableImageParameters::ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode) +{ + if (originalPalette) + { + palette = SDL_AllocPalette(originalPalette->ncolors); + SDL_SetPaletteColors(palette, originalPalette->colors, 0, originalPalette->ncolors); + preparePalette(originalPalette, blitMode); + } +} + +ScalableImageParameters::~ScalableImageParameters() +{ + SDL_FreePalette(palette); +} + +void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode) +{ + switch(blitMode) + { + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_OVERLAY: + adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0); + break; + } + + switch(blitMode) + { + case EImageBlitMode::SIMPLE: + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + setShadowTransparency(originalPalette, 1.0); + break; + case EImageBlitMode::ONLY_BODY: + case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: + case EImageBlitMode::ONLY_OVERLAY: + setShadowTransparency(originalPalette, 0.0); + break; + } + + switch(blitMode) + { + case EImageBlitMode::ONLY_OVERLAY: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + setOverlayColor(originalPalette, Colors::WHITE_TRUE); + break; + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_BODY: + setOverlayColor(originalPalette, Colors::TRANSPARENCY); + break; + } +} + +void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color) +{ + palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color)); + + for (int i : {6,7}) + { + if (colorsSimilar(originalPalette->colors[i], sourcePalette[i])) + palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color)); + } +} + +void ScalableImageParameters::shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{ + std::vector shifterColors(colorsToMove); + + for(uint32_t i=0; icolors[firstColorID + i]; + + SDL_SetPaletteColors(palette, shifterColors.data(), firstColorID, colorsToMove); +} + +void ScalableImageParameters::setShadowTransparency(const SDL_Palette * originalPalette, float factor) +{ + ColorRGBA shadow50(0, 0, 0, 128 * factor); + ColorRGBA shadow25(0, 0, 0, 64 * factor); + + std::array colorsSDL = { + originalPalette->colors[0], + originalPalette->colors[1], + originalPalette->colors[2], + originalPalette->colors[3], + originalPalette->colors[4] + }; + + // seems to be used unconditionally + colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY); + colorsSDL[1] = CSDL_Ext::toSDL(shadow25); + colorsSDL[4] = CSDL_Ext::toSDL(shadow50); + + // seems to be used only if color matches + if (colorsSimilar(originalPalette->colors[2], sourcePalette[2])) + colorsSDL[2] = CSDL_Ext::toSDL(shadow25); + + if (colorsSimilar(originalPalette->colors[3], sourcePalette[3])) + colorsSDL[3] = CSDL_Ext::toSDL(shadow50); + + SDL_SetPaletteColors(palette, colorsSDL.data(), 0, colorsSDL.size()); +} + +void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask) +{ + // If shadow is enabled, following colors must be skipped unconditionally + if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY) + colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4); + + // Note: here we skip first colors in the palette that are predefined in H3 images + for(int i = 0; i < palette->ncolors; i++) + { + if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i])) + continue; + + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) + continue; + + palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); + } +} + +ScalableImageShared::ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr & baseImage) + :locator(locator) +{ + scaled[1].body[0] = baseImage; + assert(scaled[1].body[0] != nullptr); + + loadScaledImages(GH.screenHandler().getScalingFactor(), PlayerColor::CANNOT_DETERMINE); +} + +Point ScalableImageShared::dimensions() const +{ + return scaled[1].body[0]->dimensions(); +} + +void ScalableImageShared::exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const +{ + scaled[1].body[0]->exportBitmap(path, parameters.palette); +} + +bool ScalableImageShared::isTransparent(const Point & coords) const +{ + return scaled[1].body[0]->isTransparent(coords); +} + +Rect ScalableImageShared::contentRect() const +{ + return scaled[1].body[0]->contentRect(); +} + +void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor) +{ + const auto & getFlippedImage = [&](FlippedImages & images){ + int index = 0; + if (parameters.flipVertical) + { + if (!images[index|1]) + images[index|1] = images[index]->verticalFlip(); + + index |= 1; + } + + if (parameters.flipHorizontal) + { + if (!images[index|2]) + images[index|2] = images[index]->horizontalFlip(); + + index |= 2; + } + + return images[index]; + }; + + const auto & flipAndDraw = [&](FlippedImages & images, const ColorRGBA & colorMultiplier, uint8_t alphaValue){ + + getFlippedImage(images)->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer); + }; + + bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading(); + bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading(); + bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading(); + bool playerLoading = parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->isLoading(); + + if (shadowLoading || bodyLoading || overlayLoading || playerLoading) + { + getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer); + return; + } + + if (scaled.at(scalingFactor).shadow.at(0)) + flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue); + + if (parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())) + { + scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->draw(where, parameters.palette, dest, src, Colors::WHITE_TRUE, parameters.alphaValue, locator.layer); + } + else + { + if (scaled.at(scalingFactor).body.at(0)) + flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue); + } + + if (scaled.at(scalingFactor).overlay.at(0)) + flipAndDraw(scaled.at(scalingFactor).overlay, parameters.ovelayColorMultiplier, static_cast(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255); +} + +const SDL_Palette * ScalableImageShared::getPalette() const +{ + return scaled[1].body[0]->getPalette(); +} + +std::shared_ptr ScalableImageShared::createImageReference() +{ + return std::make_shared(shared_from_this(), locator.layer); +} + +ScalableImageInstance::ScalableImageInstance(const std::shared_ptr & image, EImageBlitMode blitMode) + :image(image) + ,parameters(image->getPalette(), blitMode) + ,blitMode(blitMode) +{ + assert(image); +} + +void ScalableImageInstance::scaleTo(const Point & size, EScalingAlgorithm algorithm) +{ + scaledImage = nullptr; + + auto newScaledImage = GH.renderHandler().createImage(dimensions(), CanvasScalingPolicy::AUTO); + + newScaledImage->getCanvas().draw(*this, Point(0, 0)); + newScaledImage->scaleTo(size, algorithm); + scaledImage = newScaledImage; +} + +void ScalableImageInstance::exportBitmap(const boost::filesystem::path & path) const +{ + image->exportBitmap(path, parameters); +} + +bool ScalableImageInstance::isTransparent(const Point & coords) const +{ + return image->isTransparent(coords); +} + +Rect ScalableImageInstance::contentRect() const +{ + return image->contentRect(); +} + +Point ScalableImageInstance::dimensions() const +{ + if (scaledImage) + return scaledImage->dimensions() / GH.screenHandler().getScalingFactor(); + return image->dimensions(); +} + +void ScalableImageInstance::setAlpha(uint8_t value) +{ + parameters.alphaValue = value; +} + +void ScalableImageInstance::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const +{ + if (scaledImage) + scaledImage->draw(where, pos, src, scalingFactor); + else + image->draw(where, pos, src, parameters, scalingFactor); +} + +void ScalableImageInstance::setOverlayColor(const ColorRGBA & color) +{ + parameters.ovelayColorMultiplier = color; + + if (parameters.palette) + parameters.setOverlayColor(image->getPalette(), color); +} + +void ScalableImageInstance::playerColored(const PlayerColor & player) +{ + parameters.player = player; + + if (parameters.palette) + parameters.playerColored(player); + + image->preparePlayerColoredImage(player); +} + +void ScalableImageParameters::playerColored(PlayerColor player) +{ + graphics->setPlayerPalette(palette, player); +} + +void ScalableImageInstance::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{ + if (parameters.palette) + parameters.shiftPalette(image->getPalette(),firstColorID, colorsToMove, distanceToMove); +} + +void ScalableImageInstance::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) +{ + if (parameters.palette) + parameters.adjustPalette(image->getPalette(), blitMode, shifter, colorsToSkipMask); +} + +void ScalableImageInstance::horizontalFlip() +{ + parameters.flipHorizontal = !parameters.flipHorizontal; +} + +void ScalableImageInstance::verticalFlip() +{ + parameters.flipVertical = !parameters.flipVertical; +} + +std::shared_ptr ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const +{ + ImageLocator loadingLocator; + + loadingLocator.image = locator.image; + loadingLocator.defFile = locator.defFile; + loadingLocator.defFrame = locator.defFrame; + loadingLocator.defGroup = locator.defGroup; + loadingLocator.layer = mode; + loadingLocator.scalingFactor = scalingFactor; + loadingLocator.playerColored = color; + + // best case - requested image is already available in filesystem + auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator); + if (loadedImage) + return loadedImage; + + if (scalingFactor == 1) + { + // optional images for 1x resolution - only try load them, don't attempt to generate + // this block should never be called for 'body' layer - that image is loaded unconditionally before construction + assert(mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE); + return nullptr; + } + + // alternatively, find largest pre-scaled image, load it and rescale to desired scaling + for (int8_t scaling = 4; scaling > 0; --scaling) + { + loadingLocator.scalingFactor = scaling; + auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator); + if (loadedImage) + { + if (scaling == 1) + { + if (mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE) + { + ScalableImageParameters parameters(getPalette(), mode); + return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode); + } + } + else + { + Point targetSize = scaled[1].body[0]->dimensions() * scalingFactor; + return loadedImage->scaleTo(targetSize, nullptr); + } + } + } + + ScalableImageParameters parameters(getPalette(), mode); + // if all else fails - use base (presumably, indexed) image and convert it to desired form + if (color != PlayerColor::CANNOT_DETERMINE) + parameters.playerColored(color); + + if (upscalingSource) + return upscalingSource->scaleInteger(scalingFactor, parameters.palette, mode); + else + return scaled[1].body[0]->scaleInteger(scalingFactor, parameters.palette, mode); +} + +void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor color) +{ + if (scaled[scalingFactor].body[0] == nullptr && scalingFactor != 1) + { + switch(locator.layer) + { + case EImageBlitMode::OPAQUE: + case EImageBlitMode::COLORKEY: + case EImageBlitMode::SIMPLE: + scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]); + break; + + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + case EImageBlitMode::ONLY_BODY: + scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]); + break; + + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: + scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]); + break; + } + } + + if (color != PlayerColor::CANNOT_DETERMINE && scaled[scalingFactor].playerColored[1+color.getNum()] == nullptr) + { + switch(locator.layer) + { + case EImageBlitMode::OPAQUE: + case EImageBlitMode::COLORKEY: + case EImageBlitMode::SIMPLE: + scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]); + break; + + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + case EImageBlitMode::ONLY_BODY: + scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]); + break; + + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: + scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]); + break; + } + } + + if (scaled[scalingFactor].shadow[0] == nullptr) + { + switch(locator.layer) + { + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]); + break; + default: + break; + } + } + + if (scaled[scalingFactor].overlay[0] == nullptr) + { + switch(locator.layer) + { + case EImageBlitMode::ONLY_OVERLAY: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]); + break; + default: + break; + } + } +} + +void ScalableImageShared::preparePlayerColoredImage(PlayerColor color) +{ + loadScaledImages(GH.screenHandler().getScalingFactor(), color); +} diff --git a/client/renderSDL/ScalableImage.h b/client/renderSDL/ScalableImage.h new file mode 100644 index 000000000..d662f9a01 --- /dev/null +++ b/client/renderSDL/ScalableImage.h @@ -0,0 +1,125 @@ +/* + * ScalableImage.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../render/IImage.h" +#include "../render/ImageLocator.h" +#include "../render/Colors.h" + +#include "../../lib/Color.h" + +struct SDL_Palette; + +class ScalableImageInstance; +class CanvasImage; + +struct ScalableImageParameters : boost::noncopyable +{ + SDL_Palette * palette = nullptr; + + ColorRGBA colorMultiplier = Colors::WHITE_TRUE; + ColorRGBA ovelayColorMultiplier = Colors::WHITE_TRUE; + + PlayerColor player = PlayerColor::CANNOT_DETERMINE; + uint8_t alphaValue = 255; + + bool flipVertical = false; + bool flipHorizontal = false; + + ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode); + ~ScalableImageParameters(); + + void setShadowTransparency(const SDL_Palette * originalPalette, float factor); + void shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove); + void playerColored(PlayerColor player); + void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color); + void preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode); + void adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask); +}; + +class ScalableImageShared final : public std::enable_shared_from_this, boost::noncopyable +{ + static constexpr int scalingSize = 5; // 0-4 range. TODO: switch to 1-4 since there is no '0' scaling + static constexpr int maxFlips = 4; + + using ImageType = std::shared_ptr; + using FlippedImages = std::array; + using PlayerColoredImages = std::array; // all valid colors+neutral + + struct ScaledImage + { + /// Upscaled shadow of our image, may be null + FlippedImages shadow; + + /// Upscaled main part of our image, may be null + FlippedImages body; + + /// Upscaled overlay (player color, selection highlight) of our image, may be null + FlippedImages overlay; + + /// player-colored images of this particular scale, mostly for UI. These are never flipped in h3 + PlayerColoredImages playerColored; + }; + + /// 1x-4x images. body for 1x scaling is guaranteed to be loaded + std::array scaled; + + /// Locator of this image, for loading additional (e.g. upscaled) images + const SharedImageLocator locator; + + std::shared_ptr loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const; + + void loadScaledImages(int8_t scalingFactor, PlayerColor color); + +public: + ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr & baseImage); + + Point dimensions() const; + void exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const; + bool isTransparent(const Point & coords) const; + Rect contentRect() const; + void draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor); + + const SDL_Palette * getPalette() const; + + std::shared_ptr createImageReference(); + + void preparePlayerColoredImage(PlayerColor color); +}; + +class ScalableImageInstance final : public IImage +{ + friend class ScalableImageShared; + + std::shared_ptr image; + std::shared_ptr scaledImage; + + ScalableImageParameters parameters; + EImageBlitMode blitMode; + +public: + ScalableImageInstance(const std::shared_ptr & image, EImageBlitMode blitMode); + + void scaleTo(const Point & size, EScalingAlgorithm algorithm) override; + void exportBitmap(const boost::filesystem::path & path) const override; + bool isTransparent(const Point & coords) const override; + Rect contentRect() const override; + Point dimensions() const override; + void setAlpha(uint8_t value) override; + void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override; + void setOverlayColor(const ColorRGBA & color) override; + void playerColored(const PlayerColor & player) override; + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; + + void horizontalFlip(); + void verticalFlip(); +}; + diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 935ed8f4b..93864b9d2 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -11,13 +11,14 @@ #include "StdInc.h" #include "ScreenHandler.h" +#include "../eventsSDL/NotificationHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../renderSDL/SDL_Extensions.h" +#include "CMT.h" + #include "../../lib/CConfigHandler.h" #include "../../lib/constants/StringConstants.h" -#include "../gui/CGuiHandler.h" -#include "../eventsSDL/NotificationHandler.h" -#include "../gui/WindowHandler.h" -#include "CMT.h" -#include "SDL_Extensions.h" #ifdef VCMI_ANDROID #include "../lib/CAndroidVMHelper.h" diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 4cbb6be1a..6ebabdc0f 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -13,7 +13,6 @@ #include "MiscWidgets.h" #include "../gui/CGuiHandler.h" -#include "../renderSDL/SDL_Extensions.h" #include "../render/AssetGenerator.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" @@ -53,8 +52,8 @@ CPicture::CPicture( const ImagePath & bmpname ) : CPicture(bmpname, Point(0,0)) {} -CPicture::CPicture( const ImagePath & bmpname, const Point & position ) - : bg(GH.renderHandler().loadImage(bmpname, EImageBlitMode::COLORKEY)) +CPicture::CPicture( const ImagePath & bmpname, const Point & position, EImageBlitMode mode ) + : bg(GH.renderHandler().loadImage(bmpname, mode)) , needRefresh(false) { pos.x += position.x; @@ -74,6 +73,10 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position ) addUsedEvents(SHOW_POPUP); } +CPicture::CPicture( const ImagePath & bmpname, const Point & position ) + :CPicture(bmpname, position, EImageBlitMode::COLORKEY) +{} + CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y) : CPicture(bmpname, Point(x,y)) { @@ -118,7 +121,7 @@ void CPicture::setAlpha(uint8_t value) void CPicture::scaleTo(Point size) { - bg->scaleTo(size); + bg->scaleTo(size, EScalingAlgorithm::BILINEAR); pos.w = bg->width(); pos.h = bg->height(); @@ -160,7 +163,7 @@ CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position, Rect void CFilledTexture::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CanvasClipRectGuard guard(to, pos); for (int y=pos.top(); y < pos.bottom(); y+= imageArea.h) { @@ -266,7 +269,7 @@ void CAnimImage::showAll(Canvas & to) if(auto img = anim->getImage(targetFrame, group)) { if(isScaled()) - img->scaleTo(scaledSize); + img->scaleTo(scaledSize, EScalingAlgorithm::BILINEAR); to.draw(img, pos.topLeft()); } diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 80b100fc4..123bf3dac 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -21,6 +21,7 @@ class CAnimImage; class CLabel; class CAnimation; class IImage; +enum class EImageBlitMode : uint8_t; // Image class class CPicture : public CIntObject @@ -49,6 +50,7 @@ public: /// Loads image from specified file name CPicture(const ImagePath & bmpname); + CPicture(const ImagePath & bmpname, const Point & position, EImageBlitMode mode); CPicture(const ImagePath & bmpname, const Point & position); CPicture(const ImagePath & bmpname, int x, int y); diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 16745214f..b976a8e10 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -18,7 +18,6 @@ #include "../windows/CMessage.h" #include "../windows/InfoWindows.h" #include "../adventureMap/CInGameConsole.h" -#include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" #include "../render/Graphics.h" #include "../render/IFont.h" @@ -298,7 +297,7 @@ void CMultiLineLabel::showAll(Canvas & to) Point lineStart = getTextLocation().topLeft() - visibleSize + Point(0, beginLine * fontPtr->getLineHeight()); Point lineSize = Point(getTextLocation().w, fontPtr->getLineHeight()); - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), getTextLocation()); // to properly trim text that is too big to fit + CanvasClipRectGuard guard(to, getTextLocation()); // to properly trim text that is too big to fit for(int i = beginLine; i < std::min(totalLines, endLine); i++) { diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index f8b69fa2c..e35a3ebd0 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -86,7 +86,7 @@ void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) void VideoWidgetBase::show(Canvas & to) { if(videoInstance) - videoInstance->show(pos.topLeft(), to); + to.draw(*videoInstance, pos.topLeft()); if(subTitle) subTitle->showAll(to); } @@ -162,7 +162,7 @@ void VideoWidgetBase::deactivate() void VideoWidgetBase::showAll(Canvas & to) { if(videoInstance) - videoInstance->show(pos.topLeft(), to); + to.draw(*videoInstance, pos.topLeft()); if(subTitle) subTitle->showAll(to); } diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 63fba04ea..7549e793b 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -570,9 +570,8 @@ CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): { OBJECT_CONSTRUCTION; - background = std::make_shared(town->getTown()->clientInfo.townBackground); + background = std::make_shared(town->getTown()->clientInfo.townBackground, Point(0,0), EImageBlitMode::OPAQUE); background->needRefresh = true; - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); pos.w = background->pos.w; pos.h = background->pos.h; @@ -974,7 +973,7 @@ void CCastleBuildings::enterCastleGate(BuildingID building) if(settings["general"]["enableUiEnhancements"].Bool()) { auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(35, 23)); + image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); images.push_back(image); } } diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index c0845133d..0ab1bc2ad 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -20,7 +20,7 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" -#include "../render/Canvas.h" +#include "../render/CanvasImage.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../render/Graphics.h" @@ -58,9 +58,10 @@ CMapOverview::CMapOverview(const std::string & mapName, const std::string & file fitToScreen(10); } -Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const +std::shared_ptr CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const { - Canvas canvas = Canvas(Point(map->width, map->height), CanvasScalingPolicy::IGNORE); + auto canvasImage = GH.renderHandler().createImage(Point(map->width, map->height), CanvasScalingPolicy::IGNORE); + auto canvas = canvasImage->getCanvas(); for (int y = 0; y < map->height; ++y) for (int x = 0; x < map->width; ++x) @@ -91,12 +92,12 @@ Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, in canvas.drawPoint(Point(x, y), color); } - return canvas; + return canvasImage; } -std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) const +std::vector> CMapOverviewWidget::createMinimaps(ResourcePath resource) const { - auto ret = std::vector(); + std::vector> ret; CMapService mapService; std::unique_ptr map; @@ -113,9 +114,9 @@ std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) co return createMinimaps(map); } -std::vector CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const +std::vector> CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const { - auto ret = std::vector(); + std::vector> ret; for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) ret.push_back(createMinimapForLayer(map, i)); @@ -133,17 +134,15 @@ std::shared_ptr CMapOverviewWidget::buildDrawMinimap(const JsonNode & if(id >= minimaps.size()) return nullptr; - Rect minimapRect = minimaps[id].getRenderArea(); - double maxSideLengthSrc = std::max(minimapRect.w, minimapRect.h); + Point minimapRect = minimaps[id]->dimensions(); + double maxSideLengthSrc = std::max(minimapRect.x, minimapRect.y); double maxSideLengthDst = std::max(rect.w, rect.h); double resize = maxSideLengthSrc / maxSideLengthDst; - Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize); + Point newMinimapSize = Point(minimapRect.x / resize, minimapRect.y / resize); - Canvas canvasScaled = Canvas(Point(rect.w, rect.h), CanvasScalingPolicy::AUTO); - canvasScaled.drawScaled(minimaps[id], Point((rect.w - newMinimapSize.x) / 2, (rect.h - newMinimapSize.y) / 2), newMinimapSize); - std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); + minimaps[id]->scaleTo(newMinimapSize, EScalingAlgorithm::NEAREST); // for sharp-looking minimap - return std::make_shared(img, Point(rect.x, rect.y)); + return std::make_shared(minimaps[id], Point(rect.x, rect.y)); } CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index f1b7caf25..870a6fa04 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -22,7 +22,7 @@ class CPicture; class CFilledTexture; class CTextBox; class IImage; -class Canvas; +class CanvasImage; class TransparentFilledRectangle; enum class ESelectionScreen : ui8; @@ -33,11 +33,11 @@ class CMapOverviewWidget : public InterfaceObjectConfigurable CMapOverview& p; bool drawPlayerElements; - std::vector minimaps; + std::vector> minimaps; - Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; - std::vector createMinimaps(ResourcePath resource) const; - std::vector createMinimaps(std::unique_ptr & map) const; + std::shared_ptr createMinimapForLayer(std::unique_ptr & map, int layer) const; + std::vector> createMinimaps(ResourcePath resource) const; + std::vector> createMinimaps(std::unique_ptr & map) const; std::shared_ptr buildDrawMinimap(const JsonNode & config) const; public: diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index dbc39d439..a903e2dd5 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -21,7 +21,6 @@ #include "../adventureMap/AdventureMapInterface.h" #include "../adventureMap/CMinimap.h" #include "../render/Canvas.h" -#include "../renderSDL/SDL_Extensions.h" #include "../../CCallback.h" #include "../../lib/CArtHandler.h" @@ -61,7 +60,7 @@ void CQuestIcon::clickPressed(const Point & cursorPosition) void CQuestIcon::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos); + CanvasClipRectGuard guard(to, parent->pos); CAnimImage::showAll(to); } diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index d60bf3a9d..a9f13b719 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -23,6 +23,7 @@ #include "../render/IScreenHandler.h" #include "../render/IRenderHandler.h" #include "../render/Canvas.h" +#include "../render/CanvasImage.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" @@ -87,8 +88,7 @@ std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, b if(imageName.empty()) return nullptr; - auto image = std::make_shared(imageName); - image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + auto image = std::make_shared(imageName, Point(0,0), EImageBlitMode::OPAQUE); if(playerColored) image->setPlayerColor(LOCPLINT->playerID); return image; @@ -116,8 +116,7 @@ void CWindowObject::updateShadow() void CWindowObject::setShadow(bool on) { //size of shadow - int sizeOriginal = 8; - int size = sizeOriginal * GH.screenHandler().getScalingFactor(); + int size = 8; if(on == !shadowParts.empty()) return; @@ -130,61 +129,12 @@ void CWindowObject::setShadow(bool on) if(on) { - - //helper to set last row - auto blitAlphaRow = [](SDL_Surface *surf, size_t row) - { - uint8_t * ptr = (uint8_t*)surf->pixels + surf->pitch * (row); - - for (size_t i=0; i< surf->w; i++) - { - Channels::px<4>::a.set(ptr, 128); - ptr+=4; - } - }; - - // helper to set last column - auto blitAlphaCol = [](SDL_Surface *surf, size_t col) - { - uint8_t * ptr = (uint8_t*)surf->pixels + 4 * (col); - - for (size_t i=0; i< surf->h; i++) - { - Channels::px<4>::a.set(ptr, 128); - ptr+= surf->pitch; - } - }; - - static SDL_Surface * shadowCornerTempl = nullptr; - static SDL_Surface * shadowBottomTempl = nullptr; - static SDL_Surface * shadowRightTempl = nullptr; - - //one-time initialization - if(!shadowCornerTempl) - { - //create "template" surfaces - shadowCornerTempl = CSDL_Ext::createSurfaceWithBpp<4>(size, size); - shadowBottomTempl = CSDL_Ext::createSurfaceWithBpp<4>(1, size); - shadowRightTempl = CSDL_Ext::createSurfaceWithBpp<4>(size, 1); - - //fill with shadow body color - CSDL_Ext::fillSurface(shadowCornerTempl, { 0, 0, 0, 192 } ); - CSDL_Ext::fillSurface(shadowBottomTempl, { 0, 0, 0, 192 } ); - CSDL_Ext::fillSurface(shadowRightTempl, { 0, 0, 0, 192 } ); - - //fill last row and column with more transparent color - blitAlphaCol(shadowRightTempl , size-1); - blitAlphaCol(shadowCornerTempl, size-1); - blitAlphaRow(shadowBottomTempl, size-1); - blitAlphaRow(shadowCornerTempl, size-1); - } - //FIXME: do something with this points Point shadowStart; if (options & BORDERED) - shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14); + shadowStart = Point(size - 14, size - 14); else - shadowStart = Point(sizeOriginal, sizeOriginal); + shadowStart = Point(size, size); Point shadowPos; if (options & BORDERED) @@ -198,26 +148,36 @@ void CWindowObject::setShadow(bool on) else fullsize = Point(pos.w, pos.h); - //create base 8x8 piece of shadow - SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl); - SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size); - SDL_Surface * shadowRight = CSDL_Ext::scaleSurface(shadowRightTempl, size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor()); + Point sizeCorner(size, size); + Point sizeRight(fullsize.x - size, size); + Point sizeBottom(size, fullsize.y - size); - blitAlphaCol(shadowBottom, 0); - blitAlphaRow(shadowRight, 0); + //create base 8x8 piece of shadow + auto imageCorner = GH.renderHandler().createImage(sizeCorner, CanvasScalingPolicy::AUTO); + auto imageRight = GH.renderHandler().createImage(sizeRight, CanvasScalingPolicy::AUTO); + auto imageBottom = GH.renderHandler().createImage(sizeBottom, CanvasScalingPolicy::AUTO); + + Canvas canvasCorner = imageCorner->getCanvas(); + Canvas canvasRight = imageRight->getCanvas(); + Canvas canvasBottom = imageBottom->getCanvas(); + + canvasCorner.drawColor(Rect(Point(0,0), sizeCorner), { 0, 0, 0, 128 }); + canvasRight.drawColor(Rect(Point(0,0), sizeRight), { 0, 0, 0, 128 }); + canvasBottom.drawColor(Rect(Point(0,0), sizeBottom), { 0, 0, 0, 128 }); + + canvasCorner.drawColor(Rect(Point(0,0), sizeCorner - Point(1,1)), { 0, 0, 0, 192 }); + canvasRight.drawColor(Rect(Point(0,0), sizeRight - Point(0,1)), { 0, 0, 0, 192 }); + canvasBottom.drawColor(Rect(Point(0,0), sizeBottom - Point(1,0)), { 0, 0, 0, 192 }); //generate "shadow" object with these 3 pieces in it { OBJECT_CONSTRUCTION; - shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowCorner), Point(shadowPos.x, shadowPos.y))); - shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowRight ), Point(shadowPos.x, shadowStart.y))); - shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowBottom), Point(shadowStart.x, shadowPos.y))); + shadowParts.push_back(std::make_shared( imageCorner, Point(shadowPos.x, shadowPos.y))); + shadowParts.push_back(std::make_shared( imageRight, Point(shadowStart.x, shadowPos.y))); + shadowParts.push_back(std::make_shared( imageBottom, Point(shadowPos.x, shadowStart.y))); } - SDL_FreeSurface(shadowCorner); - SDL_FreeSurface(shadowBottom); - SDL_FreeSurface(shadowRight); } } diff --git a/client/windows/CWindowWithArtifacts.cpp b/client/windows/CWindowWithArtifacts.cpp index 67afb0c1c..bd8193ddf 100644 --- a/client/windows/CWindowWithArtifacts.cpp +++ b/client/windows/CWindowWithArtifacts.cpp @@ -236,7 +236,7 @@ void CWindowWithArtifacts::setCursorAnimation(const CArtifactInstance & artInst) { assert(artInst.getScrollSpellID().num >= 0); auto image = GH.renderHandler().loadImage(AnimationPath::builtin("spellscr"), artInst.getScrollSpellID().num, 0, EImageBlitMode::COLORKEY); - image->scaleTo(Point(44,34)); + image->scaleTo(Point(44,34), EScalingAlgorithm::BILINEAR); CCS->curh->dragAndDropCursor(image); } diff --git a/docs/modders/HD_Graphics.md b/docs/modders/HD_Graphics.md index c5a8d3d2a..0b3f697d9 100644 --- a/docs/modders/HD_Graphics.md +++ b/docs/modders/HD_Graphics.md @@ -20,7 +20,7 @@ For upscaled images you have to use following folders (next to `sprites`, `data` The sprites should have the same name and folder structure as in `sprites`, `data` and `video` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images. -### Shadows / Overlays +### Shadows / Overlays / Player-colored images It's also possible (but not necessary) to add high-definition shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`. In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell. @@ -36,3 +36,11 @@ Currently needed for: - Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player - Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover + +For images that are used for player-colored interface, it is possible to provide custom images for each player. For example `HeroScr4-red.png` will be used for hero window of red player. + +- Currently needed for all UI elements that are player-colored in HoMM3. +- Can NOT be used for player-owned adventure objects. Use `-overlay` images for such objects. +- Possible suffixes are `red`, `blue`, `tan`, `green`, `orange`, `purple`, `teal`, `pink`, `neutral` (used only for turn order queue in combat) + +It is possible to use such additional images for both upscaled (xbrz) graphics, as well as for original / 1x images. When using this feature for original / 1x image, make sure that your base image (without suffix) is rgb/rgba image, and not indexed / with palette diff --git a/lib/Point.h b/lib/Point.h index fd3bde33c..afc87c702 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -54,6 +54,11 @@ public: return Point(x*mul, y*mul); } + constexpr Point operator/(const Point &b) const + { + return Point(x/b.x,y/b.y); + } + constexpr Point operator*(const Point &b) const { return Point(x*b.x,y*b.y);