ソースを参照

basic algorithm

Laserlicht 3 ヶ月 前
コミット
7eae917497

+ 10 - 0
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<SharedImageLocator::ShadowMode>(config["generateShadow"].Integer());
+
+	if(!config["generateOverlay"].isNull())
+		generateOverlay = static_cast<SharedImageLocator::OverlayMode>(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;
 }

+ 16 - 0
client/render/ImageLocator.h

@@ -16,12 +16,28 @@
 
 struct SharedImageLocator
 {
+	enum ShadowMode
+	{
+		SHADOW_NONE,
+		SHADOW_NORMAL,
+		SHADOW_SHEAR
+	};
+	enum OverlayMode
+	{
+		OVERLAY_NONE,
+		OVERLAY_OUTLINE,
+		OVERLAY_FLAG
+	};
+
 	std::optional<ImagePath> image;
 	std::optional<AnimationPath> defFile;
 	int defFrame = -1;
 	int defGroup = -1;
 	EImageBlitMode layer = EImageBlitMode::OPAQUE;
 
+	std::optional<ShadowMode> generateShadow;
+	std::optional<OverlayMode> generateOverlay;
+
 	SharedImageLocator() = default;
 	SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer);
 	SharedImageLocator(const JsonNode & config, EImageBlitMode layer);

+ 22 - 9
client/renderSDL/RenderHandler.cpp

@@ -291,9 +291,14 @@ std::shared_ptr<SDLImageShared> 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;
+
+	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 +309,24 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 	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<SDLImageShared>(imagePathSprites);
+	std::shared_ptr<SDLImageShared> img = nullptr;
 
-	if(CResourceHandler::get()->existsResource(imagePathData))
-		return std::make_shared<SDLImageShared>(imagePathData);
+	if(CResourceHandler::get()->existsResource(imagePathSprites))
+		img = std::make_shared<SDLImageShared>(imagePathSprites);
+	else if(CResourceHandler::get()->existsResource(imagePathData))
+		img = std::make_shared<SDLImageShared>(imagePathData);
+	else if(CResourceHandler::get()->existsResource(imagePath))
+		img = std::make_shared<SDLImageShared>(imagePath);
 
-	if(CResourceHandler::get()->existsResource(imagePath))
-		return std::make_shared<SDLImageShared>(imagePath);
+	if(img)
+	{
+		if(isShadow && generateShadow)
+			img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEER);
+		if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE)
+			img = img->drawOutline(Colors::WHITE, getScalingFactor());
+	}
 
-	return nullptr;
+	return img;
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator)

+ 41 - 0
client/renderSDL/SDLImage.cpp

@@ -429,6 +429,47 @@ std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 	return ret;
 }
 
+std::shared_ptr<const ISharedImage> SDLImageShared::drawShadow(bool doSheer) const
+{
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
+	if (!surf)
+		return shared_from_this();
+
+	SDL_Surface * shadow = CSDL_Ext::drawShadow(surf, doSheer);
+	auto ret = std::make_shared<SDLImageShared>(shadow);
+	ret->fullSize = fullSize;
+	ret->margins.x = margins.x;
+	ret->margins.y = margins.y;
+
+	// erase our own reference
+	SDL_FreeSurface(shadow);
+
+	return ret;
+}
+
+std::shared_ptr<const ISharedImage> 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 shared_from_this();
+
+	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<SDLImageShared>(outline);
+	ret->fullSize = fullSize;
+	ret->margins.x = margins.x;
+	ret->margins.y = margins.y;
+
+	// erase our own reference
+	SDL_FreeSurface(outline);
+
+	return ret;
+}
+
 // Keep the original palette, in order to do color switching operation
 void SDLImageShared::savePalette()
 {

+ 3 - 0
client/renderSDL/SDLImage.h

@@ -71,5 +71,8 @@ public:
 	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override;
 	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
 
+	std::shared_ptr<const ISharedImage> drawShadow(bool doSheer) const;
+	std::shared_ptr<const ISharedImage> drawOutline(const ColorRGBA & color, int thickness) const;
+
 	friend class SDLImageLoader;
 };

+ 310 - 0
client/renderSDL/SDL_Extensions.cpp

@@ -685,3 +685,313 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other)
 
 	other = CSDL_Ext::fromSDL(rect);
 }
+
+SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness)
+{
+	// ensure format
+	SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0);
+	SDL_Surface *destSurface = newSurface(Point(source->w, source->h));
+
+	int width = sourceSurface->w;
+	int height = sourceSurface->h;
+
+	// Iterate through the pixels of the image
+	for (int y = 0; y < height; y++)
+	{
+		for (int x = 0; x < width; x++)
+		{
+			Uint8 maxPixel = 0;
+			Uint8 minPixel = 255;
+			int halfThickness = (thickness + 1) / 2;
+
+			// Loop over the neighborhood around (x, y)
+			for(int offsetY = -halfThickness; offsetY <= halfThickness; offsetY++)
+			{
+				for(int offsetX = -halfThickness; offsetX <= halfThickness; offsetX++)
+				{
+					// Circle instead of rectangle
+					if(offsetX * offsetX + offsetY * offsetY > halfThickness * halfThickness)
+						continue;
+
+					int neighborX = x + offsetX;
+					int neighborY = y + offsetY;
+
+					// Check image bounds
+					if(neighborX >= 0 && neighborX < destSurface->w && neighborY >= 0 && neighborY < destSurface->h)
+					{
+						// Get the pixel at the neighbor position
+						Uint32 pixel = *((Uint32*)sourceSurface->pixels + neighborY * width + neighborX);
+						Uint8 r, g, b, a;
+						SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a);
+						
+						// Compare the pixel alpha value to find the maximum and maximum
+						if(a > maxPixel)
+							maxPixel = a;
+						if(a < minPixel)
+							minPixel = a;
+					}
+				}
+			}
+
+			Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, maxPixel - minPixel);
+			*((Uint32*)destSurface->pixels + y * width + x) = newPixel;
+		}
+	}
+
+	SDL_FreeSurface(sourceSurface);
+
+	return destSurface;
+}
+
+void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b, double c, double d, double tx, double ty)
+{
+	assert(src->format->format == SDL_PIXELFORMAT_ARGB8888);
+	assert(dst->format->format == SDL_PIXELFORMAT_ARGB8888);
+
+	// 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 (det == 0)
+		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;
+
+	// For each pixel in the destination image
+	for(int y = 0; y < dst->h; 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)
+			int srcXi = static_cast<int>(round(srcX));
+			int srcYi = static_cast<int>(round(srcY));
+
+			// Check bounds
+			if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h)
+			{
+				Uint32* srcPixels = (Uint32*)src->pixels;
+				Uint32* dstPixels = (Uint32*)dst->pixels;
+
+				Uint32 pixel = srcPixels[srcYi * src->w + srcXi];
+				dstPixels[y * dst->w + x] = pixel;
+			}
+			else
+			{
+				// Outside source bounds: set transparent or black
+				Uint32* dstPixels = (Uint32*)dst->pixels;
+				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)
+{
+	assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888);
+
+	if(SDL_MUSTLOCK(surface))
+		SDL_LockSurface(surface);
+
+	int w = surface->w;
+	int h = surface->h;
+	int bpp = surface->format->BytesPerPixel;
+	Uint8* pixels = (Uint8*)surface->pixels;
+
+	for(int y = h - 1; y >= 0; --y)
+	{
+		Uint8* row = pixels + y * surface->pitch;
+
+		for(int x = 0; x < w; ++x)
+		{
+			Uint32 pixel = *(Uint32*)(row + x * bpp);
+
+			Uint8 r, g, b, a;
+			SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
+
+			if (a > 0)
+			{
+				if(SDL_MUSTLOCK(surface))
+					SDL_UnlockSurface(surface);
+				return y;
+			}
+		}
+	}
+
+	if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface);
+	return -1;  // fully transparent
+}
+
+void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
+{
+	assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888);
+
+	if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface);
+
+	Uint32* pixels = (Uint32*)surface->pixels;
+	int pixelCount = surface->w * surface->h;
+
+	for (int i = 0; i < pixelCount; i++)
+	{
+		Uint32 pixel = pixels[i];
+
+		Uint8 pr, pg, pb, 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 == 0)
+			newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0);
+
+		pixels[i] = newPixel;
+	}
+
+	if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface);
+}
+
+void gaussianBlur(SDL_Surface* surface, int amount)
+{
+	assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888);
+
+	if (!surface || amount <= 0) return;
+
+	if (SDL_MUSTLOCK(surface))
+	{
+		if (SDL_LockSurface(surface) != 0)
+			throw std::runtime_error("Failed to lock surface!");
+	}
+
+	int width = surface->w;
+	int height = surface->h;
+	int pixelCount = width * height;
+
+	Uint32* pixels = static_cast<Uint32*>(surface->pixels);
+
+	std::vector<Uint8> srcR(pixelCount);
+	std::vector<Uint8> srcG(pixelCount);
+	std::vector<Uint8> srcB(pixelCount);
+	std::vector<Uint8> srcA(pixelCount);
+
+	std::vector<Uint8> dstR(pixelCount);
+	std::vector<Uint8> dstG(pixelCount);
+	std::vector<Uint8> dstB(pixelCount);
+	std::vector<Uint8> dstA(pixelCount);
+
+	// Initialize src channels from surface pixels
+	for (int y = 0; y < height; ++y)
+	{
+		for (int x = 0; x < width; ++x)
+		{
+			Uint32 pixel = pixels[y * width + x];
+			Uint8 r, g, b, a;
+			SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
+
+			int idx = y * width + x;
+			srcR[idx] = r;
+			srcG[idx] = g;
+			srcB[idx] = b;
+			srcA[idx] = a;
+		}
+	}
+
+	// 3x3 Gaussian kernel
+	float kernel[3][3] = {
+		{1.f/16, 2.f/16, 1.f/16},
+		{2.f/16, 4.f/16, 2.f/16},
+		{1.f/16, 2.f/16, 1.f/16}
+	};
+
+	// Apply the blur 'amount' times for stronger blur
+	for (int iteration = 0; iteration < amount; ++iteration) {
+		for (int y = 0; y < height; ++y) {
+			for (int x = 0; x < width; ++x) {
+				float sumR = 0.f, sumG = 0.f, sumB = 0.f, sumA = 0.f;
+
+				for (int ky = -1; ky <= 1; ++ky) {
+					for (int kx = -1; kx <= 1; ++kx) {
+						int nx = x + kx;
+						int ny = y + ky;
+
+						// Clamp edges
+						if (nx < 0) nx = 0;
+						else if (nx >= width) nx = width - 1;
+						if (ny < 0) ny = 0;
+						else if (ny >= height) ny = height - 1;
+
+						int nIdx = ny * width + nx;
+						float kval = kernel[ky + 1][kx + 1];
+
+						sumR += srcR[nIdx] * kval;
+						sumG += srcG[nIdx] * kval;
+						sumB += srcB[nIdx] * kval;
+						sumA += srcA[nIdx] * kval;
+					}
+				}
+
+				int idx = y * width + x;
+				dstR[idx] = static_cast<Uint8>(sumR);
+				dstG[idx] = static_cast<Uint8>(sumG);
+				dstB[idx] = static_cast<Uint8>(sumB);
+				dstA[idx] = static_cast<Uint8>(sumA);
+			}
+		}
+		// Swap src and dst for next iteration (blur chaining)
+		srcR.swap(dstR);
+		srcG.swap(dstG);
+		srcB.swap(dstB);
+		srcA.swap(dstA);
+	}
+
+	// After final iteration, write back to surface pixels
+	for (int y = 0; y < height; ++y) {
+		for (int x = 0; x < width; ++x) {
+			int idx = y * width + x;
+			pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]);
+		}
+	}
+
+	if (SDL_MUSTLOCK(surface)) {
+		SDL_UnlockSurface(surface);
+	}
+}
+
+SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer)
+{
+	SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0);
+	SDL_Surface *destSurface = newSurface(Point(source->w, source->h));
+
+	assert(destSurface->format->format == SDL_PIXELFORMAT_ARGB8888);
+
+	double shearX = doSheer ? 0.5 : 0.0;
+	double scaleY = 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);
+	gaussianBlur(destSurface, 1);
+
+	SDL_FreeSurface(sourceSurface);
+
+	return destSurface;
+}

+ 3 - 0
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);
 }

+ 2 - 0
client/renderSDL/ScalableImage.cpp

@@ -440,6 +440,8 @@ std::shared_ptr<const ISharedImage> 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;