merging several png files into 1 spritesheet pragmatically

Hello everyone,

I have some 2D animations that are in separate PNG files and I’d like pragmatically to merge them together into one spritesheet for better performance. I would like to avoid using external software to merge them, I’d like my engine to do it automatically during startup with the following piece of code:

public static Texture2D LoadTexturesWithMerge(string fullPath, int startFrame, int endFrame)
    {
        List<Texture2D> textures = LoadTextures(fullPath, startFrame, endFrame); // this works correctly, it returns the 3 textures
        int resWidth = 0;
        int resHeight = 0;
        foreach (Texture2D texture in textures)
        {
            resWidth += texture.Width;
            resHeight += texture.Height;
        }
        Color[] resultColor = new Color[resWidth * resHeight];
        int i = 0;
        Color[] currentColor;
        Texture2D result = new Texture2D(GraphicsDeviceManager.GraphicsDevice, resWidth, resHeight);
        foreach (Texture2D texture in textures)
        {
            currentColor = new Color[texture.Width * texture.Height];
            texture.GetData(currentColor);
            currentColor.CopyTo(resultColor, i);
            i += currentColor.Length + 1;
        }
        
        result.SetData(resultColor);
        return result;
    }

Which yields the following weird result (using a 3 frames animation as an example):
image

I understand that it’s probably due to the following line:

Texture2D result = new Texture2D(GraphicsDeviceManager.GraphicsDevice, resWidth, resHeight);

but I don’t see right now how to fix this. If I decrease the height of the result image, of course my code will throw an error not having enough bytes allocated to the color data.
Does anybody see the issue with my code, or have a better way to do this?

Thank you

I think it’s because, for each input sheet, you’re adding to both width and height. Consider three 100x100 pixel input sheets. That would be 30 000 pixels worth of data (3 * 100 * 100). What you’re ending up with, though, is a 300x300 pixel result sheet. This is 90 000 pixels worth of data (300x300).

You need to decide what arrangement you’d like your source sheets to have in your final image. For example, do you want them all in a row (300x100), all vertically (100x300), or do you want to tile them in some way (import until some max width is reached, then start a new row).

1 Like

Probably the best way would be to create a new rendertarget2d set to preserve contents

Switch to the rendertarget. Draw onto that rendertarget

A rendertarget is a texture2d. So when needing to draw animation your source texture can be the rendertarget you’ve drawn.

This will be a lot faster than using getdata

1 Like

Thank you for your input guys, I’ll try them as soon as I can!

I made a texture packing algorithm previously for a content pipeline project ill post the link.

Its pretty simple its in the game1 file with the spritesheet class i stuck them all in the game1 file.
public class SpriteSheetCreator

the method
public void MakeSpriteSheet(GraphicsDevice graphics, string sheetName, int w, int h, List<Texture2D> textures, out SpriteSheet spriteSheet, bool saveToFile, string savepath).

The heart of the algorithm itself is very simple in the below method it’s called by the aforementioned method.

The premise is…
Have a list of textures that need to be placed in a single spritesheet texture.
Then first algorithmically find rectangles were they will fit into the new spritesheet in a compacted way.
Once we figure out were each sprite goes in the spritesheet then we return that spritesheet instance populated by sprite instances with rectangles and names. That hold the location in the spritesheet were each sprite belongs.
At that point its just a matter of transfering texels from each full sprite texture aka its width and height, into the rectangle position we found for them stored now in the sprite sheet class.
Then save that spritesheet texture to disk along with a description file of were and what each sprite is in that sheet.

The algorithm itself packs the sprites into the spritesheet as close as it can pack them together with the space between allowed. It systematically searches by brute force (a bit wasteful but completely thorough) every eligible pixel start location to fit the sprite into the spritesheet.

Note i didn’t add a field for which animation set or frame a sprite belonged to i sort of wish i would have but it really should be trivial to add to the sprite class the spritesheet class holds a list of.

private int PrepPositionUiImagesIntoSprites(string sheetName, int w, int h, List<Texture2D> textureList, out SpriteSheet spriteSheet, out Point requisiteSize)
{
    int result = 0;
    requisiteSize = Point.Zero;
    spriteSheet = new SpriteSheet();
    spriteSheet.sheetWidth = w;
    spriteSheet.sheetHeight = h;
    spriteSheet.name = sheetName;
    Rectangle spriteSheetBounds = new Rectangle(0, 0, w, h);
    for (int i = 0; i < textureList.Count; i++)
    {
        var t = textureList[i];
        var bounds = textureList[i].Bounds;
        bool successResult = false;

        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                bool placeFound = true;
                bounds.X = x;
                bounds.Y = y;
                if (spriteSheet.sprites.Count > 0)
                {
                    // check to make sure we intersect no sprites.
                    for (int si = 0; si < spriteSheet.sprites.Count; si++)
                    {
                        var alreadyPlacedSpriteBounds = spriteSheet.sprites[si].sourceRectangle;
                        alreadyPlacedSpriteBounds.Location = alreadyPlacedSpriteBounds.Location - new Point(2, 2);
                        alreadyPlacedSpriteBounds.Size = alreadyPlacedSpriteBounds.Size + new Point(4, 4);

                        bool isInsideSpriteSheet = bounds.Left >= 0 && bounds.Top >= 0 && bounds.Right < spriteSheetBounds.Right && bounds.Bottom < spriteSheetBounds.Bottom;
                        //if (spriteBounds.Intersects(bounds) || spriteBounds.Contains(bounds) || isInsideSpriteSheet)
                        if (IsNotOverlapping(alreadyPlacedSpriteBounds, bounds) == false || isInsideSpriteSheet == false)
                        {
                            si = spriteSheet.sprites.Count;
                            placeFound = false;
                        }
                    }
                    if (placeFound)
                    {
                        // in this case its a good position to add.
                        successResult = true;
                        placeFound = true;
                        //if (t.Name != null)
                        //    tmpSprite.nameOfSprite = t.Name;
                        spriteSheet.Add(t.Name, t, bounds);
                        // break all the way out to the next texture to find a place.
                        x = w;
                        y = h;
                        result++;
                        if (bounds.Right > requisiteSize.X)
                            requisiteSize.X = bounds.Right + 1;
                        if (bounds.Bottom > requisiteSize.Y)
                            requisiteSize.Y = bounds.Bottom + 1;
                        var s = spriteSheet.sprites[spriteSheet.sprites.Count - 1];
                        Console.WriteLine(" " + i + "  Sprite.Name " + s.nameOfSprite + " Sprite.source " + s.sourceRectangle);
                    }
                }
                else
                {
                    // no sprites to check against so just check if it fits inside.
                    bool isInside = bounds.Left >= 0 && bounds.Top >= 0 && bounds.Right < spriteSheetBounds.Right && bounds.Bottom < spriteSheetBounds.Bottom;
                    if (isInside)
                    {
                        // in this case its a good position to add.
                        successResult = true;
                        placeFound = true;
                        spriteSheet.Add(t.Name, t, bounds);
                        if (bounds.Right > requisiteSize.X)
                            requisiteSize.X = bounds.Right + 1;
                        if (bounds.Bottom > requisiteSize.Y)
                            requisiteSize.Y = bounds.Bottom + 1;
                        // break all the way out to the next texture to find a place for.
                        x = w;
                        y = h;
                        result++;
                        var s = spriteSheet.sprites[spriteSheet.sprites.Count - 1];
                        Console.WriteLine(" 0 Sprite.Name " + s.nameOfSprite + " Sprite.source " + s.sourceRectangle);
                    }
                    else
                    {
                        placeFound = false;
                    }
                }
            }
        }
        if (successResult == false)
        {
            // In this case we are in sort of a problem area we can't fit this texture so we really should just stop.
            // We could get more fancy if we knew for sure that it was ok to save this for another texture but we dont.
            // break out of the checking loop and the method.
            //result = i;
            i = textureList.Count;
        }
    }
    return result;
}

The post that goes thru me making a pipeline project for this can be found in the link below.
Ultimately this pipeline allows for the creation of a single texture and a associated .spr file that describes the sprites in it. Files of these types can then be loaded thru the content pipeline tool.

1 Like

Woah, that’s amazing, thank you very much!

Welcome glad someone found it useful.

1 Like