recrownedathenaeum/RecrownedAthenaeum.ConsoleTools/TextureAtlasTools/TexturePacker.cs

251 lines
9.9 KiB
C#
Raw Normal View History

2018-12-06 18:19:30 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using RecrownedAthenaeum.Pipeline.TextureAtlas;
using RecrownedAthenaeum.Pipeline.NinePatch;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
using System.Linq;
using Newtonsoft.Json;
2018-12-06 18:19:30 +00:00
namespace RecrownedAthenaeum.Tools.TextureAtlas
2018-12-06 18:19:30 +00:00
{
2018-12-07 08:27:34 +00:00
public class TexturePacker : IDisposable
2018-12-06 18:19:30 +00:00
{
private enum SupportedExtensions
{
jpeg, jpg, png
}
int powLimit;
2018-12-06 18:19:30 +00:00
Node masterNode;
Dictionary<string, ImageHandler> imageHandlers;
int textureLength;
2018-12-06 18:19:30 +00:00
/// <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)
2018-12-06 18:19:30 +00:00
{
this.powLimit = powLimit;
2018-12-06 18:19:30 +00:00
string[] paths = Directory.GetFiles(rootDirectoryPath);
textureLength = startingPower;
List<ImageHandler> imageHandlers = new List<ImageHandler>();
2018-12-06 18:19:30 +00:00
for (int pathID = 0; pathID < paths.Length; pathID++)
{
SupportedExtensions extension;
if (Enum.TryParse(Path.GetExtension(paths[pathID]), out extension))
{
ImageHandler image = new ImageHandler(paths[pathID]);
imageHandlers.Add(image);
}
}
imageHandlers.Sort();
this.imageHandlers = new Dictionary<string, ImageHandler>();
foreach (ImageHandler imageHandler in imageHandlers)
{
this.imageHandlers.Add(imageHandler.Name, imageHandler);
}
}
2018-12-06 18:19:30 +00:00
/// <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>(imageHandlers.Values);
ImageHandler imageHandler;
while (imageHandlerQueue.TryDequeue(out imageHandler))
{
2018-12-07 17:20:46 +00:00
Node activeNode = null;
do
{
2018-12-07 17:20:46 +00:00
activeNode = masterNode.InsertImageHandler(imageHandler);
if (activeNode == null)
{
2018-12-07 17:20:46 +00:00
if (!AutoCorrectAtlasSize || (textureLength *= 2) > Math.Pow(2, powLimit))
{
throw new InvalidOperationException("Dimensions of texture goes past limit amount of " + powLimit + " which is " + Math.Pow(2, powLimit) + ". New texture size would be " + textureLength + "x" + textureLength + ".");
}
2018-12-06 18:19:30 +00:00
}
}
2018-12-07 17:20:46 +00:00
while (activeNode == null);
2018-12-06 18:19:30 +00:00
}
}
/// <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[imageHandlers.Count];
2018-12-06 18:19:30 +00:00
using (Image<Rgba32> atlasTexture = new Image<Rgba32>(textureLength, textureLength))
{
ImageHandler[] imageHandlers = this.imageHandlers.Values.ToArray();
for (int i = 0; i < imageHandlers.Length; i++)
{
regions[i] = new TextureAtlasData.TextureAtlasRegion();
ImageHandler imageH = imageHandlers[i];
regions[i].SetBounds(imageH.x, imageH.y, imageH.Width, imageH.Height);
regions[i].ninePatchData = imageH.ninePatchData;
atlasTexture.Mutate(img => img.DrawImage(gOptions, imageH.image, new Point(imageH.x, imageH.y)));
}
using (FileStream stream = new FileStream(output + atlasName + ".png", FileMode.Create))
{
atlasTexture.SaveAsPng(stream);
}
}
string serialized = JsonConvert.SerializeObject(new TextureAtlasData(atlasName + ".png", regions));
using (StreamWriter stream = new StreamWriter(output + atlasName + ".tatlas"))
{
stream.WriteLine(serialized);
}
}
public void SetNinePatch(string fileName, int a, int b, int c, int d)
{
ImageHandler imageHandler = imageHandlers[fileName];
NinePatchData ninePatchData = new NinePatchData(fileName, a, b, c, d);
imageHandler.ninePatchData = ninePatchData;
}
public void RemoveNinePatch(string fileName)
{
imageHandlers[fileName].ninePatchData = null;
2018-12-06 18:19:30 +00:00
}
public void Dispose()
{
ImageHandler[] imageHandlers = this.imageHandlers.Values.ToArray();
this.imageHandlers.Clear();
foreach (ImageHandler imageHandler in imageHandlers)
{
imageHandler.Dispose();
}
}
2018-12-06 18:19:30 +00:00
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;
2018-12-07 17:20:46 +00:00
public bool containsImage = false;
public bool Filled { get { return containsImage || (a != null && b != null && a.Filled && b.Filled); } }
public Node(Node parent = null)
{
this.parent = parent;
region = parent.region;
}
2018-12-06 18:19:30 +00:00
2018-12-07 17:20:46 +00:00
/// <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;
childB.region.Width = region.Width - childA.region.Width;
}
if (!childA.Filled && imageHandler.Width <= childA.region.Width)
{
return childA.InsertImageHandler(imageHandler);
}
if (!childB.Filled)
{
return childB.InsertImageHandler(imageHandler);
}
}
}
else if (imageHandler.Height != region.Height)
{
if (imageHandler.Height < region.Height)
{
if (a == null)
{
childA.region.Height = imageHandler.Height;
childB.region.Height = region.Height - childA.region.Height;
}
if (!childA.Filled && imageHandler.Width <= childA.region.Width)
{
return childA.InsertImageHandler(imageHandler);
}
if (!childB.Filled)
{
return childB.InsertImageHandler(imageHandler);
}
}
}
else
{
imageHandler.x = region.X;
imageHandler.y = region.Y;
2018-12-07 17:20:46 +00:00
containsImage = true;
return this;
}
2018-12-07 17:20:46 +00:00
return null;
2018-12-06 18:19:30 +00:00
}
}
private class ImageHandler : IComparable<ImageHandler>, IDisposable
2018-12-06 18:19:30 +00:00
{
public readonly string path;
public readonly Image<Rgba32> 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;
2018-12-06 18:19:30 +00:00
internal ImageHandler(String path)
2018-12-06 18:19:30 +00:00
{
this.path = path;
using (FileStream stream = new FileStream(path, FileMode.Open))
{
image = Image.Load(stream);
}
}
public int CompareTo(ImageHandler tImage)
{
return Area - tImage.Area;
}
public void Dispose()
{
image.Dispose();
2018-12-06 18:19:30 +00:00
}
}
}
}