3
0

AtlasBuilderWorker.cpp 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "AtlasBuilderWorker.h"
  9. #include <AzCore/Math/MathIntrinsics.h>
  10. #include <AzCore/std/string/conversions.h>
  11. #include <AzCore/Serialization/Utils.h>
  12. #include <AzCore/Serialization/SerializeContext.h>
  13. #include <AzCore/std/sort.h>
  14. #include <AzCore/IO/FileIO.h>
  15. #include <AzCore/IO/SystemFile.h>
  16. #include <AzCore/std/string/regex.h>
  17. #include <AzFramework/StringFunc/StringFunc.h>
  18. #include <AzFramework/API/ApplicationAPI.h>
  19. #include <AzFramework/IO/LocalFileIO.h>
  20. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  21. #include <Atom/ImageProcessing/ImageObject.h>
  22. #include <Atom/ImageProcessing/ImageProcessingBus.h>
  23. #include <Atom/ImageProcessing/PixelFormats.h>
  24. #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
  25. #include <qimage.h>
  26. #include <QString>
  27. #include <QDir>
  28. #include <qfileinfo.h>
  29. namespace TextureAtlasBuilder
  30. {
  31. //! Used for sorting ImageDimensions
  32. bool operator<(ImageDimension a, ImageDimension b);
  33. //! Used to expose the ImageDimension in a pair to AZStd::Sort
  34. bool operator<(IndexImageDimension a, IndexImageDimension b);
  35. //! Returns true if two coordinate sets overlap
  36. bool Collides(AtlasCoordinates a, AtlasCoordinates b);
  37. //! Returns true if item collides with any object in list
  38. bool Collides(AtlasCoordinates item, AZStd::vector<AtlasCoordinates> list);
  39. //! Returns the portion of the second item that overlaps with the first
  40. AtlasCoordinates GetOverlap(AtlasCoordinates a, AtlasCoordinates b);
  41. //! Performs an operation that copies a pixel to the output
  42. void SetPixels(AZ::u8* dest, const AZ::u8* source, int destBytes);
  43. //! Checks if we can insert an image into a slot
  44. bool CanInsert(AtlasCoordinates slot, ImageDimension image, int padding, int farRight, int farBot);
  45. //! Adds the necessary padding to an Atlas Coordinate
  46. void AddPadding(AtlasCoordinates& slot, int padding, int farRight, int farBot);
  47. //! Counts leading zeros
  48. uint32_t CountLeadingZeros32(uint32_t x)
  49. {
  50. return x == 0 ? 32 : az_clz_u32(x);
  51. }
  52. //! Integer log2
  53. uint32_t IntegerLog2(uint32_t x)
  54. {
  55. return 31 - CountLeadingZeros32(x);
  56. }
  57. bool IsFolderPath(const AZStd::string& path)
  58. {
  59. bool hasExtension = AzFramework::StringFunc::Path::HasExtension(path.c_str());
  60. return !hasExtension;
  61. }
  62. bool HasTrailingSlash(const AZStd::string& path)
  63. {
  64. size_t pathLength = path.size();
  65. return (pathLength > 0 && (path.at(pathLength - 1) == '/' || path.at(pathLength - 1) == '\\'));
  66. }
  67. bool GetCanonicalPathFromFullPath(const AZStd::string& fullPath, AZStd::string& canonicalPathOut)
  68. {
  69. AZStd::string curPath = fullPath;
  70. // We avoid using LocalFileIO::ConvertToAbsolutePath for this because it does not behave consistently across platforms.
  71. // On non-Windows platforms, LocalFileIO::ConvertToAbsolutePath requires that the path exist, otherwise the path
  72. // remains unchanged. This won't work for paths that include wildcards.
  73. // Also, on non-Windows platforms, if the path is already a full path, it will remain unchanged even if it contains
  74. // "./" or "../" somewhere other than the beginning of the path
  75. // Normalize path
  76. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, curPath);
  77. const AZStd::string slash("/");
  78. // Replace "/./" occurrances with "/"
  79. const AZStd::string slashDotSlash("/./");
  80. bool replaced = false;
  81. do
  82. {
  83. // Replace first occurrance
  84. replaced = AzFramework::StringFunc::Replace(curPath, slashDotSlash.c_str(), slash.c_str(), false, true, false);
  85. } while (replaced);
  86. // Replace "/xxx/../" with "/"
  87. const AZStd::regex slashDotDotSlash("\\/[^/.]*\\/\\.\\.\\/");
  88. AZStd::string prevPath;
  89. while (prevPath != curPath)
  90. {
  91. prevPath = curPath;
  92. curPath = AZStd::regex_replace(prevPath, slashDotDotSlash, slash, AZStd::regex_constants::match_flag_type::format_first_only);
  93. }
  94. if ((curPath.find("..") != AZStd::string::npos) || (curPath.find("./") != AZStd::string::npos) || (curPath.find("/.") != AZStd::string::npos))
  95. {
  96. return false;
  97. }
  98. canonicalPathOut = curPath;
  99. return true;
  100. }
  101. bool ResolveRelativePath(const AZStd::string& relativePath, const AZStd::string& watchDirectory, AZStd::string& resolvedFullPathOut)
  102. {
  103. bool resolved = false;
  104. if (relativePath[0] == '@')
  105. {
  106. // Get full path by resolving the alias at the front of the path
  107. char resolvedPath[AZ_MAX_PATH_LEN];
  108. AZ::IO::FileIOBase::GetInstance()->ResolvePath(relativePath.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
  109. resolvedFullPathOut = resolvedPath;
  110. resolved = true;
  111. }
  112. else
  113. {
  114. // Get full path by appending the relative path to the watch directory
  115. AZStd::string fullPath = watchDirectory;
  116. fullPath.append("/");
  117. fullPath.append(relativePath);
  118. // Resolve to canonical path (remove "./" and "../")
  119. resolved = GetCanonicalPathFromFullPath(fullPath, resolvedFullPathOut);
  120. }
  121. return resolved;
  122. }
  123. bool GetAbsoluteSourcePathFromRelativePath(const AZStd::string& relativeSourcePath, AZStd::string& absoluteSourcePathOut)
  124. {
  125. bool result = false;
  126. AZ::Data::AssetInfo info;
  127. AZStd::string watchFolder;
  128. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath, relativeSourcePath.c_str(), info, watchFolder);
  129. if (result)
  130. {
  131. absoluteSourcePathOut = AZStd::string::format("%s/%s", watchFolder.c_str(), info.m_relativePath.c_str());
  132. // Normalize path
  133. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, absoluteSourcePathOut);
  134. }
  135. return result;
  136. }
  137. // Reflect the input parameters
  138. void AtlasBuilderInput::Reflect(AZ::ReflectContext* context)
  139. {
  140. if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
  141. {
  142. serialize->Class<AtlasBuilderInput>()
  143. ->Version(1)
  144. ->Field("Force Square", &AtlasBuilderInput::m_forceSquare)
  145. ->Field("Force Power of Two", &AtlasBuilderInput::m_forcePowerOf2)
  146. ->Field("Include White Texture", &AtlasBuilderInput::m_includeWhiteTexture)
  147. ->Field("Maximum Dimension", &AtlasBuilderInput::m_maxDimension)
  148. ->Field("Padding", &AtlasBuilderInput::m_padding)
  149. ->Field("UnusedColor", &AtlasBuilderInput::m_unusedColor)
  150. ->Field("PresetName", &AtlasBuilderInput::m_presetName)
  151. ->Field("Textures to Add", &AtlasBuilderInput::m_filePaths);
  152. }
  153. }
  154. // Supports a custom parser format
  155. AtlasBuilderInput AtlasBuilderInput::ReadFromFile(const AZStd::string& path, const AZStd::string& directory, bool& valid)
  156. {
  157. // Open the file
  158. AZ::IO::FileIOBase* input = AZ::IO::FileIOBase::GetInstance();
  159. AZ::IO::HandleType handle;
  160. input->Open(path.c_str(), AZ::IO::OpenMode::ModeRead, handle);
  161. // Read the file
  162. AZ::u64 size;
  163. input->Size(handle, size);
  164. char* buffer = new char[size + 1];
  165. input->Read(handle, buffer, size);
  166. buffer[size] = 0;
  167. // Close the file
  168. input->Close(handle);
  169. // Prepare the output
  170. AtlasBuilderInput data;
  171. // Parse the input into lines
  172. AZStd::vector<AZStd::string> lines;
  173. AzFramework::StringFunc::Tokenize(buffer, lines, "\n\t");
  174. delete[] buffer;
  175. // Parse the individual lines
  176. for (auto line : lines)
  177. {
  178. line = AzFramework::StringFunc::TrimWhiteSpace(line, true, true);
  179. // Check for comments and empty lines
  180. if ((line.length() >= 2 && line[0] == '/' && line[1] == '/') || line.length() < 1)
  181. {
  182. continue;
  183. }
  184. else if (line.find('=') != -1)
  185. {
  186. AZStd::vector<AZStd::string> args;
  187. AzFramework::StringFunc::Tokenize(line.c_str(), args, '=', true, true);
  188. if (args.size() > 2)
  189. {
  190. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to parse line: Excessive '=' symbols were found: \"%s\"", line.c_str()).c_str());
  191. valid = false;
  192. }
  193. // Trim whitespace
  194. args[0] = AzFramework::StringFunc::TrimWhiteSpace(args[0], true, true);
  195. args[1] = AzFramework::StringFunc::TrimWhiteSpace(args[1], true, true);
  196. // No case sensitivity for property names
  197. AZStd::to_lower(args[0].begin(), args[0].end());
  198. // Keep track of if the value is rejected
  199. bool accepted = false;
  200. if (args[0] == "square")
  201. {
  202. accepted = AzFramework::StringFunc::LooksLikeBool(args[1].c_str());
  203. if (accepted)
  204. {
  205. data.m_forceSquare = AzFramework::StringFunc::ToBool(args[1].c_str());
  206. }
  207. }
  208. else if (args[0] == "poweroftwo")
  209. {
  210. accepted = AzFramework::StringFunc::LooksLikeBool(args[1].c_str());
  211. if (accepted)
  212. {
  213. data.m_forcePowerOf2 = AzFramework::StringFunc::ToBool(args[1].c_str());
  214. }
  215. }
  216. else if (args[0] == "whitetexture")
  217. {
  218. accepted = AzFramework::StringFunc::LooksLikeBool(args[1].c_str());
  219. if (accepted)
  220. {
  221. data.m_includeWhiteTexture = AzFramework::StringFunc::ToBool(args[1].c_str());
  222. }
  223. }
  224. else if (args[0] == "maxdimension")
  225. {
  226. accepted = AzFramework::StringFunc::LooksLikeInt(args[1].c_str());
  227. if (accepted)
  228. {
  229. data.m_maxDimension = AzFramework::StringFunc::ToInt(args[1].c_str());
  230. }
  231. }
  232. else if (args[0] == "padding")
  233. {
  234. accepted = AzFramework::StringFunc::LooksLikeInt(args[1].c_str());
  235. if (accepted)
  236. {
  237. data.m_padding = AzFramework::StringFunc::ToInt(args[1].c_str());
  238. }
  239. }
  240. else if (args[0] == "unusedcolor")
  241. {
  242. accepted = args[1].at(0) == '#' && args[1].length() == 9;
  243. if (accepted)
  244. {
  245. AZStd::string color = AZStd::string::format("%s%s%s%s", args[1].substr(7).c_str(), args[1].substr(5, 2).c_str(),
  246. args[1].substr(3, 2).c_str(), args[1].substr(1, 2).c_str());
  247. data.m_unusedColor.FromU32(static_cast<AZ::u32>(AZStd::stoul(color, nullptr, 16)));
  248. }
  249. }
  250. else if (args[0] == "presetname")
  251. {
  252. accepted = true;
  253. data.m_presetName = args[1];
  254. }
  255. else
  256. {
  257. // Supress accepted error because this error superceeds it
  258. accepted = true;
  259. valid = false;
  260. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to parse line: Unrecognized property: \"%s\"", args[0].c_str()).c_str());
  261. }
  262. // If the property is recognized but the value is rejected, fail the job
  263. if (!accepted)
  264. {
  265. valid = false;
  266. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to parse line: Invalid value assigned to property: Property: \"%s\" Value: \"%s\"", args[0].c_str(), args[1].c_str()).c_str());
  267. }
  268. }
  269. else if ((line[0] == '-'))
  270. {
  271. // Remove image files
  272. AZStd::string remove = line.substr(1);
  273. remove = AzFramework::StringFunc::TrimWhiteSpace(remove, true, true);
  274. if (remove.find('*') != -1)
  275. {
  276. AZStd::string resolvedAbsolutePath;
  277. bool resolved = ResolveRelativePath(remove, directory, resolvedAbsolutePath);
  278. if (resolved)
  279. {
  280. RemoveFilesUsingWildCard(data.m_filePaths, resolvedAbsolutePath);
  281. }
  282. else
  283. {
  284. valid = false;
  285. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to resolve relative path: %s", remove.c_str()).c_str());
  286. }
  287. }
  288. else if (IsFolderPath(remove))
  289. {
  290. AZStd::string resolvedAbsolutePath;
  291. bool resolved = ResolveRelativePath(remove, directory, resolvedAbsolutePath);
  292. if (resolved)
  293. {
  294. RemoveFolderContents(data.m_filePaths, resolvedAbsolutePath);
  295. }
  296. else
  297. {
  298. valid = false;
  299. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to resolve relative path: %s", remove.c_str()).c_str());
  300. }
  301. }
  302. else
  303. {
  304. // Get the full path to the source image from the relative source path
  305. AZStd::string fullSourceAssetPathName;
  306. bool fullPathFound = GetAbsoluteSourcePathFromRelativePath(remove, fullSourceAssetPathName);
  307. if (!fullPathFound)
  308. {
  309. // Try to resolve relative path as it might be using "./" or "../"
  310. fullPathFound = ResolveRelativePath(remove, directory, fullSourceAssetPathName);
  311. }
  312. if (fullPathFound)
  313. {
  314. for (size_t i = 0; i < data.m_filePaths.size(); ++i)
  315. {
  316. if (data.m_filePaths[i] == fullSourceAssetPathName)
  317. {
  318. data.m_filePaths.erase(data.m_filePaths.begin() + i);
  319. }
  320. }
  321. }
  322. else
  323. {
  324. valid = false;
  325. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to get source asset path for image: %s", remove.c_str()).c_str());
  326. }
  327. }
  328. }
  329. else
  330. {
  331. // Add image files
  332. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, line);
  333. bool duplicate = false;
  334. if (line.find('*') != -1)
  335. {
  336. AZStd::string resolvedAbsolutePath;
  337. bool resolved = ResolveRelativePath(line, directory, resolvedAbsolutePath);
  338. if (resolved)
  339. {
  340. AddFilesUsingWildCard(data.m_filePaths, resolvedAbsolutePath);
  341. }
  342. else
  343. {
  344. valid = false;
  345. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to resolve relative path: %s", line.c_str()).c_str());
  346. }
  347. }
  348. else if (IsFolderPath(line))
  349. {
  350. AZStd::string resolvedAbsolutePath;
  351. bool resolved = ResolveRelativePath(line, directory, resolvedAbsolutePath);
  352. if (resolved)
  353. {
  354. AddFolderContents(data.m_filePaths, resolvedAbsolutePath, valid);
  355. }
  356. else
  357. {
  358. valid = false;
  359. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to resolve relative path: %s", line.c_str()).c_str());
  360. }
  361. }
  362. else
  363. {
  364. // Get the full path to the source image from the relative source path
  365. AZStd::string fullSourceAssetPathName;
  366. bool fullPathFound = GetAbsoluteSourcePathFromRelativePath(line, fullSourceAssetPathName);
  367. if (!fullPathFound)
  368. {
  369. // Try to resolve relative path as it might be using "./" or "../"
  370. fullPathFound = ResolveRelativePath(line, directory, fullSourceAssetPathName);
  371. }
  372. if (fullPathFound)
  373. {
  374. // Prevent duplicates
  375. for (size_t i = 0; i < data.m_filePaths.size() && !duplicate; ++i)
  376. {
  377. duplicate = data.m_filePaths[i] == fullSourceAssetPathName;
  378. }
  379. if (!duplicate)
  380. {
  381. data.m_filePaths.push_back(fullSourceAssetPathName);
  382. }
  383. }
  384. else
  385. {
  386. valid = false;
  387. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to get source asset path for image: %s", line.c_str()).c_str());
  388. }
  389. }
  390. }
  391. }
  392. return data;
  393. }
  394. void AtlasBuilderInput::AddFilesUsingWildCard(AZStd::vector<AZStd::string>& paths, const AZStd::string& insert)
  395. {
  396. const AZStd::string& fullPath = insert;
  397. AZStd::vector<AZStd::string> candidates;
  398. AZStd::string fixedPath = fullPath.substr(0, fullPath.find('*'));
  399. fixedPath = fixedPath.substr(0, fixedPath.find_last_of('/'));
  400. candidates.push_back(fixedPath);
  401. AZStd::vector<AZStd::string> wildPath;
  402. AzFramework::StringFunc::Tokenize(fullPath.substr(fixedPath.length()).c_str(), wildPath, "/");
  403. for (size_t i = 0; i < wildPath.size() && candidates.size() > 0; ++i)
  404. {
  405. AZStd::vector<AZStd::string> nextCandidates;
  406. for (size_t j = 0; j < candidates.size(); ++j)
  407. {
  408. AZStd::string compare = AZStd::string::format("%s/%s", candidates[j].c_str(), wildPath[i].c_str());
  409. QDir inputFolder(candidates[j].c_str());
  410. if (inputFolder.exists())
  411. {
  412. QFileInfoList entries = inputFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
  413. for (const QFileInfo& entry : entries)
  414. {
  415. AZStd::string child = (entry.filePath().toStdString()).c_str();
  416. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, child);
  417. if (DoesPathnameMatchWildCard(compare, child))
  418. {
  419. nextCandidates.push_back(child);
  420. }
  421. }
  422. }
  423. }
  424. candidates = nextCandidates;
  425. }
  426. for (size_t i = 0; i < candidates.size(); ++i)
  427. {
  428. if (!IsFolderPath(candidates[i]) && !HasTrailingSlash(fullPath))
  429. {
  430. AZStd::string ext;
  431. AzFramework::StringFunc::Path::GetExtension(candidates[i].c_str(), ext, false);
  432. if (ext != "dds")
  433. {
  434. bool duplicate = false;
  435. for (size_t j = 0; j < paths.size() && !duplicate; ++j)
  436. {
  437. duplicate = paths[j] == candidates[i];
  438. }
  439. if (!duplicate)
  440. {
  441. paths.push_back(candidates[i]);
  442. }
  443. }
  444. }
  445. else if (IsFolderPath(candidates[i]) && HasTrailingSlash(fullPath))
  446. {
  447. bool waste = true;
  448. AddFolderContents(paths, candidates[i], waste);
  449. }
  450. }
  451. }
  452. void AtlasBuilderInput::RemoveFilesUsingWildCard(AZStd::vector<AZStd::string>& paths, const AZStd::string& remove)
  453. {
  454. bool isDir = (remove.at(remove.length() - 1) == '/');
  455. for (size_t i = 0; i < paths.size(); ++i)
  456. {
  457. if (isDir ? DoesWildCardDirectoryIncludePathname(remove, paths[i]) : DoesPathnameMatchWildCard(remove, paths[i]))
  458. {
  459. paths.erase(paths.begin() + i);
  460. --i;
  461. }
  462. }
  463. }
  464. // Tells us if the child follows the rule
  465. bool AtlasBuilderInput::DoesPathnameMatchWildCard(const AZStd::string& rule, const AZStd::string& child)
  466. {
  467. AZStd::vector<AZStd::string> rulePathTokens;
  468. AzFramework::StringFunc::Tokenize(rule.c_str(), rulePathTokens, "/");
  469. AZStd::vector<AZStd::string> pathTokens;
  470. AzFramework::StringFunc::Tokenize(child.c_str(), pathTokens, "/");
  471. if (rulePathTokens.size() != pathTokens.size())
  472. {
  473. return false;
  474. }
  475. for (size_t i = 0; i < rulePathTokens.size(); ++i)
  476. {
  477. if (!TokenMatchesWildcard(rulePathTokens[i], pathTokens[i]))
  478. {
  479. return false;
  480. }
  481. }
  482. return true;
  483. }
  484. bool AtlasBuilderInput::DoesWildCardDirectoryIncludePathname(const AZStd::string& rule, const AZStd::string& child)
  485. {
  486. AZStd::vector<AZStd::string> rulePathTokens;
  487. AzFramework::StringFunc::Tokenize(rule.c_str(), rulePathTokens, "/");
  488. AZStd::vector<AZStd::string> pathTokens;
  489. AzFramework::StringFunc::Tokenize(child.c_str(), pathTokens, "/");
  490. if (rulePathTokens.size() >= pathTokens.size())
  491. {
  492. return false;
  493. }
  494. for (size_t i = 0; i < rulePathTokens.size(); ++i)
  495. {
  496. if (!TokenMatchesWildcard(rulePathTokens[i], pathTokens[i]))
  497. {
  498. return false;
  499. }
  500. }
  501. return true;
  502. }
  503. bool AtlasBuilderInput::TokenMatchesWildcard(const AZStd::string& rule, const AZStd::string& child)
  504. {
  505. AZStd::vector<AZStd::string> ruleTokens;
  506. AzFramework::StringFunc::Tokenize(rule.c_str(), ruleTokens, "*");
  507. size_t pos = 0;
  508. int token = 0;
  509. if (rule.at(0) != '*' && child.find(ruleTokens[0]) != 0)
  510. {
  511. return false;
  512. }
  513. while (pos != AZStd::string::npos && token < ruleTokens.size())
  514. {
  515. pos = child.find(ruleTokens[token], pos);
  516. if (pos != AZStd::string::npos)
  517. {
  518. pos += ruleTokens[token].size();
  519. }
  520. ++token;
  521. }
  522. return pos == child.size() || (pos != AZStd::string::npos && rule.at(rule.length() - 1) == '*');
  523. }
  524. // Replaces all folder paths with the files they contain
  525. void AtlasBuilderInput::AddFolderContents(AZStd::vector<AZStd::string>& paths, const AZStd::string& insert, bool& valid)
  526. {
  527. QDir inputFolder(insert.c_str());
  528. if (inputFolder.exists())
  529. {
  530. QFileInfoList entries = inputFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files);
  531. for (const QFileInfo& entry : entries)
  532. {
  533. AZStd::string child = (entry.filePath().toStdString()).c_str();
  534. AZStd::string ext;
  535. bool isDir = !AzFramework::StringFunc::Path::GetExtension(child.c_str(), ext, false);
  536. if (isDir)
  537. {
  538. AddFolderContents(paths, child, valid);
  539. }
  540. else if (ext != "dds")
  541. {
  542. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, child);
  543. bool duplicate = false;
  544. for (size_t i = 0; i < paths.size() && !duplicate; ++i)
  545. {
  546. duplicate = paths[i] == child;
  547. }
  548. if (!duplicate)
  549. {
  550. paths.push_back(child);
  551. }
  552. }
  553. }
  554. }
  555. else
  556. {
  557. valid = false;
  558. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to find requested directory: %s", insert.c_str()).c_str());
  559. }
  560. }
  561. // Removes all of the contents of a folder
  562. void AtlasBuilderInput::RemoveFolderContents(AZStd::vector<AZStd::string>& paths, const AZStd::string& remove)
  563. {
  564. AZStd::string folder = remove;
  565. AzFramework::StringFunc::Strip(folder, "/", false, false, true);
  566. folder.append("/");
  567. for (size_t i = 0; i < paths.size(); ++i)
  568. {
  569. if (paths[i].find(folder) == 0)
  570. {
  571. paths.erase(paths.begin() + i);
  572. --i;
  573. }
  574. }
  575. }
  576. // Note - Shutdown will be called on a different thread than your process job thread
  577. void AtlasBuilderWorker::ShutDown() { m_isShuttingDown = true; }
  578. void AtlasBuilderWorker::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request,
  579. AssetBuilderSDK::CreateJobsResponse& response)
  580. {
  581. // Read in settings/filepaths to set dependencies
  582. AZStd::string fullPath;
  583. AzFramework::StringFunc::Path::Join(
  584. request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), fullPath, true, true);
  585. // Check if input is valid
  586. bool valid = true;
  587. AtlasBuilderInput input = AtlasBuilderInput::ReadFromFile(fullPath, request.m_watchFolder, valid);
  588. // Set dependencies
  589. for (int i = 0; i < input.m_filePaths.size(); ++i)
  590. {
  591. AssetBuilderSDK::SourceFileDependency dependency;
  592. dependency.m_sourceFileDependencyPath = input.m_filePaths[i].c_str();
  593. response.m_sourceFileDependencyList.push_back(dependency);
  594. }
  595. // We process the same file for all platforms
  596. for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
  597. {
  598. bool doesSupportPlatform = false;
  599. ImageProcessingAtom::ImageBuilderRequestBus::BroadcastResult(doesSupportPlatform,
  600. &ImageProcessingAtom::ImageBuilderRequests::DoesSupportPlatform,
  601. info.m_identifier);
  602. if (doesSupportPlatform)
  603. {
  604. AssetBuilderSDK::JobDescriptor descriptor = GetJobDescriptor(request.m_sourceFile, input);
  605. descriptor.SetPlatformIdentifier(info.m_identifier.c_str());
  606. response.m_createJobOutputs.push_back(descriptor);
  607. }
  608. }
  609. if (valid)
  610. {
  611. response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
  612. }
  613. return;
  614. }
  615. AssetBuilderSDK::JobDescriptor AtlasBuilderWorker::GetJobDescriptor(const AZStd::string& sourceFile, const AtlasBuilderInput& input)
  616. {
  617. // Get the extension of the file
  618. AZStd::string ext;
  619. AzFramework::StringFunc::Path::GetExtension(sourceFile.c_str(), ext, false);
  620. AZStd::to_upper(ext.begin(), ext.end());
  621. AssetBuilderSDK::JobDescriptor descriptor;
  622. descriptor.m_jobKey = ext + " Atlas";
  623. descriptor.m_critical = false;
  624. descriptor.m_jobParameters[AZ_CRC("forceSquare")] = input.m_forceSquare ? "true" : "false";
  625. descriptor.m_jobParameters[AZ_CRC("forcePowerOf2")] = input.m_forcePowerOf2 ? "true" : "false";
  626. descriptor.m_jobParameters[AZ_CRC("includeWhiteTexture")] = input.m_includeWhiteTexture ? "true" : "false";
  627. descriptor.m_jobParameters[AZ_CRC("padding")] = AZStd::to_string(input.m_padding);
  628. descriptor.m_jobParameters[AZ_CRC("maxDimension")] = AZStd::to_string(input.m_maxDimension);
  629. descriptor.m_jobParameters[AZ_CRC("filePaths")] = AZStd::to_string(input.m_filePaths.size());
  630. AZ::u32 col = input.m_unusedColor.ToU32();
  631. descriptor.m_jobParameters[AZ_CRC("unusedColor")] = AZStd::to_string(*reinterpret_cast<int*>(&col));
  632. descriptor.m_jobParameters[AZ_CRC("presetName")] = input.m_presetName;
  633. // The starting point for the list
  634. const int start = static_cast<int>(descriptor.m_jobParameters.size()) + 1;
  635. descriptor.m_jobParameters[AZ_CRC("startPoint")] = AZStd::to_string(start);
  636. for (int i = 0; i < input.m_filePaths.size(); ++i)
  637. {
  638. descriptor.m_jobParameters[start + i] = input.m_filePaths[i];
  639. }
  640. return descriptor;
  641. }
  642. void AtlasBuilderWorker::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request,
  643. AssetBuilderSDK::ProcessJobResponse& response)
  644. {
  645. // Before we begin, let's make sure we are not meant to abort.
  646. AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
  647. const AZStd::string path = request.m_fullPath;
  648. // read in settings/filepaths
  649. AtlasBuilderInput input;
  650. input.m_forceSquare = AzFramework::StringFunc::ToBool(request.m_jobDescription.m_jobParameters.find(AZ_CRC("forceSquare"))->second.c_str());
  651. input.m_forcePowerOf2 = AzFramework::StringFunc::ToBool(request.m_jobDescription.m_jobParameters.find(AZ_CRC("forcePowerOf2"))->second.c_str());
  652. input.m_includeWhiteTexture = AzFramework::StringFunc::ToBool(request.m_jobDescription.m_jobParameters.find(AZ_CRC("includeWhiteTexture"))->second.c_str());
  653. input.m_padding = AzFramework::StringFunc::ToInt(request.m_jobDescription.m_jobParameters.find(AZ_CRC("padding"))->second.c_str());
  654. input.m_maxDimension = AzFramework::StringFunc::ToInt(request.m_jobDescription.m_jobParameters.find(AZ_CRC("maxDimension"))->second.c_str());
  655. int startAsInt = AzFramework::StringFunc::ToInt(request.m_jobDescription.m_jobParameters.find(AZ_CRC("startPoint"))->second.c_str());
  656. int sizeAsInt = AzFramework::StringFunc::ToInt(request.m_jobDescription.m_jobParameters.find(AZ_CRC("filePaths"))->second.c_str());
  657. AZ::u32 start = static_cast<AZ::u32>(AZStd::max(0, startAsInt));
  658. AZ::u32 size = static_cast<AZ::u32>(AZStd::max(0, sizeAsInt));
  659. int col = AzFramework::StringFunc::ToInt(request.m_jobDescription.m_jobParameters.find(AZ_CRC("unusedColor"))->second.c_str());
  660. input.m_unusedColor.FromU32(*reinterpret_cast<AZ::u32*>(&col));
  661. input.m_presetName = request.m_jobDescription.m_jobParameters.find(AZ_CRC("presetName"))->second;
  662. for (AZ::u32 i = 0; i < size; ++i)
  663. {
  664. input.m_filePaths.push_back(request.m_jobDescription.m_jobParameters.find(start + i)->second);
  665. }
  666. if (input.m_filePaths.empty())
  667. {
  668. AZ_Error("AtlasBuilder", false, "No image files specified. Cannot create an empty atlas.");
  669. return;
  670. }
  671. // Don't allow padding to be less than zero
  672. if (input.m_padding < 0)
  673. {
  674. input.m_padding = 0;
  675. }
  676. if (input.m_presetName.empty())
  677. {
  678. // Default to the TextureAtlas preset which is currently set to use compression
  679. const AZStd::string defaultPresetName = "UserInterface_Compressed";
  680. input.m_presetName = defaultPresetName;
  681. }
  682. bool isFormatSquarePow2 = false;
  683. ImageProcessingAtom::ImageBuilderRequestBus::BroadcastResult(isFormatSquarePow2,
  684. &ImageProcessingAtom::ImageBuilderRequests::IsPresetFormatSquarePow2,
  685. input.m_presetName, request.m_platformInfo.m_identifier);
  686. if (isFormatSquarePow2)
  687. {
  688. // Override the user config settings to force square and power of 2.
  689. // Otherwise the image conversion process will stretch the image to satisfy these requirements
  690. input.m_forceSquare = true;
  691. input.m_forcePowerOf2 = true;
  692. }
  693. // Read in images
  694. AZStd::vector<ImageProcessingAtom::IImageObjectPtr> images;
  695. AZ::u64 totalArea = 0;
  696. int maxArea = input.m_maxDimension * input.m_maxDimension;
  697. bool sizeFailure = false;
  698. for (int i = 0; i < input.m_filePaths.size() && !jobCancelListener.IsCancelled(); ++i)
  699. {
  700. ImageProcessingAtom::IImageObjectPtr inputImage;
  701. ImageProcessingAtom::ImageProcessingRequestBus::BroadcastResult(inputImage, &ImageProcessingAtom::ImageProcessingRequests::LoadImage, input.m_filePaths[i]);
  702. // Check if we were able to load the image
  703. if (inputImage)
  704. {
  705. images.push_back(inputImage);
  706. totalArea += inputImage->GetWidth(0) * inputImage->GetHeight(0);
  707. }
  708. else
  709. {
  710. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to load file: %s", input.m_filePaths[i].c_str()).c_str());
  711. return;
  712. }
  713. if (maxArea < totalArea)
  714. {
  715. sizeFailure = true;
  716. }
  717. }
  718. // If we get cancelled, return
  719. if (jobCancelListener.IsCancelled())
  720. {
  721. return;
  722. }
  723. if (sizeFailure)
  724. {
  725. AZ_Error("AtlasBuilder", false, AZStd::string::format("Total image area exceeds maximum alotted area. %llu > %d", totalArea, maxArea).c_str());
  726. return;
  727. }
  728. // Convert all image paths to their output format referenced at runtime
  729. for (auto& filePath : input.m_filePaths)
  730. {
  731. // Get path relative to the watch folder
  732. bool result = false;
  733. AZ::Data::AssetInfo info;
  734. AZStd::string watchFolder;
  735. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath, filePath.c_str(), info, watchFolder);
  736. if (!result)
  737. {
  738. AZ_Error("AtlasBuilder", false, AZStd::string::format("Atlas Builder unable to get relative source path for image: %s", filePath.c_str()).c_str());
  739. return;
  740. }
  741. // Remove extension
  742. filePath = info.m_relativePath.substr(0, info.m_relativePath.find_last_of('.'));
  743. // Normalize path
  744. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::NormalizePathKeepCase, filePath);
  745. }
  746. // Add white texture if we need to
  747. if (input.m_includeWhiteTexture)
  748. {
  749. ImageProcessingAtom::IImageObjectPtr texture;
  750. ImageProcessingAtom::ImageBuilderRequestBus::BroadcastResult(texture,
  751. &ImageProcessingAtom::ImageBuilderRequests::CreateImage,
  752. aznumeric_cast<AZ::u32>(cellSize),
  753. aznumeric_cast<AZ::u32>(cellSize),
  754. 1,
  755. ImageProcessingAtom::EPixelFormat::ePixelFormat_R8G8B8A8);
  756. // Make the texture white
  757. texture->ClearColor(1, 1, 1, 1);
  758. images.push_back(texture);
  759. input.m_filePaths.push_back("WhiteTexture");
  760. }
  761. // Generate algorithm inputs
  762. ImageDimensionData data;
  763. for (int i = 0; i < images.size(); ++i)
  764. {
  765. data.push_back(IndexImageDimension(i,
  766. ImageDimension(images[i]->GetWidth(0),
  767. images[i]->GetHeight(0))));
  768. }
  769. AZStd::sort(data.begin(), data.end());
  770. // Run algorithm
  771. // Variables that keep track of the optimal solution
  772. int resultWidth = -1;
  773. int resultHeight = -1;
  774. // Check that the max dimension is not large enough for the area to loop past the maximum integer
  775. // This is important because we do not want the area to be calculated negative
  776. if (input.m_maxDimension > 65535)
  777. {
  778. input.m_maxDimension = 65535;
  779. }
  780. // Get the optimal mappings based on the input settings
  781. AZStd::vector<AtlasCoordinates> paddedMap;
  782. size_t amountFit = 0;
  783. if (!TryTightening(
  784. input, data, GetWidest(data), GetTallest(data), aznumeric_cast<int>(totalArea), input.m_padding, resultWidth, resultHeight, amountFit, paddedMap))
  785. {
  786. AZ_Error("AtlasBuilder", false, AZStd::string::format("Cannot fit images into given maximum atlas size (%dx%d). Only %zu out of %zu images fit.", input.m_maxDimension, input.m_maxDimension, amountFit, input.m_filePaths.size()).c_str());
  787. // For some reason, failing the assert isn't enough to stop the Asset builder. It will still fail further
  788. // down when it tries to assemble the atlas, but returning here is cleaner.
  789. return;
  790. }
  791. // Move coordinates from algorithm space to padded result space
  792. TextureAtlasNamespace::AtlasCoordinateSets output;
  793. resultWidth = 0;
  794. resultHeight = 0;
  795. AZStd::vector<AtlasCoordinates> map;
  796. for (int i = 0; i < paddedMap.size(); ++i)
  797. {
  798. map.push_back(AtlasCoordinates(paddedMap[i].GetLeft(), paddedMap[i].GetLeft() + images[data[i].first]->GetWidth(0), paddedMap[i].GetTop(), paddedMap[i].GetTop() + images[data[i].first]->GetHeight(0)));
  799. resultHeight = resultHeight > map[i].GetBottom() ? resultHeight : map[i].GetBottom();
  800. resultWidth = resultWidth > map[i].GetRight() ? resultWidth : map[i].GetRight();
  801. const AZStd::string& outputFilePath = input.m_filePaths[data[i].first];
  802. output.push_back(AZStd::pair<AZStd::string, AtlasCoordinates>(outputFilePath, map[i]));
  803. }
  804. if (input.m_forcePowerOf2)
  805. {
  806. resultWidth = aznumeric_cast<int>(pow(2, 1 + IntegerLog2(static_cast<uint32_t>(resultWidth - 1))));
  807. resultHeight = aznumeric_cast<int>(pow(2, 1 + IntegerLog2(static_cast<uint32_t>(resultHeight - 1))));
  808. }
  809. else
  810. {
  811. resultWidth = (resultWidth + (cellSize - 1)) / cellSize * cellSize;
  812. resultHeight = (resultHeight + (cellSize - 1)) / cellSize * cellSize;
  813. }
  814. if (input.m_forceSquare)
  815. {
  816. if (resultWidth > resultHeight)
  817. {
  818. resultHeight = resultWidth;
  819. }
  820. else
  821. {
  822. resultWidth = resultHeight;
  823. }
  824. }
  825. // Process texture sheet
  826. ImageProcessingAtom::IImageObjectPtr outImage;
  827. ImageProcessingAtom::ImageBuilderRequestBus::BroadcastResult(outImage,
  828. &ImageProcessingAtom::ImageBuilderRequests::CreateImage,
  829. aznumeric_cast<AZ::u32>(resultWidth),
  830. aznumeric_cast<AZ::u32>(resultHeight),
  831. 1,
  832. ImageProcessingAtom::EPixelFormat::ePixelFormat_R8G8B8A8);
  833. // Clear the sheet
  834. outImage->ClearColor(input.m_unusedColor.GetR(), input.m_unusedColor.GetG(), input.m_unusedColor.GetB(), input.m_unusedColor.GetA());
  835. AZ::u8* outBuffer = nullptr;
  836. AZ::u32 outPitch;
  837. outImage->GetImagePointer(0, outBuffer, outPitch);
  838. // Copy images over
  839. for (int i = 0; i < map.size() && !jobCancelListener.IsCancelled(); ++i)
  840. {
  841. AZ::u8* inBuffer = nullptr;
  842. AZ::u32 inPitch;
  843. images[data[i].first]->GetImagePointer(0, inBuffer, inPitch);
  844. int j = 0;
  845. // The padding calculated here is the amount of excess horizontal space measured in bytes that are in each
  846. // row of the destination space AFTER the placement of the source row.
  847. int rightPadding = (paddedMap[i].GetRight() - map[i].GetRight() - input.m_padding);
  848. if (map[i].GetRight() + rightPadding > resultWidth)
  849. {
  850. rightPadding = resultWidth - map[i].GetRight();
  851. }
  852. rightPadding *= bytesPerPixel;
  853. int bottomPadding = (paddedMap[i].GetBottom() - map[i].GetBottom() - input.m_padding);
  854. if (map[i].GetBottom() + bottomPadding > resultHeight)
  855. {
  856. bottomPadding = resultHeight - map[i].GetBottom();
  857. }
  858. int leftPadding = 0;
  859. if (map[i].GetLeft() - input.m_padding >= 0)
  860. {
  861. leftPadding = input.m_padding * bytesPerPixel;
  862. }
  863. int topPadding = 0;
  864. if (map[i].GetTop() - input.m_padding >= 0)
  865. {
  866. topPadding = input.m_padding;
  867. }
  868. for (j = 0; j < map[i].GetHeight(); ++j)
  869. {
  870. // When we multiply `map[i].GetLeft()` by 4, we are changing the measure from atlas space, to byte array
  871. // space. The number is 4 because in this format, each pixel is 4 bytes long.
  872. memcpy(outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel),
  873. inBuffer + inPitch * j,
  874. inPitch);
  875. // Fill in the last bit of the row in the destination space with the same colors
  876. SetPixels(outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel) + inPitch,
  877. outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel) + inPitch - bytesPerPixel,
  878. rightPadding);
  879. // Fill in the first bit of the row in the destination space with the same colors
  880. SetPixels(outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel) - leftPadding,
  881. outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel),
  882. leftPadding);
  883. }
  884. // Fill in the last few rows of the buffer with the same colors
  885. for (; j < map[i].GetHeight() + bottomPadding; ++j)
  886. {
  887. memcpy(outBuffer + (map[i].GetTop() + j) * outPitch + (map[i].GetLeft() * bytesPerPixel) - leftPadding,
  888. outBuffer + (map[i].GetBottom() - 1) * outPitch + (map[i].GetLeft() * bytesPerPixel) - leftPadding,
  889. inPitch + leftPadding + rightPadding);
  890. }
  891. for (j = 1; j <= topPadding; ++j)
  892. {
  893. memcpy(outBuffer + (map[i].GetTop() - j) * outPitch + (map[i].GetLeft() * bytesPerPixel) - leftPadding,
  894. outBuffer + map[i].GetTop() * outPitch + (map[i].GetLeft() * bytesPerPixel) - leftPadding,
  895. inPitch + rightPadding + leftPadding);
  896. }
  897. }
  898. // If we get cancelled, return
  899. if (jobCancelListener.IsCancelled())
  900. {
  901. return;
  902. }
  903. // Output Atlas Coordinates
  904. AZStd::string fileName;
  905. AZStd::string outputPath;
  906. AzFramework::StringFunc::Path::GetFullFileName(request.m_sourceFile.c_str(), fileName);
  907. fileName = fileName.append("idx");
  908. AzFramework::StringFunc::Path::Join(
  909. request.m_tempDirPath.c_str(), fileName.c_str(), outputPath, true, true);
  910. // Output texture sheet
  911. AZStd::string imageFileName, imageOutputPath;
  912. AzFramework::StringFunc::Path::GetFileName(request.m_sourceFile.c_str(), imageFileName);
  913. imageFileName += ".texatlas";
  914. AzFramework::StringFunc::Path::Join(
  915. request.m_tempDirPath.c_str(), imageFileName.c_str(), imageOutputPath, true, true);
  916. AZStd::vector<AssetBuilderSDK::JobProduct> outProducts;
  917. ImageProcessingAtom::ImageBuilderRequestBus::BroadcastResult(outProducts,
  918. &ImageProcessingAtom::ImageBuilderRequests::ConvertImageObject,
  919. outImage,
  920. input.m_presetName,
  921. request.m_platformInfo.m_identifier,
  922. imageOutputPath,
  923. request.m_sourceFileUUID,
  924. request.m_sourceFile);
  925. if (!outProducts.empty())
  926. {
  927. TextureAtlasNamespace::TextureAtlasRequestBus::Broadcast(
  928. &TextureAtlasNamespace::TextureAtlasRequests::SaveAtlasToFile, outputPath, output, resultWidth, resultHeight);
  929. response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(outputPath));
  930. response.m_outputProducts[static_cast<int>(Product::TexatlasidxProduct)].m_productAssetType = azrtti_typeid<TextureAtlasNamespace::TextureAtlasAsset>();
  931. response.m_outputProducts[static_cast<int>(Product::TexatlasidxProduct)].m_productSubID = 0;
  932. // The Image Processing Gem can produce multiple output files under certain
  933. // circumstances, but the texture atlas is not expected to produce such output
  934. // There should only be the texture atlas and its abdata file
  935. if (outProducts.size() > 2)
  936. {
  937. AZ_Error("AtlasBuilder", false, "Image processing resulted in multiple output files. Texture atlas is expected to produce one output.");
  938. response.m_outputProducts.clear();
  939. return;
  940. }
  941. response.m_outputProducts.push_back(outProducts[0]);
  942. // The texatlasidx file is a data file that indicates where the original parts are inside the atlas,
  943. // and this would usually imply that it refers to its dds file in some way or needs it to function.
  944. // The texatlasidx file should be the one that depends on the DDS because it's possible to use the DDS
  945. // without the texatlasid, but not the other way around
  946. AZ::Data::AssetId productAssetId(request.m_sourceFileUUID, response.m_outputProducts.back().m_productSubID);
  947. response.m_outputProducts[static_cast<int>(Product::TexatlasidxProduct)].m_dependencies.push_back(AssetBuilderSDK::ProductDependency(productAssetId, 0));
  948. response.m_outputProducts[static_cast<int>(Product::TexatlasidxProduct)].m_dependenciesHandled = true; // We've populated the dependencies immediately above so it's OK to tell the AP we've handled dependencies
  949. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  950. }
  951. }
  952. bool AtlasBuilderWorker::TryPack(const ImageDimensionData& images,
  953. int targetWidth,
  954. int targetHeight,
  955. int padding,
  956. size_t& amountFit,
  957. AZStd::vector<AtlasCoordinates>& out)
  958. {
  959. // Start with one open slot and initialize a vector to store the closed products
  960. AZStd::vector<AtlasCoordinates> open;
  961. AZStd::vector<AtlasCoordinates> closed;
  962. open.push_back(AtlasCoordinates(0, targetWidth, 0, targetHeight));
  963. bool slotNotFound = false;
  964. for (size_t i = 0; i < images.size() && !slotNotFound; ++i)
  965. {
  966. slotNotFound = true;
  967. // Try to place the image in every open slot
  968. for (size_t j = 0; j < open.size(); ++j)
  969. {
  970. if (CanInsert(open[j], images[i].second, padding, targetWidth, targetHeight))
  971. {
  972. // if it fits, subdivide the excess space in the slot, add it back to the open list and place the
  973. // filled space into the closed vector
  974. slotNotFound = false;
  975. AtlasCoordinates spent(open[j].GetLeft(),
  976. open[j].GetLeft() + images[i].second.m_width,
  977. open[j].GetTop(),
  978. open[j].GetTop() + images[i].second.m_height);
  979. // We are going to try pushing the object up / left to try to avoid creating tight open spaces.
  980. bool needTrim = false;
  981. AtlasCoordinates coords = spent;
  982. // Modifying left will preserve width
  983. coords.SetLeft(coords.GetLeft() - 1);
  984. AddPadding(coords, padding, targetWidth, targetHeight);
  985. while (spent.GetLeft() > 0 && !Collides(coords, closed))
  986. {
  987. spent.SetLeft(coords.GetLeft());
  988. coords = spent;
  989. coords.SetLeft(coords.GetLeft() - 1);
  990. AddPadding(coords, padding, targetWidth, targetHeight);
  991. needTrim = true;
  992. }
  993. // Refocus the search to see if we can push up
  994. coords = spent;
  995. coords.SetTop(coords.GetTop() - 1);
  996. AddPadding(coords, padding, targetWidth, targetHeight);
  997. while (spent.GetTop() > 0 && !Collides(coords, closed))
  998. {
  999. spent.SetTop(coords.GetTop());
  1000. coords = spent;
  1001. coords.SetTop(coords.GetTop() - 1);
  1002. AddPadding(coords, padding, targetWidth, targetHeight);
  1003. needTrim = true;
  1004. }
  1005. AddPadding(spent, padding, targetWidth, targetHeight);
  1006. if (needTrim)
  1007. {
  1008. TrimOverlap(open, spent);
  1009. closed.push_back(spent);
  1010. break;
  1011. }
  1012. AtlasCoordinates bigCoords;
  1013. AtlasCoordinates smallCoords;
  1014. // Create the largest possible subdivision and another subdivision that uses the left over space
  1015. if (open[j].GetBottom() - spent.GetBottom() < open[j].GetRight() - spent.GetRight())
  1016. {
  1017. smallCoords = AtlasCoordinates(
  1018. open[j].GetLeft(), spent.GetRight(), spent.GetBottom(), open[j].GetBottom());
  1019. bigCoords = AtlasCoordinates(spent.GetRight(), open[j].GetRight(), open[j].GetTop(), smallCoords.GetBottom());
  1020. }
  1021. else
  1022. {
  1023. bigCoords = AtlasCoordinates(
  1024. open[j].GetLeft(), open[j].GetRight(), spent.GetBottom(), open[j].GetBottom());
  1025. smallCoords = AtlasCoordinates(spent.GetRight(), open[j].GetRight(), open[j].GetTop(), bigCoords.GetTop());
  1026. }
  1027. open.erase(open.begin() + j, open.begin() + j + 1);
  1028. if (bigCoords.GetHeight() > 0 && bigCoords.GetHeight() > 0)
  1029. {
  1030. InsertInOrder(open, bigCoords);
  1031. }
  1032. if (smallCoords.GetHeight() > 0 && smallCoords.GetHeight() > 0)
  1033. {
  1034. InsertInOrder(open, smallCoords);
  1035. }
  1036. closed.push_back(spent);
  1037. break;
  1038. }
  1039. }
  1040. if (slotNotFound)
  1041. {
  1042. // If no single open slot can fit the object, do one last check to see if we can fit it in at any open
  1043. // corner. The reason we perform this check is in case the object can be fit across multiple different
  1044. // open spaces. If there is a space that an object can be fit in, it will probably involve the top left
  1045. // corner of that object in the top left corner of an open slot. This may miss some odd fits, but due to
  1046. // the nature of the packing algorithm, such solutions are highly unlikely to exist. If we wanted to
  1047. // expand the algorithm, we could theoretically base it on edges instead of corners to find all results,
  1048. // but it would not be time efficient.
  1049. for (size_t j = 0; j < open.size(); ++j)
  1050. {
  1051. AtlasCoordinates insert = AtlasCoordinates(open[j].GetLeft(),
  1052. open[j].GetLeft() + images[i].second.m_width,
  1053. open[j].GetTop(),
  1054. open[j].GetTop() + images[i].second.m_height);
  1055. AddPadding(insert, padding, targetWidth, targetHeight);
  1056. if (insert.GetRight() <= targetWidth && insert.GetBottom() <= targetHeight)
  1057. {
  1058. bool collision = Collides(insert, closed);
  1059. if (!collision)
  1060. {
  1061. closed.push_back(insert);
  1062. // Trim overlapping open slots
  1063. TrimOverlap(open, insert);
  1064. slotNotFound = false;
  1065. break;
  1066. }
  1067. }
  1068. }
  1069. }
  1070. }
  1071. // If we succeeded, update the output
  1072. if (!slotNotFound)
  1073. {
  1074. out = closed;
  1075. }
  1076. amountFit = amountFit > closed.size() ? amountFit : closed.size();
  1077. return !slotNotFound;
  1078. }
  1079. // Modifies slotList so that no items in slotList overlap with item
  1080. void AtlasBuilderWorker::TrimOverlap(AZStd::vector<AtlasCoordinates>& slotList, AtlasCoordinates item)
  1081. {
  1082. for (size_t i = 0; i < slotList.size(); ++i)
  1083. {
  1084. if (Collides(slotList[i], item))
  1085. {
  1086. // Subdivide the overlapping slot to seperate overlapping and non overlapping portions
  1087. AtlasCoordinates overlap = GetOverlap(item, slotList[i]);
  1088. AZStd::vector<AtlasCoordinates> excess;
  1089. excess.push_back(AtlasCoordinates(
  1090. slotList[i].GetLeft(), overlap.GetRight(), slotList[i].GetTop(), overlap.GetTop()));
  1091. excess.push_back(AtlasCoordinates(
  1092. slotList[i].GetLeft(), overlap.GetLeft(), overlap.GetTop(), slotList[i].GetBottom()));
  1093. excess.push_back(AtlasCoordinates(
  1094. overlap.GetRight(), slotList[i].GetRight(), slotList[i].GetTop(), overlap.GetBottom()));
  1095. excess.push_back(AtlasCoordinates(
  1096. overlap.GetLeft(), slotList[i].GetRight(), overlap.GetBottom(), slotList[i].GetBottom()));
  1097. slotList.erase(slotList.begin() + i);
  1098. for (size_t j = 0; j < excess.size(); ++j)
  1099. {
  1100. if (excess[j].GetWidth() > 0 && excess[j].GetHeight() > 0)
  1101. {
  1102. InsertInOrder(slotList, excess[j]);
  1103. }
  1104. }
  1105. --i;
  1106. }
  1107. }
  1108. }
  1109. // This function interprets input and performs the proper tightening option
  1110. bool AtlasBuilderWorker::TryTightening(AtlasBuilderInput input,
  1111. const ImageDimensionData& images,
  1112. int smallestWidth,
  1113. int smallestHeight,
  1114. int targetArea,
  1115. int padding,
  1116. int& resultWidth,
  1117. int& resultHeight,
  1118. size_t& amountFit,
  1119. AZStd::vector<AtlasCoordinates>& out)
  1120. {
  1121. if (input.m_forceSquare)
  1122. {
  1123. return TryTighteningSquare(images,
  1124. smallestWidth > smallestHeight ? smallestWidth : smallestHeight,
  1125. input.m_maxDimension,
  1126. targetArea,
  1127. input.m_forcePowerOf2,
  1128. padding,
  1129. resultWidth,
  1130. resultHeight,
  1131. amountFit,
  1132. out);
  1133. }
  1134. else
  1135. {
  1136. return TryTighteningOptimal(images,
  1137. smallestWidth,
  1138. smallestHeight,
  1139. input.m_maxDimension,
  1140. targetArea,
  1141. input.m_forcePowerOf2,
  1142. padding,
  1143. resultWidth,
  1144. resultHeight,
  1145. amountFit,
  1146. out);
  1147. }
  1148. }
  1149. // Finds the optimal square solution by starting with the ideal solution and expanding the size of the space until everything fits
  1150. bool AtlasBuilderWorker::TryTighteningSquare(const ImageDimensionData& images,
  1151. int lowerBound,
  1152. int maxDimension,
  1153. int targetArea,
  1154. bool powerOfTwo,
  1155. int padding,
  1156. int& resultWidth,
  1157. int& resultHeight,
  1158. size_t& amountFit,
  1159. AZStd::vector<AtlasCoordinates>& out)
  1160. {
  1161. // Square solution cannot be smaller than the target area
  1162. int dimension = aznumeric_cast<int>(sqrt(static_cast<float>(targetArea)));
  1163. // Solution cannot be smaller than the smallest side
  1164. dimension = dimension > lowerBound ? dimension : lowerBound;
  1165. if (powerOfTwo)
  1166. {
  1167. // Starting dimension needs to be rounded up to the nearest power of two
  1168. dimension = aznumeric_cast<int>(pow(2, 1 + IntegerLog2(static_cast<uint32_t>(dimension - 1))));
  1169. }
  1170. AZStd::vector<AtlasCoordinates> track;
  1171. // Expand the square until the contents fit
  1172. while (!TryPack(images, dimension, dimension, padding, amountFit, track) && dimension <= maxDimension)
  1173. {
  1174. // Step to the next valid value
  1175. dimension = powerOfTwo ? dimension * 2 : dimension + cellSize;
  1176. }
  1177. // Make sure we found a solution
  1178. if (dimension > maxDimension)
  1179. {
  1180. return false;
  1181. }
  1182. resultHeight = dimension;
  1183. resultWidth = dimension;
  1184. out = track;
  1185. return true;
  1186. }
  1187. // Finds the optimal solution by starting with a somewhat optimal solution and searching for better solutions
  1188. bool AtlasBuilderWorker::TryTighteningOptimal(const ImageDimensionData& images,
  1189. int smallestWidth,
  1190. int smallestHeight,
  1191. int maxDimension,
  1192. int targetArea,
  1193. bool powerOfTwo,
  1194. int padding,
  1195. int& resultWidth,
  1196. int& resultHeight,
  1197. size_t& amountFit,
  1198. AZStd::vector<AtlasCoordinates>& out)
  1199. {
  1200. AZStd::vector<AtlasCoordinates> track;
  1201. // round max dimension down to a multiple of cellSize
  1202. AZ::u32 maxDimensionRounded = maxDimension - (maxDimension % cellSize);
  1203. // The starting width is the larger of the widest individual texture and the width required
  1204. // to fit the total texture area given the max dimension
  1205. AZ::u32 smallestWidthDueToArea = targetArea / maxDimensionRounded;
  1206. AZ::u32 minWidth = AZStd::max(static_cast<AZ::u32>(smallestWidth), smallestWidthDueToArea);
  1207. if (powerOfTwo)
  1208. {
  1209. // Starting dimension needs to be rounded up to the nearest power of two
  1210. minWidth = aznumeric_cast<AZ::u32>(pow(2, 1 + IntegerLog2(static_cast<uint32_t>(minWidth - 1))));
  1211. }
  1212. // Round min width up to the nearest compression unit
  1213. minWidth = (minWidth + (cellSize - 1)) / cellSize * cellSize;
  1214. AZ::u32 height = 0;
  1215. // Finds the optimal thin solution
  1216. // This uses a standard binary search to find the smallest width that can pack everything
  1217. AZ::u32 lower = minWidth;
  1218. AZ::u32 upper = maxDimensionRounded;
  1219. AZ::u32 width = 0;
  1220. while (lower <= upper)
  1221. {
  1222. AZ::u32 testWidth = (lower + upper) / 2; // must be divisible by cellSize because lower and upper are
  1223. bool canPack = TryPack(images, testWidth, maxDimension, padding, amountFit, track);
  1224. if (canPack)
  1225. {
  1226. // it packed, continue looking for smaller widths that pack
  1227. width = testWidth; // best fit so far
  1228. upper = testWidth - cellSize;
  1229. }
  1230. else
  1231. {
  1232. // it failed to pack, don't try any widths smaller than this
  1233. lower = testWidth + cellSize;
  1234. }
  1235. }
  1236. // Make sure we found a solution
  1237. if (width == 0)
  1238. {
  1239. return false;
  1240. }
  1241. // Find the height of the solution
  1242. for (int i = 0; i < track.size(); ++i)
  1243. {
  1244. uint32_t bottom = static_cast<uint32_t>(AZStd::max(0, track[i].GetBottom()));
  1245. if (height < bottom)
  1246. {
  1247. height = bottom;
  1248. }
  1249. }
  1250. // Fix height for power of two when applicable
  1251. if (powerOfTwo)
  1252. {
  1253. // Starting dimensions need to be rounded up to the nearest power of two
  1254. height = aznumeric_cast<AZ::u32>(pow(2, 1 + IntegerLog2(static_cast<uint32_t>(height - 1))));
  1255. }
  1256. AZ::u32 resultArea = height * width;
  1257. // This for loop starts with the optimal thin width and makes it wider at each step. For each width, it
  1258. // calculates what height would be neccesary to have a more optimal solution than the stored solution. If the
  1259. // more optimal solution is valid, it tries shrinking the height until the solution fails. The loop ends when it
  1260. // is determined that a valid solution cannot exist at further steps
  1261. for (AZ::u32 testWidth = width; testWidth <= maxDimensionRounded && resultArea / testWidth >= static_cast<AZ::u32>(smallestHeight);
  1262. testWidth = powerOfTwo ? testWidth * 2 : testWidth + cellSize)
  1263. {
  1264. // The area of test height and width should be equal or less than resultArea
  1265. // Note: We don't need to force powers of two here because the Area and the width are already powers of two
  1266. int testHeight = resultArea / testWidth * cellSize / cellSize;
  1267. // Try the tighter pack
  1268. while (TryPack(images, static_cast<int>(testWidth), testHeight, padding, amountFit, track))
  1269. {
  1270. // Loop and continue to shrink the height until you cannot do so any further
  1271. width = testWidth;
  1272. height = testHeight;
  1273. resultArea = height * width;
  1274. // Try to step down a level
  1275. testHeight = powerOfTwo ? testHeight / 2 : testHeight - cellSize;
  1276. }
  1277. }
  1278. // Output the results of the function
  1279. out = track;
  1280. resultHeight = height;
  1281. resultWidth = width;
  1282. return true;
  1283. }
  1284. // Allows us to keep the list of open spaces in order from lowest to highest area
  1285. void AtlasBuilderWorker::InsertInOrder(AZStd::vector<AtlasCoordinates>& slotList, AtlasCoordinates item)
  1286. {
  1287. int area = item.GetWidth() * item.GetHeight();
  1288. for (size_t i = 0; i < slotList.size(); ++i)
  1289. {
  1290. if (area < slotList[i].GetWidth() * slotList[i].GetHeight())
  1291. {
  1292. slotList.insert(slotList.begin() + i, item);
  1293. return;
  1294. }
  1295. }
  1296. slotList.push_back(item);
  1297. }
  1298. // Defines priority so that sorting can be meaningful. It may seem odd that larger items are "less than" smaller
  1299. // ones, but as this is a deduction of priority, not value, it is correct.
  1300. bool operator<(ImageDimension a, ImageDimension b)
  1301. {
  1302. // Prioritize first by longest size
  1303. if ((a.m_width > a.m_height ? a.m_width : a.m_height) != (b.m_width > b.m_height ? b.m_width : b.m_height))
  1304. {
  1305. return (a.m_width > a.m_height ? a.m_width : a.m_height) > (b.m_width > b.m_height ? b.m_width : b.m_height);
  1306. }
  1307. // Prioritize second by the length of the smaller side
  1308. if (a.m_width * a.m_height != b.m_width * b.m_height)
  1309. {
  1310. return a.m_width * a.m_height > b.m_width * b.m_height;
  1311. }
  1312. // Prioritize wider objects over taller objects for objects of the same size
  1313. else
  1314. {
  1315. return a.m_width > b.m_width;
  1316. }
  1317. }
  1318. // Exposes priority logic to the sorting algorithm
  1319. bool operator<(IndexImageDimension a, IndexImageDimension b) { return a.second < b.second; }
  1320. // Tests if two coordinate sets intersect
  1321. bool Collides(AtlasCoordinates a, AtlasCoordinates b)
  1322. {
  1323. return !((a.GetRight() <= b.GetLeft()) || (a.GetBottom() <= b.GetTop()) || (b.GetRight() <= a.GetLeft())
  1324. || (b.GetBottom() <= a.GetTop()));
  1325. }
  1326. // Tests if an item collides with any items in a list
  1327. bool Collides(AtlasCoordinates item, AZStd::vector<AtlasCoordinates> list)
  1328. {
  1329. for (size_t i = 0; i < list.size(); ++i)
  1330. {
  1331. if (Collides(list[i], item))
  1332. {
  1333. return true;
  1334. }
  1335. }
  1336. return false;
  1337. }
  1338. // Returns the overlap of two intersecting coordinate sets
  1339. AtlasCoordinates GetOverlap(AtlasCoordinates a, AtlasCoordinates b)
  1340. {
  1341. return AtlasCoordinates(b.GetLeft() > a.GetLeft() ? b.GetLeft() : a.GetLeft(),
  1342. b.GetRight() < a.GetRight() ? b.GetRight() : a.GetRight(),
  1343. b.GetTop() > a.GetTop() ? b.GetTop() : a.GetTop(),
  1344. b.GetBottom() < a.GetBottom() ? b.GetBottom() : a.GetBottom());
  1345. }
  1346. // Returns the width of the widest element in imageList
  1347. int AtlasBuilderWorker::GetWidest(const ImageDimensionData& imageList)
  1348. {
  1349. int max = 0;
  1350. for (size_t i = 0; i < imageList.size(); ++i)
  1351. {
  1352. if (max < imageList[i].second.m_width)
  1353. {
  1354. max = imageList[i].second.m_width;
  1355. }
  1356. }
  1357. return max;
  1358. }
  1359. // Returns the height of the tallest element in imageList
  1360. int AtlasBuilderWorker::GetTallest(const ImageDimensionData& imageList)
  1361. {
  1362. int max = 0;
  1363. for (size_t i = 0; i < imageList.size(); ++i)
  1364. {
  1365. if (max < imageList[i].second.m_height)
  1366. {
  1367. max = imageList[i].second.m_height;
  1368. }
  1369. }
  1370. return max;
  1371. }
  1372. // Performs an operation that copies a pixel to the output
  1373. void SetPixels(AZ::u8* dest, const AZ::u8* source, int destBytes)
  1374. {
  1375. if (destBytes >= bytesPerPixel)
  1376. {
  1377. memcpy(dest, source, bytesPerPixel);
  1378. int bytesCopied = bytesPerPixel;
  1379. while (bytesCopied * 2 < destBytes)
  1380. {
  1381. memcpy(dest + bytesCopied, dest, bytesCopied);
  1382. bytesCopied *= 2;
  1383. }
  1384. memcpy(dest + bytesCopied, dest, destBytes - bytesCopied);
  1385. }
  1386. }
  1387. // Checks if we can insert an image into a slot
  1388. bool CanInsert(AtlasCoordinates slot, ImageDimension image, int padding, int farRight, int farBot)
  1389. {
  1390. int right = slot.GetLeft() + image.m_width;
  1391. if (slot.GetRight() < farRight)
  1392. {
  1393. // Add padding for my right border
  1394. right += padding;
  1395. // Round up to the nearest compression unit
  1396. right = (right + (cellSize - 1)) / cellSize * cellSize;
  1397. // Add padding for an adjacent unit's left border
  1398. right += padding;
  1399. }
  1400. int bot = slot.GetTop() + image.m_height;
  1401. if (slot.GetBottom() < farBot)
  1402. {
  1403. // Add padding for my right border
  1404. bot += padding;
  1405. // Round up to the nearest compression unit
  1406. bot = (bot + (cellSize - 1)) / cellSize * cellSize;
  1407. // Add padding for an adjacent unit's left border
  1408. bot += padding;
  1409. }
  1410. return slot.GetRight() >= right && slot.GetBottom() >= bot;
  1411. }
  1412. // Adds the necessary padding to an Atlas Coordinate
  1413. void AddPadding(AtlasCoordinates& slot, int padding, [[maybe_unused]] int farRight, [[maybe_unused]] int farBot)
  1414. {
  1415. // Add padding for my right border
  1416. int right = slot.GetRight() + padding;
  1417. // Round up to the nearest compression unit
  1418. right = (right + (cellSize - 1)) / cellSize * cellSize;
  1419. // Add padding for an adjacent unit's left border
  1420. right += padding;
  1421. // Add padding for my right border
  1422. int bot = slot.GetBottom() + padding;
  1423. // Round up to the nearest compression unit
  1424. bot = (bot + (cellSize - 1)) / cellSize * cellSize;
  1425. // Add padding for an adjacent unit's left border
  1426. bot += padding;
  1427. slot.SetRight(right);
  1428. slot.SetBottom(bot);
  1429. }
  1430. }