using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace RecrownedAthenaeum.Tools.TextureAtlas { public class TexturePacker : IDisposable { private enum SupportedExtensions { jpeg, jpg, png } int powLimit; Rectangle theoreticalSpace; Node masterNode; ImageHandler[] imageHandlers; Queue imageHandlerQueue; int textureLength; /// /// Machine to pack multiple textures into one large texture. /// /// Path to textures. /// Power of two limit for auto expanding texture. Default is 12 which is a 4096x4096 texture. /// What power to start at and build up from. Default is 8 which is a 256x256 texture. internal TexturePacker(string rootDirectoryPath, int powLimit = 12, int startingPower = 8) { this.powLimit = powLimit; string[] paths = Directory.GetFiles(rootDirectoryPath); int currentPoT = startingPower; List imageHandlers = new List(); 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 = imageHandlers.ToArray(); imageHandlerQueue = new Queue(imageHandlers); textureLength = currentPoT; } /// /// Builds a texture atlas. /// /// Whether or not to automatically upscale atlas' texture in the case it is too small. Goes up to 4096 by default. public void Build(bool AutoCorrectAtlasSize = true) { masterNode = new Node(); masterNode.region.Width = textureLength; masterNode.region.Height = textureLength; ImageHandler imageHandler; while (imageHandlerQueue.TryDequeue(out imageHandler)) { try { masterNode.InsertImageHandler(imageHandler); } catch (InvalidOperationException) { textureLength *= 2; if (!AutoCorrectAtlasSize || textureLength > 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 + "."); } Build(); } } } /// /// Renders the build into a PNG file and generates the respective meant for serialization and later to be loaded. /// public void Save() { } public void Dispose() { foreach (ImageHandler imageHandler in imageHandlers) { imageHandler.Dispose(); } } 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 ImageHandler imageHandler; public bool Filled { get { return (imageHandler != null) || (a != null && b != null && a.Filled && b.Filled); } } public Node(Node parent = null) { this.parent = parent; region = parent.region; } public void SetNodeRegion(int x, int y, int width, int height, ImageHandler imageHandler) { region.X = x; region.Y = y; region.Width = width; region.Height = height; this.imageHandler = imageHandler; } public void SetNodeRegion(ImageHandler imageHandler) { SetNodeRegion(imageHandler.x, imageHandler.y, imageHandler.Width, imageHandler.Height, imageHandler); } 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; this.imageHandler = imageHandler; return this; } throw new ArgumentException(); } } private class ImageHandler : IComparable, IDisposable { public readonly string path; public readonly Image 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; internal ImageHandler(String path) { 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(); } } } }