2021-12-21 09:40:39 +08:00

1525 lines
57 KiB
C#

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
#region Image Class
[System.Serializable]
public sealed class ImageAtlasBase
{
[SerializeField]
private Texture2D m_image;
[SerializeField]
private Texture2D m_rotatedImage;
public Texture2D image
{
get
{
return m_attr.isRotated ? m_rotatedImage : m_image;
}
}
[SerializeField]
private ImageAttributes m_attr;
public ImageAttributes attr
{
get
{
return m_attr;
}
}
public ImageAtlasBase(Texture2D image, ImageAttributes attr)
: this(image, attr, true)
{
}
public void Dispose()
{
if (string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m_image)))
Editor.DestroyImmediate(m_image);
if (string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m_rotatedImage)))
Editor.DestroyImmediate(m_rotatedImage);
}
public ImageAtlasBase(Texture2D image, ImageAttributes attr, bool startDefault)
{
m_attr = attr;
if (m_attr.isRotated)
{
m_rotatedImage = image;
GenerateDefaultImage();
m_rotatedImage.name = m_image.name = m_attr.name;
if (startDefault)
m_attr.ChangeOrientation();
}
else
{
m_image = image;
GenerateRotatedImage();
m_image.name = m_rotatedImage.name = m_attr.name;
}
}
public void LoadNewImage(Texture2D image)
{
if (m_attr.isRotated)
{
m_rotatedImage = image;
GenerateDefaultImage();
m_rotatedImage.name = m_image.name = m_attr.name;
m_attr.ChangeOrientation();
}
else
{
m_image = image;
GenerateRotatedImage();
m_image.name = m_rotatedImage.name = m_attr.name;
}
}
public void ChangeRotation()
{
m_attr.isRotated = !m_attr.isRotated;
}
public void SetDefaultOrientation()
{
if (m_attr.isRotated)
m_attr.ChangeOrientation();
}
private void GenerateDefaultImage()
{
m_image = new Texture2D(m_rotatedImage.height, m_rotatedImage.width);
for (int y = 0; y < m_rotatedImage.height; y++)
for (int x = 0; x < m_rotatedImage.width; x++)
m_image.SetPixel(y, x, m_rotatedImage.GetPixel(x, m_rotatedImage.height - y));
m_image.Apply();
}
private void GenerateRotatedImage()
{
m_rotatedImage = new Texture2D(m_image.height, m_image.width);
for (int y = 0; y < m_image.height; y++)
for (int x = 0; x < m_image.width; x++)
m_rotatedImage.SetPixel(y, x, m_image.GetPixel(m_image.width - x - 1, y));
m_rotatedImage.Apply();
}
}
#endregion
#region ImageAttributes Class
[System.Serializable]
public sealed class ImageAttributes
{
public string name;
public Rect position;
public bool isRotated;
public bool canBeRotated = true;
private ImageAttributes()
: this("texture", new Rect(), false)
{
}
public ImageAttributes(string name, Rect position, bool isRotated)
: this(name, position, isRotated, true)
{
}
public ImageAttributes(string name, Rect position, bool isRotated, bool canBeRotated)
{
if (string.IsNullOrEmpty(name))
this.name = "no_name_image";
else
this.name = name;
this.position = position;
this.isRotated = isRotated;
this.canBeRotated = canBeRotated;
}
public void ChangeOrientation()
{
isRotated = !isRotated;
position = new Rect(position.x, position.y, position.height, position.width);
}
}
#endregion
#region ImageAtlas Static Class
public static class ImageAtlas
{
public static ImageAtlasBase[] GetParts(Texture2D atlas, TextAsset codings, TextureImporter importer, bool readFromText)
{
ImageAtlasBase[] parts;
if (readFromText || importer.spritesheet.Length == 0)
{
string[] lines = codings.text.Split('\n');
parts = new ImageAtlasBase[lines.Length];
for (int i = 0; i < parts.Length; i++)
{
ImageAttributes attr = DecodeLine(lines[i]);
Texture2D image = new Texture2D(
(int)attr.position.width,
(int)attr.position.height,
TextureFormat.RGBA32,
false
);
image.SetPixels(
atlas.GetPixels(
(int)attr.position.x,
(int)atlas.height - (int)attr.position.y - (int)attr.position.height,
(int)attr.position.width,
(int)attr.position.height
));
image.Apply();
parts[i] = new ImageAtlasBase(image, attr);
}
}
else
{
SpriteMetaData[] sprites = importer.spritesheet;
parts = new ImageAtlasBase[sprites.Length];
for (int i = 0; i < sprites.Length; i++)
{
ImageAttributes attr = new ImageAttributes(
sprites[i].name,
new Rect(
sprites[i].rect.x,
atlas.height - sprites[i].rect.y - sprites[i].rect.height,
sprites[i].rect.width,
sprites[i].rect.height),
false
);
Texture2D image = new Texture2D(
(int)attr.position.width,
(int)attr.position.height,
TextureFormat.RGBA32,
false
);
image.SetPixels(
atlas.GetPixels(
(int)attr.position.x,
(int)sprites[i].rect.y,
(int)attr.position.width,
(int)attr.position.height
));
image.Apply();
parts[i] = new ImageAtlasBase(image, attr);
}
}
return parts;
}
public static string EncodeLine(ImageAttributes attr)
{
string[] ckBegin = new string[]
{
" --",
" x-",
" y-",
" w-",
" h-",
" r-",
" xx-",
" yy-",
" aa-"
};
string[] ckEnd = new string[]
{
"-- ",
"-x ",
"-y ",
"-w ",
"-h ",
"-r ",
"-xx ",
"-yy ",
"-aa "
};
return
ckBegin[0] + attr.name + ckEnd[0] +
ckBegin[1] + (int)attr.position.x + ckEnd[1] +
ckBegin[2] + (int)attr.position.y + ckEnd[2] +
ckBegin[3] + (int)attr.position.width + ckEnd[3] +
ckBegin[4] + (int)attr.position.height + ckEnd[4] +
ckBegin[5] + (attr.isRotated ? 1 : 0).ToString() + ckEnd[5];
}
private static ImageAttributes DecodeLine(string codings)
{
string[] ckBegin = new string[]
{
" --",
" x-",
" y-",
" w-",
" h-",
" r-",
" xx-",
" yy-",
" aa-"
};
string[] ckEnd = new string[]
{
"-- ",
"-x ",
"-y ",
"-w ",
"-h ",
"-r ",
"-xx ",
"-yy ",
"-aa "
};
string name = GetValue(codings, ckBegin[0], ckEnd[0]);
Rect position = new Rect(
float.Parse(GetValue(codings, ckBegin[1], ckEnd[1])),
float.Parse(GetValue(codings, ckBegin[2], ckEnd[2])),
float.Parse(GetValue(codings, ckBegin[3], ckEnd[3])),
float.Parse(GetValue(codings, ckBegin[4], ckEnd[4]))
);
bool isRotated = int.Parse(GetValue(codings, ckBegin[5], ckEnd[5])) == 1;
return new ImageAttributes(name, position, isRotated);
}
private static string GetValue(string codings, string begin, string end)
{
int startIndex = codings.IndexOf(begin) + begin.Length;
return codings.Substring(startIndex, (codings.IndexOf(end) - startIndex));
}
#region Atlassing Methods
public static void PackParts(ref List<ImageAtlasBase> parts,
out int usedMethod, out int usedSize, int spacing, out Vector2 atlasSize)
{
atlasSize = Vector2.zero;
usedMethod = 0;
usedSize = 32;
if (parts == null || parts.Count == 0)
return;
Rect[] rects;
int[] methods =
{
0,
1,
2,
3,
4,
5,
6,
7
};
int[] sizes =
{
32,
64,
128,
256,
512,
1024,
2048,
4096
};
float bestMag = -1;
Vector2 currSize;
for (int method = 1; method < methods.Length; method++)
{
for (int size = 0; size < sizes.Length; size++)
{
parts.SetDefaultOrientation();
rects = GetPackRects(ref parts, method, sizes[size], spacing, out currSize);
if (rects == null)
continue;
float currSizeMagnitude = (currSize.x + currSize.y) * 0.5f;
if (usedMethod == 0)
{
usedMethod = method;
usedSize = size;
bestMag = currSizeMagnitude;
}
else if (currSizeMagnitude < bestMag)
{
usedMethod = method;
usedSize = size;
bestMag = currSizeMagnitude;
}
}
}
parts.SetDefaultOrientation();
usedSize = sizes[usedSize];
rects = GetPackRects(ref parts, usedMethod, usedSize, spacing, out atlasSize);
if (rects != null)
for (int i = 0; i < rects.Length; i++)
parts[i].attr.position = rects[i];
else
Debug.Log("Either something went wrong or textures don\'t fit in the package!");
}
public static void PackParts(ref List<ImageAtlasBase> parts, int method, int maxSize, int spacing, out Vector2 atlasSize)
{
atlasSize = Vector2.zero;
if (parts == null || parts.Count == 0)
return;
Rect[] rects = null;
if (method == 0)
return;
parts.SetDefaultOrientation();
rects = GetPackRects(ref parts, method, maxSize, spacing, out atlasSize);
if (rects != null)
for (int i = 0; i < rects.Length; i++)
parts[i].attr.position = rects[i];
else
Debug.Log("Either something went wrong or textures don\'t fit in the package!");
}
private static Rect[] GetPackRects(ref List<ImageAtlasBase> parts, int method, int size, int spacing, out Vector2 atlasSize)
{
switch (method)
{
case 1:
return Pack_MethodA(GetContainedElements(parts), size, spacing, out atlasSize);
case 2:
return Pack_MethodB(GetContainedElements(parts), size, spacing, out atlasSize);
default:
return Pack_MethodC(GetContainedElements(parts), size, method - 3, spacing, out atlasSize);
}
}
private static ImageAtlasBase[] GetContainedElements(List<ImageAtlasBase> parts)
{
List<ImageAtlasBase> containedParts = new List<ImageAtlasBase>();
for (int i = 0; i < parts.Count; i++)
if (parts[i].image)
containedParts.Add(parts[i]);
return containedParts.ToArray();
}
#endregion
#region Pack Methods
private static Rect[] Pack_MethodA(ImageAtlasBase[] parts, int size, int spacing, out Vector2 atlasSize)
{
int columnPosition = 0;
int x = 0;
int y = 0;
List<Rect> positions = new List<Rect>();
atlasSize = Vector2.zero;
for (int i = 0; i < parts.Length; i++)
{
int width = parts[i].image.width;
int height = parts[i].image.height;
if (y + height + spacing <= size && x + width + spacing <= size)
{
positions.Add(new Rect(x, y, width, height));
y += height + spacing;
if (columnPosition < x + width + spacing)
columnPosition = x + width + spacing;
}
else if (columnPosition + width <= size)
{
x = columnPosition;
positions.Add(new Rect(x, 0, width, height));
y = height + spacing;
columnPosition = x + width + spacing;
}
else return null;
}
for (int i = 0; i < positions.Count; i++)
{
if (positions[i].xMax > atlasSize.x)
atlasSize.x = (int)positions[i].xMax;
if (positions[i].yMax > atlasSize.y)
atlasSize.y = (int)positions[i].yMax;
}
return positions.ToArray();
}
private static Rect[] Pack_MethodB(ImageAtlasBase[] parts, int size, int spacing, out Vector2 atlasSize)
{
CygonRectanglePacker packer = new CygonRectanglePacker(size, size);
Rect[] positions = new Rect[parts.Length];
atlasSize = Vector2.zero;
for (int i = 0; i < parts.Length; i++)
{
Vector2 point;
if (!packer.TryPack(parts[i].image.width + spacing, parts[i].image.height + spacing, out point))
return null;
else
positions[i] = new Rect(point.x, point.y, parts[i].image.width, parts[i].image.height);
}
for (int i = 0; i < positions.Length; i++)
{
if (positions[i].xMax > atlasSize.x)
atlasSize.x = (int)positions[i].xMax;
if (positions[i].yMax > atlasSize.y)
atlasSize.y = (int)positions[i].yMax;
}
return positions;
}
private static Rect[] Pack_MethodC(ImageAtlasBase[] parts, int size, int method, int spacing, out Vector2 atlasSize)
{
Rect[] positions = new Rect[parts.Length];
atlasSize = Vector2.zero;
MaximalRectanglesBinPack bin = new MaximalRectanglesBinPack();
bin.Init(size, size);
for (int i = 0; i < parts.Length; i++)
{
int texWidth = parts[i].image.width;
int texHeight = parts[i].image.height;
MaximalRectanglesBinPack.FreeRectChoiceHeuristic heuristic;
switch (method)
{
case 0:
heuristic = MaximalRectanglesBinPack.FreeRectChoiceHeuristic.RectBottomLeftRule;
break;
case 1:
heuristic = MaximalRectanglesBinPack.FreeRectChoiceHeuristic.RectBestShortSideFit;
break;
case 2:
heuristic = MaximalRectanglesBinPack.FreeRectChoiceHeuristic.RectBestLongSideFit;
break;
case 3:
heuristic = MaximalRectanglesBinPack.FreeRectChoiceHeuristic.RectBestAreaFit;
break;
default:
heuristic = MaximalRectanglesBinPack.FreeRectChoiceHeuristic.RectContactPointRule;
break;
}
positions[i] = bin.Insert(texWidth + spacing, texHeight + spacing, heuristic, parts[i].attr.canBeRotated);
positions[i].width -= spacing;
positions[i].height -= spacing;
if (positions[i].height <= 0)
return null;
if (texWidth != texHeight &&
positions[i].width == texHeight &&
positions[i].height == texWidth)
parts[i].ChangeRotation();
}
for (int i = 0; i < positions.Length; i++)
{
if (positions[i].xMax > atlasSize.x)
atlasSize.x = (int)positions[i].xMax;
if (positions[i].yMax > atlasSize.y)
atlasSize.y = (int)positions[i].yMax;
}
return positions;
}
#region Maximal Rectangles Bin Pack
private class MaximalRectanglesBinPack
{
#region Variables
private int binWidth;
private int binHeight;
private List<Rect> usedRectangles = new List<Rect>();
private List<Rect> freeRectangles = new List<Rect>();
#endregion
#region Constructors
public MaximalRectanglesBinPack() : this(0, 0) { }
public MaximalRectanglesBinPack(int width, int height)
{
Init(width, height);
}
#endregion
public void Init(int width, int height)
{
binWidth = width;
binHeight = height;
Rect n = new Rect(0, 0, width, height);
usedRectangles.Clear();
freeRectangles.Clear();
freeRectangles.Add(n);
}
public enum FreeRectChoiceHeuristic
{
RectBestShortSideFit,
RectBestLongSideFit,
RectBestAreaFit,
RectBottomLeftRule,
RectContactPointRule
};
#region Insert
public void Insert(List<Rect> rects, out List<Rect> dst, FreeRectChoiceHeuristic method, bool canBeRotated)
{
dst = new List<Rect>();
while (rects.Count > 0)
{
int bestScore1 = int.MaxValue;
int bestScore2 = int.MaxValue;
int bestRectIndex = -1;
Rect bestNode = new Rect();
for (int index = 0; index < rects.Count; index++)
{
int score1;
int score2;
Rect newNode = ScoreRect((int)rects[index].width, (int)rects[index].height, method, out score1, out score2, canBeRotated);
if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2))
{
bestScore1 = score1;
bestScore2 = score2;
bestNode = newNode;
bestRectIndex = index;
}
}
if (bestRectIndex == -1)
return;
PlaceRect(bestNode);
rects.Remove(rects[bestRectIndex]);
}
}
public Rect Insert(int width, int height, FreeRectChoiceHeuristic method, bool canBeRotated)
{
Rect newNode = new Rect();
int score1;
int score2;
switch (method)
{
case FreeRectChoiceHeuristic.RectBestShortSideFit:
newNode = FindPositionForNewNodeBestShortSideFit(width, height, out score1, out score2, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectBottomLeftRule:
newNode = FindPositionForNewNodeBottomLeft(width, height, out score2, out score1, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectContactPointRule:
newNode = FindPositionForNewNodeContactPoint(width, height, out score1, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectBestLongSideFit:
newNode = FindPositionForNewNodeBestLongSideFit(width, height, out score1, out score2, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectBestAreaFit:
newNode = FindPositionForNewNodeBestAreaFit(width, height, out score1, out score2, canBeRotated);
break;
}
if (newNode.height == 0)
return newNode;
int numRectanglesToProcess = freeRectangles.Count;
for (int index = 0; index < numRectanglesToProcess; index++)
if (SplitFreeNode(freeRectangles[index], newNode))
{
freeRectangles.Remove(freeRectangles[index]);
index--;
numRectanglesToProcess--;
}
PruneFreeList();
usedRectangles.Add(newNode);
return newNode;
}
#endregion
public float Occupancy()
{
ulong usedSurfaceArea = 0;
for (int index = 0; index < usedRectangles.Count; index++)
usedSurfaceArea += (ulong)(usedRectangles[index].width * usedRectangles[index].height);
return (float)usedSurfaceArea / (binWidth * binHeight);
}
private Rect ScoreRect(int width, int height, FreeRectChoiceHeuristic method, out int score1, out int score2, bool canBeRotated)
{
Rect newNode = new Rect();
score1 = int.MaxValue;
score2 = int.MaxValue;
switch (method)
{
case FreeRectChoiceHeuristic.RectBestShortSideFit:
newNode = FindPositionForNewNodeBestShortSideFit(width, height, out score1, out score2, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectBottomLeftRule:
newNode = FindPositionForNewNodeBottomLeft(width, height, out score2, out score1, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectContactPointRule:
newNode = FindPositionForNewNodeContactPoint(width, height, out score1, canBeRotated);
score1 = -score1;
break;
case FreeRectChoiceHeuristic.RectBestLongSideFit:
newNode = FindPositionForNewNodeBestLongSideFit(width, height, out score2, out score1, canBeRotated);
break;
case FreeRectChoiceHeuristic.RectBestAreaFit:
newNode = FindPositionForNewNodeBestAreaFit(width, height, out score1, out score2, canBeRotated);
break;
}
if (newNode.height == 0)
{
score1 = int.MaxValue;
score2 = int.MaxValue;
}
return newNode;
}
private void PlaceRect(Rect node)
{
int numRectanglesToProcess = freeRectangles.Count;
for (int index = 0; index < numRectanglesToProcess; index++)
if (SplitFreeNode(freeRectangles[index], node))
{
freeRectangles.Remove(freeRectangles[index]);
index--;
numRectanglesToProcess--;
}
PruneFreeList();
usedRectangles.Add(node);
}
private 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);
}
private 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 index = 0; index < usedRectangles.Count; index++)
{
if (usedRectangles[index].x == x + width || usedRectangles[index].x + usedRectangles[index].width == x)
score += CommonIntervalLength((int)usedRectangles[index].y, (int)(usedRectangles[index].y + usedRectangles[index].height), y, y + height);
if (usedRectangles[index].y == y + height || usedRectangles[index].y + usedRectangles[index].height == y)
score += CommonIntervalLength((int)usedRectangles[index].x, (int)(usedRectangles[index].x + usedRectangles[index].width), x, x + width);
}
return score;
}
#region FindPositionForNewNode Methods
private Rect FindPositionForNewNodeBottomLeft(int width, int height, out int bestX, out int bestY, bool canBeRotated)
{
Rect bestNode = new Rect();
bestY = int.MaxValue;
bestX = int.MaxValue; // Not sure
for (int index = 0; index < freeRectangles.Count; index++)
{
if (freeRectangles[index].width >= width && freeRectangles[index].height >= height)
{
int topSideY = (int)freeRectangles[index].y + height;
if (topSideY < bestY || (topSideY == bestY && freeRectangles[index].x < bestX))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = width;
bestNode.height = height;
bestY = topSideY;
bestX = (int)freeRectangles[index].x;
}
}
if (canBeRotated)
if (freeRectangles[index].width >= height && freeRectangles[index].height >= width)
{
int topSideY = (int)freeRectangles[index].y + width;
if (topSideY < bestY || (topSideY == bestY && freeRectangles[index].x < bestX))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = height;
bestNode.height = width;
bestY = topSideY;
bestX = (int)freeRectangles[index].x;
}
}
}
return bestNode;
}
private Rect FindPositionForNewNodeBestShortSideFit(int width, int height, out int bestShortSideFit, out int bestLongSideFit, bool canBeRotated)
{
Rect bestNode = new Rect();
bestShortSideFit = int.MaxValue;
bestLongSideFit = int.MaxValue; // Not sure
for (int index = 0; index < freeRectangles.Count; index++)
{
if (freeRectangles[index].width >= width && freeRectangles[index].height >= height)
{
int leftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - width);
int leftoverVert = Mathf.Abs((int)freeRectangles[index].height - height);
int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (shortSideFit < bestShortSideFit || (shortSideFit == bestShortSideFit && longSideFit < bestLongSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
if (canBeRotated)
if (freeRectangles[index].width >= height && freeRectangles[index].height >= width)
{
int flippedLeftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - height);
int flippedLeftoverVert = Mathf.Abs((int)freeRectangles[index].height - width);
int flippedShortSideFit = Mathf.Min(flippedLeftoverHoriz, flippedLeftoverVert);
int flippedLongSideFit = Mathf.Max(flippedLeftoverHoriz, flippedLeftoverVert);
if (flippedShortSideFit < bestShortSideFit || (flippedShortSideFit == bestShortSideFit && flippedLongSideFit < bestLongSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = flippedShortSideFit;
bestLongSideFit = flippedLongSideFit;
}
}
}
return bestNode;
}
private Rect FindPositionForNewNodeBestLongSideFit(int width, int height, out int bestShortSideFit, out int bestLongSideFit, bool canBeRotated)
{
Rect bestNode = new Rect();
bestLongSideFit = int.MaxValue;
bestShortSideFit = int.MaxValue; // Not sure
for (int index = 0; index < freeRectangles.Count; index++)
{
if (freeRectangles[index].width >= width && freeRectangles[index].height >= height)
{
int leftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - width);
int leftoverVert = Mathf.Abs((int)freeRectangles[index].height - height);
int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
if (canBeRotated)
if (freeRectangles[index].width >= height && freeRectangles[index].height >= width)
{
int leftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - height);
int leftoverVert = Mathf.Abs((int)freeRectangles[index].height - width);
int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
}
return bestNode;
}
private Rect FindPositionForNewNodeBestAreaFit(int width, int height, out int bestAreaFit, out int bestShortSideFit, bool canBeRotated)
{
Rect bestNode = new Rect();
bestAreaFit = int.MaxValue;
bestShortSideFit = int.MaxValue; // Not sure
for (int index = 0; index < freeRectangles.Count; index++)
{
int areaFit = (int)freeRectangles[index].width * (int)freeRectangles[index].height - width * height;
if (freeRectangles[index].width >= width && freeRectangles[index].height >= height)
{
int leftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - width);
int leftoverVert = Mathf.Abs((int)freeRectangles[index].height - height);
int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestAreaFit = areaFit;
}
}
if (canBeRotated)
if (freeRectangles[index].width >= height && freeRectangles[index].height >= width)
{
int leftoverHoriz = Mathf.Abs((int)freeRectangles[index].width - height);
int leftoverVert = Mathf.Abs((int)freeRectangles[index].height - width);
int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit))
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = shortSideFit;
bestAreaFit = areaFit;
}
}
}
return bestNode;
}
private Rect FindPositionForNewNodeContactPoint(int width, int height, out int bestContactScore, bool canBeRotated)
{
Rect bestNode = new Rect();
bestContactScore = -1;
for (int index = 0; index < freeRectangles.Count; index++)
{
if (freeRectangles[index].width >= width && freeRectangles[index].height >= height)
{
int score = ContactPointScoreNode((int)freeRectangles[index].x, (int)freeRectangles[index].y, width, height);
if (score > bestContactScore)
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = width;
bestNode.height = height;
bestContactScore = score;
}
}
if (canBeRotated)
if (freeRectangles[index].width >= height && freeRectangles[index].height >= width)
{
int score = ContactPointScoreNode((int)freeRectangles[index].x, (int)freeRectangles[index].y, height, width);
if (score > bestContactScore)
{
bestNode.x = freeRectangles[index].x;
bestNode.y = freeRectangles[index].y;
bestNode.width = height;
bestNode.height = width;
bestContactScore = score;
}
}
}
return bestNode;
}
#endregion
private bool SplitFreeNode(Rect freeNode, Rect usedNode)
{
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)
{
if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height)
{
Rect newNode = freeNode;
newNode.height = usedNode.y - newNode.y;
freeRectangles.Add(newNode);
}
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)
{
if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width)
{
Rect newNode = freeNode;
newNode.width = usedNode.x - newNode.x;
freeRectangles.Add(newNode);
}
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;
}
private 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;
}
private void PruneFreeList()
{
for (int index = 0; index < freeRectangles.Count; index++)
for (int i = index + 1; i < freeRectangles.Count; i++)
{
if (IsContainedIn(freeRectangles[index], freeRectangles[i]))
{
freeRectangles.Remove(freeRectangles[index]);
index--;
break;
}
if (IsContainedIn(freeRectangles[i], freeRectangles[index]))
{
freeRectangles.Remove(freeRectangles[i]);
i--;
}
}
}
}
#endregion
#region OutOfSpaceException
#if !NO_SERIALIZATION
[System.Serializable]
#endif
public class OutOfSpaceException : System.Exception
{
/// <summary>Initializes the exception</summary>
public OutOfSpaceException() { }
/// <summary>Initializes the exception with an error message</summary>
/// <param name="message">Error message describing the cause of the exception</param>
public OutOfSpaceException(string message) : base(message) { }
/// <summary>Initializes the exception as a followup exception</summary>
/// <param name="message">Error message describing the cause of the exception</param>
/// <param name="inner">Preceding exception that has caused this exception</param>
public OutOfSpaceException(string message, System.Exception inner) : base(message, inner) { }
#if !NO_SERIALIZATION
/// <summary>Initializes the exception from its serialized state</summary>
/// <param name="info">Contains the serialized fields of the exception</param>
/// <param name="context">Additional environmental informations</param>
protected OutOfSpaceException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context
) :
base(info, context) { }
#endif
}
#endregion
#region RectanglePacker
public abstract class RectanglePacker
{
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="packingAreaWidth">Width of the packing area</param>
/// <param name="packingAreaHeight">Height of the packing area</param>
protected RectanglePacker(int packingAreaWidth, int packingAreaHeight)
{
this.packingAreaWidth = packingAreaWidth;
this.packingAreaHeight = packingAreaHeight;
}
/// <summary>Allocates space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <returns>The location at which the rectangle has been placed</returns>
public virtual Vector2 Pack(int rectangleWidth, int rectangleHeight)
{
Vector2 point;
if (!TryPack(rectangleWidth, rectangleHeight, out point))
throw new OutOfSpaceException("Rectangle does not fit in packing area");
return point;
}
/// <summary>Tries to allocate space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public abstract bool TryPack(
int rectangleWidth, int rectangleHeight, out Vector2 placement
);
/// <summary>Maximum width the packing area is allowed to have</summary>
protected int PackingAreaWidth
{
get { return this.packingAreaWidth; }
}
/// <summary>Maximum height the packing area is allowed to have</summary>
protected int PackingAreaHeight
{
get { return this.packingAreaHeight; }
}
/// <summary>Maximum allowed width of the packing area</summary>
private int packingAreaWidth;
/// <summary>Maximum allowed height of the packing area</summary>
private int packingAreaHeight;
}
#endregion
#region Cygon Packer
public class CygonRectanglePacker : RectanglePacker
{
#region class SliceStartComparer
/// <summary>Compares the starting position of height slices</summary>
private class SliceStartComparer : IComparer<Vector2>
{
/// <summary>Provides a default instance for the anchor rank comparer</summary>
public static SliceStartComparer Default = new SliceStartComparer();
/// <summary>Compares the starting position of two height slices</summary>
/// <param name="left">Left slice start that will be compared</param>
/// <param name="right">Right slice start that will be compared</param>
/// <returns>The relation of the two slice starts ranks to each other</returns>
public int Compare(Vector2 left, Vector2 right)
{
return (int)left.x - (int)right.x;
}
}
#endregion
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="packingAreaHeight">Maximum height of the packing area</param>
public CygonRectanglePacker(int packingAreaWidth, int packingAreaHeight) :
base(packingAreaWidth, packingAreaHeight)
{
this.heightSlices = new List<Vector2>();
// At the beginning, the packing area is a single slice of height 0
this.heightSlices.Add(new Vector2(0, 0));
}
/// <summary>Tries to allocate space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public override bool TryPack(
int rectangleWidth, int rectangleHeight, out Vector2 placement
)
{
// If the rectangle is larger than the packing area in any dimension,
// it will never fit!
if (
(rectangleWidth > PackingAreaWidth) || (rectangleHeight > PackingAreaHeight)
)
{
placement = Vector2.zero;
return false;
}
// Determine the placement for the new rectangle
bool fits = tryFindBestPlacement(rectangleWidth, rectangleHeight, out placement);
// If a place for the rectangle could be found, update the height slice table to
// mark the region of the rectangle as being taken.
if (fits)
integrateRectangle((int)placement.x, rectangleWidth, (int)placement.y + rectangleHeight);
return fits;
}
/// <summary>Finds the best position for a rectangle of the given dimensions</summary>
/// <param name="rectangleWidth">Width of the rectangle to find a position for</param>
/// <param name="rectangleHeight">Height of the rectangle to find a position for</param>
/// <param name="placement">Receives the best placement found for the rectangle</param>
/// <returns>True if a valid placement for the rectangle could be found</returns>
private bool tryFindBestPlacement(
int rectangleWidth, int rectangleHeight, out Vector2 placement
)
{
int bestSliceIndex = -1; // Slice index where the best placement was found
int bestSliceY = 0; // Y position of the best placement found
int bestScore = PackingAreaHeight; // lower == better!
// This is the counter for the currently checked position. The search works by
// skipping from slice to slice, determining the suitability of the location for the
// placement of the rectangle.
int leftSliceIndex = 0;
// Determine the slice in which the right end of the rectangle is located when
// the rectangle is placed at the far left of the packing area.
int rightSliceIndex = this.heightSlices.BinarySearch(
new Vector2(rectangleWidth, 0), SliceStartComparer.Default
);
if (rightSliceIndex < 0)
rightSliceIndex = ~rightSliceIndex;
while (rightSliceIndex <= this.heightSlices.Count)
{
// Determine the highest slice within the slices covered by the rectangle at
// its current placement. We cannot put the rectangle any lower than this without
// overlapping the other rectangles.
int highest = (int)this.heightSlices[leftSliceIndex].y;
for (int index = leftSliceIndex + 1; index < rightSliceIndex; ++index)
if (this.heightSlices[index].y > highest)
highest = (int)this.heightSlices[index].y;
// Only process this position if it doesn't leave the packing area
if ((highest + rectangleHeight <= PackingAreaHeight))
{
int score = highest;
if (score < bestScore)
{
bestSliceIndex = leftSliceIndex;
bestSliceY = highest;
bestScore = score;
}
}
// Advance the starting slice to the next slice start
++leftSliceIndex;
if (leftSliceIndex >= this.heightSlices.Count)
break;
// Advance the ending slice until we're on the proper slice again, given the new
// starting position of the rectangle.
int rightRectangleEnd = (int)this.heightSlices[leftSliceIndex].x + rectangleWidth;
for (; rightSliceIndex <= this.heightSlices.Count; ++rightSliceIndex)
{
int rightSliceStart;
if (rightSliceIndex == this.heightSlices.Count)
rightSliceStart = PackingAreaWidth;
else
rightSliceStart = (int)this.heightSlices[rightSliceIndex].x;
// Is this the slice we're looking for?
if (rightSliceStart > rightRectangleEnd)
break;
}
// If we crossed the end of the slice array, the rectangle's right end has left
// the packing area, and thus, our search ends.
if (rightSliceIndex > this.heightSlices.Count)
break;
} // while rightSliceIndex <= this.heightSlices.Count
// Return the best placement we found for this rectangle. If the rectangle
// didn't fit anywhere, the slice index will still have its initialization value
// of -1 and we can report that no placement could be found.
if (bestSliceIndex == -1)
{
placement = Vector2.zero;
return false;
}
else
{
placement = new Vector2(this.heightSlices[bestSliceIndex].x, bestSliceY);
return true;
}
}
/// <summary>Integrates a new rectangle into the height slice table</summary>
/// <param name="left">Position of the rectangle's left side</param>
/// <param name="width">Width of the rectangle</param>
/// <param name="bottom">Position of the rectangle's lower side</param>
private void integrateRectangle(int left, int width, int bottom)
{
// Find the first slice that is touched by the rectangle
int startSlice = this.heightSlices.BinarySearch(
new Vector2(left, 0), SliceStartComparer.Default
);
int firstSliceOriginalHeight;
// Since the placement algorithm always places rectangles on the slices,
// the binary search should never some up with a miss!
//if (startSlice >= 0)
// Debug.Log("Slice starts within another slice");
//Debug.Assert(
// startSlice >= 0, "Slice starts within another slice"
//);
// We scored a direct hit, so we can replace the slice we have hit
firstSliceOriginalHeight = (int)this.heightSlices[startSlice].y;
this.heightSlices[startSlice] = new Vector2(left, bottom);
int right = left + width;
++startSlice;
// Special case, the rectangle started on the last slice, so we cannot
// use the start slice + 1 for the binary search and the possibly already
// modified start slice height now only remains in our temporary
// firstSliceOriginalHeight variable
if (startSlice >= this.heightSlices.Count)
{
// If the slice ends within the last slice (usual case, unless it has the
// exact same width the packing area has), add another slice to return to
// the original height at the end of the rectangle.
if (right < PackingAreaWidth)
this.heightSlices.Add(new Vector2(right, firstSliceOriginalHeight));
}
else
{ // The rectangle doesn't start on the last slice
int endSlice = this.heightSlices.BinarySearch(
startSlice, this.heightSlices.Count - startSlice,
new Vector2(right, 0), SliceStartComparer.Default
);
// Another direct hit on the final slice's end?
if (endSlice > 0)
{
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
}
else
{ // No direct hit, rectangle ends inside another slice
// Make index from negative BinarySearch() result
endSlice = ~endSlice;
// Find out to which height we need to return at the right end of
// the rectangle
int returnHeight;
if (endSlice == startSlice)
returnHeight = firstSliceOriginalHeight;
else
returnHeight = (int)this.heightSlices[endSlice - 1].y;
// Remove all slices covered by the rectangle and begin a new slice at its end
// to return back to the height of the slice on which the rectangle ends.
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
if (right < PackingAreaWidth)
this.heightSlices.Insert(startSlice, new Vector2(right, returnHeight));
} // if endSlice > 0
} // if startSlice >= this.heightSlices.Count
}
/// <summary>Stores the height silhouette of the rectangles</summary>
private List<Vector2> heightSlices;
}
#endregion
#endregion
#region Alphanumeric Comparator Fast
public class AlphanumComparatorFast : IComparer<FileInfo>
{
//public int Compare(object x, object y)
public int Compare(FileInfo x, FileInfo y)
{
//string s1 = x as string;
string s1 = x.Name;
if (s1 == null)
{
return 0;
}
//string s2 = y as string;
string s2 = y.Name;
if (s2 == null)
{
return 0;
}
int len1 = s1.Length;
int len2 = s2.Length;
int marker1 = 0;
int marker2 = 0;
// Walk through two the strings with two markers.
while (marker1 < len1 && marker2 < len2)
{
char ch1 = s1[marker1];
char ch2 = s2[marker2];
// Some buffers we can build up characters in for each chunk.
char[] space1 = new char[len1];
int loc1 = 0;
char[] space2 = new char[len2];
int loc2 = 0;
// Walk through all following characters that are digits or
// characters in BOTH strings starting at the appropriate marker.
// Collect char arrays.
do
{
space1[loc1++] = ch1;
marker1++;
if (marker1 < len1)
{
ch1 = s1[marker1];
}
else
{
break;
}
} while (char.IsDigit(ch1) == char.IsDigit(space1[0]));
do
{
space2[loc2++] = ch2;
marker2++;
if (marker2 < len2)
{
ch2 = s2[marker2];
}
else
{
break;
}
} while (char.IsDigit(ch2) == char.IsDigit(space2[0]));
// If we have collected numbers, compare them numerically.
// Otherwise, if we have strings, compare them alphabetically.
string str1 = new string(space1);
string str2 = new string(space2);
int result;
if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]))
{
int thisNumericChunk = int.Parse(str1);
int thatNumericChunk = int.Parse(str2);
result = thisNumericChunk.CompareTo(thatNumericChunk);
}
else
{
result = str1.CompareTo(str2);
}
if (result != 0)
{
return result;
}
}
return len1 - len2;
}
}
#endregion
}
#endregion
public static class ImageAtlasBaseExtensions
{
public static void Dispose(this List<ImageAtlasBase> images)
{
for (int i = 0; i < images.Count; i++)
images[i].Dispose();
images.Clear();
}
public static void SetDefaultOrientation(this List<ImageAtlasBase> images)
{
for (int i = 0; i < images.Count; i++)
images[i].SetDefaultOrientation();
}
}