/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
******************************************************************************/
package com.badlogic.gdx.graphics.g2d;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.glutils.PixmapTextureData;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap.Keys;
import com.badlogic.gdx.utils.OrderedMap;
/**
* Packs {@link Pixmap} instances into one more more {@link Page} instances to generate an atlas of Pixmap instances.
* Provides means to directly convert the pixmap atlas to a {@link TextureAtlas}. The packer supports padding and border
* pixel duplication, specified during construction. The packer supports incremental inserts and updates of
* TextureAtlases generated with this class.</p>
*
* All methods except {@link #getPage(String)} and {@link #getPages()} are thread safe. The methods
* {@link #generateTextureAtlas(TextureFilter, TextureFilter, boolean)} and
* {@link #updateTextureAtlas(TextureAtlas, TextureFilter, TextureFilter, boolean)} need to be called on the rendering
* thread, all other methods can be called from any thread.</p>
*
* One-off usage:
*
* <pre>
* // 512x512 pixel pages, RGB565 format, 2 pixels of padding, border duplication
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, true);
* packer.pack("First Pixmap", pixmap1);
* packer.pack("Second Pixmap", pixmap2);
* TextureAtlas altas = packer.generateTextureAtlas(TextureFilter.Nearest, TextureFilter.Nearest);
* </pre>
*
* Note that you should not dispose the packer in this usage pattern. Instead, dispose the TextureAtlas if no longer
* needed.
*
* Incremental usage:
*
* <pre>
* // 512x512 pixel pages, RGB565 format, 2 pixels of padding, no border duplication
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, false);
* TextureAtlas incrementalAtlas = new TextureAtlas();
*
* // potentially on a separate thread, e.g. downloading thumbnails
* packer.pack("thumbnail", thumbnail);
*
* // on the rendering thread, every frame
* packer.updateTextureAtlas(incrementalAtlas, TextureFilter.Linear, TextureFilter.Linear);
*
* // once the atlas is no longer needed, make sure you get the final additions. This might
* // be more elaborate depending on your threading model.
* packer.updateTextureAtlas(incrementalAtlas, TextureFilter.Linear, TextureFilter.Linear);
* incrementalAtlas.dispose();
* </pre>
*
* Pixmap-only usage:
*
* <pre>
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, true);
* packer.pack("First Pixmap", pixmap1);
* packer.pack("Second Pixmap", pixmap2);
*
* // do something interesting with the resulting pages
* for (Page page : packer.getPages()) {
* }
*
* // dispose of the packer in this case
* packer.dispose();
* </pre>
*/
public class PixmapPacker implements Disposable {
static final class Node {
public Node leftChild;
public Node rightChild;
public Rectangle rect;
public String leaveName;
public Node(int x, int y, int width, int height, Node leftChild, Node rightChild, String leaveName) {
this.rect = new Rectangle(x, y, width, height);
this.leftChild = leftChild;
this.rightChild = rightChild;
this.leaveName = leaveName;
}
public Node() {
rect = new Rectangle();
}
}
public class Page {
Node root;
OrderedMap<String, Rectangle> rects;
Pixmap image;
Texture texture;
Array<String> addedRects = new Array<String>();
public Pixmap getPixmap() {
return image;
}
}
final int pageWidth;
final int pageHeight;
final Format pageFormat;
final int padding;
final boolean duplicateBorder;
final Array<Page> pages = new Array<Page>();
Page currPage;
boolean disposed;
/**
* <p>
* Creates a new ImagePacker which will insert all supplied images into a <code>width</code> by <code>height</code>
* image. <code>padding</code> specifies the minimum number of pixels to insert between images. <code>border</code>
* will duplicate the border pixels of the inserted images to avoid seams when rendering with bi-linear filtering
* on.
* </p>
*
* @param width
* the width of the output image
* @param height
* the height of the output image
* @param padding
* the number of padding pixels
* @param duplicateBorder
* whether to duplicate the border
*/
public PixmapPacker(int width, int height, Format format, int padding, boolean duplicateBorder) {
this.pageWidth = width;
this.pageHeight = height;
this.pageFormat = format;
this.padding = padding;
this.duplicateBorder = duplicateBorder;
newPage();
}
/**
* <p>
* Inserts the given {@link Pixmap}. You can later on retrieve the images position in the output image via the
* supplied name and the method {@link #getRect(String)}.
* </p>
*
* @param name
* the name of the image
* @param image
* the image
* @return Rectangle describing the area the pixmap was rendered to or null.
* @throws RuntimeException
* in case the image did not fit due to the page size being to small or providing a duplicate name
*/
public synchronized Rectangle pack(String name, Pixmap image) {
if (disposed)
return null;
if (getRect(name) != null)
throw new RuntimeException("Key with name '" + name + "' is already in map");
int borderPixels = padding + (duplicateBorder ? 1 : 0);
borderPixels <<= 1;
Rectangle rect = new Rectangle(0, 0, image.getWidth() + borderPixels, image.getHeight() + borderPixels);
if (rect.getWidth() > pageWidth || rect.getHeight() > pageHeight)
throw new GdxRuntimeException("page size for '" + name + "' to small");
Node node = insert(currPage.root, rect);
if (node == null) {
newPage();
return pack(name, image);
}
node.leaveName = name;
rect = new Rectangle(node.rect);
rect.width -= borderPixels;
rect.height -= borderPixels;
borderPixels >>= 1;
rect.x += borderPixels;
rect.y += borderPixels;
currPage.rects.put(name, rect);
Blending blending = Pixmap.getBlending();
Pixmap.setBlending(Blending.None);
this.currPage.image.drawPixmap(image, (int) rect.x, (int) rect.y);
if (duplicateBorder) {
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
// Copy corner pixels to fill corners of the padding.
this.currPage.image.drawPixmap(image, 0, 0, 1, 1, (int) rect.x - 1, (int) rect.y - 1, 1, 1);
this.currPage.image.drawPixmap(image, imageWidth - 1, 0, 1, 1, (int) rect.x + (int) rect.width,
(int) rect.y - 1, 1, 1);
this.currPage.image.drawPixmap(image, 0, imageHeight - 1, 1, 1, (int) rect.x - 1, (int) rect.y
+ (int) rect.height, 1, 1);
this.currPage.image.drawPixmap(image, imageWidth - 1, imageHeight - 1, 1, 1, (int) rect.x
+ (int) rect.width, (int) rect.y + (int) rect.height, 1, 1);
// Copy edge pixels into padding.
this.currPage.image.drawPixmap(image, 0, 0, imageWidth, 1, (int) rect.x, (int) rect.y - 1,
(int) rect.width, 1);
this.currPage.image.drawPixmap(image, 0, imageHeight - 1, imageWidth, 1, (int) rect.x, (int) rect.y
+ (int) rect.height, (int) rect.width, 1);
this.currPage.image.drawPixmap(image, 0, 0, 1, imageHeight, (int) rect.x - 1, (int) rect.y, 1,
(int) rect.height);
this.currPage.image.drawPixmap(image, imageWidth - 1, 0, 1, imageHeight, (int) rect.x + (int) rect.width,
(int) rect.y, 1, (int) rect.height);
}
Pixmap.setBlending(blending);
currPage.addedRects.add(name);
return rect;
}
private void newPage() {
Page page = new Page();
page.image = new Pixmap(pageWidth, pageHeight, pageFormat);
page.root = new Node(0, 0, pageWidth, pageHeight, null, null, null);
page.rects = new OrderedMap<String, Rectangle>();
pages.add(page);
currPage = page;
}
private Node insert(Node node, Rectangle rect) {
if (node.leaveName == null && node.leftChild != null && node.rightChild != null) {
Node newNode = null;
newNode = insert(node.leftChild, rect);
if (newNode == null)
newNode = insert(node.rightChild, rect);
return newNode;
} else {
if (node.leaveName != null)
return null;
if (node.rect.width == rect.width && node.rect.height == rect.height)
return node;
if (node.rect.width < rect.width || node.rect.height < rect.height)
return null;
node.leftChild = new Node();
node.rightChild = new Node();
int deltaWidth = (int) node.rect.width - (int) rect.width;
int deltaHeight = (int) node.rect.height - (int) rect.height;
if (deltaWidth > deltaHeight) {
node.leftChild.rect.x = node.rect.x;
node.leftChild.rect.y = node.rect.y;
node.leftChild.rect.width = rect.width;
node.leftChild.rect.height = node.rect.height;
node.rightChild.rect.x = node.rect.x + rect.width;
node.rightChild.rect.y = node.rect.y;
node.rightChild.rect.width = node.rect.width - rect.width;
node.rightChild.rect.height = node.rect.height;
} else {
node.leftChild.rect.x = node.rect.x;
node.leftChild.rect.y = node.rect.y;
node.leftChild.rect.width = node.rect.width;
node.leftChild.rect.height = rect.height;
node.rightChild.rect.x = node.rect.x;
node.rightChild.rect.y = node.rect.y + rect.height;
node.rightChild.rect.width = node.rect.width;
node.rightChild.rect.height = node.rect.height - rect.height;
}
return insert(node.leftChild, rect);
}
}
/** @return the {@link Page} instances created so far. This method is not thread safe! */
public Array<Page> getPages() {
return pages;
}
/**
* @param name
* the name of the image
* @return the rectangle for the image in the page it's stored in or null
*/
public synchronized Rectangle getRect(String name) {
for (Page page : pages) {
Rectangle rect = page.rects.get(name);
if (rect != null)
return rect;
}
return null;
}
/**
* @param name
* the name of the image
* @return the page the image is stored in or null
*/
public synchronized Page getPage(String name) {
for (Page page : pages) {
Rectangle rect = page.rects.get(name);
if (rect != null)
return page;
}
return null;
}
/**
* Disposes all resources, including Pixmap instances for the pages created so far. These page Pixmap instances are
* shared with any {@link TextureAtlas} generated or updated by either
* {@link #generateTextureAtlas(TextureFilter, TextureFilter, boolean)} or
* {@link #updateTextureAtlas(TextureAtlas, TextureFilter, TextureFilter, boolean)}. Do not call this method if you
* generated or updated a TextureAtlas, instead dispose the TextureAtlas.
*/
public synchronized void dispose() {
for (Page page : pages) {
page.image.dispose();
}
disposed = true;
}
/**
* Generates a new {@link TextureAtlas} from the {@link Pixmap} instances inserted so far.
*
* @param minFilter
* @param magFilter
* @return the TextureAtlas
*/
public synchronized TextureAtlas generateTextureAtlas(TextureFilter minFilter, TextureFilter magFilter,
boolean useMipMaps) {
TextureAtlas atlas = new TextureAtlas();
for (Page page : pages) {
if (page.rects.size != 0) {
Texture texture = new Texture(new ManagedPixmapTextureData(page.image, page.image.getFormat(),
useMipMaps)) {
@Override
public void dispose() {
super.dispose();
getTextureData().consumePixmap().dispose();
}
};
texture.setFilter(minFilter, magFilter);
Keys<String> names = page.rects.keys();
for (String name : names) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(texture, (int) rect.x, (int) rect.y, (int) rect.width,
(int) rect.height);
atlas.addRegion(name, region);
}
}
}
return atlas;
}
/**
* Updates the given {@link TextureAtlas}, adding any new {@link Pixmap} instances packed since the last call to
* this method. This can be used to insert Pixmap instances on a separate thread via {@link #pack(String, Pixmap)}
* and update the TextureAtlas on the rendering thread. This method must be called on the rendering thread.
*/
public synchronized void updateTextureAtlas(TextureAtlas atlas, TextureFilter minFilter, TextureFilter magFilter,
boolean useMipMaps) {
for (Page page : pages) {
if (page.texture == null) {
if (page.rects.size != 0 && page.addedRects.size > 0) {
page.texture = new Texture(new ManagedPixmapTextureData(page.image, page.image.getFormat(),
useMipMaps)) {
@Override
public void dispose() {
super.dispose();
getTextureData().consumePixmap().dispose();
}
};
page.texture.setFilter(minFilter, magFilter);
for (String name : page.addedRects) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(page.texture, (int) rect.x, (int) rect.y,
(int) rect.width, (int) rect.height);
atlas.addRegion(name, region);
}
page.addedRects.clear();
}
} else {
if (page.addedRects.size > 0) {
page.texture.load(page.texture.getTextureData());
for (String name : page.addedRects) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(page.texture, (int) rect.x, (int) rect.y,
(int) rect.width, (int) rect.height);
atlas.addRegion(name, region);
}
page.addedRects.clear();
return;
}
}
}
}
public int getPageWidth() {
return pageWidth;
}
public int getPageHeight() {
return pageHeight;
}
public int getPadding() {
return padding;
}
public boolean duplicateBoarder() {
return duplicateBorder;
}
public class ManagedPixmapTextureData extends PixmapTextureData {
public ManagedPixmapTextureData(Pixmap pixmap, Format format, boolean useMipMaps) {
super(pixmap, format, useMipMaps, false);
}
@Override
public boolean isManaged() {
return true;
}
}
}