Texture atlas: use bitmap packer based on libGdx PixmapPacker (#304)

This commit is contained in:
Longri 2017-02-26 15:05:29 +01:00 committed by Emux
parent 29322acf87
commit 23d34fa00a
3 changed files with 378 additions and 133 deletions

View File

@ -1,5 +1,6 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2017 Longri
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
@ -68,6 +69,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class TextureAtlas extends Inlist<TextureAtlas> {
static final Logger log = LoggerFactory.getLogger(TextureAtlas.class);
@ -239,6 +241,10 @@ public class TextureAtlas extends Inlist<TextureAtlas> {
return r;
}
public Map<Object, TextureRegion> getRegions() {
return mRegions;
}
public void clear() {
mRects = null;
mSlots = new Slot(1, 1, mWidth - 2);

View File

@ -0,0 +1,332 @@
/*
* Copyright 2017 Longri
*
* Based on PixmapPacker from LibGdx converted to use VTM Bitmaps without any LibGdx dependencies:
* https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/PixmapPacker.java
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.utils;
import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.canvas.Bitmap;
import org.oscim.backend.canvas.Canvas;
import org.oscim.backend.canvas.Color;
import org.oscim.renderer.atlas.TextureAtlas;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BitmapPacker {
private final int atlasWidth, atlasHeight;
private final int padding;
private final PackStrategy packStrategy;
private final boolean flipY;
private final List<PackerAtlasItem> packerAtlasItems = new ArrayList<>();
public BitmapPacker(int atlasWidth, int atlasHeight, int padding, boolean flipY) {
this(atlasWidth, atlasHeight, padding, new GuillotineStrategy(), flipY);
}
public BitmapPacker(int atlasWidth, int atlasHeight, int padding, PackStrategy packStrategy,
boolean flipY) {
this.atlasWidth = atlasWidth;
this.atlasHeight = atlasHeight;
this.padding = padding;
this.packStrategy = packStrategy;
this.flipY = flipY;
}
public synchronized Rect add(Object key, Bitmap image) {
Rect rect = new Rect(0, 0, image.getWidth(), image.getHeight());
if (rect.width > atlasWidth || rect.height > atlasHeight) {
if (key == null)
throw new RuntimeException("PackerAtlasItem size too small for Bitmap.");
throw new RuntimeException("PackerAtlasItem size too small for Bitmap: " + key);
}
PackerAtlasItem packerAtlasItem = packStrategy.pack(this, key, rect);
if (key != null) {
packerAtlasItem.rects.put(key, rect);
packerAtlasItem.addedRects.add(key);
}
int rectX = rect.x, rectY = rect.y, rectWidth = rect.width, rectHeight = rect.height;
packerAtlasItem.drawBitmap(image, rectX,
flipY ? packerAtlasItem.image.getHeight() - rectY : rectY);
return rect;
}
public synchronized PackerAtlasItem getAtlasItem(int index) {
return packerAtlasItems.get(index);
}
public int getAtlasCount() {
return packerAtlasItems.size();
}
public static class PackerAtlasItem {
HashMap<Object, Rect> rects = new HashMap<>();
final Bitmap image;
final Canvas canvas;
final ArrayList<Object> addedRects = new ArrayList<>();
PackerAtlasItem(BitmapPacker packer) {
image = CanvasAdapter.newBitmap(packer.atlasWidth, packer.atlasHeight, 0);
canvas = CanvasAdapter.newCanvas();
canvas.setBitmap(this.image);
canvas.fillColor(Color.TRANSPARENT);
}
public TextureAtlas getAtlas() {
TextureAtlas atlas = new TextureAtlas(image);
//add regions
for (Map.Entry<Object, Rect> entry : rects.entrySet()) {
atlas.addTextureRegion(entry.getKey(), entry.getValue().getAtlasRect());
}
return atlas;
}
void drawBitmap(Bitmap image, int x, int y) {
canvas.drawBitmap(image, x, y);
}
}
public interface PackStrategy {
void sort(ArrayList<Bitmap> images);
PackerAtlasItem pack(BitmapPacker packer, Object key, Rect rect);
}
/**
* Does bin packing by inserting to the right or below previously packed rectangles.
* This is good at packing arbitrarily sized images.
*
* @author mzechner
* @author Nathan Sweet
* @author Rob Rendell
*/
public static class GuillotineStrategy implements PackStrategy {
Comparator<Bitmap> comparator;
public void sort(ArrayList<Bitmap> Bitmaps) {
if (comparator == null) {
comparator = new Comparator<Bitmap>() {
public int compare(Bitmap o1, Bitmap o2) {
return Math.max(o1.getWidth(), o1.getHeight()) - Math.max(o2.getWidth(), o2.getHeight());
}
};
}
Collections.sort(Bitmaps, comparator);
}
public PackerAtlasItem pack(BitmapPacker packer, Object key, Rect rect) {
GuillotineAtlasItem atlasItem;
if (packer.packerAtlasItems.size() == 0) {
// Add a atlas item if empty.
atlasItem = new GuillotineAtlasItem(packer);
packer.packerAtlasItems.add(atlasItem);
} else {
// Always try to pack into the last atlas item.
atlasItem = (GuillotineAtlasItem) packer.packerAtlasItems.get(packer.packerAtlasItems.size() - 1);
}
int padding = packer.padding;
rect.width += padding;
rect.height += padding;
Node node = insert(atlasItem.root, rect);
if (node == null) {
// Didn't fit, pack into a new atlas item.
atlasItem = new GuillotineAtlasItem(packer);
packer.packerAtlasItems.add(atlasItem);
node = insert(atlasItem.root, rect);
}
node.full = true;
rect.set(node.rect.x, node.rect.y, node.rect.width - padding, node.rect.height - padding);
return atlasItem;
}
private Node insert(Node node, Rect rect) {
if (!node.full && node.leftChild != null && node.rightChild != null) {
Node newNode = insert(node.leftChild, rect);
if (newNode == null) newNode = insert(node.rightChild, rect);
return newNode;
} else {
if (node.full) 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 = node.rect.width - rect.width;
int deltaHeight = node.rect.height - 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);
}
}
static final class Node {
Node leftChild;
Node rightChild;
final Rect rect = new Rect();
boolean full;
}
static class GuillotineAtlasItem extends PackerAtlasItem {
Node root;
GuillotineAtlasItem(BitmapPacker packer) {
super(packer);
root = new Node();
root.rect.x = packer.padding;
root.rect.y = packer.padding;
root.rect.width = packer.atlasWidth - packer.padding * 2;
root.rect.height = packer.atlasHeight - packer.padding * 2;
}
}
}
/**
* Does bin packing by inserting in rows. This is good at packing images that have similar heights.
*
* @author Nathan Sweet
*/
public static class SkylineStrategy implements PackStrategy {
Comparator<Bitmap> comparator;
public void sort(ArrayList<Bitmap> images) {
if (comparator == null) {
comparator = new Comparator<Bitmap>() {
public int compare(Bitmap o1, Bitmap o2) {
return o1.getHeight() - o2.getHeight();
}
};
}
Collections.sort(images, comparator);
}
public PackerAtlasItem pack(BitmapPacker packer, Object key, Rect rect) {
int padding = packer.padding;
int atlasWidth = packer.atlasWidth - padding * 2, atlasHeight = packer.atlasHeight - padding * 2;
int rectWidth = rect.width + padding, rectHeight = rect.height + padding;
for (int i = 0, n = packer.packerAtlasItems.size(); i < n; i++) {
SkylineAtlasItem atlasItem = (SkylineAtlasItem) packer.packerAtlasItems.get(i);
SkylineAtlasItem.Row bestRow = null;
// Fit in any row before the last.
for (int ii = 0, nn = atlasItem.rows.size() - 1; ii < nn; ii++) {
SkylineAtlasItem.Row row = atlasItem.rows.get(ii);
if (row.x + rectWidth >= atlasWidth) continue;
if (row.y + rectHeight >= atlasHeight) continue;
if (rectHeight > row.height) continue;
if (bestRow == null || row.height < bestRow.height) bestRow = row;
}
if (bestRow == null) {
// Fit in last row, increasing height.
SkylineAtlasItem.Row row = atlasItem.rows.get(atlasItem.rows.size() - 1);
if (row.y + rectHeight >= atlasHeight) continue;
if (row.x + rectWidth < atlasWidth) {
row.height = Math.max(row.height, rectHeight);
bestRow = row;
} else {
// Fit in new row.
bestRow = new SkylineAtlasItem.Row();
bestRow.y = row.y + row.height;
bestRow.height = rectHeight;
if (bestRow.y + bestRow.height > atlasHeight) continue;
atlasItem.rows.add(bestRow);
}
}
rect.x = bestRow.x;
rect.y = bestRow.y;
bestRow.x += rectWidth;
return atlasItem;
}
// Fit in new atlas item.
SkylineAtlasItem atlasItem = new SkylineAtlasItem(packer);
packer.packerAtlasItems.add(atlasItem);
SkylineAtlasItem.Row row = new SkylineAtlasItem.Row();
row.x = padding + rectWidth;
row.y = padding;
row.height = rectHeight;
atlasItem.rows.add(row);
rect.x = padding;
rect.y = padding;
return atlasItem;
}
static class SkylineAtlasItem extends PackerAtlasItem {
ArrayList<Row> rows = new ArrayList<>();
SkylineAtlasItem(BitmapPacker packer) {
super(packer);
}
static class Row {
int x, y, height;
}
}
}
private static class Rect {
int x, y, width, height;
Rect() {
}
Rect(int x, int y, int width, int height) {
this.set(x, y, width, height);
}
void set(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
TextureAtlas.Rect getAtlasRect() {
return new TextureAtlas.Rect(x, y, width, height);
}
}
}

View File

@ -14,150 +14,67 @@
*/
package org.oscim.utils;
import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.canvas.Bitmap;
import org.oscim.backend.canvas.Canvas;
import org.oscim.renderer.atlas.TextureAtlas;
import org.oscim.renderer.atlas.TextureRegion;
import org.oscim.utils.math.MathUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class TextureAtlasUtils {
private final static int MAX_ATLAS_SIZE = 1024;
private final static int PAD = 2;
private static final int MAX_ATLAS_SIZE = 2048;
private static final int PAD = 2;
/**
* Create a Map<Object, TextureRegion> from Map<Object, Bitmap>!
* <br/>
* The List<TextureAtlas> contains the generated TextureAtlas object, for disposing if no longer needed!<br/>
* With tha param disposeBitmap, all Bitmaps will released!<br/>
* With parameter flipY, the Atlas TextureItem will flipped over Y. (Is needed by iOS)<br/>
* Create atlas texture regions from bitmaps.
*
* @param inputMap Map<Object, Bitmap> input Map with all Bitmaps, from which the regions are to be created
* @param outputMap Map<Object, TextureRegion> contains all generated TextureRegions
* @param atlasList List<TextureAtlas> contains all created TextureAtlases
* @param disposeBitmaps boolean (will recycle all Bitmap's)
* @param flipY boolean (set True with iOS)
* @param inputMap input bitmaps
* @param outputMap generated texture regions
* @param atlasList created texture atlases
* @param disposeBitmaps recycle input bitmaps
* @param flipY texture items flip over y (needed on iOS)
*/
public static void createTextureRegions(final Map<Object, Bitmap> inputMap, Map<Object, TextureRegion> outputMap,
List<TextureAtlas> atlasList, boolean disposeBitmaps, boolean flipY) {
// step 1: sort inputMap by Bitmap size
List<Map.Entry<Object, Bitmap>> list =
new LinkedList<>(inputMap.entrySet());
Collections.sort(list, new Comparator<Map.Entry<Object, Bitmap>>() {
public int compare(Map.Entry<Object, Bitmap> o1, Map.Entry<Object, Bitmap> o2) {
int width1 = o1.getValue().getWidth();
int width2 = o2.getValue().getWidth();
int height1 = o1.getValue().getHeight();
int height2 = o2.getValue().getHeight();
return Math.max(width2, height2) - Math.max(width1, height1);
}
});
Map<Object, Object> sortedByValues = new LinkedHashMap<>();
for (Map.Entry<Object, Bitmap> entry : list) {
sortedByValues.put(entry.getKey(), entry.getValue());
}
//step 2: calculate Atlas count and size
public static void createTextureRegions(final Map<Object, Bitmap> inputMap,
Map<Object, TextureRegion> outputMap,
List<TextureAtlas> atlasList, boolean disposeBitmaps,
boolean flipY) {
// calculate atlas size
int completePixel = PAD * PAD;
for (Map.Entry<Object, Object> entry : sortedByValues.entrySet()) {
completePixel += (((Bitmap) entry.getValue()).getWidth() + PAD)
* (((Bitmap) entry.getValue()).getHeight() + PAD);
int minHeight = Integer.MAX_VALUE;
int maxHeight = Integer.MIN_VALUE;
for (Map.Entry<Object, Bitmap> entry : inputMap.entrySet()) {
int height = entry.getValue().getHeight();
completePixel += (entry.getValue().getWidth() + PAD) * (height + PAD);
minHeight = Math.min(minHeight, height);
maxHeight = Math.max(maxHeight, height);
}
BitmapPacker.PackStrategy strategy = maxHeight - minHeight < 50
? new BitmapPacker.SkylineStrategy()
: new BitmapPacker.GuillotineStrategy();
completePixel *= 1.2; // add estimated blank pixels
int atlasWidth = (int) Math.sqrt(completePixel);
int atlasCount = (atlasWidth / MAX_ATLAS_SIZE) + 1;
if (atlasCount > 1) atlasWidth = MAX_ATLAS_SIZE;
// next power of two
atlasWidth = MathUtils.nextPowerOfTwo(MathUtils.nextPowerOfTwo(atlasWidth) + 1);
// limit to max
atlasWidth = Math.min(MAX_ATLAS_SIZE, atlasWidth);
BitmapPacker bitmapPacker = new BitmapPacker(atlasWidth, atlasWidth, PAD, strategy, flipY);
//step 3: replace value object with object that holds the Bitmap and a Rectangle
for (Map.Entry<Object, Object> entry : sortedByValues.entrySet()) {
BmpRectangleObject newObject = new BmpRectangleObject();
newObject.bitmap = (Bitmap) entry.getValue();
newObject.rec = new TextureAtlas.Rect(0, 0,
newObject.bitmap.getWidth(), newObject.bitmap.getHeight());
entry.setValue(newObject);
for (Map.Entry<Object, Bitmap> entry : inputMap.entrySet()) {
completePixel += (entry.getValue().getWidth() + PAD)
* (entry.getValue().getHeight() + PAD);
bitmapPacker.add(entry.getKey(), entry.getValue());
}
//step 4: calculate Regions(rectangles) and split to atlases
List<AtlasElement> atlases = new ArrayList<>();
atlases.add(new AtlasElement());
int atlasIndex = 0;
int maxLineHeight = PAD;
for (Map.Entry<Object, Object> entry : sortedByValues.entrySet()) {
BmpRectangleObject obj = (BmpRectangleObject) entry.getValue();
AtlasElement atlas = atlases.get(atlasIndex);
if ((atlas.width + obj.rec.w + PAD) > atlasWidth) {
// not enough space, try next line
if ((atlas.height + maxLineHeight + obj.rec.h + PAD) > MAX_ATLAS_SIZE) {
// not enough space, take new Atlas
atlas.width = atlasWidth;
atlas.height += PAD + maxLineHeight;
atlasIndex++;
atlas = new AtlasElement();
atlases.add(atlas);
obj.rec.x = PAD + atlas.width;
obj.rec.y = PAD + atlas.height;
atlas.width += obj.rec.w + PAD;
maxLineHeight = obj.rec.h + PAD;
} else {
// new line
atlas.width = 0;
atlas.height += PAD + maxLineHeight;
obj.rec.x = PAD + atlas.width;
obj.rec.y = PAD + atlas.height;
atlas.width += obj.rec.w + PAD;
}
} else {
// new row
obj.rec.x = PAD + atlas.width;
obj.rec.y = PAD + atlas.height;
atlas.width += obj.rec.w + PAD;
maxLineHeight = Math.max(maxLineHeight, obj.rec.h + PAD);
}
atlas.map.put(entry.getKey(), obj);
}
AtlasElement lastAtlas = atlases.get(atlases.size() - 1);
lastAtlas.width = atlasWidth;
lastAtlas.height += maxLineHeight;
//step 5: create TextureAtlases and there TextureRegions
for (AtlasElement atlas : atlases) {
Bitmap atlasBitmap = CanvasAdapter.newBitmap(atlas.width, atlas.height, 0);
Canvas canvas = CanvasAdapter.newCanvas();
canvas.setBitmap(atlasBitmap);
// draw regions into texture
for (Map.Entry<Object, Object> entry : atlas.map.entrySet()) {
BmpRectangleObject obj = (BmpRectangleObject) entry.getValue();
if (obj.rec.x + obj.rec.w > atlasBitmap.getWidth() ||
obj.rec.y + obj.rec.h > atlasBitmap.getHeight()) {
throw new RuntimeException("atlas region outside of textureRegion");
}
canvas.drawBitmap(obj.bitmap, obj.rec.x, flipY ? atlas.height - obj.rec.y - obj.rec.h : obj.rec.y);
}
TextureAtlas textureAtlas = new TextureAtlas(atlasBitmap);
atlasList.add(textureAtlas);
//register regions and put there into outputMap
for (Map.Entry<Object, Object> entry : atlas.map.entrySet()) {
textureAtlas.addTextureRegion(entry.getKey(), ((BmpRectangleObject) entry.getValue()).rec);
outputMap.put(entry.getKey(), textureAtlas.getTextureRegion(entry.getKey()));
}
for (int i = 0, n = bitmapPacker.getAtlasCount(); i < n; i++) {
BitmapPacker.PackerAtlasItem packerAtlasItem = bitmapPacker.getAtlasItem(i);
TextureAtlas atlas = packerAtlasItem.getAtlas();
atlasList.add(atlas);
outputMap.putAll(atlas.getRegions());
}
if (disposeBitmaps) {
@ -167,14 +84,4 @@ public class TextureAtlasUtils {
inputMap.clear();
}
}
private static class BmpRectangleObject {
private Bitmap bitmap;
private TextureAtlas.Rect rec;
}
private static class AtlasElement {
int width = 0, height = 0;
Map<Object, Object> map = new LinkedHashMap<>();
}
}