/* * Copyright (c) 2016, Metron, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Metron, Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.metsci.glimpse.dnc; import static com.metsci.glimpse.dnc.util.DncMiscUtils.nextPowerOfTwo; import static java.lang.Math.ceil; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.sqrt; import static java.util.Collections.unmodifiableMap; import java.awt.Dimension; import java.awt.Graphics2D; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import com.jogamp.opengl.util.packrect.Rect; import com.jogamp.opengl.util.packrect.RectVisitor; import com.jogamp.opengl.util.packrect.RectanglePacker; import com.metsci.glimpse.dnc.util.AnchoredImage; import com.metsci.glimpse.dnc.util.NullBackingStoreManager; import com.metsci.glimpse.dnc.util.TexturableImage; public class DncAtlases { public static final int imagePadding = 1; public static <K> DncHostAtlas<K> createHostAtlas( final Map<K,AnchoredImage> anchoredImages, int maxTextureDim ) throws IOException { double totalArea = 0; for ( AnchoredImage anchoredImage : anchoredImages.values( ) ) { int wPadded = anchoredImage.image.getWidth( ) + 2*imagePadding; int hPadded = anchoredImage.image.getHeight( ) + 2*imagePadding; totalArea += ( wPadded * hPadded ); } // Initial dims must be at least 2, or RectanglePacker will hang int initialWidth = max( 2, min( maxTextureDim, nextPowerOfTwo( ( int ) ceil( sqrt( totalArea ) ) ) ) ); int initialHeight = max( 2, ( int ) ceil( totalArea / initialWidth ) ); RectanglePacker rectPacker = new RectanglePacker( new NullBackingStoreManager( ), initialWidth, initialHeight ); rectPacker.setMaxSize( maxTextureDim, maxTextureDim ); for ( Entry<K,AnchoredImage> en : anchoredImages.entrySet( ) ) { K key = en.getKey( ); AnchoredImage anchoredImage = en.getValue( ); int wPadded = anchoredImage.image.getWidth( ) + 2*imagePadding; int hPadded = anchoredImage.image.getHeight( ) + 2*imagePadding; rectPacker.add( new Rect( 0, 0, wPadded, hPadded, key ) ); } final Map<K,DncAtlasEntry> atlasEntries = new HashMap<>( ); Dimension totalSize = ( Dimension ) rectPacker.getBackingStore( ); final int wTotal = nextPowerOfTwo( totalSize.width ); final int hTotal = nextPowerOfTwo( totalSize.height ); TexturableImage atlasImage = new TexturableImage( wTotal, hTotal ); final Graphics2D g = atlasImage.createGraphics( ); rectPacker.visit( new RectVisitor( ) { public void visit( Rect rect ) { @SuppressWarnings( "unchecked" ) K key = ( K ) rect.getUserData( ); AnchoredImage anchoredImage = anchoredImages.get( key ); int xPadded = rect.x( ); int yPadded = rect.y( ); int wPadded = rect.w( ); int hPadded = rect.h( ); int x = xPadded + imagePadding; int y = yPadded + imagePadding; int w = wPadded - 2*imagePadding; int h = hPadded - 2*imagePadding; g.drawImage( anchoredImage.image, x, y, w, h, null ); float sMin = ( xPadded ) / ( float ) wTotal; float sMax = ( xPadded + wPadded ) / ( float ) wTotal; float tMin = ( yPadded ) / ( float ) hTotal; float tMax = ( yPadded + hPadded ) / ( float ) hTotal; // XXX: Off by half a pixel? float xAlign = ( anchoredImage.iAnchor + imagePadding ) / ( float ) wPadded; float yAlign = ( anchoredImage.jAnchor + imagePadding ) / ( float ) hPadded; atlasEntries.put( key, new DncAtlasEntry( wPadded, hPadded, sMin, sMax, tMin, tMax, xAlign, yAlign ) ); } } ); g.dispose( ); return new DncHostAtlas<K>( atlasEntries, atlasImage ); } public static class DncHostAtlas<K> { public final Map<K,DncAtlasEntry> entries; public final TexturableImage textureImage; public DncHostAtlas( Map<K,DncAtlasEntry> entries, TexturableImage textureImage ) { this.entries = unmodifiableMap( entries ); this.textureImage = textureImage; } } public static class DncAtlasEntry { // Subimage size, in pixels public final int w; public final int h; // Subimage bounds, as a fraction of atlas-texture dimensions public final float sMin; public final float sMax; public final float tMin; public final float tMax; // Alignment: 0 = bottom/left, 1 = top/right public final float xAlign; public final float yAlign; public DncAtlasEntry( int w, int h, float sMin, float sMax, float tMin, float tMax, float xAlign, float yAlign ) { this.w = w; this.h = h; this.sMin = sMin; this.sMax = sMax; this.tMin = tMin; this.tMax = tMax; this.xAlign = xAlign; this.yAlign = yAlign; } } }