🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

How to work with not grids sprite sheets?

Started by
11 comments, last by rafaelsantana 4 years, 3 months ago

Hi!

I'm working with an sprite sheet that I found which doesn't follow the grid design that I normally see on some other sprite sheets (see below). My question is if there's a way to work with this kind of sprite sheets from a programming point of view or should I redesign it to a grid Sprite sheet in order to make it better. The second alternative seems to be a bit complicate for me because of some of the sprites that I have.

Thanks!

Advertisement

Amazing 2d art work there but a big screw up on behalf of the artist for not arranging the pixel art into a grid. Unfortunately, you have to sit down and extract each pixel art and create your sprite sheet from it, it could take a few hours to do this. As to why the artist decided to do it this way is beyond my imagination.

You can see that they don't even line up at all. I tried all sorts of rectangle dimensions on the top left corner and didn't get an alignment. If I was going to use this sprite sheet, I would have to sit down and painstakingly align each image one by one until I had my sprite sheet, that's a lot of work.

You should contact the artist as this may be the FREE version of the sprite sheet while the good one will cost you money because it is already good to go for use.

Thanks for replying Rafael. It's a free sprite sheet that I found on the web. I was thinking about doing what you suggest. I'm also evaluating of doing something like this:

https://stefankussmaul.home.blog/2018/10/14/coding-a-texture-atlas-c-c-sdl/

but I think I will have to get all the positions, height and weight of each of the sprites :(

Write a program that finds all blobs of non-background on the sheet, and compute a rectangle around it.

EDIT: Ie, never do manually what a computer can do faster ?

Likely alignment of the sprites in the animation needs a bit of work as well, so write a program that animates the blobs, and allows re-positioning each frame.

pablo9891 said:
Thanks for replying Rafael. It's a free sprite sheet that I found on the web. I was thinking about doing what you suggest. I'm also evaluating of doing something like this:

Sorry, but thats not a free sprite sheet. Its an emulator-rips of the commercial game "MegaMan X", meaning it is illegal to use those sprites in any of your products without consent of the IP-holders. Keep that in mind if you want to make something that you share on the internet (or even more if you intend on selling).

@Alberth Can you suggest any idea to do that?

@Juliean I'm not planning to release a game with those sprites. I want to learn and found that on the Web. Thanks for letting me know.

It's quite literally "search for non-background coloured blobs".

An image is a 2D array of some width and height, and each element has a colour, usually in RGB for sprite sheets. This is usually the most basic form of loading an image, so find an image loading library for your favorite programming language that loads images in your file format, and can give width, height, and pixel colour at some (x,y) coordinate in the image. I tend to use Python and the Pillow library.

A corner is likely at (0, 0). Read its colour, and assume that is background colour (you can check that in any paint-ish program, where you can typically also get the RGB data of an arbitrary pixel).

Then scan the image by looping in x and y over the image, querying the pixel colour at each (x,y) until you find a colour different from the background color. That is a non-background blob pixel.

Explore the blob by flood filling https://en.wikipedia.org/wiki/Flood_fill . To find what other pixels belong to the blob, collect (x,y) information of found blob pixels in a set, starting with the above first pixel. For each pixel in the collection, examine its 4 or 8 direct neighboring pixels, and if they are non-background, they are part of the same blob, so add such pixel coordinates to the collection as well. (You may want to keep track of found-but-not-yet-examined coordinates here.) Continue until you run out of new neighboring blob pixels.

The floodfill gives you the the set of coordinate pairs of all pixels that are direct neighbor to some other pixel in the same set, ie a blob of non-background colour. A rectangle is then a simple matter of finding the extreme x and y values in that collection, and that gives you a rectangle at the extreme ends of the blob. If you want it around the blob make the rectangle 1 pixel larger at all 4 sides.

When done with one blob, go back to scanning the image for other blobs, until the scan is complete.

Obviously, you'll find the same blob lots of times, so you may either handle that in the program as well, or simply ignore it, and filter the rectangles for duplicates afterwards.

Here is something that I wrote that may be able to help guide you and give you an example. Mind you that this was written many years ago and could stand to be refactored a little bit and some more error handling / parameter checking. Regardless, hopefully it will help. Also, it's in C#, but you should be able to convert it to whatever you need.

You can read the summary to see what this method does. You can easily change this to be any background color key instead of alpha. For instance, on that sprite sheet it has a greenish background so you could check for that. You can also change it to pass in a color key value so it's not hard coded. I was always using images with alpha so I didn't need this functionality at the time.

/// <summary>
/// The BitmapEmptyAt will scan an array of pixels (at the specified row of pixels or
/// the specified column of pixels) and will return false once it hits a non
/// empty space (alpha > 0). If all pixels are empty then it will return true.
/// </summary>
/// <param name="pixelBytes">
/// The array of pixels to scan
/// </param>
/// <param name="format">
/// The format of the image being processed
/// </param>
/// <param name="stride">
/// The stride (scan width) of the pixels
/// </param>
/// <param name="size">
/// The row or column size
/// </param>
/// <param name="position">
/// The current row or column position to check
/// </param>
/// <param name="isRow">
/// True if checking row values and false if checking column values
/// </param>
/// <returns>
/// True if the entire row or column is empty and false otherwise
/// </returns>
/// <remarks>
/// This function assumes a 32 bit image (Format32bppArgb) with bgrabgra... order.
/// </remarks>
private static bool BitmapEmptyAt(IList<byte> pixelBytes, PixelFormat format,
                                    int stride, int size, int position, bool isRow)
{
    var result = true;
    var bytesPerPixel = GetBytesPerPixel(format);

    if (bytesPerPixel != 0)
    {
        try { }
        finally
        {
            Parallel.For(0, size, (i, loop) =>
            {
                var index = (isRow ? i * stride + position * bytesPerPixel :
                            position * stride + i * bytesPerPixel);
                
                if (pixelBytes[index + (bytesPerPixel - 1)] != 0)
                {
                    result = false;
                    loop.Stop();
                }
            });
        }
    }

    return result;
}

Here is the GetBytesPerPixel method:

/// <summary>
/// Method to get the bytes per pixel, or channels, of an image based on the
/// bitmaps pixel format.
/// </summary>
/// <param name="format">
/// The pixel format to get the bytes per pixel from
/// </param>
/// <returns>
/// The bytes per pixel of the image
/// </returns>
public static int GetBytesPerPixel(PixelFormat format)
{
    switch (format)
    {
        case PixelFormat.Format24bppRgb:
            return 3;

        case PixelFormat.Format32bppArgb:
        case PixelFormat.Format32bppPArgb:
            return 4;
    }

    return 0;
}

This method will scan the entire boundary of the image and find the extents. So basically it will update the extents rectangle to include the boundary of the area you see here. The red areas would be excluded.

/// <summary>
/// The GetEmptyExtents function will return the bounds of the actual image
/// data. Empty space (alpha surrounding) is ignored.
/// </summary>
/// <param name="pixelBytes">
/// The array of pixels to scan
/// </param>
/// <param name="format">
/// The format of the image being processed
/// </param>
/// <param name="stride">
/// The stride (scan width) of the pixels
/// </param>
/// <param name="extents">
/// The extents to adjust
/// </param>
/// <remarks>
/// The extents must be set to (0, 0, image width - 1, image height - 1).
/// This method as adopted by an early Microsoft XNA example.
/// </remarks>
private static void GetEmptyExtents(IList<byte> pixelBytes, PixelFormat format,
                                    int stride, ref Rectangle extents)
{
    while ((extents.X < extents.Width) &&
            (BitmapEmptyAt(pixelBytes, format, stride, extents.Height, extents.X, true)))
    {
        extents.X++;
    }

    while ((extents.Width > extents.X) &&
            (BitmapEmptyAt(pixelBytes, format, stride, extents.Height, extents.Width, true)))
    {
        extents.Width--;
    }

    while ((extents.Y < extents.Height) &&
            (BitmapEmptyAt(pixelBytes, format, stride, extents.Width, extents.Y, false)))
    {
        extents.Y++;
    }

    while ((extents.Height > extents.Y) &&
            (BitmapEmptyAt(pixelBytes, format, stride, extents.Width, extents.Height, false)))
    {
        extents.Height--;
    }
}

Here is how to actually use it. Note that the AwBitmap is a class that acts like the normal Bitmap class, but is very fast. It's irrelevant here for the example.

/// <summary>
/// The TightenBitmap function will remove all empty space around an image
/// </summary>
/// <param name="bitmap">
/// The bitmap to tighten
/// </param>
/// <returns>
/// The tightened bitmap
/// </returns>
/// <exception cref="System.ArgumentNullException">
/// Thrown if the specified bitmap is null.
/// </exception>
/// <exception cref="System.ArgumentException">
/// Thrown if the bitmap is not a supported type.
/// </exception>
public static AwBitmap TightenBitmap(AwBitmap bitmap)
{
    #region [ Parameter Checks ]
    if (bitmap == null)
    {
        throw new ArgumentNullException(nameof(bitmap));
    }

    if (!IsSupportedImageFormat(bitmap.Bitmap.PixelFormat))
    {
        const string message = "Pixel format of supplied bitmap is not supported";

        throw new ArgumentException(message, nameof(bitmap));
    }
    #endregion

    bitmap.Lock();

    try
    {
        var width = bitmap.Width;
        var height = bitmap.Height;
        var extents = new Rectangle(0, 0, width - 1, height - 1);

        using (var output = (AwBitmap)bitmap.Bitmap.Clone())
        {
            var bitmapData = output.Bitmap.LockBits(new Rectangle(0, 0, width, height),
                                                    ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            var pixelBytes = new byte[bitmapData.Stride * bitmapData.Height];
            Marshal.Copy(bitmapData.Scan0, pixelBytes, 0, bitmapData.Stride * bitmapData.Height);
            GetEmptyExtents(pixelBytes, bitmap.Bitmap.PixelFormat, bitmapData.Stride, ref extents);
            Marshal.Copy(pixelBytes, 0, bitmapData.Scan0, bitmapData.Stride * bitmapData.Height);
            bitmap.Unlock();
            output.Bitmap.UnlockBits(bitmapData);
        }

        return CropBitmap(bitmap, extents);
    }
    catch { }
    finally
    {
        bitmap.Unlock();
    }

    return null;
}

So if you change these to look for individual sprites instead of the boundary then you are gold. The only problem I see is the author note in the big white box. It will see that as a sprite and try to move it to the grid… When that happens other sprites will overlap it or it may overlap other sprites. A utility could be written to add excluded parts the sprite sheet that can be ignored.

I should write this utility for fun… I'm all cooped up in my house and getting bored lol. If you're interested let me know and I can mock something up and open source it for you.

Yes it is possible to write an application that reads the sprite sheet compute the shapes size and width and separate the individual bits into a nicely organized grid. You still have the issue of making sure that the sprites overlap perfectly on the tile so that they animate correctly.

One game engine that does this for you is Unreal engine, if you want to experiment with it. It is jam packed with tons of features and is really powerful but with a steep learning curve.

Juliean said:

pablo9891 said:
Thanks for replying Rafael. It's a free sprite sheet that I found on the web. I was thinking about doing what you suggest. I'm also evaluating of doing something like this:

Sorry, but thats not a free sprite sheet. Its an emulator-rips of the commercial game "MegaMan X", meaning it is illegal to use those sprites in any of your products without consent of the IP-holders. Keep that in mind if you want to make something that you share on the internet (or even more if you intend on selling).

Actually it's not a rip from Mega Man X - it's the player character from Mega Man X redrawn in the style of a completely different game. That doesn't make it any less illegal to use, though.

This topic is closed to new replies.

Advertisement