|
UGUI Image已Sprite为主,而简单的合并图集可以使用自带的SpritePacker。 而当在使用AssetBundle的时候情况有些不同,会造成每个AB都会引入完整的Sprite图集,显然就增加了AB的大小,重复资源。 为了解决这个问题我们可以手动合并图集,那么方案也有多种,这里我们可以编辑器自带的API来实现一个小的图集合并工具。
private static bool CombineSpritesHelper(string path, string dpath, string name, int padding) { string[] paths = AssetDatabase.FindAssets("t:sprite", new string[] { path }); List<Sprite> spriteList = new List<Sprite>(); List<Texture2D> texList = new List<Texture2D>();
foreach (var o in paths) { Sprite s = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(o), typeof(Sprite)) as Sprite; if (null != s) { spriteList.Add(s); texList.Add(s.texture); } }
if (texList.Count > 0) { Texture2D tex = new Texture2D(1024, 1024, TextureFormat.ARGB32, true); Rect[] uvs = UITexturePacker.PackTextures(tex, texList.ToArray(), 4, 4, padding, 1024); if (null == uvs) { EditorUtility.DisplayDialog(path, "图集超过1024,需要分组成多张图集", "点击退出"); Object.DestroyImmediate(tex); tex = null; return false; } else { List<SpriteMetaData> metaList = new List<SpriteMetaData>(); for (int i = 0; i < uvs.Length; ++i) { SpriteMetaData data = new SpriteMetaData(); data.alignment = (int)SpriteAlignment.Center; data.border = spriteList[i].border; data.name = spriteList[i].name; data.pivot = spriteList[i].pivot; data.rect = new Rect(uvs[i].x * tex.width, uvs[i].y * tex.height, uvs[i].width * tex.width, uvs[i].height * tex.height); metaList.Add(data); }
//string dpath = path.Substring(0, path.Length - obj.name.Length) + "SpriteSet"; if (!System.IO.Directory.Exists(dpath)) { System.IO.Directory.CreateDirectory(dpath); }
string file = dpath + "/" + name + ".png"; if (System.IO.File.Exists(file)) { System.IO.File.Delete(file); } System.IO.File.WriteAllBytes(file, tex.EncodeToPNG());
AssetDatabase.ImportAsset(file, ImportAssetOptions.ForceUpdate); TextureImporter importer = AssetImporter.GetAtPath(file) as TextureImporter; importer.spritesheet = metaList.ToArray(); importer.spriteImportMode = SpriteImportMode.Multiple; importer.textureType = TextureImporterType.Sprite; importer.textureFormat = TextureImporterFormat.ARGB32; importer.mipmapEnabled = true; importer.mipmapFilter = TextureImporterMipFilter.BoxFilter; importer.assetBundleName = "ui_image/" + name.ToLower(); AssetDatabase.ImportAsset(file); AssetDatabase.Refresh(); } } return true; }
[MenuItem("Tool/Combine Sprites")] [MenuItem("Assets/Tool/Combine Sprites")] public static void CombineSprites() { EditorUtility.DisplayProgressBar("Combine Sprites", "Initializing ", 0); try { Object obj = Selection.activeObject; string path = AssetDatabase.GetAssetPath(obj.GetInstanceID()); string dpath = path.Substring(0, path.Length - obj.name.Length) + "SpriteSet";
if (System.IO.Directory.Exists(path)) { string[] directories = System.IO.Directory.GetDirectories(path); int count = 0; if (directories.Length > 0) { foreach (var directory in directories) { count++; EditorUtility.DisplayProgressBar("Combine Sprites", string.Format("combing {0}", count), (float)(count) / (directories.Length)); if (!CombineSpritesHelper(directory, dpath, string.Concat(obj.name, "_", count.ToString()), 1)) { break; } } } else { EditorUtility.DisplayProgressBar("Combine Sprites", "combing 0", 1); CombineSpritesHelper(path, dpath, obj.name, 1); } } } catch (System.Exception e) { Debug.LogError(e); } EditorUtility.ClearProgressBar(); } 使用方法很简单,可以修改源码中的路径,可以将多个Single Sprite合成一个Multi Sprite。然后在供,UGUI使用,注意最好保存好合并前的源文件,不然可能造成新增图片打图集后对应不上从而造成引用丢失。 补充UITextPacker.cs :引用自NGUI
/* Based on the Public Domain MaxRectsBinPack.cpp source by Jukka Jylänki https://github.com/juj/RectangleBinPack/
Ported to C# by Sven Magnus This version is also public domain - do whatever you want with it. */ using UnityEngine; using System.Collections; using System.Collections.Generic;
public class UITexturePacker { public int binWidth = 0; public int binHeight = 0; public bool allowRotations;
public List<Rect> usedRectangles = new List<Rect>(); public List<Rect> freeRectangles = new List<Rect>();
public enum FreeRectChoiceHeuristic { RectBestShortSideFit, ///< -BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best. RectBestLongSideFit, ///< -BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best. RectBestAreaFit, ///< -BAF: Positions the rectangle into the smallest free rect into which it fits. RectBottomLeftRule, ///< -BL: Does the Tetris placement. RectContactPointRule ///< -CP: Choosest the placement where the rectangle touches other rects as much as possible. };
public UITexturePacker (int width, int height, bool rotations) { Init(width, height, rotations); }
public void Init (int width, int height, bool rotations) { binWidth = width; binHeight = height; allowRotations = rotations;
Rect n = new Rect(); n.x = 0; n.y = 0; n.width = width; n.height = height;
usedRectangles.Clear();
freeRectangles.Clear(); freeRectangles.Add(n); }
private struct Storage { public Rect rect; public bool paddingX; public bool paddingY; }
public static Rect[] PackTextures (Texture2D texture, Texture2D[] textures, int width, int height, int padding, int maxSize) { if (width > maxSize || height > maxSize) return null; if (width > maxSize || height > maxSize) { int temp = width; width = height; height = temp; } // Force square by sizing up /* if (NGUISettings.forceSquareAtlas) { if (width > height) height = width; else if (height > width) width = height; } */ UITexturePacker bp = new UITexturePacker(width, height, false); Storage[] storage = new Storage[textures.Length];
for (int i = 0; i < textures.Length; i++) { Texture2D tex = textures[i]; if (!tex) continue;
Rect rect = new Rect();
int xPadding = 1; int yPadding = 1;
for (xPadding = 1; xPadding >= 0; --xPadding) { for (yPadding = 1; yPadding >= 0; --yPadding) { rect = bp.Insert(tex.width + (xPadding * padding), tex.height + (yPadding * padding), UITexturePacker.FreeRectChoiceHeuristic.RectBestAreaFit); if (rect.width != 0 && rect.height != 0) break;
// After having no padding if it still doesn't fit -- increase texture size. else if (xPadding == 0 && yPadding == 0) { return PackTextures(texture, textures, width * (width <= height ? 2 : 1), height * (height < width ? 2 : 1), padding, maxSize); } } if (rect.width != 0 && rect.height != 0) break; }
storage[i] = new Storage(); storage[i].rect = rect; storage[i].paddingX = (xPadding != 0); storage[i].paddingY = (yPadding != 0); }
texture.Resize(width, height); texture.SetPixels(new Color[width * height]);
// The returned rects Rect[] rects = new Rect[textures.Length];
for (int i = 0; i < textures.Length; i++) { Texture2D tex = textures[i]; if (!tex) continue;
Rect rect = storage[i].rect; int xPadding = (storage[i].paddingX ? padding : 0); int yPadding = (storage[i].paddingY ? padding : 0); Color[] colors = tex.GetPixels();
// Would be used to rotate the texture if need be. if (rect.width != tex.width + xPadding) { Color[] newColors = tex.GetPixels();
for (int x = 0; x < rect.width; x++) { for (int y = 0; y < rect.height; y++) { int prevIndex = ((int)rect.height - (y + 1)) + x * (int)tex.width; newColors[x + y * (int)rect.width] = colors[prevIndex]; } }
colors = newColors; }
texture.SetPixels((int)rect.x, (int)rect.y, (int)rect.width - xPadding, (int)rect.height - yPadding, colors); rect.x /= width; rect.y /= height; rect.width = (rect.width - xPadding) / width; rect.height = (rect.height - yPadding) / height; rects[i] = rect; } texture.Apply(); return rects; }
public Rect Insert (int width, int height, FreeRectChoiceHeuristic method) { Rect newNode = new Rect(); int score1 = 0; // Unused in this function. We don't need to know the score after finding the position. int score2 = 0; switch (method) { case FreeRectChoiceHeuristic.RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, ref score1, ref score2); break; case FreeRectChoiceHeuristic.RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, ref score1, ref score2); break; case FreeRectChoiceHeuristic.RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, ref score1); break; case FreeRectChoiceHeuristic.RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, ref score2, ref score1); break; case FreeRectChoiceHeuristic.RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, ref score1, ref score2); break; }
if (newNode.height == 0) return newNode;
int numRectanglesToProcess = freeRectangles.Count; for (int i = 0; i < numRectanglesToProcess; ++i) { if (SplitFreeNode(freeRectangles[i], ref newNode)) { freeRectangles.RemoveAt(i); --i; --numRectanglesToProcess; } }
PruneFreeList();
usedRectangles.Add(newNode); return newNode; }
public void Insert (List<Rect> rects, List<Rect> dst, FreeRectChoiceHeuristic method) { dst.Clear();
while (rects.Count > 0) { int bestScore1 = int.MaxValue; int bestScore2 = int.MaxValue; int bestRectIndex = -1; Rect bestNode = new Rect();
for (int i = 0; i < rects.Count; ++i) { int score1 = 0; int score2 = 0; Rect newNode = ScoreRect((int)rects[i].width, (int)rects[i].height, method, ref score1, ref score2);
if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) { bestScore1 = score1; bestScore2 = score2; bestNode = newNode; bestRectIndex = i; } }
if (bestRectIndex == -1) return;
PlaceRect(bestNode); rects.RemoveAt(bestRectIndex); } }
void PlaceRect (Rect node) { int numRectanglesToProcess = freeRectangles.Count; for (int i = 0; i < numRectanglesToProcess; ++i) { if (SplitFreeNode(freeRectangles[i], ref node)) { freeRectangles.RemoveAt(i); --i; --numRectanglesToProcess; } }
PruneFreeList();
usedRectangles.Add(node); }
Rect ScoreRect (int width, int height, FreeRectChoiceHeuristic method, ref int score1, ref int score2) { Rect newNode = new Rect(); score1 = int.MaxValue; score2 = int.MaxValue; switch (method) { case FreeRectChoiceHeuristic.RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, ref score1, ref score2); break; case FreeRectChoiceHeuristic.RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, ref score1, ref score2); break; case FreeRectChoiceHeuristic.RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, ref score1); score1 = -score1; // Reverse since we are minimizing, but for contact point score bigger is better. break; case FreeRectChoiceHeuristic.RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, ref score2, ref score1); break; case FreeRectChoiceHeuristic.RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, ref score1, ref score2); break; }
// Cannot fit the current rectangle. if (newNode.height == 0) { score1 = int.MaxValue; score2 = int.MaxValue; }
return newNode; }
/// Computes the ratio of used surface area. public float Occupancy () { ulong usedSurfaceArea = 0; for (int i = 0; i < usedRectangles.Count; ++i) usedSurfaceArea += (uint)usedRectangles[i].width * (uint)usedRectangles[i].height;
return (float)usedSurfaceArea / (binWidth * binHeight); }
Rect FindPositionForNewNodeBottomLeft (int width, int height, ref int bestY, ref int bestX) { Rect bestNode = new Rect(); //memset(bestNode, 0, sizeof(Rect));
bestY = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { // Try to place the rectangle in upright (non-flipped) orientation. if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { int topSideY = (int)freeRectangles[i].y + height; if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = width; bestNode.height = height; bestY = topSideY; bestX = (int)freeRectangles[i].x; } } if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) { int topSideY = (int)freeRectangles[i].y + width; if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = height; bestNode.height = width; bestY = topSideY; bestX = (int)freeRectangles[i].x; } } } return bestNode; }
Rect FindPositionForNewNodeBestShortSideFit (int width, int height, ref int bestShortSideFit, ref int bestLongSideFit) { Rect bestNode = new Rect(); //memset(&bestNode, 0, sizeof(Rect));
bestShortSideFit = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { // Try to place the rectangle in upright (non-flipped) orientation. if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (shortSideFit < bestShortSideFit || (shortSideFit == bestShortSideFit && longSideFit < bestLongSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = width; bestNode.height = height; bestShortSideFit = shortSideFit; bestLongSideFit = longSideFit; } }
if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) { int flippedLeftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); int flippedLeftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); int flippedShortSideFit = Mathf.Min(flippedLeftoverHoriz, flippedLeftoverVert); int flippedLongSideFit = Mathf.Max(flippedLeftoverHoriz, flippedLeftoverVert);
if (flippedShortSideFit < bestShortSideFit || (flippedShortSideFit == bestShortSideFit && flippedLongSideFit < bestLongSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = height; bestNode.height = width; bestShortSideFit = flippedShortSideFit; bestLongSideFit = flippedLongSideFit; } } } return bestNode; }
Rect FindPositionForNewNodeBestLongSideFit (int width, int height, ref int bestShortSideFit, ref int bestLongSideFit) { Rect bestNode = new Rect(); //memset(&bestNode, 0, sizeof(Rect));
bestLongSideFit = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { // Try to place the rectangle in upright (non-flipped) orientation. if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = width; bestNode.height = height; bestShortSideFit = shortSideFit; bestLongSideFit = longSideFit; } }
if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) { int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = height; bestNode.height = width; bestShortSideFit = shortSideFit; bestLongSideFit = longSideFit; } } } return bestNode; }
Rect FindPositionForNewNodeBestAreaFit (int width, int height, ref int bestAreaFit, ref int bestShortSideFit) { Rect bestNode = new Rect(); //memset(&bestNode, 0, sizeof(Rect));
bestAreaFit = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { int areaFit = (int)freeRectangles[i].width * (int)freeRectangles[i].height - width * height;
// Try to place the rectangle in upright (non-flipped) orientation. if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = width; bestNode.height = height; bestShortSideFit = shortSideFit; bestAreaFit = areaFit; } }
if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) { int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) { bestNode.x = freeRectangles[i].x; bestNode.y = freeRectangles[i].y; bestNode.width = height; bestNode.height = width; bestShortSideFit = shortSideFit; bestAreaFit = areaFit; } } } return bestNode; }
/// Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise. int CommonIntervalLength (int i1start, int i1end, int i2start, int i2end) { if (i1end < i2start || i2end < i1start) return 0; return Mathf.Min(i1end, i2end) - Mathf.Max(i1start, i2start); }
int ContactPointScoreNode (int x, int y, int width, int height) { int score = 0;
if (x == 0 || x + width == binWidth) score += height; if (y == 0 || y + height == binHeight) score += width;
for (int i = 0; i < usedRectangles.Count; ++i) { if (usedRectangles[i].x == x + width || usedRectangles[i].x + usedRectangles[i].width == x) score += CommonIntervalLength((int)usedRectangles[i].y, (int)usedRectangles[i].y + (int)usedRectangles[i].height, y, y + height); if (usedRectangles[i].y == y + height || usedRectangles[i].y + usedRectangles[i].height == y) score += CommonIntervalLength((int)usedRectangles[i].x, (int)usedRectangles[i].x + (int)usedRectangles[i].width, x, x + width); } return score; }
Rect FindPositionForNewNodeContactPoint (int width, int height, ref int bestContactScore) { Rect bestNode = new Rect(); //memset(&bestNode, 0, sizeof(Rect));
bestContactScore = -1;
for (int i = 0; i < freeRectangles.Count; ++i) { // Try to place the rectangle in upright (non-flipped) orientation. if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { int score = ContactPointScoreNode((int)freeRectangles[i].x, (int)freeRectangles[i].y, width, height); if (score > bestContactScore) { bestNode.x = (int)freeRectangles[i].x; bestNode.y = (int)freeRectangles[i].y; bestNode.width = width; bestNode.height = height; bestContactScore = score; } } if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) { int score = ContactPointScoreNode((int)freeRectangles[i].x, (int)freeRectangles[i].y, height, width); if (score > bestContactScore) { bestNode.x = (int)freeRectangles[i].x; bestNode.y = (int)freeRectangles[i].y; bestNode.width = height; bestNode.height = width; bestContactScore = score; } } } return bestNode; }
bool SplitFreeNode (Rect freeNode, ref Rect usedNode) { // Test with SAT if the rectangles even intersect. if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x || usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y) return false;
if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) { // New node at the top side of the used node. if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) { Rect newNode = freeNode; newNode.height = usedNode.y - newNode.y; freeRectangles.Add(newNode); }
// New node at the bottom side of the used node. if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) { Rect newNode = freeNode; newNode.y = usedNode.y + usedNode.height; newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height); freeRectangles.Add(newNode); } }
if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) { // New node at the left side of the used node. if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) { Rect newNode = freeNode; newNode.width = usedNode.x - newNode.x; freeRectangles.Add(newNode); }
// New node at the right side of the used node. if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) { Rect newNode = freeNode; newNode.x = usedNode.x + usedNode.width; newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width); freeRectangles.Add(newNode); } }
return true; }
void PruneFreeList () { for (int i = 0; i < freeRectangles.Count; ++i) for (int j = i + 1; j < freeRectangles.Count; ++j) { if (IsContainedIn(freeRectangles[i], freeRectangles[j])) { freeRectangles.RemoveAt(i); --i; break; } if (IsContainedIn(freeRectangles[j], freeRectangles[i])) { freeRectangles.RemoveAt(j); --j; } } }
bool IsContainedIn (Rect a, Rect b) { return a.x >= b.x && a.y >= b.y && a.x + a.width <= b.x + b.width && a.y + a.height <= b.y + b.height; } }
|