SpritePacker.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // SpritePacker.cs
  4. //
  5. // Microsoft Game Technology Group
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. #region Using Statements
  10. using System;
  11. using System.Collections.Generic;
  12. using Microsoft.Xna.Framework;
  13. using Microsoft.Xna.Framework.Graphics;
  14. using Microsoft.Xna.Framework.Content.Pipeline;
  15. using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
  16. #endregion
  17. namespace SpriteSheetPipeline
  18. {
  19. /// <summary>
  20. /// Helper for arranging many small sprites into a single larger sheet.
  21. /// </summary>
  22. public static class SpritePacker
  23. {
  24. /// <summary>
  25. /// Packs a list of sprites into a single big texture,
  26. /// recording where each one was stored.
  27. /// </summary>
  28. public static BitmapContent PackSprites(IList<BitmapContent> sourceSprites,
  29. ICollection<Rectangle> outputSprites,
  30. ContentProcessorContext context)
  31. {
  32. if (sourceSprites.Count == 0)
  33. throw new InvalidContentException("There are no sprites to arrange");
  34. // Build up a list of all the sprites needing to be arranged.
  35. List<ArrangedSprite> sprites = new List<ArrangedSprite>();
  36. for (int i = 0; i < sourceSprites.Count; i++)
  37. {
  38. ArrangedSprite sprite = new ArrangedSprite();
  39. // Include a single pixel padding around each sprite, to avoid
  40. // filtering problems if the sprite is scaled or rotated.
  41. sprite.Width = sourceSprites[i].Width + 2;
  42. sprite.Height = sourceSprites[i].Height + 2;
  43. sprite.Index = i;
  44. sprites.Add(sprite);
  45. }
  46. // Sort so the largest sprites get arranged first.
  47. sprites.Sort(CompareSpriteSizes);
  48. // Work out how big the output bitmap should be.
  49. int outputWidth = GuessOutputWidth(sprites);
  50. int outputHeight = 0;
  51. int totalSpriteSize = 0;
  52. // Choose positions for each sprite, one at a time.
  53. for (int i = 0; i < sprites.Count; i++)
  54. {
  55. PositionSprite(sprites, i, outputWidth);
  56. outputHeight = Math.Max(outputHeight, sprites[i].Y + sprites[i].Height);
  57. totalSpriteSize += sprites[i].Width * sprites[i].Height;
  58. }
  59. // Sort the sprites back into index order.
  60. sprites.Sort(CompareSpriteIndices);
  61. context.Logger.LogImportantMessage(
  62. "Packed {0} sprites into a {1}x{2} sheet, {3}% efficiency",
  63. sprites.Count, outputWidth, outputHeight,
  64. totalSpriteSize * 100 / outputWidth / outputHeight);
  65. return CopySpritesToOutput(sprites, sourceSprites, outputSprites,
  66. outputWidth, outputHeight);
  67. }
  68. /// <summary>
  69. /// Once the arranging is complete, copies the bitmap data for each
  70. /// sprite to its chosen position in the single larger output bitmap.
  71. /// </summary>
  72. static BitmapContent CopySpritesToOutput(List<ArrangedSprite> sprites,
  73. IList<BitmapContent> sourceSprites,
  74. ICollection<Rectangle> outputSprites,
  75. int width, int height)
  76. {
  77. BitmapContent output = new PixelBitmapContent<Color>(width, height);
  78. foreach (ArrangedSprite sprite in sprites)
  79. {
  80. BitmapContent source = sourceSprites[sprite.Index];
  81. int x = sprite.X;
  82. int y = sprite.Y;
  83. int w = source.Width;
  84. int h = source.Height;
  85. // Copy the main sprite data to the output sheet.
  86. BitmapContent.Copy(source, new Rectangle(0, 0, w, h),
  87. output, new Rectangle(x + 1, y + 1, w, h));
  88. // Copy a border strip from each edge of the sprite, creating
  89. // a one pixel padding area to avoid filtering problems if the
  90. // sprite is scaled or rotated.
  91. BitmapContent.Copy(source, new Rectangle(0, 0, 1, h),
  92. output, new Rectangle(x, y + 1, 1, h));
  93. BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, h),
  94. output, new Rectangle(x + w + 1, y + 1, 1, h));
  95. BitmapContent.Copy(source, new Rectangle(0, 0, w, 1),
  96. output, new Rectangle(x + 1, y, w, 1));
  97. BitmapContent.Copy(source, new Rectangle(0, h - 1, w, 1),
  98. output, new Rectangle(x + 1, y + h + 1, w, 1));
  99. // Copy a single pixel from each corner of the sprite,
  100. // filling in the corners of the one pixel padding area.
  101. BitmapContent.Copy(source, new Rectangle(0, 0, 1, 1),
  102. output, new Rectangle(x, y, 1, 1));
  103. BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, 1),
  104. output, new Rectangle(x + w + 1, y, 1, 1));
  105. BitmapContent.Copy(source, new Rectangle(0, h - 1, 1, 1),
  106. output, new Rectangle(x, y + h + 1, 1, 1));
  107. BitmapContent.Copy(source, new Rectangle(w - 1, h - 1, 1, 1),
  108. output, new Rectangle(x + w + 1, y + h + 1, 1, 1));
  109. // Remember where we placed this sprite.
  110. outputSprites.Add(new Rectangle(x + 1, y + 1, w, h));
  111. }
  112. return output;
  113. }
  114. /// <summary>
  115. /// Internal helper class keeps track of a sprite while it is being arranged.
  116. /// </summary>
  117. class ArrangedSprite
  118. {
  119. public int Index;
  120. public int X;
  121. public int Y;
  122. public int Width;
  123. public int Height;
  124. }
  125. /// <summary>
  126. /// Works out where to position a single sprite.
  127. /// </summary>
  128. static void PositionSprite(List<ArrangedSprite> sprites,
  129. int index, int outputWidth)
  130. {
  131. int x = 0;
  132. int y = 0;
  133. while (true)
  134. {
  135. // Is this position free for us to use?
  136. int intersects = FindIntersectingSprite(sprites, index, x, y);
  137. if (intersects < 0)
  138. {
  139. sprites[index].X = x;
  140. sprites[index].Y = y;
  141. return;
  142. }
  143. // Skip past the existing sprite that we collided with.
  144. x = sprites[intersects].X + sprites[intersects].Width;
  145. // If we ran out of room to move to the right,
  146. // try the next line down instead.
  147. if (x + sprites[index].Width > outputWidth)
  148. {
  149. x = 0;
  150. y++;
  151. }
  152. }
  153. }
  154. /// <summary>
  155. /// Checks if a proposed sprite position collides with anything
  156. /// that we already arranged.
  157. /// </summary>
  158. static int FindIntersectingSprite(List<ArrangedSprite> sprites,
  159. int index, int x, int y)
  160. {
  161. int w = sprites[index].Width;
  162. int h = sprites[index].Height;
  163. for (int i = 0; i < index; i++)
  164. {
  165. if (sprites[i].X >= x + w)
  166. continue;
  167. if (sprites[i].X + sprites[i].Width <= x)
  168. continue;
  169. if (sprites[i].Y >= y + h)
  170. continue;
  171. if (sprites[i].Y + sprites[i].Height <= y)
  172. continue;
  173. return i;
  174. }
  175. return -1;
  176. }
  177. /// <summary>
  178. /// Comparison function for sorting sprites by size.
  179. /// </summary>
  180. static int CompareSpriteSizes(ArrangedSprite a, ArrangedSprite b)
  181. {
  182. int aSize = a.Height * 1024 + a.Width;
  183. int bSize = b.Height * 1024 + b.Width;
  184. return bSize.CompareTo(aSize);
  185. }
  186. /// <summary>
  187. /// Comparison function for sorting sprites by their original indices.
  188. /// </summary>
  189. static int CompareSpriteIndices(ArrangedSprite a, ArrangedSprite b)
  190. {
  191. return a.Index.CompareTo(b.Index);
  192. }
  193. /// <summary>
  194. /// Heuristic guesses what might be a good output width for a list of sprites.
  195. /// </summary>
  196. static int GuessOutputWidth(List<ArrangedSprite> sprites)
  197. {
  198. // Gather the widths of all our sprites into a temporary list.
  199. List<int> widths = new List<int>();
  200. foreach (ArrangedSprite sprite in sprites)
  201. {
  202. widths.Add(sprite.Width);
  203. }
  204. // Sort the widths into ascending order.
  205. widths.Sort();
  206. // Extract the maximum and median widths.
  207. int maxWidth = widths[widths.Count - 1];
  208. int medianWidth = widths[widths.Count / 2];
  209. // Heuristic assumes an NxN grid of median sized sprites.
  210. int width = medianWidth * (int)Math.Round(Math.Sqrt(sprites.Count));
  211. // Make sure we never choose anything smaller than our largest sprite.
  212. return Math.Max(width, maxWidth);
  213. }
  214. }
  215. }