Browse Source

Support for shadow, overlay and player-colored premade images for 1x

Ivan Savenko 9 months ago
parent
commit
301086d956

+ 5 - 2
client/renderSDL/RenderHandler.cpp

@@ -26,6 +26,7 @@
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/VCMIDirs.h"
+#include "../../lib/constants/StringConstants.h"
 
 #include <vcmi/ArtifactService.h>
 #include <vcmi/CreatureService.h>
@@ -218,8 +219,6 @@ void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_p
 
 std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocator & locator)
 {
-	assert(locator.scalingFactor > 1);
-
 	static constexpr std::array scaledDataPath = {
 		"", // 0x
 		"DATA/",
@@ -260,6 +259,10 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 		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));

+ 69 - 54
client/renderSDL/ScalableImage.cpp

@@ -202,30 +202,30 @@ void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette,
 ScalableImageShared::ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage)
 	:locator(locator)
 {
-	base[0] = baseImage;
-	assert(base[0] != nullptr);
+	scaled[1].body[0] = baseImage;
+	assert(scaled[1].body[0] != nullptr);
 
 	loadScaledImages(GH.screenHandler().getScalingFactor(), PlayerColor::CANNOT_DETERMINE);
 }
 
 Point ScalableImageShared::dimensions() const
 {
-	return base[0]->dimensions();
+	return scaled[1].body[0]->dimensions();
 }
 
 void ScalableImageShared::exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const
 {
-	base[0]->exportBitmap(path, parameters.palette);
+	scaled[1].body[0]->exportBitmap(path, parameters.palette);
 }
 
 bool ScalableImageShared::isTransparent(const Point & coords) const
 {
-	return base[0]->isTransparent(coords);
+	return scaled[1].body[0]->isTransparent(coords);
 }
 
 Rect ScalableImageShared::contentRect() const
 {
-	return base[0]->contentRect();
+	return scaled[1].body[0]->contentRect();
 }
 
 void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor)
@@ -256,44 +256,37 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 		getFlippedImage(images)->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer);
 	};
 
-	if (scalingFactor == 1)
+	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)
 	{
-		flipAndDraw(base, parameters.colorMultiplier, parameters.alphaValue);
+		getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
+		return;
 	}
-	else
-	{
-		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.isValidPlayer() && scaled.at(scalingFactor).playerColored.at(parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(parameters.player.getNum())->isLoading();
 
-		if (shadowLoading || bodyLoading || overlayLoading || playerLoading)
-		{
-			getFlippedImage(base)->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 (scaled.at(scalingFactor).shadow.at(0))
-			flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue);
-
-		if (parameters.player.isValidPlayer())
-		{
-			scaled.at(scalingFactor).playerColored.at(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<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
+	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<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
 }
 
 const SDL_Palette * ScalableImageShared::getPalette() const
 {
-	return base[0]->getPalette();
+	return scaled[1].body[0]->getPalette();
 }
 
 std::shared_ptr<ScalableImageInstance> ScalableImageShared::createImageReference()
@@ -400,7 +393,7 @@ void ScalableImageInstance::verticalFlip()
 	parameters.flipVertical = !parameters.flipVertical;
 }
 
-std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color) const
+std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const
 {
 	ImageLocator loadingLocator;
 
@@ -417,68 +410,90 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 	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
-	Point targetSize = base[0]->dimensions() * scalingFactor;
-	for (int8_t scaling = 4; scaling > 1; --scaling)
+	for (int8_t scaling = 4; scaling > 0; --scaling)
 	{
 		loadingLocator.scalingFactor = scaling;
 		auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator);
 		if (loadedImage)
-			return loadedImage->scaleTo(targetSize, nullptr);
+		{
+			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);
+			}
+		}
 	}
 
-	// if all else fails - use base (presumably, indexed) image and convert it to desired form
 	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);
-	return base[0]->scaleInteger(scalingFactor, parameters.palette, mode);
+
+	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 (scalingFactor == 1)
-		return; // no op. TODO: consider loading 1x images for mods, as alternative to palette-based animations
-
-	if (scaled[scalingFactor].body[0] == nullptr)
+	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[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[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[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
 				break;
 		}
 	}
 
-	if (color.isValidPlayer() && scaled[scalingFactor].playerColored[color.getNum()] == nullptr)
+	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[color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color);
+				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[color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color);
+				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[color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color);
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
 				break;
 		}
 	}
@@ -490,7 +505,7 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 			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[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
 				break;
 			default:
 				break;
@@ -503,7 +518,7 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 		{
 			case EImageBlitMode::ONLY_OVERLAY:
 			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
 				break;
 			default:
 				break;

+ 6 - 8
client/renderSDL/ScalableImage.h

@@ -46,11 +46,12 @@ struct ScalableImageParameters : boost::noncopyable
 
 class ScalableImageShared final : public std::enable_shared_from_this<ScalableImageShared>, boost::noncopyable
 {
-	static constexpr int scalingSize = 5; // 0-4 range. TODO: switch to either 2-4 or 1-4
+	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 FlippedImages = std::array<std::shared_ptr<const ISharedImage>, maxFlips>;
-	using PlayerColoredImages = std::array<std::shared_ptr<const ISharedImage>, PlayerColor::PLAYER_LIMIT_I>;
+	using ImageType = std::shared_ptr<const ISharedImage>;
+	using FlippedImages = std::array<ImageType, maxFlips>;
+	using PlayerColoredImages = std::array<ImageType, PlayerColor::PLAYER_LIMIT_I + 1>; // all valid colors+neutral
 
 	struct ScaledImage
 	{
@@ -67,16 +68,13 @@ class ScalableImageShared final : public std::enable_shared_from_this<ScalableIm
 		PlayerColoredImages playerColored;
 	};
 
-	/// 1x image, usually indexed. Array of 4 images for every potential flip
-	FlippedImages base;
-
-	/// 1x-4x images. 1x are currently unused, but can be used for providing non-palette version of these images
+	/// 1x-4x images. body for 1x scaling is guaranteed to be loaded
 	std::array<ScaledImage, scalingSize> scaled;
 
 	/// Locator of this image, for loading additional (e.g. upscaled) images
 	const SharedImageLocator locator;
 
-	std::shared_ptr<const ISharedImage> loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color) const;
+	std::shared_ptr<const ISharedImage> loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const;
 
 	void loadScaledImages(int8_t scalingFactor, PlayerColor color);
 

+ 9 - 1
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