#region File Description
//-----------------------------------------------------------------------------
// SpritePacker.cs
//
// Microsoft Game Technology Group
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
#endregion
namespace SpriteSheetPipeline
{
///
/// Helper for arranging many small sprites into a single larger sheet.
///
public static class SpritePacker
{
///
/// Packs a list of sprites into a single big texture,
/// recording where each one was stored.
///
public static BitmapContent PackSprites(IList sourceSprites,
ICollection outputSprites,
ContentProcessorContext context)
{
if (sourceSprites.Count == 0)
throw new InvalidContentException("There are no sprites to arrange");
// Build up a list of all the sprites needing to be arranged.
List sprites = new List();
for (int i = 0; i < sourceSprites.Count; i++)
{
ArrangedSprite sprite = new ArrangedSprite();
// Include a single pixel padding around each sprite, to avoid
// filtering problems if the sprite is scaled or rotated.
sprite.Width = sourceSprites[i].Width + 2;
sprite.Height = sourceSprites[i].Height + 2;
sprite.Index = i;
sprites.Add(sprite);
}
// Sort so the largest sprites get arranged first.
sprites.Sort(CompareSpriteSizes);
// Work out how big the output bitmap should be.
int outputWidth = GuessOutputWidth(sprites);
int outputHeight = 0;
int totalSpriteSize = 0;
// Choose positions for each sprite, one at a time.
for (int i = 0; i < sprites.Count; i++)
{
PositionSprite(sprites, i, outputWidth);
outputHeight = Math.Max(outputHeight, sprites[i].Y + sprites[i].Height);
totalSpriteSize += sprites[i].Width * sprites[i].Height;
}
// Sort the sprites back into index order.
sprites.Sort(CompareSpriteIndices);
context.Logger.LogImportantMessage(
"Packed {0} sprites into a {1}x{2} sheet, {3}% efficiency",
sprites.Count, outputWidth, outputHeight,
totalSpriteSize * 100 / outputWidth / outputHeight);
return CopySpritesToOutput(sprites, sourceSprites, outputSprites,
outputWidth, outputHeight);
}
///
/// Once the arranging is complete, copies the bitmap data for each
/// sprite to its chosen position in the single larger output bitmap.
///
static BitmapContent CopySpritesToOutput(List sprites,
IList sourceSprites,
ICollection outputSprites,
int width, int height)
{
BitmapContent output = new PixelBitmapContent(width, height);
foreach (ArrangedSprite sprite in sprites)
{
BitmapContent source = sourceSprites[sprite.Index];
int x = sprite.X;
int y = sprite.Y;
int w = source.Width;
int h = source.Height;
// Copy the main sprite data to the output sheet.
BitmapContent.Copy(source, new Rectangle(0, 0, w, h),
output, new Rectangle(x + 1, y + 1, w, h));
// Copy a border strip from each edge of the sprite, creating
// a one pixel padding area to avoid filtering problems if the
// sprite is scaled or rotated.
BitmapContent.Copy(source, new Rectangle(0, 0, 1, h),
output, new Rectangle(x, y + 1, 1, h));
BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, h),
output, new Rectangle(x + w + 1, y + 1, 1, h));
BitmapContent.Copy(source, new Rectangle(0, 0, w, 1),
output, new Rectangle(x + 1, y, w, 1));
BitmapContent.Copy(source, new Rectangle(0, h - 1, w, 1),
output, new Rectangle(x + 1, y + h + 1, w, 1));
// Copy a single pixel from each corner of the sprite,
// filling in the corners of the one pixel padding area.
BitmapContent.Copy(source, new Rectangle(0, 0, 1, 1),
output, new Rectangle(x, y, 1, 1));
BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, 1),
output, new Rectangle(x + w + 1, y, 1, 1));
BitmapContent.Copy(source, new Rectangle(0, h - 1, 1, 1),
output, new Rectangle(x, y + h + 1, 1, 1));
BitmapContent.Copy(source, new Rectangle(w - 1, h - 1, 1, 1),
output, new Rectangle(x + w + 1, y + h + 1, 1, 1));
// Remember where we placed this sprite.
outputSprites.Add(new Rectangle(x + 1, y + 1, w, h));
}
return output;
}
///
/// Internal helper class keeps track of a sprite while it is being arranged.
///
class ArrangedSprite
{
public int Index;
public int X;
public int Y;
public int Width;
public int Height;
}
///
/// Works out where to position a single sprite.
///
static void PositionSprite(List sprites,
int index, int outputWidth)
{
int x = 0;
int y = 0;
while (true)
{
// Is this position free for us to use?
int intersects = FindIntersectingSprite(sprites, index, x, y);
if (intersects < 0)
{
sprites[index].X = x;
sprites[index].Y = y;
return;
}
// Skip past the existing sprite that we collided with.
x = sprites[intersects].X + sprites[intersects].Width;
// If we ran out of room to move to the right,
// try the next line down instead.
if (x + sprites[index].Width > outputWidth)
{
x = 0;
y++;
}
}
}
///
/// Checks if a proposed sprite position collides with anything
/// that we already arranged.
///
static int FindIntersectingSprite(List sprites,
int index, int x, int y)
{
int w = sprites[index].Width;
int h = sprites[index].Height;
for (int i = 0; i < index; i++)
{
if (sprites[i].X >= x + w)
continue;
if (sprites[i].X + sprites[i].Width <= x)
continue;
if (sprites[i].Y >= y + h)
continue;
if (sprites[i].Y + sprites[i].Height <= y)
continue;
return i;
}
return -1;
}
///
/// Comparison function for sorting sprites by size.
///
static int CompareSpriteSizes(ArrangedSprite a, ArrangedSprite b)
{
int aSize = a.Height * 1024 + a.Width;
int bSize = b.Height * 1024 + b.Width;
return bSize.CompareTo(aSize);
}
///
/// Comparison function for sorting sprites by their original indices.
///
static int CompareSpriteIndices(ArrangedSprite a, ArrangedSprite b)
{
return a.Index.CompareTo(b.Index);
}
///
/// Heuristic guesses what might be a good output width for a list of sprites.
///
static int GuessOutputWidth(List sprites)
{
// Gather the widths of all our sprites into a temporary list.
List widths = new List();
foreach (ArrangedSprite sprite in sprites)
{
widths.Add(sprite.Width);
}
// Sort the widths into ascending order.
widths.Sort();
// Extract the maximum and median widths.
int maxWidth = widths[widths.Count - 1];
int medianWidth = widths[widths.Count / 2];
// Heuristic assumes an NxN grid of median sized sprites.
int width = medianWidth * (int)Math.Round(Math.Sqrt(sprites.Count));
// Make sure we never choose anything smaller than our largest sprite.
return Math.Max(width, maxWidth);
}
}
}