278 lines
12 KiB
C#
278 lines
12 KiB
C#
using Newtonsoft.Json;
|
|
using RecrownedAthenaeum.Pipeline.NinePatch;
|
|
using RecrownedAthenaeum.Pipeline.TextureAtlas;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Processing;
|
|
using SixLabors.Primitives;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security;
|
|
|
|
namespace RecrownedAthenaeum.Tools.TextureAtlas
|
|
{
|
|
|
|
public class TexturePacker
|
|
{
|
|
private enum SupportedExtensions
|
|
{
|
|
jpeg, jpg, png
|
|
}
|
|
|
|
int powLimit;
|
|
Node masterNode;
|
|
Dictionary<string, ImageHandler> imageHandlersDictionary;
|
|
Dictionary<string, NinePatchData> ninePatchDictionary;
|
|
int tpl;
|
|
int TexturePowerLength { get { return tpl; } set { TextureLength = (int)Math.Pow(2, value); tpl = value; } }
|
|
public int TextureLength { get; private set; }
|
|
public int TexturesFound { get { return imageHandlersDictionary.Count; } }
|
|
/// <summary>
|
|
/// Machine to pack multiple textures into one large texture.
|
|
/// </summary>
|
|
/// <param name="rootDirectoryPath">Path to textures.</param>
|
|
/// <param name="powLimit">Power of two limit for auto expanding texture. Default is 12 which is a 4096x4096 texture.</param>
|
|
/// <param name="startingPower">What power to start at and build up from. Default is 8 which is a 256x256 texture.</param>
|
|
internal TexturePacker(string rootDirectoryPath, int powLimit = 12, int startingPower = 8)
|
|
{
|
|
this.powLimit = powLimit;
|
|
string[] paths;
|
|
try
|
|
{
|
|
paths = Directory.GetFiles(rootDirectoryPath);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
throw new ArgumentException("Path " + rootDirectoryPath + " couldn't be resolved.");
|
|
}
|
|
TexturePowerLength = startingPower;
|
|
List<ImageHandler> imageHandlers = new List<ImageHandler>();
|
|
int minAreaRequired = 0;
|
|
for (int pathID = 0; pathID < paths.Length; pathID++)
|
|
{
|
|
SupportedExtensions extension;
|
|
if (Enum.TryParse<SupportedExtensions>(Path.GetExtension(paths[pathID]).ToLower().Substring(1), out extension))
|
|
{
|
|
ImageHandler image = new ImageHandler(paths[pathID]);
|
|
imageHandlers.Add(image);
|
|
minAreaRequired += image.Area;
|
|
while (minAreaRequired > TextureLength * TextureLength)
|
|
{
|
|
TexturePowerLength++;
|
|
}
|
|
}
|
|
else if (Path.GetExtension(paths[pathID]).ToLower() == ".9p")
|
|
{
|
|
if (ninePatchDictionary == null) ninePatchDictionary = new Dictionary<string, NinePatchData>();
|
|
ConsoleUtilities.WriteWrappedLine("Reading ninepatch data for: " + paths[pathID]);
|
|
string serialized = File.ReadAllText(paths[pathID]);
|
|
NinePatchData npData = JsonConvert.DeserializeObject<NinePatchData>(serialized);
|
|
ninePatchDictionary.Add(npData.textureName, npData);
|
|
}
|
|
}
|
|
imageHandlers.Sort();
|
|
this.imageHandlersDictionary = new Dictionary<string, ImageHandler>();
|
|
foreach (ImageHandler imageHandler in imageHandlers)
|
|
{
|
|
this.imageHandlersDictionary.Add(imageHandler.Name, imageHandler);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a texture atlas.
|
|
/// </summary>
|
|
/// <param name="AutoCorrectAtlasSize">Whether or not to automatically upscale atlas' texture in the case it is too small. Goes up to 4096 by default.</param>
|
|
public void Build(bool AutoCorrectAtlasSize = true)
|
|
{
|
|
masterNode = new Node();
|
|
masterNode.region.Width = TextureLength;
|
|
masterNode.region.Height = TextureLength;
|
|
Queue<ImageHandler> imageHandlerQueue = new Queue<ImageHandler>(imageHandlersDictionary.Values);
|
|
ImageHandler imageHandler;
|
|
while (imageHandlerQueue.TryDequeue(out imageHandler))
|
|
{
|
|
Node activeNode = null;
|
|
activeNode = masterNode.InsertImageHandler(imageHandler);
|
|
if (activeNode == null)
|
|
{
|
|
if (!AutoCorrectAtlasSize || TexturePowerLength + 1 > powLimit)
|
|
{
|
|
throw new InvalidOperationException("Texture not large enough. Current size: " + TextureLength + "x" + TextureLength + ".");
|
|
}
|
|
TexturePowerLength += 1;
|
|
imageHandlerQueue.Clear();
|
|
Build(AutoCorrectAtlasSize);
|
|
}
|
|
if (ninePatchDictionary.ContainsKey(imageHandler.Name))
|
|
{
|
|
imageHandler.ninePatchData = ninePatchDictionary[imageHandler.Name];
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the build into a PNG file and generates the respective <see cref="TextureAtlasData"/> meant for serialization and later to be loaded.
|
|
/// </summary>
|
|
/// <param name="output">directory to output to.</param>
|
|
/// <param name="atlasName">name of atlas.</param>
|
|
public void Save(string output, string atlasName)
|
|
{
|
|
GraphicsOptions gOptions = new GraphicsOptions();
|
|
|
|
TextureAtlasData.TextureAtlasRegion[] regions = new TextureAtlasData.TextureAtlasRegion[imageHandlersDictionary.Count];
|
|
|
|
using (Image<Rgba32> atlasTexture = new Image<Rgba32>(TextureLength, TextureLength))
|
|
{
|
|
ImageHandler[] imageHandlers = this.imageHandlersDictionary.Values.ToArray();
|
|
|
|
for (int i = 0; i < imageHandlers.Length; i++)
|
|
{
|
|
regions[i] = new TextureAtlasData.TextureAtlasRegion();
|
|
ImageHandler ih = imageHandlers[i];
|
|
regions[i].SetBounds(ih.x, ih.y, ih.Width, ih.Height);
|
|
regions[i].ninePatchData = ih.ninePatchData;
|
|
regions[i].name = ih.Name;
|
|
using (Image<Rgba32> image = Image.Load<Rgba32>(ih.path))
|
|
{
|
|
atlasTexture.Mutate(img => img.DrawImage(gOptions, image, new Point(ih.x, ih.y)));
|
|
}
|
|
}
|
|
Directory.CreateDirectory(output);
|
|
using (FileStream stream = new FileStream(output + "/" + atlasName + ".png", FileMode.Create))
|
|
{
|
|
atlasTexture.SaveAsPng(stream);
|
|
}
|
|
}
|
|
string serialized = JsonConvert.SerializeObject(new TextureAtlasData(atlasName + ".png", regions), Formatting.Indented);
|
|
File.WriteAllText(output + "/" + atlasName + ".tatlas", serialized);
|
|
}
|
|
|
|
public void SetNinePatch(string fileName, int a, int b, int c, int d)
|
|
{
|
|
ImageHandler imageHandler = imageHandlersDictionary[fileName];
|
|
NinePatchData ninePatchData = new NinePatchData(fileName, a, b, c, d);
|
|
imageHandler.ninePatchData = ninePatchData;
|
|
}
|
|
|
|
public void RemoveNinePatch(string fileName)
|
|
{
|
|
imageHandlersDictionary[fileName].ninePatchData = null;
|
|
}
|
|
|
|
private class Node
|
|
{
|
|
public Node parent;
|
|
private Node a, b;
|
|
public Node childA { get { if (a == null) a = new Node(this); return a; } set { value.parent = this; a = value; } }
|
|
public Node childB { get { if (b == null) { b = new Node(this); } return b; } set { value.parent = this; b = value; } }
|
|
public Rectangle region;
|
|
public bool ContainsImage = false;
|
|
public bool CanPlaceImage { get { return (a == null && b == null && !ContainsImage); } }
|
|
public Node(Node parent = null)
|
|
{
|
|
this.parent = parent;
|
|
if (parent != null) region = parent.region;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to insert image within the node. This builds the node to have children if needed.
|
|
/// </summary>
|
|
/// <param name="imageHandler">the image to insert.</param>
|
|
/// <returns>The node the image is placed in.</returns>
|
|
public Node InsertImageHandler(ImageHandler imageHandler)
|
|
{
|
|
if (imageHandler.Width != region.Width)
|
|
{
|
|
if (imageHandler.Width < region.Width)
|
|
{
|
|
if (a == null)
|
|
{
|
|
childA.region.Width = imageHandler.Width;
|
|
}
|
|
Node attemptedNode = null;
|
|
if (!childA.ContainsImage && imageHandler.Width <= childA.region.Width)
|
|
{
|
|
attemptedNode = childA.InsertImageHandler(imageHandler);
|
|
}
|
|
|
|
if (attemptedNode == null && !childB.ContainsImage)
|
|
{
|
|
childB.region.Width = region.Width - childA.region.Width;
|
|
childB.region.X = childA.region.X + childA.region.Width;
|
|
attemptedNode = childB.InsertImageHandler(imageHandler);
|
|
}
|
|
|
|
return attemptedNode;
|
|
}
|
|
}
|
|
else if (imageHandler.Height != region.Height)
|
|
{
|
|
if (imageHandler.Height < region.Height)
|
|
{
|
|
if (a == null)
|
|
{
|
|
childA.region.Height = imageHandler.Height;
|
|
}
|
|
Node attemptedNode = null;
|
|
if (!childA.ContainsImage && imageHandler.Height <= childA.region.Height)
|
|
{
|
|
attemptedNode = childA.InsertImageHandler(imageHandler);
|
|
}
|
|
|
|
if (attemptedNode == null && !childB.ContainsImage)
|
|
{
|
|
childB.region.Height = region.Height - childA.region.Height;
|
|
childB.region.Y = childA.region.Y + childA.region.Height;
|
|
attemptedNode = childB.InsertImageHandler(imageHandler);
|
|
}
|
|
return attemptedNode;
|
|
}
|
|
}
|
|
else if (CanPlaceImage)
|
|
{
|
|
imageHandler.x = region.X;
|
|
imageHandler.y = region.Y;
|
|
ContainsImage = true;
|
|
return this;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private class ImageHandler : IComparable<ImageHandler>
|
|
{
|
|
public readonly string path;
|
|
public readonly IImageInfo image;
|
|
public int Area { get { return image.Width * image.Height; } }
|
|
public string Name { get { return Path.GetFileName(path); } }
|
|
public int Width { get { return image.Width; } }
|
|
public int Height { get { return image.Height; } }
|
|
public int x, y;
|
|
public NinePatchData ninePatchData;
|
|
|
|
internal ImageHandler(String path)
|
|
{
|
|
this.path = path;
|
|
try
|
|
{
|
|
using (FileStream stream = new FileStream(path, FileMode.Open))
|
|
{
|
|
image = Image.Identify(stream);
|
|
}
|
|
} catch (SecurityException)
|
|
{
|
|
throw new ArgumentException("Security exception occurred for image: " + path);
|
|
}
|
|
}
|
|
|
|
public int CompareTo(ImageHandler tImage)
|
|
{
|
|
return Area - tImage.Area;
|
|
}
|
|
}
|
|
}
|
|
}
|