要写游戏Demo,所以临时扒了游戏资源来用,你懂的。然鹅~,非Unity开发的游戏,大多使用TexturePacker制作图集,这东东Unity无法直接使用,虽然Sprite Editor自带三种拆分图集的方法,但是会有误差,往往不能达到要求,尤其是帧动画图集对子图大小和偏移要求必须精准,稍有误差就会鬼畜。
网上找了好久图集拆分工具,要么是根据透明像素自动拆分,要么是python写的,还需要配置一堆环境,安装所需库。最终无果,一怒之下自己写个吧,毕竟也算个痛点。
图集一般会包含两个文件,一个是记录子图位置和偏移的图集配置表,通常是plist格式,另一个是合图。
一.生成图集:这里的生成图集指的是根据图集表精准还原生成子图的大小和偏移数值。
实际上就是根据图集表自动填充如图所示区域的数据:

这个需求很简单,怎么实现我不管😎:
很明显只需以下两步:
1. 解析图集配置表:
很遗憾,我随手扒来的游戏资源是使用libgdx(一款开源的小众游戏引擎,我没用过的统统算小众!!!) 开发的。它使用的图集格式与plist不同,所以想要解析plist格式,那就自己个儿写解析部分吧!
libgdx所用图集格式:
img_atlas.png
format: RGBA8888
filter: Linear, Linear
repeat: none
img_0
rotate: false
xy: 2, 2
size: 353, 62
orig: 353, 62
offset: 0, 0
index: -1
img_1
rotate: false
xy: 2, 66
size: 353, 62
orig: 353, 62
offset: 0, 0
index: -1
只需要拿到每个子图的x,y和size值就OK啦。代码如下:
/// <summary>
/// 子图 信息
/// </summary>
internal class GDXAtlasElement
{
internal string name;
internal Rect rect;
}
/// <summary>
/// 图集 信息
/// </summary>
internal class GDXAtlas
{
internal string name;
internal List<GDXAtlasElement> elements;
}
/// <summary>
/// 解析图集配置表工具类
/// </summary>
internal class GDXAtlasUtility
{
internal static bool ParseAtlas(string atlasTxtFile, out GDXAtlas atlas)
{
atlas = null;
if (!File.Exists(atlasTxtFile))
{
return false;
}
var lines = new List<string>();
foreach (var line in File.ReadLines(atlasTxtFile))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
lines.Add(line);
}
if (lines.Count <= 0)
{
return false;
}
string atlasName = lines[0].Trim();
List<GDXAtlasElement> elements = new List<GDXAtlasElement>();
for (int i = 4; i < lines.Count;)
{
if (i + 6 > lines.Count)
{
break;
}
string eName = lines[i];
string[] eXyValues = lines[i + 2].Trim().Split(':')[1].Trim().Split(',');
string[] eSizeValues = lines[i + 3].Trim().Split(':')[1].Trim().Split(',');
GDXAtlasElement spElement = new GDXAtlasElement();
spElement.name = eName.Trim();
Vector2 position = new Vector2(float.Parse(eXyValues[0].Trim()), float.Parse(eXyValues[1].Trim()));
Vector2 size = new Vector2(float.Parse(eSizeValues[0].Trim()), float.Parse(eSizeValues[1].Trim()));
spElement.rect = new Rect(position, size);
elements.Add(spElement);
i += 7;
}
atlas = new GDXAtlas();
atlas.name = atlasName;
atlas.elements = elements;
return true;
}
}
2.填充子图数据:
也就是把第1部解析的数据填充到图1所示红框部分。
恕我直言,最大的难点就是找到哪个类中的哪个变量储存着子图信息,反正我是找了好久。划重点!!!这个存放图集子图的变量就是:TextureImporter.spritesheet
private void AutoSliceAtlas()
{
EditorUtility.DisplayProgressBar("Progress", "Slice Atlas...", 0);
string[] dirs = { "Assets/MainGame/Textures/zombie_assets" };
var assetIds = AssetDatabase.FindAssets("t:Texture", dirs);
for (int i = 0; i < assetIds.Length; i++)
{
string spFileName = AssetDatabase.GUIDToAssetPath(assetIds[i]);
var spTex = AssetDatabase.LoadAssetAtPath<Texture>(spFileName);
string atlasTxtFile = string.Format("{0}{1}", Application.dataPath.Substring(0, Application.dataPath.Length - 6), spFileName.Substring(0, spFileName.Length - 4));
if (!GDXAtlasUtility.ParseAtlas(atlasTxtFile, out GDXAtlas gdxAtlas))
{
continue;
}
SpriteMetaData[] spSheet = new SpriteMetaData[gdxAtlas.elements.Count];
for (int elemIndex = 0; elemIndex < gdxAtlas.elements.Count; elemIndex++)
{
var eDt = gdxAtlas.elements[elemIndex];
var spDt = new SpriteMetaData();
var fixRect = eDt.rect;
//libgdx图集坐标系原点是左上角, 这里需要转换到Unity坐标系(左下角)
fixRect.y = spTex.height - fixRect.y - fixRect.size.y;
spDt.name = eDt.name;
spDt.rect = fixRect;
spDt.pivot = Vector2.one * 0.5f;
spDt.border = Vector4.zero;
spDt.alignment = (int)SpriteAlignment.Custom;
spSheet[elemIndex] = spDt;
}
var texImporter = AssetImporter.GetAtPath(spFileName) as TextureImporter;
texImporter.spritesheet = spSheet;
texImporter.spriteImportMode = SpriteImportMode.Multiple;
texImporter.isReadable = true;
//texImporter.SaveAndReimport();//这段需要注释掉,否则修改不会生效.修改完只能手动Apply,很不爽
EditorUtility.DisplayProgressBar("Progress", "Slice Atlas...", (i + 1) / (float)assetIds.Length);
}
EditorUtility.ClearProgressBar();
}
最终得到这样的效果:
二.拆分图集:
就是把前面生成图集里的所有子图单独拆分成图片文件:
Talking is cheap,show you my code:
private void SplitAtlasSprites()
{
EditorUtility.DisplayProgressBar("Progress", "Slice Atlas...", 0);
string[] dirs = { "Assets/MainGame/Textures/zombie_assets" };
var assetIds = AssetDatabase.FindAssets("t:Texture", dirs);
for (int i = 0; i < assetIds.Length; i++)
{
string spFileName = AssetDatabase.GUIDToAssetPath(assetIds[i]);
var spTex = AssetDatabase.LoadAssetAtPath<Texture2D>(spFileName);
var texImporter = AssetImporter.GetAtPath(spFileName) as TextureImporter;
if (texImporter.spriteImportMode != SpriteImportMode.Multiple)
{
continue;
}
if (!texImporter.isReadable)
{
Debug.LogWarning(string.Format("图集不可读:{0}", spFileName));
continue;
}
for (int spIndex = 0; spIndex < texImporter.spritesheet.Length; spIndex++)
{
var spDt = texImporter.spritesheet[spIndex];
var tex = new Texture2D((int)spDt.rect.width, (int)spDt.rect.height);
tex.SetPixels(spTex.GetPixels((int)spDt.rect.x, (int)spDt.rect.y, tex.width, tex.height));
tex.Apply();
FileInfo fInfo = new FileInfo(string.Format("{0}{1}", Application.dataPath.Substring(0, Application.dataPath.Length - 6), spFileName));
string savePath = string.Format("{0}/{1}_slice", fInfo.DirectoryName, spTex.name);
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
string fileName = string.Format("{0}/{1}.png", savePath, spDt.name);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.WriteAllBytes(fileName, tex.EncodeToPNG());
}
AssetDatabase.Refresh();
EditorUtility.DisplayProgressBar("Progress", "Slice Atlas...", (i + 1) / (float)assetIds.Length);
}
EditorUtility.ClearProgressBar();
}
需要注意的是,导入的合图必须设置为可读。