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 }
+ };
+}