SpritePacker.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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 <Urho3D/Engine/WinWrapped.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 (const String& inputFile : inputFiles)
  134. {
  135. URHO3D_LOGINFO("Checking " + inputFile + " to see if file exists.");
  136. if (!fileSystem->FileExists(inputFile))
  137. ErrorExit("File " + inputFile + " 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 (const String& path : inputFiles)
  144. {
  145. String name = ReplaceExtension(GetFileName(path), "");
  146. File file(context, path);
  147. Image image(context);
  148. if (!image.Load(file))
  149. ErrorExit("Could not load image " + path + ".");
  150. if (image.IsCompressed())
  151. ErrorExit(path + " is compressed. Compressed images are not allowed.");
  152. SharedPtr<PackerInfo> packerInfo(new PackerInfo(path, name));
  153. int imageWidth = image.GetWidth();
  154. int imageHeight = image.GetHeight();
  155. int trimOffsetX = 0;
  156. int trimOffsetY = 0;
  157. int adjustedWidth = imageWidth;
  158. int adjustedHeight = imageHeight;
  159. if (trim)
  160. {
  161. int minX = imageWidth;
  162. int minY = imageHeight;
  163. int maxX = 0;
  164. int maxY = 0;
  165. for (int y = 0; y < imageHeight; ++y)
  166. {
  167. for (int x = 0; x < imageWidth; ++x)
  168. {
  169. bool found = (image.GetPixelInt(x, y) & 0x000000ffu) != 0;
  170. if (found) {
  171. minX = Min(minX, x);
  172. minY = Min(minY, y);
  173. maxX = Max(maxX, x);
  174. maxY = Max(maxY, y);
  175. }
  176. }
  177. }
  178. trimOffsetX = minX;
  179. trimOffsetY = minY;
  180. adjustedWidth = maxX - minX + 1;
  181. adjustedHeight = maxY - minY + 1;
  182. }
  183. if (trim)
  184. {
  185. packerInfo->frameWidth = imageWidth;
  186. packerInfo->frameHeight = imageHeight;
  187. }
  188. else if (frameWidth || frameHeight)
  189. {
  190. packerInfo->frameWidth = frameWidth;
  191. packerInfo->frameHeight = frameHeight;
  192. }
  193. packerInfo->width = adjustedWidth;
  194. packerInfo->height = adjustedHeight;
  195. packerInfo->offsetX -= trimOffsetX;
  196. packerInfo->offsetY -= trimOffsetY;
  197. packerInfos.Push(packerInfo);
  198. }
  199. int packedWidth = MAX_TEXTURE_SIZE;
  200. int packedHeight = MAX_TEXTURE_SIZE;
  201. {
  202. // fill up an list of tries in increasing size and take the first win
  203. Vector<IntVector2> tries;
  204. for(unsigned x=2; x<11; ++x)
  205. {
  206. for(unsigned y=2; y<11; ++y)
  207. tries.Push(IntVector2((1u<<x), (1u<<y)));
  208. }
  209. // Load rectangles
  210. stbrp_rect* packerRects = new stbrp_rect[packerInfos.Size()];
  211. for (i32 i = 0; i < packerInfos.Size(); ++i)
  212. {
  213. PackerInfo* packerInfo = packerInfos[i];
  214. stbrp_rect* packerRect = &packerRects[i];
  215. packerRect->id = i;
  216. packerRect->h = packerInfo->height + padY;
  217. packerRect->w = packerInfo->width + padX;
  218. }
  219. bool success = false;
  220. while (tries.Size() > 0)
  221. {
  222. IntVector2 size = tries[0];
  223. tries.Erase(0);
  224. bool fit = true;
  225. int textureHeight = size.y_;
  226. int textureWidth = size.x_;
  227. if (success && textureHeight * textureWidth > packedWidth * packedHeight)
  228. continue;
  229. stbrp_context packerContext;
  230. stbrp_node packerMemory[PACKER_NUM_NODES];
  231. stbrp_init_target(&packerContext, textureWidth, textureHeight, packerMemory, PACKER_NUM_NODES);
  232. if (!stbrp_pack_rects(&packerContext, packerRects, packerInfos.Size()))
  233. {
  234. // check to see if everything fit
  235. fit = false;
  236. }
  237. if (fit)
  238. {
  239. success = true;
  240. // distribute values to packer info
  241. for (i32 i = 0; i < packerInfos.Size(); ++i)
  242. {
  243. stbrp_rect* packerRect = &packerRects[i];
  244. PackerInfo* packerInfo = packerInfos[packerRect->id];
  245. packerInfo->x = packerRect->x;
  246. packerInfo->y = packerRect->y;
  247. }
  248. packedWidth = size.x_;
  249. packedHeight = size.y_;
  250. }
  251. }
  252. delete[] packerRects;
  253. if (!success)
  254. ErrorExit("Could not allocate for all images. The max sprite sheet texture size is " + String(MAX_TEXTURE_SIZE) + "x" + String(MAX_TEXTURE_SIZE) + ".");
  255. }
  256. // create image for spritesheet
  257. Image spriteSheetImage(context);
  258. spriteSheetImage.SetSize(packedWidth, packedHeight, 4);
  259. // zero out image
  260. spriteSheetImage.SetData(nullptr);
  261. XMLFile xml(context);
  262. XMLElement root = xml.CreateRoot("TextureAtlas");
  263. root.SetAttribute("imagePath", GetFileNameAndExtension(outputFile));
  264. for (const SharedPtr<PackerInfo>& packerInfo : packerInfos)
  265. {
  266. XMLElement subTexture = root.CreateChild("SubTexture");
  267. subTexture.SetString("name", packerInfo->name);
  268. subTexture.SetInt("x", packerInfo->x + offsetX);
  269. subTexture.SetInt("y", packerInfo->y + offsetY);
  270. subTexture.SetInt("width", packerInfo->width);
  271. subTexture.SetInt("height", packerInfo->height);
  272. if (packerInfo->frameWidth || packerInfo->frameHeight)
  273. {
  274. subTexture.SetInt("frameWidth", packerInfo->frameWidth);
  275. subTexture.SetInt("frameHeight", packerInfo->frameHeight);
  276. subTexture.SetInt("offsetX", packerInfo->offsetX);
  277. subTexture.SetInt("offsetY", packerInfo->offsetY);
  278. }
  279. URHO3D_LOGINFO("Transferring " + packerInfo->path + " to sprite sheet.");
  280. File file(context, packerInfo->path);
  281. Image image(context);
  282. if (!image.Load(file))
  283. ErrorExit("Could not load image " + packerInfo->path + ".");
  284. for (int y = 0; y < packerInfo->height; ++y)
  285. {
  286. for (int x = 0; x < packerInfo->width; ++x)
  287. {
  288. unsigned color = image.GetPixelInt(x - packerInfo->offsetX, y - packerInfo->offsetY);
  289. spriteSheetImage.SetPixelInt(
  290. packerInfo->x + offsetX + x,
  291. packerInfo->y + offsetY + y, color);
  292. }
  293. }
  294. }
  295. if (debug)
  296. {
  297. unsigned OUTER_BOUNDS_DEBUG_COLOR = Color::BLUE.ToUInt();
  298. unsigned INNER_BOUNDS_DEBUG_COLOR = Color::GREEN.ToUInt();
  299. URHO3D_LOGINFO("Drawing debug information.");
  300. for (const SharedPtr<PackerInfo>& packerInfo : packerInfos)
  301. {
  302. // Draw outer bounds
  303. for (int x = 0; x < packerInfo->frameWidth; ++x)
  304. {
  305. spriteSheetImage.SetPixelInt(packerInfo->x + x, packerInfo->y, OUTER_BOUNDS_DEBUG_COLOR);
  306. spriteSheetImage.SetPixelInt(packerInfo->x + x, packerInfo->y + packerInfo->frameHeight, OUTER_BOUNDS_DEBUG_COLOR);
  307. }
  308. for (int y = 0; y < packerInfo->frameHeight; ++y)
  309. {
  310. spriteSheetImage.SetPixelInt(packerInfo->x, packerInfo->y + y, OUTER_BOUNDS_DEBUG_COLOR);
  311. spriteSheetImage.SetPixelInt(packerInfo->x + packerInfo->frameWidth, packerInfo->y + y, OUTER_BOUNDS_DEBUG_COLOR);
  312. }
  313. // Draw inner bounds
  314. for (int x = 0; x < packerInfo->width; ++x)
  315. {
  316. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + x, packerInfo->y + offsetY, INNER_BOUNDS_DEBUG_COLOR);
  317. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + x, packerInfo->y + offsetY + packerInfo->height, INNER_BOUNDS_DEBUG_COLOR);
  318. }
  319. for (int y = 0; y < packerInfo->height; ++y)
  320. {
  321. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX, packerInfo->y + offsetY + y, INNER_BOUNDS_DEBUG_COLOR);
  322. spriteSheetImage.SetPixelInt(packerInfo->x + offsetX + packerInfo->width, packerInfo->y + offsetY + y, INNER_BOUNDS_DEBUG_COLOR);
  323. }
  324. }
  325. }
  326. URHO3D_LOGINFO("Saving output image.");
  327. spriteSheetImage.SavePNG(outputFile);
  328. URHO3D_LOGINFO("Saving SpriteSheet xml file.");
  329. File spriteSheetFile(context);
  330. spriteSheetFile.Open(spriteSheetFileName, FILE_WRITE);
  331. xml.Save(spriteSheetFile);
  332. }