/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* 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 the 'Xith3D Project Group' 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.loaders.texture;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.Iterator;
import java.util.LinkedList;
import org.jagatoo.util.image.ImageUtility;
import org.openmali.types.twodee.Dim2i;
import org.openmali.vecmath2.Colorf;
import org.jagatoo.loaders.textures.MipmapGenerator;
import org.jagatoo.loaders.textures.pixelprocessing.PixelProcessor;
import org.jagatoo.opengl.enums.TextureBoundaryMode;
import org.jagatoo.opengl.enums.TextureFilter;
import org.jagatoo.opengl.enums.TextureFormat;
import org.jagatoo.opengl.enums.TextureImageFormat;
import org.xith3d.loaders.texture.TextureLoader.FlipMode;
import org.xith3d.scenegraph.TextureImage2D;
import org.xith3d.scenegraph.Texture;
import org.xith3d.scenegraph.Texture2D;
/**
* The TextureCreator is capable of creating (empty) Textures.
*
* @author Matthias Mann
* @author Marvin Froehlich (aka Qudus)
*/
public class TextureCreator
{
private static TextureCreator singletonInstance = null;
private static Graphics2D metricsGraphics = null;
// Simplistic LRUCache for BufferedImages used for text writing
private static class BICache
{
// TODO: use SoftReference once I know how to circumvent the timestamp update for constraint checking
// private static class BIEntry extends SoftReference<BufferedImage>
private static class BIEntry
{
final BufferedImage image;
long lastUsed;
public BIEntry( BufferedImage referent )
{
//super( referent );
this.image = referent;
}
public void touch()
{
lastUsed = System.currentTimeMillis();
}
//@Override
//@Deprecated
//public BufferedImage get()
//{
// throw new IllegalStateException( "Don't call get!" );
//}
}
// TODO: check performance against LinkedList because of the Iterator.remove()-calls in getImage()
private LinkedList<BIEntry> images = new LinkedList<BIEntry>();
private int capacity;
private long maxAge;
public BICache( int capacity, long maxAge )
{
this.capacity = capacity;
this.maxAge = maxAge;
}
public BufferedImage getImage( int width, int height )
{
BIEntry candidate = null;
BIEntry match = null;
// Try to find a matching cache entry
final int size = images.size();
Iterator<BIEntry> it = images.iterator();
for ( int i = 0; i < size; i++ )
{
candidate = it.next();
if ( ( candidate.image.getWidth() == width ) && ( candidate.image.getHeight() == height ) )
{
match = candidate;
// remove the candidate from the cache to reinsert it at first position later
it.remove();
break;
}
}
// If no match was found create a new BufferedImage
if ( match == null )
match = new BIEntry( new BufferedImage( width, height, BufferedImage.TYPE_4BYTE_ABGR ) );
// update lastUsed, so we can check against this to get rid of old cache entries
match.touch();
images.addFirst( match );
housekeep();
return ( match.image );
}
/**
* does some housekeeping by keeping the cache within its limits and removing too old entries
*/
public void housekeep()
{
if ( ( images.size() > capacity ) || ( System.currentTimeMillis() > images.getLast().lastUsed + maxAge ) )
images.getLast();
}
}
// Hold up to 64 images as long as their age is not greater than 30 seconds
private static BICache imageCache = new BICache( 64, 30 * 1000 );
/**
* @return the singleton instance of TextureCreator
*
* @deprecated this class has static method only now!
*/
@Deprecated
public static TextureCreator getInstance()
{
if ( singletonInstance == null )
{
singletonInstance = new TextureCreator();
}
return ( singletonInstance );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @param width The desired texture width.
* @param height The desired texture height.
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, TextureFormat format, Texture.MipmapMode mipmapMode, int width, int height, Texture2D tex )
{
final int orgWidth = img.getWidth();
final int orgHeight = img.getHeight();
if ( orgWidth != width || orgHeight != height )
{
img = ImageUtility.scaleImage( img, width, height, format.hasAlpha() );
}
boolean flip = ( flipVertically != null ) ? flipVertically.getBooleanValue() : true;
final PixelProcessor pp = PixelProcessor.selectPixelProcessor( img, format );
TextureImage2D ic = (TextureImage2D)pp.createTextureImage( img, orgWidth, orgHeight, format, flip, Xith3DTextureFactory2D.getInstance() );
tex.setImage( 0, ic );
if ( ( mipmapMode == null ) || ( mipmapMode.booleanValue() ) )
{
MipmapGenerator.createMipMaps( ic, tex, Xith3DTextureFactory2D.getInstance() );
}
return ( tex );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @param width The desired texture width.
* @param height The desired texture height.
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, TextureFormat format, Texture.MipmapMode mipmapMode, int width, int height )
{
Texture2D tex = new Texture2D( format );
return ( createTexture( img, flipVertically, format, mipmapMode, width, height, tex ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param flipVertically flip the image vertically or not
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @param allowStreching
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, TextureFormat format, Texture.MipmapMode mipmapMode, boolean allowStreching )
{
final int width;
final int height;
if ( allowStreching )
{
width = ImageUtility.roundUpPower2( img.getWidth() );
height = ImageUtility.roundUpPower2( img.getHeight() );
}
else
{
width = img.getWidth();
height = img.getHeight();
}
return ( createTexture( img, flipVertically, format, mipmapMode, width, height ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param flipVertically flip the image vertically or not
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, TextureFormat format, Texture.MipmapMode mipmapMode )
{
return ( createTexture( img, flipVertically, format, mipmapMode, true ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, TextureFormat format, Texture.MipmapMode mipmapMode )
{
return ( createTexture( img, (FlipMode)null, format, mipmapMode ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param flipVertically flip the image vertically or not
* @param mipmapMode Should the texture contain mipmaps ?
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, Texture.MipmapMode mipmapMode )
{
return ( createTexture( img, flipVertically, TextureFormat.RGBA, mipmapMode ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param mipmapMode Should the texture contain mipmaps ?
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, Texture.MipmapMode mipmapMode )
{
return ( createTexture( img, (FlipMode)null, mipmapMode ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param format The desired Texture format.
* @param mipmapMode Should the texture contain mipmaps ?
* @param width The desired texture width.
* @param height The desired texture height.
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, TextureFormat format, Texture.MipmapMode mipmapMode, int width, int height )
{
return ( createTexture( img, (FlipMode)null, format, mipmapMode, width, height ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param flipVertically flip the image vertically or not
* @param mipmapMode Should the texture contain mipmaps ?
* @param width The desired texture width.
* @param height The desired texture height.
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, FlipMode flipVertically, Texture.MipmapMode mipmapMode, int width, int height )
{
return ( createTexture( img, flipVertically, TextureFormat.RGBA, mipmapMode, width, height ) );
}
/**
* Creates a Texture from the given BufferedImage. The generated Texture is
* not cached.
*
* @param img The BufferedImage which should be converted.
* @param mipmapMode Should the texture contain mipmaps ?
* @param width The desired texture width.
* @param height The desired texture height.
* @return Texture A Texture object that is based on the current content of
* the given image (NOT byRef!)
*/
public static Texture2D createTexture( BufferedImage img, Texture.MipmapMode mipmapMode, int width, int height )
{
return ( createTexture( img, (FlipMode)null, mipmapMode, width, height ) );
}
/**
* Creates a new Texture2D with the given color.
*
* @param format
* @param width
* @param height
* @param color
*
* @return the new Texture2D
*/
public static Texture2D createTexture( TextureFormat format, int width, int height, Colorf color )
{
if ( format == TextureFormat.LUMINANCE )
{
throw new UnsupportedOperationException( "Creating Luminance Textures is not yet supported." );
}
final TextureImageFormat tiFormat = format.getDefaultTextureImageFormat();
if ( color == null )
color = new Colorf( 0f, 0f, 0f, 1f );
Texture2D tex = new Texture2D( format );
TextureImage2D ic = new TextureImage2D( tiFormat, width, height, width, height, false );
ic.setImageData( null, ic.getDataSize() );
ic.clear( color );
tex.setImage( 0, ic );
return ( tex );
}
/**
* Creates a new Texture2D with the given color.
*
* @param format
* @param width
* @param height
*
* @return the new Texture2D
*/
public static Texture2D createTexture( TextureFormat format, int width, int height )
{
return ( createTexture( format, width, height, null ) );
}
public static class TextMetrics extends Dimension
{
private static final long serialVersionUID = 1L;
private int lineHeight;
private int ascend;
private int descend;
private int[] lineWidths;
public final int getLineHeight()
{
return ( lineHeight );
}
public final int getAscend()
{
return ( ascend );
}
public final int getDescend()
{
return ( descend );
}
public TextMetrics( int width, int height, int[] lineWidths, int lineHeight, int ascend, int descend )
{
super( width, height );
this.lineWidths = lineWidths;
this.lineHeight = lineHeight;
this.ascend = ascend;
this.descend = descend;
}
}
/**
* Creates a Tuple2f containing the size of the text on a Texture.
*
* @param text
* @param font
*/
private static TextMetrics getTextMetrics( String[] lines, Font font )
{
if ( metricsGraphics == null )
{
BufferedImage image = new BufferedImage( 1024, 128, BufferedImage.TYPE_4BYTE_ABGR );
metricsGraphics = image.createGraphics();
}
int maxWidth = 0;
int totalHeight = 0;
final FontMetrics metrics = metricsGraphics.getFontMetrics( font );
final int ascend = metrics.getAscent();
final int descend = metrics.getDescent();
final int lineHeight = ascend + descend;
int[] lineWidths = new int[ lines.length ];
for ( int i = 0; i < lines.length; i++ )
{
lineWidths[ i ] = metrics.stringWidth( lines[ i ] );
if ( lineWidths[ i ] > maxWidth )
maxWidth = lineWidths[ i ];
totalHeight += lineHeight;
}
return ( new TextMetrics( maxWidth, totalHeight, lineWidths, lineHeight, ascend, descend ) );
}
public static final int TEXT_ALIGNMENT_HORIZONTAL_LEFT = 0;
public static final int TEXT_ALIGNMENT_HORIZONTAL_CENTER = 1;
public static final int TEXT_ALIGNMENT_HORIZONTAL_RIGHT = 2;
/**
* Creates a new transparent Texture with a String drawn on it.<br>
* Call texture.getUserData( "EFFECTIVE_SIZE" ) to retrieve the effective size when padded.
*
* @see #TEXT_ALIGNMENT_HORIZONTAL_LEFT
* @see #TEXT_ALIGNMENT_HORIZONTAL_CENTER
* @see #TEXT_ALIGNMENT_HORIZONTAL_RIGHT
*
* @param text the text to draw on the Texture (inline '\n' for line wrapping).
* @param color text color (null for transparent)
* @param font the font to use
* @param horizontalAlignment horizontal alignment indicator
* @param paddSizetoPower2 if true, the Texture's size is padded up to the next power of 2 to avoid resizings
* @return returns a Texture (should be casted to Character)
*/
public static Texture2D createTexture( String text, Colorf color, Font font, int horizontalAlignment, boolean paddSizetoPower2 )
{
final String[] lines = text.split( "\\n" );
TextMetrics metrics = getTextMetrics( lines, font );
// BufferedImage to draw on
final int width;
final int height;
if ( paddSizetoPower2 )
{
width = ImageUtility.roundUpPower2( metrics.width );
height = ImageUtility.roundUpPower2( metrics.height );
}
else
{
width = metrics.width;
height = metrics.height;
}
// BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_4BYTE_ABGR );
BufferedImage image = imageCache.getImage(width, height);
// Graphics from BufferedImage
Graphics2D g2 = image.createGraphics();
// enable anti-aliasing
g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
// fill background to transparent
g2.setBackground(new Color( 0.0f, 0.0f, 0.0f, 0.0f ));
g2.clearRect(0, 0, width, height);
// set the color
if ( color == null )
g2.setColor( new Color( 1.0f, 1.0f, 1.0f, 1.0f ) );
else
g2.setColor( color.getAWTColor() );
// set Font
g2.setFont( font );
// draw the text
int left = 0;
for ( int i = 0; i < lines.length; i++ )
{
if ( horizontalAlignment == TEXT_ALIGNMENT_HORIZONTAL_CENTER )
left = ( metrics.width - metrics.lineWidths[ i ] ) / 2;
else if ( horizontalAlignment == TEXT_ALIGNMENT_HORIZONTAL_RIGHT )
left = metrics.width - metrics.lineWidths[ i ];
else
left = 0;
g2.drawString( lines[ i ], left, ( metrics.lineHeight * i ) + metrics.ascend );
}
// --- convert the String to a texture
Texture2D texture = (Texture2D)createTexture( image, TextureFormat.RGBA, Texture.MipmapMode.BASE_LEVEL );
texture.setBoundaryModeS( TextureBoundaryMode.CLAMP );
texture.setBoundaryModeT( TextureBoundaryMode.CLAMP );
texture.setFilter( TextureFilter.TRILINEAR );
if ( paddSizetoPower2 )
texture.setUserData( "EFFECTIVE_SIZE", new Dim2i( metrics.width, metrics.height ) );
if ( text.length() > 30 )
texture.setName( "Text: \"" + text.substring( 0, 27 ) + "...\"" );
else
texture.setName( "Text: \"" + text + "\"" );
return ( texture );
}
/**
* Creates a new transparent Texture with a String drawn on it.
* The Texture's size is padded up to the next power of 2 to avoid resizings.<br>
* Call texture.getUserData( "EFFECTIVE_SIZE" ) to retrieve the effective size.
*
* @see #TEXT_ALIGNMENT_HORIZONTAL_LEFT
* @see #TEXT_ALIGNMENT_HORIZONTAL_CENTER
* @see #TEXT_ALIGNMENT_HORIZONTAL_RIGHT
*
* @param text the text to draw on the Texture (inline '\n' for line wrapping).
* @param color text color (null for transparent)
* @param font the font to use
* @param horizontalAlignment horizontal alignment indicator
* @return returns a Texture (should be casted to Character)
*/
public static Texture2D createTexture( String text, Colorf color, Font font, int horizontalAlignment )
{
return ( createTexture( text, color, font, horizontalAlignment, true ) );
}
}