From ddf07dc90587d2cb628009280430d8faa24b685e Mon Sep 17 00:00:00 2001 From: David Hoover Date: Thu, 29 Nov 2012 16:39:18 -0800 Subject: [PATCH] Support fancier packing of trimmed tilesets. By default, it still does the same thing, but now you can pass in a fancier packer. Provided a SLIGHTLY fancier one that's still pretty dumb and wasteful, but allows row wrapping instead of gluing everything together into one super wide image; I've got some that otherwise wind up SO insanely wide that it causes problems decoding them. --- .../media/tile/TrimmedObjectTileSet.java | 15 +- .../threerings/media/tile/TrimmedTileSet.java | 16 +- .../media/tile/util/TileSetTrimmer.java | 165 +++++++++++++++--- .../tile/bundle/tools/TileSetBundler.java | 14 +- 4 files changed, 182 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/com/threerings/media/tile/TrimmedObjectTileSet.java b/core/src/main/java/com/threerings/media/tile/TrimmedObjectTileSet.java index eadb2c0e..d9b37120 100644 --- a/core/src/main/java/com/threerings/media/tile/TrimmedObjectTileSet.java +++ b/core/src/main/java/com/threerings/media/tile/TrimmedObjectTileSet.java @@ -178,6 +178,16 @@ public class TrimmedObjectTileSet extends TileSet return trimObjectTileSet(source, destImage, FastImageIO.FILE_SUFFIX); } + /** + * Convenience function to trim the tile set to a file using the simplest packer. + */ + public static TrimmedObjectTileSet trimObjectTileSet ( + ObjectTileSet source, OutputStream destImage, String imgFormat) + throws IOException + { + return trimObjectTileSet (source, destImage, imgFormat, new TileSetTrimmer.StripPacker()); + } + /** * Creates a trimmed object tileset from the supplied source object tileset. The image path * must be set by hand to the appropriate path based on where the image data that is written to @@ -186,7 +196,8 @@ public class TrimmedObjectTileSet extends TileSet * information. */ public static TrimmedObjectTileSet trimObjectTileSet ( - ObjectTileSet source, OutputStream destImage, String imgFormat) + ObjectTileSet source, OutputStream destImage, String imgFormat, + TileSetTrimmer.Packer packer) throws IOException { final TrimmedObjectTileSet tset = new TrimmedObjectTileSet(); @@ -247,7 +258,7 @@ public class TrimmedObjectTileSet extends TileSet tset._bounds[tileIndex] = new Rectangle(imageX, imageY, trimWidth, trimHeight); } }; - TileSetTrimmer.trimTileSet(source, destImage, tmr, imgFormat); + TileSetTrimmer.trimTileSet(source, destImage, tmr, imgFormat, packer); // Log.info("Trimmed object tileset [bounds=" + StringUtil.toString(tset._bounds) + // ", metrics=" + StringUtil.toString(tset._ometrics) + "]."); diff --git a/core/src/main/java/com/threerings/media/tile/TrimmedTileSet.java b/core/src/main/java/com/threerings/media/tile/TrimmedTileSet.java index 163b8a7d..960df014 100644 --- a/core/src/main/java/com/threerings/media/tile/TrimmedTileSet.java +++ b/core/src/main/java/com/threerings/media/tile/TrimmedTileSet.java @@ -70,6 +70,16 @@ public class TrimmedTileSet extends TileSet return trimTileSet(source, destImage, FastImageIO.FILE_SUFFIX); } + /** + * Convenience function to trim the tile set to a file using the simplest packer. + */ + public static TrimmedTileSet trimTileSet ( + TileSet source, OutputStream destImage, String imgFormat) + throws IOException + { + return trimTileSet(source, destImage, imgFormat, new TileSetTrimmer.StripPacker()); + } + /** * Creates a trimmed tileset from the supplied source tileset. The image path must be set by * hand to the appropriate path based on where the image data that is written to the @@ -77,8 +87,8 @@ public class TrimmedTileSet extends TileSet * indicateds how the resulting image should be saved. If null, we save using FastImageIO * See {@link TileSetTrimmer#trimTileSet} for further information. */ - public static TrimmedTileSet trimTileSet (TileSet source, OutputStream destImage, - String imgFormat) + public static TrimmedTileSet trimTileSet ( + TileSet source, OutputStream destImage, String imgFormat, TileSetTrimmer.Packer packer) throws IOException { final TrimmedTileSet tset = new TrimmedTileSet(); @@ -101,7 +111,7 @@ public class TrimmedTileSet extends TileSet tset._obounds[tileIndex] = new Rectangle(imageX, imageY, trimWidth, trimHeight); } }; - TileSetTrimmer.trimTileSet(source, destImage, tmr, imgFormat); + TileSetTrimmer.trimTileSet(source, destImage, tmr, imgFormat, packer); return tset; } diff --git a/core/src/main/java/com/threerings/media/tile/util/TileSetTrimmer.java b/core/src/main/java/com/threerings/media/tile/util/TileSetTrimmer.java index 64906316..9c16c1c0 100644 --- a/core/src/main/java/com/threerings/media/tile/util/TileSetTrimmer.java +++ b/core/src/main/java/com/threerings/media/tile/util/TileSetTrimmer.java @@ -19,17 +19,27 @@ package com.threerings.media.tile.util; -import java.io.IOException; -import java.io.OutputStream; - +import java.awt.Dimension; import java.awt.Rectangle; +import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.awt.image.Raster; import java.awt.image.RasterFormatException; import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; import javax.imageio.ImageIO; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + import com.threerings.resource.FastImageIO; import com.threerings.media.image.ImageUtil; @@ -40,7 +50,7 @@ import com.threerings.media.tile.TileSet; * which means that each tile is converted to an image that contains the * smallest rectangular region of the original image that contains all * non-transparent pixels. These trimmed images are then written out to a - * single image, packed together left to right. + * single image, packed together. */ public class TileSetTrimmer { @@ -70,14 +80,29 @@ public class TileSetTrimmer int trimWidth, int trimHeight); } + /** + * Used to pack the trimmed tiles into the final image. + */ + public static interface Packer + { + /** Add trimmed bounds for a tile. */ + void addTile (int tileIndex, int width, int height); + + /** Do the deed and return dimensions of the packaged layout. */ + Dimension pack (); + + /** Get the packed bounds of the given tile. */ + Rectangle getPosition (int tileIndex); + } + /** * Convenience function to trim the tile set using FastImageIO to save the result. */ public static void trimTileSet ( - TileSet source, OutputStream destImage, TrimMetricsReceiver tmr) + TileSet source, OutputStream destImage, TrimMetricsReceiver tmr, Packer packer) throws IOException { - trimTileSet(source, destImage, tmr, FastImageIO.FILE_SUFFIX); + trimTileSet(source, destImage, tmr, FastImageIO.FILE_SUFFIX, packer); } /** @@ -95,7 +120,8 @@ public class TileSetTrimmer * @param imgFormat the format in which to write the image file - or if null, use FastImageIO. */ public static void trimTileSet ( - TileSet source, OutputStream destImage, TrimMetricsReceiver tmr, String imgFormat) + TileSet source, OutputStream destImage, TrimMetricsReceiver tmr, String imgFormat, + Packer packer) throws IOException { int tcount = source.getTileCount(); @@ -106,50 +132,50 @@ public class TileSetTrimmer Rectangle[] tbounds = new Rectangle[tcount]; // compute some tile metrics - int nextx = 0, maxy = 0; for (int ii = 0; ii < tcount; ii++) { // extract the image from the original tileset try { timgs[ii] = source.getRawTileImage(ii); } catch (RasterFormatException rfe) { throw new IOException("Failed to get tile image " + - "[tidx=" + ii + ", tset=" + source + - ", rfe=" + rfe + "]."); + "[tidx=" + ii + ", tset=" + source + ", rfe=" + rfe + "]."); } // figure out how tightly we can trim it tbounds[ii] = new Rectangle(); ImageUtil.computeTrimmedBounds(timgs[ii], tbounds[ii]); - // let our caller know what we did - tmr.trimmedTile(ii, nextx, 0, tbounds[ii].x, tbounds[ii].y, - tbounds[ii].width, tbounds[ii].height); + packer.addTile(ii, tbounds[ii].width, tbounds[ii].height); + } - // adjust the new tileset image dimensions - maxy = Math.max(maxy, tbounds[ii].height); - nextx += tbounds[ii].width; + Dimension bounds = packer.pack(); + + for (int ii = 0; ii < tcount; ii++) { + // let our caller know what we did + Rectangle rect = packer.getPosition(ii); + tmr.trimmedTile(ii, rect.x, rect.y, tbounds[ii].x, tbounds[ii].y, + tbounds[ii].width, tbounds[ii].height); } // create the new tileset image BufferedImage image = null; try { - image = ImageUtil.createCompatibleImage(source.getRawTileSetImage(), nextx, maxy); + image = ImageUtil.createCompatibleImage(source.getRawTileSetImage(), + bounds.width, bounds.height); } catch (RasterFormatException rfe) { throw new IOException("Failed to create trimmed tileset image " + - "[wid=" + nextx + ", hei=" + maxy + - ", tset=" + source + ", rfe=" + rfe + "]."); + "[bounds=" + bounds + ", tset=" + source + ", rfe=" + rfe + "]."); } // copy the tile data WritableRaster drast = image.getRaster(); - int xoff = 0; for (int ii = 0; ii < tcount; ii++) { + Rectangle pos = packer.getPosition(ii); Rectangle tb = tbounds[ii]; Raster srast = timgs[ii].getRaster().createChild( tb.x, tb.y, tb.width, tb.height, 0, 0, null); - drast.setRect(xoff, 0, srast); - xoff += tb.width; + drast.setRect(pos.x, pos.y, srast); } if (destImage != null) { @@ -161,4 +187,99 @@ public class TileSetTrimmer } } } + + /** Includes some boilerplate to handle bits of the packing process. */ + public static abstract class BasePacker implements Packer + { + public void addTile (int tileIndex, int width, int height) { + if (_packed) { + throw new IllegalStateException("Cannot add tile after packing."); + } + _tiles.put(tileIndex, new Rectangle(0, 0, width, height)); + } + + final public Dimension pack () { + if (_packed) { + throw new IllegalStateException("Cannot pack repeatedly."); + } + if (_tiles.isEmpty()) { + throw new IllegalStateException("Cannot pack empty tileset"); + } + + _packed = true; + return doPack(); + } + + protected abstract Dimension doPack (); + + public Rectangle getPosition (int tileIndex) { + if (!_packed) { + throw new IllegalStateException("Cannot get tile position before packing."); + } + return _tiles.get(tileIndex); + } + + protected Map _tiles = Maps.newHashMap(); + protected boolean _packed; + }; + + /** + * Packs the tiles together in a wide strip in tile order. + */ + public static class StripPacker extends RowPacker + { + public StripPacker () { + super(Integer.MAX_VALUE, false); + } + }; + + /** + * Packs into rows of the given maximum width, optionally shuffling tiles around to help + * things pack a little less wastefully if the tiles vary in height. + */ + public static class RowPacker extends BasePacker + { + public final int maxWidth; + public final boolean sort; + + public RowPacker (int maxWidth, boolean sort) { + this.maxWidth = maxWidth; + this.sort = sort; + } + + @Override protected Dimension doPack () { + Dimension dim = new Dimension(0, 0); + int x = 0; + int y = 0; + + Collection rects = _tiles.values(); + if (sort) { + rects = Lists.newArrayList(_tiles.values()); + Collections.sort((List)rects, new Comparator() { + @Override public int compare (Rectangle a, Rectangle b) { + return ComparisonChain.start(). + compare(b.height, a.height). + compare(b.width, a.width). + result(); + } + }); + } + + for (Rectangle rect : rects) { + if (x + rect.width > maxWidth) { + x = 0; + y = dim.height; + } + + rect.x = x; + x += rect.width; + rect.y = y; + + dim.height = Math.max(dim.height, y + rect.height); + dim.width = Math.max(dim.width, x); + } + + return dim; + } + } } diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java index 4f0c4c3c..b9829f33 100644 --- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java +++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java @@ -31,6 +31,7 @@ import java.io.OutputStream; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; +import com.google.common.base.Supplier; import com.google.common.collect.Lists; import net.sf.json.JSONArray; @@ -56,6 +57,7 @@ import com.threerings.media.tile.TrimmedObjectTileSet; import com.threerings.media.tile.bundle.BundleUtil; import com.threerings.media.tile.bundle.TileSetBundle; import com.threerings.media.tile.tools.xml.TileSetRuleSet; +import com.threerings.media.tile.util.TileSetTrimmer; import static com.threerings.media.Log.log; @@ -153,6 +155,15 @@ public class TileSetBundler return this; } + /** + * Sets the Packer to use when trimming. + */ + public Writer usePacker (Supplier packer) + { + this.packer = packer; + return trimImages(packer != null); + } + /** * Sets whether or not we write out raw images. */ @@ -194,6 +205,7 @@ public class TileSetBundler boolean trim = true; boolean raw = true; + Supplier packer; String imageBase; JSONConversion.Config json; } @@ -429,7 +441,7 @@ public class TileSetBundler // write the trimmed tileset image to the target file TrimmedObjectTileSet tset = TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, dest, - target.raw ? FastImageIO.FILE_SUFFIX : "png"); + target.raw ? FastImageIO.FILE_SUFFIX : "png", target.packer.get()); tset.setImagePath(imagePath); // replace the original set with the trimmed // tileset in the tileset bundle