图集预览,MaxRectBinPacker算法
- 从此类派生以创建编辑器窗口:EditorWindow
- 获取当前屏幕T类型的EditorWindow:EditorWindow.GetWindow
- 访问编辑器中的选择:Selection
- 返回所选资源的GUID:Selection.assetGUIDs
- 得到物体的完整路径:AssetDatabase.GUIDToAssetPath
- 项目所在的磁盘物理路径:Application.dataPath
Unity原生打出来的图集在算法和策略上不是很好,都是从左下往右上扩散,所以需要重写贴图合并的算法
维护“空闲矩形列表”。初始时,整张图片作为一个空闲矩形 若要放入一张图片,则在“空闲矩形列表”中寻找,找到一个合适的矩形,并从“空闲矩形列表”移除,把找到的矩形拆分为一个或多个矩形,其中一个正好容纳图片。剩余的矩形加入“空闲矩形列表”之中,因此主要的问题就是:
- 如何找到合适的矩形
- 找到矩形之后如何切分
- 如何应对多张图片的添加、删除
遍历当前的“空闲矩形列表”,忽略那些无法容纳的,然后按照公式计算得分。 得分最高者作为结果。公式有:
- BestAreaFit - 面积最接近
- BestShortSideFit - 短边最接近
- BestLongSideFit - 长边最接近
- BottomLeftRule - 放在最左下
- ContactPointRule - 尽可能与更多的矩形相邻
例如一个 100x100 的矩形在装入一张 30x30 的图片之后。将剩下以下矩形:
- 方案 1. 剩下 30x70 和 70x100
- 方案 2. 剩下 70x30 和 100x70
- 方案 3. 全都要。把上述矩形都加入“空闲矩形列表”。
方案 3 最佳。
实际的做法应该是,遍历“空闲矩形列表”中的每一个,只要其与切分的矩形有交叉,则进行切割。 具体可以看 AS3 代码的 placeRectangle, splitFreeNode 这两个函数。
完成之后,用 pruneFreeList 进行修剪。即遍历“空闲矩形列表”,如果发现某矩形包含另一个矩形,则把小的矩形删除。 pruneFreeList是一个 O(n^2) 复杂度的计算,是整个算法最耗时的部分。
如果需要把多张图片加入合图,只需要逐个处理即可。 当从合图中删除图片时,进行以下处理:
遍历“空闲矩形列表”,如果发现有矩形正好与刚才加入的矩形相邻,则判断能否把两个矩形合并。在判断时,还需要一个“已经用掉的矩形列表”。 合并之后,也调用 pruneFreeList 进行修剪。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace EditorTools.UI {
//定义贴图数据的排序
public class Texture2DComparison : IComparer<TextureData> {
public int Compare(TextureData x, TextureData y) { //进行比较的贴图数据x和y
int ret = 0;
if (Mathf.Max(x.width , x.height) > Mathf.Max(y.width , y.height)) { //x宽高最大值 比 y宽高最大值 大
ret = -1; //返回负数,代表x插入在y的前面
} else if (Mathf.Max(x.width , x.height) < Mathf.Max(y.width + y.height)) { //x的宽高最大值小于y的宽高值
ret = 1; //返回整数,代表将x插入在y的后面
}
return ret;//返回相等
}
}
public class MaxRectsBinPack {
//是否使用此算法进行图集合并
public static bool IsUseMaxRectsAlgo = true;
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: 短边最接近
RectBestLongSideFit, // BLSF: 长边最接近
RectBestAreaFit, // BAF: 面积最接近
RectBottomLeftRule, /// BL: 放在最左下
RectContactPointRule // CP: 尽可能与更多矩形相邻
};
public MaxRectsBinPack(int width, int height, bool rotations = true) { //roattions 是否可旋转
Init(width, height, rotations);
}
//一些初始化操作,清空空闲矩形列表
public void Init(int width, int height, bool rotations = true) {
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); //把初始化的矩形添加进来
}
public Rect Insert(int width, int height, FreeRectChoiceHeuristic method) {
Rect newNode = new Rect();
int score1 = 0;
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;//返回经过计算匹配到的最佳矩形
}
//优先匹配最左下角的矩形匹配方法
Rect FindPositionForNewNodeBottomLeft(int width, int height, ref int bestY, ref int bestX) {
Rect bestNode = new Rect(); //创建矩形
bestY = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { //遍历空闲矩形列表
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { //若查找到一个长宽都大于目标的长宽的矩形
int topSideY = (int)freeRectangles[i].y + height; //下边界等于查找到的空闲矩形的y坐标 + 目标矩形的高度
// 新的满足条件的矩形下边界比上一次满足条件的矩形还要小,下边界相等但是x比上一次满足条件的矩形要小,也就是说新匹配的矩形比上次满足条件的矩形更加左下
if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) { //<是由Rect的坐标系轴增量方向决定的,易混淆
bestNode.x = freeRectangles[i].x; //寻找的矩形左下点x坐标为符合条件的矩形左上点x坐标
bestNode.y = freeRectangles[i].y; //寻找的矩形左下点y坐标为符合条件的矩形左上点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();//创建一个新矩形
bestShortSideFit = int.MaxValue;//最短边匹配
for (int i = 0; i < freeRectangles.Count; ++i) { //遍历空闲矩形列表
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();//创建一个新矩形
bestLongSideFit = int.MaxValue;//最长边匹配
for (int i = 0; i < freeRectangles.Count; ++i) {//遍历空闲矩形列表
//遍历到一个可以容纳目标矩形的空闲矩形
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();//创建匹配矩形
bestAreaFit = int.MaxValue;
for (int i = 0; i < freeRectangles.Count; ++i) { //遍历空闲矩形列表
int areaFit = (int)freeRectangles[i].width * (int)freeRectangles[i].height - width * height; //计算面积匹配度,轮询空闲矩形的面积减掉目标匹配矩形的面积
// 查找到一个可以容纳目标矩形的空闲矩形
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;
}
//
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);
}
//传入匹配的空闲矩形的左下点坐标x,y,和目标矩形的宽高
int ContactPointScoreNode(int x, int y, int width, int height) {
int score = 0;
if (x == 0 || x + width == binWidth) //binWidth在初始化Init的时候赋值maxWidth,匹配的空闲矩形左边靠边或者x + 目标宽度 = 右边界 贴右边
score += height; //相邻值只加高度
if (y == 0 || y + height == binHeight) //binHeight在初始化Init的时候赋值maxHeight,匹配的空闲矩形下边界贴边或者y + 目标高度 = 上边界 贴上边
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();
bestContactScore = -1;//初始化最佳相邻值 -1
for (int i = 0; i < freeRectangles.Count; ++i) { //遍历空闲矩形列表
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;
}
//划分方法会被遍历的执行,Insert中遍历空闲矩形列表来执行该方法,划分空闲矩形和匹配到的矩形,若空闲矩形和使用矩形有交叉部分就进行分割,并把分割的矩形加入空闲列表
bool SplitFreeNode(Rect freeNode, ref 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);
}
// 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) { //O(n)的复杂度
if (IsContainedIn(freeRectangles[i], freeRectangles[j])) { //判断两矩形是否包含关系
freeRectangles.RemoveAt(i);//j包含i,则移除i
--i;
break;
}
if (IsContainedIn(freeRectangles[j], freeRectangles[i])) {//j不包含i,判断下是否i包含j,包含则移除j
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;
}
private static int Log2Floor(int n) { //贴图宽高
if (n == 0)
return -1;
int log = 0;
int value = n;
for (int i = 4; i >= 0; --i) { //
int shift = (1 << i);
int x = value >> shift;
if (x != 0) {
value = x;
log += shift;
}
}
return log;
}
private static int Log2Ceiling(int n) { //传入贴图的宽或高
if (n == 0) {
return -1;
} else {
return 1 + Log2Floor(n - 1);
}
}
private static int LogPow(int i) {
return (int)Math.Pow(2, Log2Ceiling(i));
}
//外部调用的主方法,打包贴图 textures是一个Texture2D列表,包含一个图集需要包含的所有贴图数据,最终将这个贴图数据列表的所有贴图数据合并成一张大贴图
//textures是排好序的,排序规则是矩形的宽高最大值越大,越在数组的前面,保证处理贴图数据的顺序是从较大贴图开始处理的
public static Rect[] PackTextures(Texture2D texture, Texture2D[] textures) {
int maxWidth = LogPow(textures[0].width);//寻找最接近宽度的2的n次方的数值
int maxHeight = LogPow(textures[0].height);//寻找最接近长度的2的n次方的数值
Rect[] aryRects = CalcByGrowHeight(textures, ref maxWidth, ref maxHeight);
//将纹理合成在一起
texture.Resize(maxWidth, maxHeight);
texture.SetPixels(new Color[maxWidth * maxHeight]);
Rect[] rects = new Rect[textures.Length];
//记录rect,以便让unity的sprite meta使用
for (int i = 0; i < textures.Length; i++) {
Texture2D tex = textures[i];
if (!tex) continue;
Rect rect = aryRects[i];
Color[] colors = tex.GetPixels();
texture.SetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height, colors);
rect.x /= maxWidth;
rect.y /= maxHeight;
rect.width = rect.width / maxWidth;
rect.height = rect.height / maxHeight;
rects[i] = rect;
}
texture.Apply();
return rects;
}
// 向高度增长空间
private static Rect[] CalcByGrowHeight(Texture2D[] textures, ref int maxWidth, ref int maxHeight) {
bool successed = true;
Rect[] aryRects = new Rect[textures.Length]; // 返回的矩形
while (true) {
MaxRectsBinPack packer = new MaxRectsBinPack(maxWidth, maxHeight, false); //传入合并的宽高,不可以旋转,这一步主要输为了初始化数据,空闲列表等
for (int i = 0; i < textures.Length; ++i) { //忽略数组0的元素
Texture2D tex = textures[i]; //贴图数据
Rect rect = packer.Insert(tex.width, tex.height, FreeRectChoiceHeuristic.RectBestAreaFit); //使用面积最接近的方式进行矩形的插入
aryRects[i] = rect;
if (rect.width == 0 || rect.height == 0) {
//不通过
successed = false;
break;
}
if (i == textures.Length - 1) {
successed = true;
}
}
if (successed) {
break;
}
//先往高度发展,如果发现高度大于1024,则向宽度发展
if (maxHeight >= AtlasGenerator.ATLAS_MAX_SIZE && maxWidth < AtlasGenerator.ATLAS_MAX_SIZE) {
//不允许超过2048
maxWidth *= 2;
} else if (maxHeight >= AtlasGenerator.FAVOR_ATLAS_SIZE && maxWidth < AtlasGenerator.FAVOR_ATLAS_SIZE) {
//尽量不能超过1024
maxWidth *= 2;
} else if (maxHeight >= maxWidth * 2) {
//尽量使宽和高之间的比例不要相差太大,防止IOS上会扩展成更大的正方形
maxWidth *= 2;
} else {
maxHeight *= 2;
}
aryRects = new Rect[textures.Length];
}
return aryRects;
}
}
}
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
namespace EditorTools.UI {
//图集预览工具
public class AtlasPreviewer : EditorWindow {
public const int ATLAS_MAX_SIZE = 2048;
public const int FAVOR_ATLAS_SIZE = 1024;
private static AtlasPreviewer _window;
private static Texture2D _atlas;
[MenuItem("Assets/预览图集", false, 102)]
public static void PreviewAtlas() {
const string basePath = "Assets/UI";//UI资源基础目录
string[] aryAssetGuids = Selection.assetGUIDs;//选中对象的guid
if (aryAssetGuids != null && aryAssetGuids.Length > 0) { //有选中
string folderPath = AssetDatabase.GUIDToAssetPath(aryAssetGuids[0]);//资源所在的目录 Assets/Things/Textures/UI/xxx
if (folderPath == basePath) { //不能是资源根目录
Debug.Log("请选择[{0}]的子目录:" + basePath);
_atlas = null;
} else if (folderPath.StartsWith(basePath)) { //以UI资源基础目录为开始前缀
if (HasSubDirectory(folderPath)) { //选中的目录内包含多个目录
_atlas = null;
Debug.Log("此目录下还有子目录,请选择只包含图片的子目录");
} else {
string[] assetPaths = GetAssetPaths(folderPath); //获取目录下所有资源的路径
TextureData[] textureDatas = ReadTextures(assetPaths);//读取所有资源路径对应的资源到TextureData数组
if (MaxRectsBinPack.IsUseMaxRectsAlgo) { //使用MaxRect算法
_atlas = CreateAtlasMaxRect(textureDatas);//使用MaxRect算法来创建图集
} else {
_atlas = CreateAtlas(textureDatas);//unity Texture2D自带的图集合并
}
}
} else {
Debug.Log("请选择[{0}]的子目录:" + basePath);//选择了奇怪的目录
_atlas = null;
}
}
_window = EditorWindow.GetWindow<AtlasPreviewer>("图集预览"); //获取EditorWindow
_window.Show();//显示
_window.position = new Rect(1920 / 2 - 250, 1080 / 2 - 350, 500, 600);//显示的位置
}
private static string[] GetAssetPaths(string folderPath) {
string systemPath = ToFileSystemPath(folderPath);
string[] filePaths = Directory.GetFiles(systemPath, "*.png");
string[] result = new string[filePaths.Length];
for (int i = 0; i < filePaths.Length; i++) {
result[i] = ToAssetPath(filePaths[i]);
}
return result;
}
//是否包含子文件夹
private static bool HasSubDirectory(string folderPath) {
string systemPath = ToFileSystemPath(folderPath); //获取到存储磁盘的物理路径
string[] dirPaths = Directory.GetDirectories(systemPath);// 获取目录下的子目录
return dirPaths != null && dirPaths.Length > 0;//子目录不为空 且子目录长度大于等于1
}
//读取一个图片资源路径列表的数据,返回TextureData数组
private static TextureData[] ReadTextures(string[] assetPaths) {
TextureData[] textureDatas = new TextureData[assetPaths.Length];//创建TextureData数组
for (int i = 0; i < assetPaths.Length; i++) { //遍历路径列表
Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPaths[i], typeof(Sprite)) as Sprite; //加载图片资源 Sprite
TextureData data = new TextureData();//创建TextureData对象
if (sprite == null) { //Sprite不存在则报错
Debug.LogErrorFormat("ReadTextures sprite is null at assetPaths[{0}]", assetPaths[i]);
}
data.name = sprite.name; //赋予名称
Vector4 border = sprite.border; //v4变量 返回sprite的边框的大小 X=左边框/Y=下边框/Z=右边框/W=上边框
data.top = (int)border.w;
data.right = (int)border.z;
data.bottom = (int)border.y;
data.left = (int)border.x;
Texture2D texture = AssetDatabase.LoadAssetAtPath(assetPaths[i], typeof(Texture2D)) as Texture2D;
if (textureDatas.Length > 1) { //包含多个图片资源
texture = TextureClamper.Clamp(texture);//给每个单元图片四周补两像素
} else {
texture = TextureClamper.ClampSingle(texture);//单图不进行外边框的2像素填充
}
data.texture = texture;//texture数据赋给TextureData的texture属性
data.width = texture.width;//宽
data.height = texture.height;//高
textureDatas[i] = data;//存储下来
}
return textureDatas;//将处理好的TextureData数组返回
}
private static Texture2D CreateAtlas(TextureData[] textureDatas) {
Texture2D atlas = new Texture2D(ATLAS_MAX_SIZE, ATLAS_MAX_SIZE);//创建图集按照最大尺寸
atlas.PackTextures(GetPackTextures(textureDatas), 0, ATLAS_MAX_SIZE, false);//传入纹理数组,纹理间距0,最大尺寸,不关闭可读
return atlas;//返回填充的图集,本质上为一个合并好的texture2D
}
//根据贴图数组来创建合并好的贴图图集
private static Texture2D CreateAtlasMaxRect(TextureData[] textureDatas) {
Array.Sort<TextureData>(textureDatas, new Texture2DComparison());//读贴图数据进行排序
Texture2D atlas = new Texture2D(ATLAS_MAX_SIZE, ATLAS_MAX_SIZE);//创建Texture2D图集,按照最大尺寸
MaxRectsBinPack.PackTextures(atlas, GetPackTextures(textureDatas));//将贴图数据(每个sprite)填充进atlas图集
return atlas;
}
//获取打包的texture2D贴图数据数组
private static Texture2D[] GetPackTextures(TextureData[] textureDatas) {
Texture2D[] result = new Texture2D[textureDatas.Length];//创建贴图数组
for (int i = 0; i < textureDatas.Length; i++) {//遍历贴图数据数组
result[i] = textureDatas[i].texture;//把纹理数据保存下来
}
return result;
}
private void OnGUI() {
try {
ShowAtlasTexture();
} catch (Exception e) {
Debug.Log(e);
}
}
private void OnDestroy() {
}
private static void ShowAtlasTexture() {
if (_atlas == null) {
return;
}
GUILayout.BeginHorizontal();
Color back = GUI.color;
GUI.color = Color.white;
GUILayout.Label("生成图集尺寸: " + _atlas.width.ToString() + "x" + _atlas.height.ToString(), EditorStyles.whiteLabel);
GUI.color = back;
GUILayout.EndHorizontal();
ShowTexture(_atlas);
}
private static void ShowTexture(Texture2D texture) {
int width = texture.width;
int height = texture.height;
float ratio = 1.0f;
float previewSize = 512.0f;
if (width > previewSize || height > previewSize) {
if (width > height) {
ratio = previewSize / (float)width;
} else {
ratio = previewSize / (float)height;
}
}
GUILayout.Box(texture, GUILayout.Width(width * ratio), GUILayout.Height(height * ratio));
}
public static string ToFileSystemPath(string assetPath) {
return Application.dataPath.Replace("Assets", "") + assetPath; //assetPath 资源路径
}
public static string ToAssetPath(string systemPath) {
systemPath = systemPath.Replace("\\", "/");
return "Assets" + systemPath.Substring(Application.dataPath.Length);
}
}
public class TextureData {
public string name;
public Texture2D texture;
public int width;
public int height;
public int top;
public int right;
public int bottom;
public int left;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEditor;
namespace EditorTools.UI {
public class TextureClamper {
//做图集的时候,给单元图片四周补2像素,否则界面缩放时会出现黑标或透明边的现象
public const int BORDER = 2;
public static Texture2D Clamp(Texture2D sourceTexture) {
int sourceWidth = sourceTexture.width; //贴图宽高
int sourceHeight = sourceTexture.height;
//Texture2D.GetPixels32()返回的数组是二维数组,像素布局从左到右,从底到顶(即,一行行),数组的大小是使用mip level的宽乘高。默认的mip level为0(基本纹理),这时候数组的大小是纹理的大小。
//在一般情况下,mip level大小是mipWidth=max(1,width>>miplevel) ,高度也同样。
Color32[] sourcePixels = sourceTexture.GetPixels32();
int targetWidth = sourceWidth + BORDER * 2;
int targetHeight = sourceHeight + BORDER * 2;//外围补2 pixel
Color32[] targetPixels = new Color32[targetWidth * targetHeight]; //像素数组
Texture2D targetTexture = new Texture2D(targetWidth, targetHeight); //按照贴图大小创建一个Texture对象
for (int i = 0; i < sourceHeight; i++) { //遍历源贴图的高
for (int j = 0; j < sourceWidth; j++) { //遍历源贴图的宽
targetPixels[(i + BORDER) * targetWidth + (j + BORDER)] = sourcePixels[i * sourceWidth + j]; //这一步将源贴图的像素映射到了目标生成贴图的最中心,即外围包裹2 pixel
}
}
//上下左右四周各补2像素源贴图的边缘临界像素值
//左边缘
for (int v = 0; v < sourceHeight; v++) {
for (int k = 0; k < BORDER; k++) {
targetPixels[(v + BORDER) * targetWidth + k] = sourcePixels[v * sourceWidth];
}
}
//右边缘
for (int v = 0; v < sourceHeight; v++) {
for (int k = 0; k < BORDER; k++) {
targetPixels[(v + BORDER) * targetWidth + (sourceWidth + BORDER + k)] = sourcePixels[v * sourceWidth + sourceWidth - 1];
}
}
//上边缘
for (int h = 0; h < sourceWidth; h++) {
for (int k = 0; k < BORDER; k++) {
targetPixels[(sourceHeight + BORDER + k) * targetWidth + BORDER + h] = sourcePixels[(sourceHeight - 1) * sourceWidth + h];
}
}
//下边缘
for (int h = 0; h < sourceWidth; h++) {
for (int k = 0; k < BORDER; k++) {
targetPixels[k * targetWidth + BORDER + h] = sourcePixels[h];
}
}
targetTexture.SetPixels32(targetPixels); //为贴图设置像素信息,自动将一维的像素数组转化成二维的贴图信息数组
targetTexture.Apply();//实际应用任何先前的 SetPixel 和 SetPixels 更改,将贴图数据进行应用
return targetTexture; //返回targetTexture贴图数据
}
public static Texture2D ClampSingle(Texture2D sourceTexture) {
int sourceWidth = sourceTexture.width;
int sourceHeight = sourceTexture.height;
Color32[] sourcePixels = sourceTexture.GetPixels32();
int targetWidth = sourceWidth;
int targetHeight = sourceHeight;
Texture2D targetTexture = new Texture2D(targetWidth, targetHeight);
targetTexture.SetPixels32(sourcePixels);
targetTexture.Apply();
return targetTexture;
}
}
}