static void Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); PackingRectangle[] rectangles = GetRectangles(); Console.WriteLine("Packing " + rectangles.Length + " rectangles..."); stopwatch.Restart(); RectanglePacker.Pack(rectangles, out PackingRectangle bounds); stopwatch.Stop(); Console.WriteLine("Took ~" + stopwatch.Elapsed.TotalMilliseconds.ToString() + "ms"); if (RectanglePacker.AnyIntersects(rectangles)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Some rectangles intersect!"); } else { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("No rectangles intersect."); } Console.ResetColor(); string filename = GetImageName(); Console.WriteLine("Saving as " + filename); SaveAsImage(rectangles, bounds, filename); }
void placeRooms() { var packer = new RectanglePacker(); for (int roomIndex = 0; roomIndex < rooms.Count; roomIndex++) { var roomData = rooms[roomIndex]; var roomTiledMap = roomData.roomPrefab.GetComponent <Tiled2Unity.TiledMap>(); roomData.size = myMath.getTiledMapSize(roomTiledMap); } rooms.Sort((a, b) => b.Area.CompareTo(a.Area)); for (int roomIndex = 0; roomIndex < rooms.Count; roomIndex++) { var roomData = rooms[roomIndex]; float newx; float newy; if (!packer.Pack(roomData.size.x, roomData.size.y, out newx, out newy)) { throw new Exception("Uh oh, we couldn't pack the rectangle :("); } //This rectangle is now packed into position! roomData.position = new Vector3(newx, newy); roomData.roomGameObject = Instantiate(roomData.roomPrefab, roomData.position, Quaternion.identity); } }
public Texture2D SetupTextureAtlas(GraphicsDevice graphicsDevice) { var triangles = Models.SelectMany(m => m.ModelTriangles).ToList(); var dict = new Dictionary <KeyValuePair <string, Rectangle>, TrianglesWithCroppedTexture>(); foreach (var triangle in triangles) { if (!dict.ContainsKey(triangle.TextureKey)) { if (!TextureHandler.HasCroppedTexture(triangle.TextureKey)) { TextureHandler.AddCroppedTexture(triangle.TextureKey, TextureHandler.CropTexture(triangle.OriginalTexture, triangle.OriginalTextureRectangle)); } dict.Add(triangle.TextureKey, new TrianglesWithCroppedTexture() { CroppedTexture = TextureHandler.GetCroppedTexture(triangle.TextureKey).Key, HasTransparentPixels = TextureHandler.GetCroppedTexture(triangle.TextureKey).Value }); } dict[triangle.TextureKey].ModelTriangles.Add(triangle); } var packer = new RectanglePacker(4096, 4096); foreach (var pair in dict) { var rect = pair.Value.CroppedTexture.Bounds; if (!packer.Pack(rect.Width, rect.Height, out rect.X, out rect.Y)) { throw new Exception("Uh oh, we couldn't pack the rectangles"); } pair.Value.AtlasRectangle = rect; } foreach (var pair in dict) { foreach (var triangle in pair.Value.ModelTriangles) { triangle.AtlasTextureRectangle = pair.Value.AtlasRectangle; triangle.HasTransparentPixels = pair.Value.HasTransparentPixels; } } var atlas = new Texture2D(graphicsDevice, 4096, 4096); foreach (var pair in dict) { var data = new Color[pair.Value.CroppedTexture.Width * pair.Value.CroppedTexture.Height]; pair.Value.CroppedTexture.GetData(0, pair.Value.CroppedTexture.Bounds, data, 0, data.Length); atlas.SetData(0, pair.Value.AtlasRectangle, data, 0, data.Length); pair.Value.CroppedTexture = null; } return(atlas); }
private bool TryPacking(int width, int height, Packable[] packables) { var packer = new RectanglePacker(width, height, 2048, 2048); foreach (var face in packables) { int x, y; if (!packer.Pack(face.Width, face.Height, out x, out y)) { return(false); } _packing[face.Index] = new IntRect(x + 1, y + 1, face.Width - 2, face.Height - 2); } SetBoundingSize(packer.Width, packer.Height); return(true); }
public static void sort_rects(List <PackerRect> rects, int dx = 0, int dy = 0, float mx = 1.0f, float my = 1.0f) { rects.Sort((a, b) => b.Area.CompareTo(a.Area)); var packer = new RectanglePacker(); for (int i = 0; i < rects.Count; ++i) { var rect = rects[i]; int x, y; if (!packer.Pack((int)rect.w, (int)rect.h, out x, out y)) { throw new Exception("Uh oh, we couldn't pack the rectangle :("); } rect.x = x; rect.y = y; var p = currentPackage.QueryComponent(rect.ID); int [] m = { 1024, 0, 0, 1024, (int)((x + dx) * 16 * mx), (int)((y + dy) * 16 * my) }; Matrix4x4 mat = Matrix.ToMatrix4x4(m); Matrix.SetTransformFromMatrix(p.transform, ref mat); } }
public Glyph?GetGlyph(uint Unicode) { if (GlyphTexture.ContainsKey(Unicode)) { return(GlyphTexture[Unicode]); } FontCharacter FChar = new FontCharacter(); if (Msdfgen.LoadGlyphNormal(Fnt, Unicode, ref FChar)) { if (FChar.Width == 0 || FChar.Height == 0) { if (!GlyphTexture.ContainsKey(Unicode)) { GlyphTexture.Add(Unicode, new Glyph(FChar, null)); } return(GetGlyph(Unicode)); } Glyph G = new Glyph(FChar, FChar.GetBitmap()); if (!Packer.Pack(G.Bitmap.Width, G.Bitmap.Height, out G.X, out G.Y)) { throw new NotImplementedException("Cannot pack glyph"); } G.Bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY); TextureAtlas.SubRect2D(G.Bitmap, G.X, G.Y); G.Bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY); GlyphTexture.Add(Unicode, G); return(GetGlyph(Unicode)); } return(null); }
public unsafe FontAtlas(FontFamily fontFamily, int fontSize, IEnumerable <int> characters) { Stopwatch watch = new Stopwatch(); watch.Start(); Size = fontSize; Font font = new Font(fontFamily, fontSize); float factor = (float)fontSize / font.EmSize; List <PackingRectangle> glyphRectangles = new List <PackingRectangle>(); foreach (int character in characters) { GlyphInstance glyph = font.GetGlyph(character).Instance; glyphRectangles.Add(new PackingRectangle(0, 0, (uint)((glyph.LeftSideBearing + glyph.AdvanceWidth) * factor), (uint)(glyph.Height * factor), character)); } PackingRectangle[] rectArray = glyphRectangles.ToArray(); RectanglePacker.Pack(rectArray, out PackingRectangle bounds); Image <A8> image = new Image <A8>((int)bounds.Width, (int)bounds.Height); Characters = new Dictionary <char, CharacterData>(); image.Mutate(x => { foreach (PackingRectangle glyphRectangle in rectArray) { GlyphInstance glyph = font.GetGlyph(glyphRectangle.Id).Instance; Characters.Add((char)glyphRectangle.Id, new CharacterData() { uvX1 = glyphRectangle.X / (float)image.Width, uvX2 = (glyphRectangle.X + glyphRectangle.Width) / (float)image.Width, uvY1 = glyphRectangle.Y / (float)image.Height, uvY2 = (glyphRectangle.Y + glyphRectangle.Height) / (float)image.Height, xAdvance = glyph.AdvanceWidth * factor, height = glyph.Height * factor }); x.DrawText(((char)glyphRectangle.Id).ToString(), font, Color.White, new PointF(glyphRectangle.X, glyphRectangle.Y)); } }); image.TryGetSinglePixelSpan(out Span <A8> pixels); TextureId = GL.GenTexture(); GL.ActiveTexture(TextureUnit.Texture0); GL.BindTexture(TextureTarget.Texture2D, TextureId); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1); fixed(A8 *pixelPointer = pixels) { GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Alpha, image.Width, image.Height, 0, PixelFormat.Alpha, PixelType.UnsignedByte, new IntPtr(pixelPointer)); } GL.GenerateTextureMipmap(TextureId); watch.Stop(); Console.WriteLine($"Font atlas with size {image.Width}x{image.Height} was created in {watch.ElapsedMilliseconds}ms!"); }
/// <summary> /// Creates a <see cref="TrippyFontFile"/> holding information for multiple fonts. /// </summary> /// <param name="glyphSources">The <see cref="IGlyphSource"/>-s for getting the information of each font.</param> /// <param name="backgroundColor">The background color of the generated image. Null for transparent.</param> public static TrippyFontFile CreateFontFile(ReadOnlySpan <IGlyphSource> glyphSources, Color?backgroundColor = null) { // We create all the TextureFontData-s and query their basic information from the glyph sources. TextureFontData[] fontDatas = new TextureFontData[glyphSources.Length]; for (int i = 0; i < fontDatas.Length; i++) { fontDatas[i] = new TextureFontData() { Size = glyphSources[i].Size, FirstChar = glyphSources[i].FirstChar, LastChar = glyphSources[i].LastChar, Ascender = glyphSources[i].Ascender, Descender = glyphSources[i].Descender, LineGap = glyphSources[i].LineGap, Name = glyphSources[i].Name }; } // We count the total amount of characters in all glyph sources combined. int charCount = 0; for (int i = 0; i < glyphSources.Length; i++) { charCount += fontDatas[i].CharCount; } // We need to find a way to pack all the characters into a single texture. // For this we use the included Rectpack library in TrippyGL.Fonts.Rectpack. PackingRectangle[] packingRects = new PackingRectangle[charCount]; // The way we identify the PackingRectangles is with their rect.Id property. // Since we have multiple fonts, we can't set these to the characters they represent // because there can be collisions. So the way we assign IDs will be basically like this: // The IDs for the characters of the first font start at 0 and go up to font.CharCount // inclusive. These are, of course, in order of character. // The second font then gets the range that starts right after where the first font's // range ends and gets enough range for all it's characters, etc etc. // So we need to know where the range of IDs for each font starts and end. We'll store it here: Span <int> idsStart = glyphSources.Length <= 96 ? stackalloc int[glyphSources.Length] : new int[glyphSources.Length]; int packingRectCount = 0; for (int i = 0; i < fontDatas.Length; i++) { // We calculate the starting ID for the current glyph source. int idStart = i == 0 ? 0 : idsStart[i - 1] + fontDatas[i - 1].CharCount; idsStart[i] = idStart; // We go through all the characters in the current glyph source. for (int c = fontDatas[i].FirstChar; c <= fontDatas[i].LastChar; c++) { // We get the size. If it is positive, we add a PackingRectangle to represent it. System.Drawing.Point size = glyphSources[i].GetGlyphSize(c); if (size.X > 0 && size.Y > 0) { // We add 2 to the width and height of the rectangle so chars have an empty border. int id = idStart + c - fontDatas[i].FirstChar; packingRects[packingRectCount++] = new PackingRectangle(0, 0, (uint)size.X + 2, (uint)size.Y + 2, id); } } } // We trim extra elements off the packingRects array. if (packingRects.Length != packingRectCount) { Array.Resize(ref packingRects, packingRectCount); } // We use RectanglePacker to find a bin for all the rectangles. RectanglePacker.Pack(packingRects, out PackingRectangle bounds); Image <Rgba32> image = new Image <Rgba32>((int)bounds.Width, (int)bounds.Height); try { // First we clear the image to the specified background color, or transparent. Color bgColor = backgroundColor ?? Color.Transparent; image.Mutate(x => x.BackgroundColor(bgColor)); // We create the source rectangles arrays for all the fontDatas. for (int i = 0; i < fontDatas.Length; i++) { fontDatas[i].SourceRectangles = new System.Drawing.Rectangle[fontDatas[i].CharCount]; } // We go through all the packing rectangles. for (int i = 0; i < packingRects.Length; i++) { PackingRectangle rect = packingRects[i]; // We find which glyph source this rectangle belongs to. int glyphSourceIndex = 0; while (glyphSourceIndex + 1 < idsStart.Length && idsStart[glyphSourceIndex + 1] < rect.Id) { glyphSourceIndex++; } // We find which character this rectangle represents. int charIndex = rect.Id - idsStart[glyphSourceIndex]; int charCode = charIndex + fontDatas[glyphSourceIndex].FirstChar; // We draw the glyph onto the image at this rectangle's location. glyphSources[glyphSourceIndex].DrawGlyphToImage(charCode, new System.Drawing.Point((int)rect.X + 1, (int)rect.Y + 1), image); // We set the glyph's source to match this rectangle. fontDatas[glyphSourceIndex].SourceRectangles[charIndex] = new System.Drawing.Rectangle((int)rect.X + 1, (int)rect.Y + 1, (int)rect.Width - 2, (int)rect.Height - 2); } // We go through all the fontDatas and set the remaining information. for (int i = 0; i < fontDatas.Length; i++) { fontDatas[i].RenderOffsets = glyphSources[i].GetRenderOffsets(); glyphSources[i].GetAdvances(out fontDatas[i].Advances); if (!glyphSources[i].TryGetKerning(out fontDatas[i].KerningOffsets)) { fontDatas[i].KerningOffsets = null; } } // Done! return(new TrippyFontFile(fontDatas, image)); } catch { // If anything failed, we dispose the image and re-throw the exception. image.Dispose(); throw; } }
public static void Main(string[] args) { //Create a list of random rectangles, sorted by size var rand = new Random(); var rects = new List<Rect>(); for (int i = 0; i < 50; ++i) rects.Add(new Rect(0, 0, rand.Next(2, 10), rand.Next(2, 10))); for (int i = 0; i < 50; ++i) rects.Add(new Rect(0, 0, rand.Next(7, 10), rand.Next(1, 4))); for (int i = 0; i < 50; ++i) rects.Add(new Rect(0, 0, rand.Next(1, 4), rand.Next(7, 10))); for (int i = 0; i < 50; ++i) rects.Add(new Rect(0, 0, rand.Next(1, 4), rand.Next(1, 4))); rects.Sort((a, b) => b.Area.CompareTo(a.Area)); var watch = Stopwatch.StartNew(); //Pack the rectangles var packer = new RectanglePacker(); for (int i = 0; i < rects.Count; ++i) { var rect = rects[i]; //Pad the rectangles by 1px so there is a gap between them if (!packer.Pack(rect.W + 1, rect.H + 1, out rect.X, out rect.Y)) { Console.WriteLine("Packing failed"); return; } rects[i] = rect; } int packTime = (int)watch.ElapsedMilliseconds; int totalArea = packer.Width * packer.Height; int usedArea = 0; //Create a 2D grid of "pixels" and fill them where rectangles are var grid = new bool[packer.Width, packer.Height]; foreach (var rect in rects) { usedArea += (rect.W + 1) * (rect.H + 1); for (int y = 0; y < rect.H; ++y) for (int x = 0; x < rect.W; ++x) grid[rect.X + x, rect.Y + y] = true; } //Print out the results to a text file using (var writer = new StreamWriter("results.txt")) { int percent = (int)(((float)usedArea / totalArea) * 100f); writer.WriteLine("Packed: " + rects.Count + " rectangles"); writer.WriteLine("Size: " + packer.Width + " x " + packer.Height); writer.WriteLine("Usage: " + usedArea + " / " + totalArea + " (" + percent + "%)"); writer.WriteLine("Pack Time: " + packTime + " ms"); writer.WriteLine(); for (int y = 0; y < packer.Height; ++y) { for (int x = 0; x < packer.Width; ++x) writer.Write(grid[x, y] ? '#' : ' '); writer.WriteLine(); } } Console.WriteLine("Finished successfully. See results.txt"); }
// example args: -input.path=D:\some\path\to\images\here -filter=*.jpg -prefix=team -output.extension=jpg -output.path= -output.resize=240 static void Main(string[] args) { string folder = null; string filter = "*.*"; string atlasPath = Directory.GetCurrentDirectory(); string cssPath = null; string jsonPath = null; string xmlPath = null; string csvPath = null; string atlasExtension = "jpg"; string prefix = null; int resize = 0; var globalMargin = new Margin(); bool normalize = false; foreach (var entry in args) { var arg = entry; if (!arg.StartsWith("-")) { Console.WriteLine("Invalid argument: " + arg); return; } arg = arg.Substring(1); var temp = arg.Split(new char[] { '=' }, 2); var key = temp[0].ToLower(); var val = temp.Length == 2 ? temp[1] : null; switch (key) { case "input.path": folder = val; break; case "input.filter": filter = val; break; case "prefix": prefix = val; break; case "atlas.extension": atlasExtension = val.Replace(".", ""); break; case "atlas.path": atlasPath = val; break; case "resize": resize = int.Parse(val); break; case "css.path": cssPath = val; break; case "json.path": jsonPath = val; break; case "xml.path": xmlPath = val; break; case "csv.path": csvPath = val; break; case "margin.X": globalMargin.X = int.Parse(val); break; case "margin.Y": globalMargin.Y = int.Parse(val); break; case "margin": globalMargin.X = int.Parse(val); globalMargin.Y = globalMargin.X; break; case "normalize": normalize = bool.Parse(val); break; } } if (normalize && resize > 0) { Console.WriteLine("Normalize and resize options are mutually exclusive."); return; } FixPath(ref atlasPath); FixPath(ref cssPath); FixPath(ref jsonPath); FixPath(ref xmlPath); FixPath(ref csvPath); if (folder == null) { Console.WriteLine("Please specify a folder. Eg: -input.path=some_path"); return; } if (prefix == null) { Console.WriteLine("Please specify a atlas prefix. Eg: --prefix=something"); return; } var outPicName = prefix + "." + atlasExtension; var outCSSName = prefix + ".css"; var files = Directory.GetFiles(folder, filter); var images = new Dictionary <string, Bitmap>(); int count = 0; int avgWidth = 0; int avgHeight = 0; int maxWidth = 0; int maxHeight = 0; foreach (var file in files) { var img = new Bitmap(Bitmap.FromFile(file)); if (resize != 0) { img = new Bitmap(img, new Size(resize, resize)); } images[file] = img; count++; maxWidth = Math.Max(maxWidth, img.Width); maxHeight = Math.Max(maxHeight, img.Height); avgWidth += img.Width; avgHeight += img.Height; } int maxSize = Math.Max(maxHeight, maxWidth); avgWidth /= count; avgHeight /= count; int side = (int)Math.Ceiling(Math.Sqrt(count)); int atlasWidth = avgWidth * side; int atlasHeight = avgHeight * side; int tot; int tries = 0; RectanglePacker <string> packer; var margins = new Dictionary <string, Margin>(); if (normalize) { foreach (var file in files) { var img = images[file]; var margin = new Margin() { X = (maxSize - img.Width) / 2 + globalMargin.X, Y = (maxSize - img.Height) / 2 + globalMargin.Y, }; margins[file] = margin; } } else { foreach (var file in files) { margins[file] = globalMargin; } } do { packer = new RectanglePacker <string>(); foreach (var entry in images) { var margin = margins[entry.Key]; packer.AddRect(entry.Value.Width + margin.X * 2, entry.Value.Height + margin.Y * 2, entry.Key); } tot = packer.Pack(0, 0, atlasWidth, atlasHeight); if (tot == 0) { break; } if (tries % 2 == 0) { atlasWidth *= 2; } else { atlasHeight *= 2; } tries++; if (tries > 5) { break; } } while (true); if (tot == 0) { var output = new Bitmap(atlasWidth, atlasHeight); using (Graphics g = Graphics.FromImage(output)) { foreach (var file in files) { int x, y; packer.GetRect(file, out x, out y); var margin = margins[file]; x += margin.X; y += margin.Y; var img = images[file]; g.DrawImage(img, x, y, img.Width, img.Height); Console.WriteLine("Merged " + file); } } output.Save(atlasPath + outPicName); Console.WriteLine("Generated " + atlasPath + outPicName); if (cssPath != null) { var sb = new StringBuilder(); int index = 0; foreach (var file in files) { var name = Path.GetFileNameWithoutExtension(file).ToLower(); if (index > 0) { sb.Append(", "); } if (index % side == side - 1) { sb.AppendLine(); } sb.Append($".{prefix}-{name}"); index++; } sb.Append('{'); sb.AppendLine(); sb.AppendLine($"\tbackground-image: url('{outPicName}');"); sb.AppendLine("\tbackground-repeat: no-repeat;"); sb.Append('}'); sb.AppendLine(); foreach (var file in files) { int x, y; packer.GetRect(file, out x, out y); var margin = margins[file]; x += margin.X; y += margin.Y; var name = Path.GetFileNameWithoutExtension(file).ToLower(); var img = images[file]; sb.AppendLine(); sb.AppendLine("." + prefix + "-" + name + " {"); sb.AppendLine($"\twidth: {img.Width}px;"); sb.AppendLine($"\theight: {img.Height}px;"); sb.AppendLine($"\tbackground-position: -{x}px -{y}px;"); sb.Append('}'); sb.AppendLine(); } File.WriteAllText(cssPath + outCSSName, sb.ToString()); Console.WriteLine("Generated " + cssPath + outCSSName); } if (xmlPath != null || csvPath != null || jsonPath != null) { if (jsonPath != null) { var node = ExportToNode(files, images, margins, packer, false); var json = JSONWriter.WriteToString(node); File.WriteAllText(jsonPath + prefix + ".json", json); } if (xmlPath != null) { var node = ExportToNode(files, images, margins, packer, true); var xml = XMLWriter.WriteToString(node); File.WriteAllText(xmlPath + prefix + ".xml", xml); } if (csvPath != null) { var node = ExportToNode(files, images, margins, packer, false); var csv = CSVWriter.WriteToString(node); File.WriteAllText(csvPath + prefix + ".csv", csv); } } } else { Console.WriteLine("Failed packing..."); } }