diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp index af6343c19..a4c1a20e1 100644 --- a/client/render/ImageLocator.cpp +++ b/client/render/ImageLocator.cpp @@ -25,6 +25,12 @@ SharedImageLocator::SharedImageLocator(const JsonNode & config, EImageBlitMode m if(!config["defFile"].isNull()) defFile = AnimationPath::fromJson(config["defFile"]); + + if(!config["generateShadow"].isNull()) + generateShadow = static_cast(config["generateShadow"].Integer()); + + if(!config["generateOverlay"].isNull()) + generateOverlay = static_cast(config["generateOverlay"].Integer()); } SharedImageLocator::SharedImageLocator(const ImagePath & path, EImageBlitMode mode) @@ -60,6 +66,10 @@ bool SharedImageLocator::operator < (const SharedImageLocator & other) const return defFrame < other.defFrame; if(layer != other.layer) return layer < other.layer; + if(generateShadow != other.generateShadow) + return generateShadow < other.generateShadow; + if(generateOverlay != other.generateOverlay) + return generateOverlay < other.generateOverlay; return false; } diff --git a/client/render/ImageLocator.h b/client/render/ImageLocator.h index 59754b2b6..f8d26d0eb 100644 --- a/client/render/ImageLocator.h +++ b/client/render/ImageLocator.h @@ -16,12 +16,28 @@ struct SharedImageLocator { + enum class ShadowMode + { + SHADOW_NONE, + SHADOW_NORMAL, + SHADOW_SHEAR + }; + enum class OverlayMode + { + OVERLAY_NONE, + OVERLAY_OUTLINE, + OVERLAY_FLAG + }; + std::optional image; std::optional defFile; int defFrame = -1; int defGroup = -1; EImageBlitMode layer = EImageBlitMode::OPAQUE; + std::optional generateShadow; + std::optional generateOverlay; + SharedImageLocator() = default; SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer); SharedImageLocator(const JsonNode & config, EImageBlitMode layer); diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 20b9e722e..42c221472 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -291,9 +291,15 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::string imagePathString = pathToLoad.getName(); - if(locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION) + bool generateShadow = locator.generateShadow && (*locator.generateShadow) != SharedImageLocator::ShadowMode::SHADOW_NONE; + bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE; + bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR; + bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION; + bool optimizeImage = !(isShadow && generateShadow) && !(isOverlay && generateOverlay); // images needs to expanded + + if(isOverlay && !generateOverlay) imagePathString += "-OVERLAY"; - if(locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR) + if(isShadow && !generateShadow) imagePathString += "-SHADOW"; if(locator.playerColored.isValidPlayer()) imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]); @@ -304,16 +310,26 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato auto imagePathSprites = ImagePath::builtin(imagePathString).addPrefix(scaledSpritesPath.at(locator.scalingFactor)); auto imagePathData = ImagePath::builtin(imagePathString).addPrefix(scaledDataPath.at(locator.scalingFactor)); + std::shared_ptr img = nullptr; + if(CResourceHandler::get()->existsResource(imagePathSprites) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)) - return std::make_shared(imagePathSprites); + img = std::make_shared(imagePathSprites, optimizeImage); + else if(CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)) + img = std::make_shared(imagePathData, optimizeImage); + else if(CResourceHandler::get()->existsResource(imagePath)) + img = std::make_shared(imagePath, optimizeImage); - if(CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)) - return std::make_shared(imagePathData); + if(img) + { + // TODO: Performance improvement - Run algorithm on optimized ("trimmed") images + // Not implemented yet because different frame image sizes seems to cause wobbeling shadow -> needs a way around this + if(isShadow && generateShadow) + img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEAR); + if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE) + img = img->drawOutline(Colors::WHITE, 1); + } - if(CResourceHandler::get()->existsResource(imagePath)) - return std::make_shared(imagePath); - - return nullptr; + return img; } std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator) diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 9be02d437..a9096b410 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -70,7 +70,7 @@ SDLImageShared::SDLImageShared(SDL_Surface * from) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename) +SDLImageShared::SDLImageShared(const ImagePath & filename, bool optimizeImage) : surf(nullptr), margins(0, 0), fullSize(0, 0), @@ -89,7 +89,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename) fullSize.x = surf->w; fullSize.y = surf->h; - optimizeSurface(); + if(optimizeImage) + optimizeSurface(); } } @@ -429,6 +430,49 @@ std::shared_ptr SDLImageShared::verticalFlip() const return ret; } +std::shared_ptr SDLImageShared::drawShadow(bool doSheer) const +{ + if(upscalingInProgress) + throw std::runtime_error("Attempt to access images that is still being loaded!"); + + if (!surf) + return nullptr; + + SDL_Surface * shadow = CSDL_Ext::drawShadow(surf, doSheer); + auto ret = std::make_shared(shadow); + ret->fullSize = fullSize; + ret->margins.x = margins.x; + ret->margins.y = margins.y; + ret->optimizeSurface(); + + // erase our own reference + SDL_FreeSurface(shadow); + + return ret; +} + +std::shared_ptr SDLImageShared::drawOutline(const ColorRGBA & color, int thickness) const +{ + if(upscalingInProgress) + throw std::runtime_error("Attempt to access images that is still being loaded!"); + + if (!surf) + return nullptr; + + SDL_Color sdlColor = { color.r, color.g, color.b, color.a }; + SDL_Surface * outline = CSDL_Ext::drawOutline(surf, sdlColor, thickness); + auto ret = std::make_shared(outline); + ret->fullSize = fullSize; + ret->margins.x = margins.x; + ret->margins.y = margins.y; + ret->optimizeSurface(); + + // erase our own reference + SDL_FreeSurface(outline); + + return ret; +} + // Keep the original palette, in order to do color switching operation void SDLImageShared::savePalette() { diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index bd81d01cf..56f99a8cf 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -46,7 +46,7 @@ public: //Load image from def file SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImageShared(const ImagePath & filename); + SDLImageShared(const ImagePath & filename, bool optimizeImage=true); //Create using existing surface, extraRef will increase refcount on SDL_Surface SDLImageShared(SDL_Surface * from); ~SDLImageShared(); @@ -71,5 +71,8 @@ public: [[nodiscard]] std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override; [[nodiscard]] std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; + std::shared_ptr drawShadow(bool doSheer) const; + std::shared_ptr drawOutline(const ColorRGBA & color, int thickness) const; + friend class SDLImageLoader; }; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 8e953f04a..2382a90df 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -23,6 +23,7 @@ #include "../../lib/GameConstants.h" #include +#include #include #include @@ -685,3 +686,299 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } + +SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* sourceSurface, const SDL_Color& color, int thickness) +{ + if(thickness < 1) + return nullptr; + + SDL_Surface* destSurface = newSurface(Point(sourceSurface->w, sourceSurface->h)); + + if(SDL_MUSTLOCK(sourceSurface)) SDL_LockSurface(sourceSurface); + if(SDL_MUSTLOCK(destSurface)) SDL_LockSurface(destSurface); + + int width = sourceSurface->w; + int height = sourceSurface->h; + + // Helper lambda to get alpha + auto getAlpha = [&](int x, int y) -> Uint8 { + if (x < 0 || x >= width || y < 0 || y >= height) + return 0; + Uint32 pixel = *((Uint32*)sourceSurface->pixels + y * width + x); + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; + SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); + return a; + }; + + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) + { + for (int y = r.begin(); y != r.end(); ++y) + { + for (int x = 0; x < width; x++) + { + Uint8 alpha = getAlpha(x, y); + if (alpha != 0) + continue; // Skip opaque or semi-transparent pixels + + Uint8 maxNearbyAlpha = 0; + + for (int dy = -thickness; dy <= thickness; ++dy) + { + for (int dx = -thickness; dx <= thickness; ++dx) + { + if (dx * dx + dy * dy > thickness * thickness) + continue; // circular area + + int nx = x + dx; + int ny = y + dy; + if (nx < 0 || ny < 0 || nx >= width || ny >= height) + continue; + + Uint8 neighborAlpha = getAlpha(nx, ny); + if (neighborAlpha > maxNearbyAlpha) + maxNearbyAlpha = neighborAlpha; + } + } + + if (maxNearbyAlpha > 0) + { + Uint8 finalAlpha = maxNearbyAlpha - alpha; // alpha is 0 here, so effectively maxNearbyAlpha + Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, finalAlpha); + *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + } + } + } + }); + + if(SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); + if(SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); + + return destSurface; +} + +void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b, double c, double d, double tx, double ty) +{ + // Check if the transform is purely scaling (and optionally translation) + bool isPureScaling = vstd::isAlmostZero(b) && vstd::isAlmostZero(c); + + if (isPureScaling) + { + // Calculate target dimensions + int scaledW = static_cast(src->w * a); + int scaledH = static_cast(src->h * d); + + SDL_Rect srcRect = { 0, 0, src->w, src->h }; + SDL_Rect dstRect = { static_cast(tx), static_cast(ty), scaledW, scaledH }; + + // Convert surfaces to same format if needed + if (src->format->format != dst->format->format) + { + SDL_Surface* converted = SDL_ConvertSurface(src, dst->format, 0); + if (!converted) + throw std::runtime_error("SDL_ConvertSurface failed!"); + + SDL_BlitScaled(converted, &srcRect, dst, &dstRect); + SDL_FreeSurface(converted); + } + else + SDL_BlitScaled(src, &srcRect, dst, &dstRect); + + return; + } + + // Lock surfaces for direct pixel access + if (SDL_MUSTLOCK(src)) SDL_LockSurface(src); + if (SDL_MUSTLOCK(dst)) SDL_LockSurface(dst); + + // Calculate inverse matrix M_inv for mapping dst -> src + double det = a * d - b * c; + if (vstd::isAlmostZero(det)) + throw std::runtime_error("Singular transform matrix!"); + double invDet = 1.0 / det; + double ia = d * invDet; + double ib = -b * invDet; + double ic = -c * invDet; + double id = a * invDet; + + auto srcPixels = (Uint32*)src->pixels; + auto dstPixels = (Uint32*)dst->pixels; + + tbb::parallel_for(tbb::blocked_range(0, dst->h), [&](const tbb::blocked_range& r) + { + // For each pixel in the destination image + for(int y = r.begin(); y != r.end(); ++y) + { + for(int x = 0; x < dst->w; x++) + { + // Map destination pixel (x,y) back to source coordinates (srcX, srcY) + double srcX = ia * (x - tx) + ib * (y - ty); + double srcY = ic * (x - tx) + id * (y - ty); + + // Nearest neighbor sampling (can be improved to bilinear) + auto srcXi = static_cast(round(srcX)); + auto srcYi = static_cast(round(srcY)); + + // Check bounds + if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) + { + Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; + dstPixels[y * dst->w + x] = pixel; + } + else + dstPixels[y * dst->w + x] = 0x00000000; // transparent black + } + } + }); + + if (SDL_MUSTLOCK(src)) SDL_UnlockSurface(src); + if (SDL_MUSTLOCK(dst)) SDL_UnlockSurface(dst); +} + +int getLowestNonTransparentY(SDL_Surface* surface) +{ + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); + + const int w = surface->w; + const int h = surface->h; + const int bpp = surface->format->BytesPerPixel; + auto pixels = static_cast(surface->pixels); + + // Use parallel_reduce to find the max y with non-transparent pixel + int lowestY = tbb::parallel_reduce( + tbb::blocked_range(0, h), + -1, // initial lowestY = -1 (fully transparent) + [&](const tbb::blocked_range& r, int localMaxY) -> int + { + for (int y = r.begin(); y != r.end(); ++y) + { + Uint8* row = pixels + y * surface->pitch; + for (int x = 0; x < w; ++x) + { + Uint32 pixel = *(Uint32*)(row + x * bpp); + Uint8 a = (pixel >> 24) & 0xFF; // Fast path for ARGB8888 + if (a > 0) + { + localMaxY = std::max(localMaxY, y); + break; // no need to scan rest of row + } + } + } + return localMaxY; + }, + [](int a, int b) -> int + { + return std::max(a, b); + } + ); + + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); + return lowestY; +} + +void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uint8 a) +{ + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); + + auto pixels = (Uint32*)surface->pixels; + int pixelCount = surface->w * surface->h; + + tbb::parallel_for(tbb::blocked_range(0, pixelCount), [&](const tbb::blocked_range& range) + { + for(int i = range.begin(); i != range.end(); ++i) + { + Uint32 pixel = pixels[i]; + Uint8 pr; + Uint8 pg; + Uint8 pb; + Uint8 pa; + // Extract existing RGBA components using SDL_GetRGBA + SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); + + Uint32 newPixel = SDL_MapRGBA(surface->format, r, g, b, a); + if(pa < 128) + newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0); + + pixels[i] = newPixel; + } + }); + + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); +} + +void boxBlur(SDL_Surface* surface) +{ + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); + + int width = surface->w; + int height = surface->h; + int pixelCount = width * height; + + Uint32* pixels = static_cast(surface->pixels); + std::vector temp(pixelCount); + + tbb::parallel_for(0, height, [&](int y) + { + for (int x = 0; x < width; ++x) + { + int sumR = 0; + int sumG = 0; + int sumB = 0; + int sumA = 0; + int count = 0; + + for (int ky = -1; ky <= 1; ++ky) + { + int ny = std::clamp(y + ky, 0, height - 1); + for (int kx = -1; kx <= 1; ++kx) + { + int nx = std::clamp(x + kx, 0, width - 1); + Uint32 pixel = pixels[ny * width + nx]; + + sumA += (pixel >> 24) & 0xFF; + sumR += (pixel >> 16) & 0xFF; + sumG += (pixel >> 8) & 0xFF; + sumB += (pixel >> 0) & 0xFF; + ++count; + } + } + + Uint8 a = sumA / count; + Uint8 r = sumR / count; + Uint8 g = sumG / count; + Uint8 b = sumB / count; + temp[y * width + x] = (a << 24) | (r << 16) | (g << 8) | b; + } + }); + + std::copy(temp.begin(), temp.end(), pixels); + + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); +} + +SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * sourceSurface, bool doSheer) +{ + SDL_Surface *destSurface = newSurface(Point(sourceSurface->w, sourceSurface->h)); + + double shearX = doSheer ? 0.5 : 0.0; + double scaleY = doSheer ? 0.5 : 0.25; + + int lowestSource = getLowestNonTransparentY(sourceSurface); + int lowestTransformed = lowestSource * scaleY; + + // Parameters for applyAffineTransform + double a = 1.0; + double b = shearX; + double c = 0.0; + double d = scaleY; + double tx = -shearX * lowestSource; + double ty = lowestSource - lowestTransformed; + + applyAffineTransform(sourceSurface, destSurface, a, b, c, d, tx, ty); + fillAlphaPixelWithRGBA(destSurface, 0, 0, 0, 128); + boxBlur(destSurface); + + return destSurface; +} diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index daa70e28f..d90934926 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -76,4 +76,7 @@ SDL_Color toSDL(const ColorRGBA & color); void setDefaultColorKey(SDL_Surface * surface); ///set key-color to 0,255,255 only if it exactly mapped void setDefaultColorKeyPresize(SDL_Surface * surface); + + SDL_Surface * drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness); + SDL_Surface * drawShadow(SDL_Surface * source, bool doSheer); } diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp index a7a8746f8..b490994a7 100644 --- a/client/renderSDL/ScalableImage.cpp +++ b/client/renderSDL/ScalableImage.cpp @@ -440,6 +440,8 @@ std::shared_ptr ScalableImageShared::loadOrGenerateImage(EIm loadingLocator.image = locator.image; loadingLocator.defFile = locator.defFile; + loadingLocator.generateShadow = locator.generateShadow; + loadingLocator.generateOverlay = locator.generateOverlay; loadingLocator.defFrame = locator.defFrame; loadingLocator.defGroup = locator.defGroup; loadingLocator.layer = mode; diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 8b3c44082..0519eee35 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -45,7 +45,13 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def "frame" : 0, // Filename for this frame - "file" : "filename.png" + "file" : "filename.png", + + // Automatically create shadow for this frame if required. Optional, 0 = None, 1 = Normal Shadow, 2 = Sheared Shadow (e.g. for adventure map) + "generateShadow" : 1, + + // Automatically create overlay for this frame if required. Optional, 0 = None, 1 = Outline + "generateOverlay" : 1, }. ... ]