merging several png files into 1 spritesheet pragmatically

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