SpritePacker.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. // Copyright (c) 2008-2022 the Urho3D project
  2. // License: MIT
  3. #include <Urho3D/Core/Context.h>
  4. #include <Urho3D/Core/ProcessUtils.h>
  5. #include <Urho3D/Core/StringUtils.h>
  6. #include <Urho3D/IO/File.h>
  7. #include <Urho3D/IO/FileSystem.h>
  8. #include <Urho3D/IO/Log.h>
  9. #include <Urho3D/Resource/Image.h>
  10. #include <Urho3D/Resource/XMLElement.h>
  11. #include <Urho3D/Resource/XMLFile.h>
  12. #ifdef WIN32
  13. #include <windows.h>
  14. #endif
  15. #define STBRP_LARGE_RECTS
  16. #define STB_RECT_PACK_IMPLEMENTATION
  17. #include <STB/stb_rect_pack.h>
  18. #include <Urho3D/DebugNew.h>
  19. using namespace Urho3D;
  20. // number of nodes allocated to each packer info. since this packer is not suited for real time purposes we can over allocate.
  21. const int PACKER_NUM_NODES = 4096;
  22. const int MAX_TEXTURE_SIZE = 2048;
  23. int main(int argc, char** argv);
  24. void Run(Vector<String>& arguments);
  25. class PackerInfo : public RefCounted
  26. {
  27. public:
  28. String path;
  29. String name;
  30. int x{};
  31. int y{};
  32. int offsetX{};
  33. int offsetY{};
  34. int width{};
  35. int height{};
  36. int frameWidth{};
  37. int frameHeight{};
  38. PackerInfo(const String& path_, const String& name_) :
  39. path(path_),
  40. name(name_)
  41. {
  42. }
  43. ~PackerInfo() override = default;
  44. };
  45. void Help()
  46. {
  47. ErrorExit("Usage: SpritePacker -options <input file> <input file> <output png file>\n"
  48. "\n"
  49. "Options:\n"
  50. "-h Shows this help message.\n"
  51. "-px Adds x pixels of padding per image to width.\n"
  52. "-py Adds y pixels of padding per image to height.\n"
  53. "-ox Adds x pixels to the horizontal position per image.\n"
  54. "-oy Adds y pixels to the horizontal position per image.\n"
  55. "-frameHeight Sets a fixed height for image and centers within frame.\n"
  56. "-frameWidth Sets a fixed width for image and centers within frame.\n"
  57. "-trim Trims excess transparent space from individual images offsets by frame size.\n"
  58. "-xml \'path\' Generates an SpriteSheet xml file at path.\n"
  59. "-debug Draws allocation boxes on sprite.\n");
  60. }
  61. int main(int argc, char** argv)
  62. {
  63. Vector<String> arguments;
  64. #ifdef WIN32
  65. arguments = ParseArguments(GetCommandLineW());
  66. #else
  67. arguments = ParseArguments(argc, argv);
  68. #endif
  69. Run(arguments);
  70. return 0;
  71. }
  72. void Run(Vector<String>& arguments)
  73. {
  74. if (arguments.Size() < 2)
  75. Help();
  76. SharedPtr<Context> context(new Context());
  77. context->RegisterSubsystem(new FileSystem(context));
  78. context->RegisterSubsystem(new Log(context));
  79. auto* fileSystem = context->GetSubsystem<FileSystem>();
  80. Vector<String> inputFiles;
  81. String outputFile;
  82. String spriteSheetFileName;
  83. bool debug = false;
  84. unsigned padX = 0;
  85. unsigned padY = 0;
  86. unsigned offsetX = 0;
  87. unsigned offsetY = 0;
  88. unsigned frameWidth = 0;
  89. unsigned frameHeight = 0;
  90. bool help = false;
  91. bool trim = false;
  92. while (arguments.Size() > 0)
  93. {
  94. String arg = arguments[0];
  95. arguments.Erase(0);
  96. if (arg.Empty())
  97. continue;
  98. if (arg.StartsWith("-"))
  99. {
  100. if (arg == "-px") { padX = ToUInt(arguments[0]); arguments.Erase(0); }
  101. else if (arg == "-py") { padY = ToUInt(arguments[0]); arguments.Erase(0); }
  102. else if (arg == "-ox") { offsetX = ToUInt(arguments[0]); arguments.Erase(0); }
  103. else if (arg == "-oy") { offsetY = ToUInt(arguments[0]); arguments.Erase(0); }
  104. else if (arg == "-frameWidth") { frameWidth = ToUInt(arguments[0]); arguments.Erase(0); }
  105. else if (arg == "-frameHeight") { frameHeight = ToUInt(arguments[0]); arguments.Erase(0); }
  106. else if (arg == "-trim") { trim = true; }
  107. else if (arg == "-xml") { spriteSheetFileName = arguments[0]; arguments.Erase(0); }
  108. else if (arg == "-h") { help = true; break; }
  109. else if (arg == "-debug") { debug = true; }
  110. }
  111. else
  112. inputFiles.Push(arg);
  113. }
  114. if (help)
  115. Help();
  116. if (inputFiles.Size() < 2)
  117. ErrorExit("An input and output file must be specified.");
  118. if (frameWidth ^ frameHeight)
  119. ErrorExit("Both frameHeight and frameWidth must be omitted or specified.");
  120. // take last input file as output
  121. if (inputFiles.Size() > 1)
  122. {
  123. outputFile = inputFiles[inputFiles.Size() - 1];
  124. URHO3D_LOGINFO("Output file set to " + outputFile + ".");
  125. inputFiles.Erase(inputFiles.Size() - 1);
  126. }
  127. // set spritesheet name to outputfile.xml if not specified
  128. if (spriteSheetFileName.Empty())
  129. spriteSheetFileName = ReplaceExtension(outputFile, ".xml");
  130. if (GetParentPath(spriteSheetFileName) != GetParentPath(outputFile))
  131. ErrorExit("Both output xml and png must be in the same folder");
  132. // check all input files exist
  133. for (unsigned i = 0; i < inputFiles.Size(); ++i)
  134. {
  135. URHO3D_LOGINFO("Checking " + inputFiles[i] + " to see if file exists.");
  136. if (!fileSystem->FileExists(inputFiles[i]))
  137. ErrorExit("File " + inputFiles[i] + " does not exist.");
  138. }
  139. // Set the max offset equal to padding to prevent images from going out of bounds
  140. offsetX = Min((int)offsetX, (int)padX);
  141. offsetY = Min((int)offsetY, (int)padY);
  142. Vector<SharedPtr<PackerInfo>> packerInfos;
  143. for (unsigned i = 0; i < inputFiles.Size(); ++i)
  144. {
  145. String path = inputFiles[i];
  146. String name = ReplaceExtension(GetFileName(path), "");
  147. File file(context, path);
  148. Image image(context);
  149. if (!image.Load(file))
  150. ErrorExit("Could not load image " + path + ".");
  151. if (image.IsCompressed())
  152. ErrorExit(path + " is compressed. Compressed images are not allowed.");
  153. SharedPtr<PackerInfo> packerInfo(new PackerInfo(path, name));
  154. int imageWidth = image.GetWidth();
  155. int imageHeight = image.GetHeight();
  156. int trimOffsetX = 0;
  157. int trimOffsetY = 0;
  158. int adjustedWidth = imageWidth;
  159. int adjustedHeight = imageHeight;
  160. if (trim)
  161. {
  162. int minX = imageWidth;
  163. int minY = imageHeight;
  164. int maxX = 0;
  165. int maxY = 0;
  166. for (int y = 0; y < imageHeight; ++y)
  167. {
  168. for (int x = 0; x < imageWidth; ++x)
  169. {
  170. bool found = (image.GetPixelInt(x, y) & 0x000000ffu) != 0;
  171. if (found) {
  172. minX = Min(minX, x);
  173. minY = Min(minY, y);
  174. maxX = Max(maxX, x);
  175. maxY = Max(maxY, y);
  176. }
  177. }
  178. }
  179. trimOffsetX = minX;
  180. trimOffsetY = minY;
  181. adjustedWidth = maxX - minX + 1;
  182. adjustedHeight = maxY - minY + 1;
  183. }
  184. if (trim)
  185. {
  186. packerInfo->frameWidth = imageWidth;
  187. packerInfo->frameHeight = imageHeight;
  188. }
  189. else if (frameWidth || frameHeight)
  190. {
  191. packerInfo->frameWidth = frameWidth;
  192. packerInfo->frameHeight = frameHeight;
  193. }
  194. packerInfo->width = adjustedWidth;
  195. packerInfo->height = adjustedHeight;
  196. packerInfo->offsetX -= trimOffsetX;
  197. packerInfo->offsetY -= trimOffsetY;
  198. packerInfos.Push(packerInfo);
  199. }
  200. int packedWidth = MAX_TEXTURE_SIZE;
  201. int packedHeight = MAX_TEXTURE_SIZE;
  202. {
  203. // fill up an list of tries in increasing size and take the first win
  204. Vector<IntVector2> tries;
  205. for(unsigned x=2; x<11; ++x)
  206. {
  207. for(unsigned y=2; y<11; ++y)
  208. tries.Push(IntVector2((1u<<x), (1u<<y)));
  209. }
  210. // load rectangles
  211. auto* packerRects = new stbrp_rect[packerInfos.Size()];
  212. for (unsigned i = 0; i < packerInfos.Size(); ++i)
  213. {
  214. PackerInfo* packerInfo = packerInfos[i];
  215. stbrp_rect* packerRect = &packerRects[i];
  216. packerRect->id = i;
  217. packerRect->h = packerInfo->height + padY;
  218. packerRect->w = packerInfo->width + padX;
  219. }
  220. bool success = false;
  221. while (tries.Size() > 0)
  222. {
  223. IntVector2 size = tries[0];
  224. tries.Erase(0);
  225. bool fit = true;
  226. int textureHeight = size.y_;
  227. int textureWidth = size.x_;
  228. if (success && textureHeight * textureWidth > packedWidth * packedHeight)
  229. continue;
  230. stbrp_context packerContext;
  231. stbrp_node packerMemory[PACKER_NUM_NODES];
  232. stbrp_init_target(&packerContext, textureWidth, textureHeight, packerMemory, PACKER_NUM_NODES);
  233. if (!stbrp_pack_rects(&packerContext, packerRects, packerInfos.Size()))
  234. {
  235. // check to see if everything fit
  236. fit = false;
  237. }
  238. if (fit)
  239. {
  240. success = true;
  241. // distribute values to packer info
  242. for (unsigned i = 0; i < packerInfos.Size(); ++i)
  243. {
  244. stbrp_rect* packerRect = &packerRects[i];
  245. PackerInfo* packerInfo = packerInfos[packerRect->id];
  246. packerInfo->x = packerRect->x;
  247. packerInfo->y = packerRect->y;
  248. }
  249. packedWidth = size.x_;
  250. packedHeight = size.y_;
  251. }
  252. }
  253. delete[] packerRects;
  254. if (!success)
  255. ErrorExit("Could not allocate for all images. The max sprite sheet texture size is " + String(MAX_TEXTURE_SIZE) + "x" + String(MAX_TEXTURE_SIZE) + ".");
  256. }
  257. // create image for spritesheet
  258. Image spriteSheetImage(context);
  259. spriteSheetImage.SetSize(packedWidth, packedHeight, 4);
  260. // zero out image
  261. spriteSheetImage.SetData(nullptr);
  262. XMLFile xml(context);
  263. XMLElement root = xml.CreateRoot("TextureAtlas");
  264. root.SetAttribute("imagePath", GetFileNameAndExtension(outputFile));
  265. for (unsigned i = 0; i < packerInfos.Size(); ++i)
  266. {
  267. SharedPtr<PackerInfo> packerInfo = packerInfos[i];
  268. XMLElement subTexture = root.CreateChild("SubTexture");
  269. subTexture.SetString("name", packerInfo->name);
  270. subTexture.SetInt("x", packerInfo->x + offsetX);
  271. subTexture.SetInt("y", packerInfo->y + offsetY);
  272. subTexture.SetInt("width", packerInfo->width);
  273. subTexture.SetInt("height", packerInfo->height);
  274. if (packerInfo->frameWidth || packerInfo->frameHeight)
  275. {
  276. subTexture.SetInt("frameWidth", packerInfo->frameWidth);
  277. subTexture.SetInt("frameHeight", packerInfo->frameHeight);
  278. subTexture.SetInt("offsetX", packerInfo->offsetX);
  279. subTexture.SetInt("offsetY", packerInfo->offsetY);
  280. }
  281. URHO3D_LOGINFO("Transferring " + packerInfo->path + " to sprite sheet.");
  282. File file(context, packerInfo->path);
  283. Image image(context);
  284. if (!image.Load(file))
  285. ErrorExit("Could not load image " + packerInfo->path + ".");
  286. for (int y = 0; y < packerInfo->height; ++y)
  287. {
  288. for (int x = 0; x < packerInfo->width; ++x)
  289. {
  290. unsigned color = image.GetPixelInt(x - packerInfo->offsetX, y - packerInfo->offsetY);
  291. spriteSheetImage.SetPixelInt(
  292. packerInfo->x + offsetX + x,
  293. packerInfo->y + offsetY + y, color);
  294. }
  295. }
  296. }
  297. if (debug)
  298. {
  299. unsigned OUTER_BOUNDS_DEBUG_COLOR = Color::BLUE.ToUInt();
  300. unsigned INNER_BOUNDS_DEBUG_COLOR = Color::GREEN.ToUInt();
  301. URHO3D_LOGINFO("Drawing debug information.");
  302. for (unsigned i = 0; i < packerInfos.Size(); ++i)
  303. {
  304. SharedPtr<PackerInfo> packerInfo = packerInfos[i];
  305. // Draw outer bounds
  306. for (int x = 0; x < packerInfo->frameWidth; ++x)
  307. {
  308. spriteSheetImage.SetPixelInt(packerInfo->x + x, packerInfo->y, OUTER_BOUNDS_DEBUG_COLOR);
  309. spriteSheetImage.SetPixelInt(packerInfo->x + x, packerInfo->y + packerInfo->frameHeight, OUTER_BOUNDS_DEBUG_COLOR);
  310. }
  311. for (int y = 0; y < packerInfo->frameHeight; ++y)
  312. {
  313. spriteSheetImage.SetPixelInt(packerInfo->x, packerInfo->y + y, OUTER_BOUNDS_DEBUG_COLOR);
  314. spriteSheetImage.SetPixelInt(packerInfo->x + packerInfo->frameWidth, packerInfo->y + y, OUTER_BOUNDS_DEBUG_COLOR);
  315. }
  316. // Draw inner bounds
  317. for (int x = 0; x < packerInfo->width; ++x)
  318. {
  319. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + x, packerInfo->y + offsetY, INNER_BOUNDS_DEBUG_COLOR);
  320. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + x, packerInfo->y + offsetY + packerInfo->height, INNER_BOUNDS_DEBUG_COLOR);
  321. }
  322. for (int y = 0; y < packerInfo->height; ++y)
  323. {
  324. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX, packerInfo->y + offsetY + y, INNER_BOUNDS_DEBUG_COLOR);
  325. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + packerInfo->width, packerInfo->y + offsetY + y, INNER_BOUNDS_DEBUG_COLOR);
  326. }
  327. }
  328. }
  329. URHO3D_LOGINFO("Saving output image.");
  330. spriteSheetImage.SavePNG(outputFile);
  331. URHO3D_LOGINFO("Saving SpriteSheet xml file.");
  332. File spriteSheetFile(context);
  333. spriteSheetFile.Open(spriteSheetFileName, FILE_WRITE);
  334. xml.Save(spriteSheetFile);
  335. }