. 最近工做上有一個需求,須要將圖片打包成圖集,以便於讓資源更緊湊,利用率更高,提高性能,遊戲行內的同志應該很熟練這個操做.一般咱們須要用一個app來完成這項工做,最出名的莫過於Texture Packer。c++
Texture Packer官方示意圖
web
. 最先接觸到這一律唸的時候,我仍是一個學生,當時玩的《暗黑魔破壞神2》,它有一個揹包系統,這個揹包系統跟如今大多數遊戲都不同,它的道具存放並不是等大小,好比長劍比匕首長,斧頭比長劍寬,所以在擺放道具時,不能亂放,否則揹包就不夠用。算法
暗黑破壞神2揹包截圖
app
. 這是一個頗有意思的設計,但如今基本上絕跡了,以致於我想到遊戲中的揹包就會不由想起暗黑破壞神2的揹包系統,我在《天龍八部》遊戲中,第一次發現了揹包自動整理這一項功能,可是它的揹包道具都是等大小的,因此平平無奇,但卻不禁讓我想到《暗黑破壞神2》的揹包自動整理,它必然會涉及到一個最優排列組合算法。這個疑惑一直困擾了我不少年,它究竟是怎麼實現的?直到上週,我從新打開了《暗黑破壞神2》這款遊戲,發現它根本沒有這個功能,童年幻想破滅~性能
. 雖然童年幻想破滅了,但成年夢想得跟上,正好工做有這麼一個讓我去實現的機會,因而我嘗試去思考算法,起初想到經典的《揹包算法》,仔細研究後發現不太適用,因而放棄。不得不說,廁所是一個很適合思考的地方,由於後來的實現方案是我在蹲廁所的時候忽然就想出來了。有沒有科學上的解釋?優化
. 這個算法跟咱們平常使用收納箱的思路很類似,起初收納箱是空的,它只有一個存放空間,可是很大.以後咱們逐個物品往裏面放,每次放進去一個物品,原來的空間都會被拆分紅兩個,最後空間愈來愈小,直到不能裝下剩下的物品,或者物品放完了,這一過程就結束了.ui
收納箱示意圖
設計
. 上圖清晰描述了收納箱的存放過程,綠色是收納箱,黃色是物品,起初收納箱是空的,只有一個空間,放進物品後,空間被拆分了,這一步是關鍵,由於放進一個物品會有兩種拆分策略,紅色線和棕色線分表分表顯示了兩種拆分策略,若是按紅色線拆分,則會產生一個很小的空間和一個很大的空間,若是按棕色線拆分,則產生兩個大小均勻的空間。這兩種策略對應不一樣的物品有奇效。每次放進去一個物品,收納箱的一處空間就會被佔用,而算法只考慮沒有被佔用的空間便可。3d
. 接下來就是逐個物品往收納箱裏放,這一步也是關鍵,由於每次放進一個物品,空間都會被劃分,這個劃分依據是物品的大小,若是先放進去一個很小的物品,好比1x1大小的物品,那麼空間不管怎麼分,都會產生一個很狹窄的空間,這個狹窄的空間極可能存不下後續的任何物品,那麼這個空間就浪費了,所以在此以前,先對全部物品進行一個排序,這個排序能夠按物品的面積,物品的寬度,物品的高度,物品的最長的邊,等等。這些排列策略也會影響最終的排列組合。code
空間被浪費
. 上圖是一個空間被浪費的例子,由於上文描述的算法須要已知收納箱大小,好比已知收納箱512x512大小,若是物品總面積超出這個大小,則裝不下,若是物品遠小於這個大小,則浪費空間,因此算法能動態計算大小,那就再好不過了。不過謝天謝地,很容易就能夠搞定這個問題,咱們只須要提早計算出單個物品最大須要的大小,用這個大小做爲收納箱的大小就能夠了。
自動適應
. 優化事後,已經提升了很多空間利用率,第一個收納箱裝進了最大的物品,後面的收納箱採用較小的尺寸繼續裝剩下的物品,直到所有裝完爲止。可是這裏產生了一個新問題,會生成多個收納箱,若是多個收納箱都很小,那資源複用率就降低了,因此還須要進一步優化,儘量提升資源複用率。好比兩個128x128能夠合併成一個256x256,兩個256x256能夠合併成一個512x512。
. 思路是這樣的,設定一個打包級別,當這個級別的打包數量達到一個值,就將這個級別提高一級,再從新打包,直到全部級別的收納箱都沒有超出限制。好比,128x128 2,256x256 2,512x512 3 分別表明三個不一樣的級別,他們是遞增關係,當128x128的收納箱達到2個的時候,說明它須要提高一個級別從新打包,因而級別提高到256x256,依次類推。
最終版本
以上結果是採用按棕色線拆分空間,按物品最長邊排序,及如下打包級別:
{ 128, 128, 1 }, { 256, 256, 2 }, { 512, 512, 3 }, { 1024, 1024, 4 }, { 2048, 2048, 100 },
動態圖
// storage_box.h #pragma once #include <list> #include <tuple> #include <array> #include <vector> #include <cassert> #include <algorithm> using iint = int; using uint = unsigned int; class StorageBox { public: struct Item { uint i; uint w; uint h; uint GetV() const { return std::max(w, h); } }; struct ResultItem { uint i; uint x; uint y; uint w; uint h; ResultItem(): i((uint)~0) { } uint GetV() const { return w * h; } bool IsReady() const { return i != (uint)~0; } bool IsContains(const Item & item) const { return w >= item.w && h >= item.h; } bool AddItem(const Item & item, ResultItem * out0, ResultItem * out1) { if (!IsContains(item)) { return false; } auto nx = x + item.w; auto ny = y + item.h; auto s0 = (w - item.w) * item.h; auto s1 = (h - item.h) * w; auto s2 = (w - item.w) * h; auto s3 = (h - item.h) * item.w; // 兩種切分策略: // 按最大面積切分 // 按均勻面積切分 //if (std::max(s0, s1) > std::max(s2, s3)) if (std::max(s0, s1) - std::min(s0, s1) < std::max(s2, s3) - std::min(s2, s3)) { out0->x = nx; out0->y = y; out0->w = w - item.w; out0->h = item.h; out1->x = x; out1->y = ny; out1->w = w; out1->h = h - item.h; } else { out0->x = nx; out0->y = y; out0->w = w - item.w; out0->h = h; out1->x = x; out1->y = ny; out1->w = item.w; out1->h = h - item.h; } w = item.w; h = item.h; i = item.i; return true; } }; struct ResultBox { uint level; std::vector<ResultItem> items; }; // 打包級別 static constexpr iint PACK_LEVEL[][3] = { { 128, 128, 1 }, { 256, 256, 2 }, { 512, 512, 3 }, { 1024, 1024, 4 }, { 2048, 2048, 100 }, }; std::vector<ResultBox> Pack(std::vector<Item> items); private: // 肯定使用哪一個級別打包圖集 uint CheckLevel(const Item & item); uint CheckLevel(const std::vector<Item> & items); // 根據圖片的V值進行排序 void SortItems(std::vector<Item> & items); void SortItems(std::vector<ResultItem> & items); uint CheckLimit( std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end); // 打包 ResultBox PackBox( std::vector<Item> & items, uint level); void PackBox( std::vector<Item> & items, uint level, std::vector<ResultBox> & retBoxs); // 解包 void UnpackBox(std::vector<Item> & items, std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end); };
// storage_box.cpp #include "storage_box.h" std::vector<StorageBox::ResultBox> StorageBox::Pack(std::vector<Item> items) { std::vector<ResultBox> retBoxs; PackBox(items, 0, retBoxs); for (auto it = retBoxs.begin(); it != retBoxs.end();) { auto level = it->level; auto limit = StorageBox::PACK_LEVEL[level][2]; auto count = CheckLimit(it, retBoxs.end()); if (count > limit) { UnpackBox(items, it, retBoxs.end()); retBoxs.erase(it, retBoxs.end()); PackBox(items, level+1, retBoxs); it = retBoxs.begin(); } else { ++it; } } return retBoxs; } uint StorageBox::CheckLevel(const Item & item) { for (auto i = 0; i != sizeof(PACK_LEVEL) / sizeof(PACK_LEVEL[0]); ++i) { if ((uint)PACK_LEVEL[i][0] >= item.w && (uint)PACK_LEVEL[i][1] >= item.h) { return i; } } return (uint)~0; } uint StorageBox::CheckLevel(const std::vector<Item>& items) { uint level = 0; for (auto & item : items) { auto i = CheckLevel(item); assert((uint)~0 != i); if (i > level) { level = i; } } return level; } void StorageBox::SortItems(std::vector<Item>& items) { std::sort(items.begin(), items.end(), [](const Item & item0, const Item & item1) { return item0.GetV() > item1.GetV(); }); } void StorageBox::SortItems(std::vector<ResultItem> & items) { std::sort(items.begin(), items.end(), [](const ResultItem & item0, const ResultItem & item1) { return item0.GetV() < item1.GetV(); }); } uint StorageBox::CheckLimit(std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end) { uint count = 0; uint level = cur->level; cur = std::next(cur); while (cur != end && cur->level == level) { ++cur; ++count; } return count; } StorageBox::ResultBox StorageBox::PackBox(std::vector<Item> & items, uint level) { ResultBox retBox; retBox.level = level; std::vector<ResultItem> retItems; ResultItem retItem; retItem.i = (uint)~0; retItem.x = 0; retItem.y = 0; retItem.w = PACK_LEVEL[level][0]; retItem.h = PACK_LEVEL[level][1]; retItems.push_back(retItem); auto itemIndex = 0u; ResultItem retItem0; ResultItem retItem1; while (itemIndex != items.size()) { auto isNewItem = false; for (auto it = retItems.begin(); it != retItems.end(); ++it) { if (it->AddItem(items.at(itemIndex), &retItem0, &retItem1)) { isNewItem = true; // 添加到收納箱 retBox.items.push_back(*it); retItems.erase(it); // 新增2個新收納箱 retItems.push_back(retItem0); retItems.push_back(retItem1); SortItems(retItems); // 刪除物品 items.erase(items.begin() + itemIndex); break; } } if (!isNewItem) { ++itemIndex; } } return retBox; } void StorageBox::PackBox(std::vector<Item>& items, uint level, std::vector<ResultBox>& retBoxs) { SortItems(items); while (!items.empty()) { retBoxs.push_back(PackBox(items, level == 0? CheckLevel(items): level)); level = 0; } } void StorageBox::UnpackBox(std::vector<Item> & items, std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end) { for (; cur != end; ++cur) { for (auto & retItem : cur->items) { if (retItem.IsReady()) { Item item; item.i = retItem.i; item.w = retItem.w; item.h = retItem.h; items.push_back(item); } } } }
這個算法並不能獲得最優排列組合,可是這個算法簡單並且在大多數狀況下都夠用。