diff --git a/src/java/com/threerings/jme/Log.java b/src/java/com/threerings/jme/Log.java index da1cee03..c72bf932 100644 --- a/src/java/com/threerings/jme/Log.java +++ b/src/java/com/threerings/jme/Log.java @@ -1,8 +1,8 @@ // -// $Id: Log.java 3099 2004-08-27 02:21:06Z mdb $ +// $Id$ // // Narya library - tools for developing networked games -// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/narya/ // // This library is free software; you can redistribute it and/or modify it @@ -21,19 +21,21 @@ package com.threerings.jme; +import java.util.logging.Level; +import java.util.logging.Logger; + /** - * A placeholder class that contains a reference to the log object used by - * this package. + * A placeholder class that contains a reference to the log object used by this package. */ public class Log { - public static com.samskivert.util.Log log = - new com.samskivert.util.Log("narya.jme"); + /** We dispatch our log messages through this logger. */ + public static Logger log = Logger.getLogger("com.threerings.nenya.jme"); /** Convenience function. */ public static void debug (String message) { - log.debug(message); + log.fine(message); } /** Convenience function. */ @@ -51,6 +53,6 @@ public class Log /** Convenience function. */ public static void logStackTrace (Throwable t) { - log.logStackTrace(com.samskivert.util.Log.WARNING, t); + log.log(Level.WARNING, t.getMessage(), t); } } diff --git a/src/java/com/threerings/jme/util/ImageCache.java b/src/java/com/threerings/jme/util/ImageCache.java new file mode 100644 index 00000000..6a26863b --- /dev/null +++ b/src/java/com/threerings/jme/util/ImageCache.java @@ -0,0 +1,395 @@ +// +// $Id$ + +package com.threerings.jme.util; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.logging.Level; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import java.awt.Graphics2D; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.BandCombineOp; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; + +import javax.imageio.ImageIO; + +import com.jme.image.Image; +import com.jmex.bui.BImage; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageUtil; + +import com.threerings.resource.ResourceManager; + +import static com.threerings.jme.Log.log; + +/** + * Manages a weak cache of image data to make life simpler for callers that don't want to worry + * about coordinating shared use of the same images. + */ +public class ImageCache +{ + /** + * Creates a JME-compatible image from the supplied buffered image. + */ + public static Image createImage (BufferedImage bufimg, boolean flip) + { + return createImage(bufimg, 1f, flip); + } + + /** + * Colorizes the supplied buffered image (which must be an 8-bit colormapped image), then + * converts the colorized image into a form that JME can display. + */ + public static Image createImage (BufferedImage bufimg, Colorization[] zations, boolean flip) + { + return createImage(bufimg, zations, 1f, flip); + } + + /** + * Colorizes the supplied buffered image (which must be an 8-bit colormapped image), then + * converts the colorized image into a form that JME can display. + */ + public static Image createImage (BufferedImage bufimg, Colorization[] zations, float scale, + boolean flip) + { + return createImage(ImageUtil.recolorImage(bufimg, zations), scale, flip); + } + + /** + * Creates a JME-compatible image from the supplied buffered image. + */ + public static Image createImage (BufferedImage bufimg, float scale, boolean flip) + { + // make sure images are square powers of two + int width = (int)(bufimg.getWidth() * scale); + int height = (int)(bufimg.getHeight() * scale); + int tsize = nextPOT(Math.max(width, height)); + + // convert the the image to the format that OpenGL prefers + BufferedImage dispimg = createCompatibleImage( + tsize, tsize, bufimg.getColorModel().hasAlpha()); + + // flip the image to convert into OpenGL's coordinate system + AffineTransform tx = null; + if (flip) { + tx = AffineTransform.getScaleInstance(scale, -scale); + tx.translate((tsize - width) / 2, (height - tsize) / 2 - bufimg.getHeight()); + } + + // "convert" the image by rendering the old into the new + Graphics2D gfx = (Graphics2D)dispimg.getGraphics(); + gfx.drawImage(bufimg, tx, null); + gfx.dispose(); + + // now extract the image data into a JME image + return convertImage(dispimg); + } + + /** + * Creates a buffered image in a format compatible with LWJGL with the specified dimensions. + * + * @param transparent if true, the image will be four bytes per pixel (RGBA), if false it will + * be three (and have no alpha channel). + */ + public static BufferedImage createCompatibleImage (int width, int height, boolean transparent) + { + if (transparent) { + return new BufferedImage(GL_ALPHA_MODEL, Raster.createInterleavedRaster( + DataBuffer.TYPE_BYTE, width, height, 4, null), + false, null); + } else { + return new BufferedImage(GL_OPAQUE_MODEL, Raster.createInterleavedRaster( + DataBuffer.TYPE_BYTE, width, height, 3, null), + false, null); + } + } + + /** + * Converts an image that was created with {@link #createCompatibleImage} into a JME {@link + * Image}. The data is assumed to have already been "flipped". + */ + public static Image convertImage (BufferedImage bufimg) + { + Image image = new Image(); + image.setType(bufimg.getColorModel().hasAlpha() ? Image.RGBA8888 : Image.RGB888); + image.setWidth(bufimg.getWidth()); + image.setHeight(bufimg.getHeight()); + image.setData(convertImage(bufimg, null)); + return image; + } + + /** + * Converts the supplied image (which must have been created with {@link + * #createCompatibleImage}) into a {@link ByteBuffer} that can be passed to {@link + * Image#setData}. + * + * @param target the results of a previous call to {@link #convertImage} that will be + * overwritten or null if a new buffer should be allocated. Of course a reused buffer must be + * used with the same image or one with the exact same configuration. + */ + public static ByteBuffer convertImage (BufferedImage image, ByteBuffer target) + { + DataBufferByte dbuf = (DataBufferByte)image.getRaster().getDataBuffer(); + byte[] data = dbuf.getData(); + if (target == null) { + target = ByteBuffer.allocateDirect(data.length); + target.order(ByteOrder.nativeOrder()); + } + target.clear(); + target.put(data); + target.flip(); + return target; + } + + /** + * Creates an image cache that will obtain its image data from the supplied resource manager. + */ + public ImageCache (ResourceManager rsrcmgr) + { + _rsrcmgr = rsrcmgr; + } + + /** + * See {@link #getImage(String,boolean)}. + */ + public Image getImage (String rsrcPath) + { + return getImage(rsrcPath, 1f, true); + } + + /** + * See {@link #getImage(String,float,boolean)}. + */ + public Image getImage (String rsrcPath, float scale) + { + return getImage(rsrcPath, scale, true); + } + + /** + * Loads up an image from the cache if possible or from the resource manager otherwise, in + * which case it is prepared for use by JME and OpenGL. Note: these images are cached + * separately from the {@link BImage} and {@link BufferedImage} caches. + * + * @param flip whether or not to convert the image from normal computer coordinates into OpenGL + * coordinates when loading. Note: this information is not cached, an image must + * always be requested as flipped or not flipped. + */ + public Image getImage (String rsrcPath, boolean flip) + { + return getImage(rsrcPath, 1f, flip); + } + + /** + * Loads up an image from the cache if possible or from the resource manager otherwise, in + * which case it is prepared for use by JME and OpenGL. Note: these images are cached + * separately from the {@link BImage} and {@link BufferedImage} caches. + * + * @param flip whether or not to convert the image from normal computer coordinates into OpenGL + * coordinates when loading. Note: this information is not cached, an image must + * always be requested as flipped or not flipped. + * @param scale a scale factor to apply to the image. As with the flip setting, the scale + * factor is not cached. + */ + public Image getImage (String rsrcPath, float scale, boolean flip) + { + // first check the cache + WeakReference iref = _imgcache.get(rsrcPath); + Image image; + if (iref != null && (image = iref.get()) != null) { + return image; + } + + // load the image data from the resource manager + BufferedImage bufimg; + File ifile = _rsrcmgr.getResourceFile(rsrcPath); + try { + bufimg = ImageIO.read(ifile); + } catch (Throwable t) { + log.log(Level.WARNING, "Unable to load image resource " + + "[path=" + ifile + "].", t); + // cope; return an error image of abitrary size + bufimg = ImageUtil.createErrorImage(64, 64); + } + + // create and cache a new JME image with the appropriate data + image = createImage(bufimg, scale, flip); + _imgcache.put(rsrcPath, new WeakReference(image)); + return image; + } + + /** + * Loads up an image from the cache if possible or from the resource manager otherwise, in + * which case it is prepared for use by BUI. Note: these images are cached separately + * from the {@link Image} and {@link BufferedImage} caches. + */ + public BImage getBImage (String rsrcPath) + { + return getBImage(rsrcPath, false); + } + + /** + * Loads up an image from the cache if possible or from the resource manager otherwise, in + * which case it is prepared for use by BUI. Note: these images are cached separately + * from the {@link Image} and {@link BufferedImage} caches. + * + * @param returnNull If set to true, will return a null instead of generating an error image on + * failure + */ + public BImage getBImage (String rsrcPath, boolean returnNull) + { + // first check the cache + WeakReference iref = _buicache.get(rsrcPath); + BImage image; + if (iref != null && (image = iref.get()) != null) { + return image; + } + + // load the image data from the resource manager + BufferedImage bufimg; + File ifile = _rsrcmgr.getResourceFile(rsrcPath); + try { + bufimg = ImageIO.read(ifile); + } catch (Throwable t) { + if (returnNull) { + return null; + } + log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); + // cope; return an error image of abitrary size + bufimg = ImageUtil.createErrorImage(64, 64); + } + + // create and cache a new BUI image with the appropriate data + image = new BImage(bufimg, true); + _buicache.put(rsrcPath, new WeakReference(image)); + return image; + } + + /** + * Loads up a silhouette image (in which all non-transparent pixels are set to black) from the + * cache if possible or from the resource manager otherwise, in which case it is prepared for + * use by BUI. + */ + public BImage getSilhouetteBImage (String rsrcPath, boolean returnNull) + { + // first check the cache + String key = "silhouette:" + rsrcPath; + WeakReference iref = _buicache.get(key); + BImage image; + if (iref != null && (image = iref.get()) != null) { + return image; + } + + // load the image data from the resource manager + BufferedImage bufimg, silimg; + File ifile = _rsrcmgr.getResourceFile(rsrcPath); + try { + bufimg = ImageIO.read(ifile); + + // now turn it into a silhouette + silimg = new BufferedImage(bufimg.getWidth(), bufimg.getHeight(), + BufferedImage.TYPE_4BYTE_ABGR); + new BandCombineOp(NON_ALPHA_TO_BROWN, null).filter( + bufimg.getRaster(), silimg.getRaster()); + + } catch (Throwable t) { + if (returnNull) { + return null; + } + log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); + // cope; return an error image of abitrary size + silimg = ImageUtil.createErrorImage(64, 64); + } + + // create and cache a new BUI image with the appropriate data + image = new BImage(silimg, true); + _buicache.put(key, new WeakReference(image)); + return image; + } + + /** + * Colorizes the image with supplied path (which must be an 8-bit colormapped image), then + * converts the colorized image into a form that JME can display. + */ + public BImage createColorizedBImage (String path, Colorization[] zations, boolean flip) + { + return new BImage(ImageUtil.recolorImage(getBufferedImage(path), zations), flip); + } + + /** + * Loads up a buffered image from the cache if possible or from the resource manager + * otherwise. Note: these images are cached separately from the {@link Image} and + * {@link BImage} caches. + */ + public BufferedImage getBufferedImage (String rsrcPath) + { + // first check the cache + WeakReference iref = _bufcache.get(rsrcPath); + BufferedImage image; + if (iref != null && (image = iref.get()) != null) { + return image; + } + + // load the image data from the resource manager + File ifile = _rsrcmgr.getResourceFile(rsrcPath); + try { + image = ImageIO.read(ifile); + } catch (Throwable t) { + log.log(Level.WARNING, "Unable to load image resource [path=" + ifile + "].", t); + // cope; return an error image of abitrary size + image = ImageUtil.createErrorImage(64, 64); + } + + _bufcache.put(rsrcPath, new WeakReference(image)); + return image; + } + + /** Rounds the supplied value up to a power of two. */ + protected static int nextPOT (int value) + { + return (Integer.bitCount(value) > 1) ? (Integer.highestOneBit(value) << 1) : value; + } + + /** Provides access to our image data. */ + protected ResourceManager _rsrcmgr; + + /** A cache of {@link Image} instances. */ + protected HashMap> _imgcache = + new HashMap>(); + + /** A cache of {@link BImage} instances. */ + protected HashMap> _buicache = + new HashMap>(); + + /** A cache of {@link BufferedImage} instances. */ + protected HashMap> _bufcache = + new HashMap>(); + + /** Used to create buffered images in a format compatible with OpenGL. */ + protected static ComponentColorModel GL_ALPHA_MODEL = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8, 8 }, + true, false, ComponentColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE); + + /** Used to create buffered images in a format compatible with OpenGL. */ + protected static ComponentColorModel GL_OPAQUE_MODEL = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8, 0 }, + false, false, ComponentColorModel.OPAQUE, DataBuffer.TYPE_BYTE); + + /** An operation that converts all channels except alpha to brown. */ + protected static final float[][] NON_ALPHA_TO_BROWN = { + { 0, 0, 0, 0x66/255f }, + { 0, 0, 0, 0x57/255f }, + { 0, 0, 0, 0x34/255f }, + { 0, 0, 0, 0.5f } + }; +}