Converted the fringing code into a more generic form that works directly

on buffered images and is not tied to the tile system or the image
manager. This could probably even move into the media.tile package.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3629 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2005-06-28 18:28:22 +00:00
parent 7428f885cf
commit 613dd3633e
4 changed files with 722 additions and 0 deletions
@@ -0,0 +1,160 @@
//
// $Id: FringeConfiguration.java 3403 2005-03-14 23:58:02Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 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
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.jme.tile;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import com.samskivert.util.StringUtil;
import com.threerings.jme.Log;
/**
* Used to manage data about which tiles fringe on which others and how
* they fringe.
*/
public class FringeConfiguration implements Serializable
{
/** Contains information on a type of tile and all of the fringe
* records associated with it. */
public static class TileRecord implements Serializable
{
/** The type of tile to which this applies. */
public String type;
/** The fringe priority of this type. */
public int priority;
/** A list of the fringe records that can be used for fringing. */
public ArrayList fringes = new ArrayList();
/** Used when parsing from an XML definition. */
public void addFringe (FringeRecord record)
{
if (record.isValid()) {
fringes.add(record);
} else {
Log.warning("Not adding invalid fringe record [tile=" + this +
", fringe=" + record + "].");
}
}
/** Did everything parse well? */
public boolean isValid ()
{
return ((type != null) && (priority > 0));
}
/** Generates a string representation of this instance. */
public String toString ()
{
return "[type=" + type + ", priority=" + priority +
", fringes=" + StringUtil.toString(fringes) + "]";
}
/** Increase this value when object's serialized state is impacted
* by a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 1;
}
/** Used to parse the type fringe definitions. */
public static class FringeRecord implements Serializable
{
/** The name of the fringe tileset image. */
public String name;
/** Is this a mask? */
public boolean mask;
/** Did everything parse well? */
public boolean isValid ()
{
return (name != null);
}
/** Generates a string representation of this instance. */
public String toString ()
{
return "[name=" + name + ", mask=" + mask + "]";
}
/** Increase this value when object's serialized state is impacted
* by a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 1;
}
/**
* Adds a parsed fringe record to this instance. This is used when
* parsing the fringe records from xml.
*/
public void addTileRecord (TileRecord record)
{
if (!record.isValid()) {
_trecs.put(record.type, record);
} else {
Log.warning("Refusing to add invalid tile record " +
"[tile=" + record + "].");
}
}
/**
* If the first type fringes upon the second, return the fringe
* priority of the first type, otherwise return -1.
*/
public int fringesOn (String fringer, String fringed)
{
TileRecord f1 = (TileRecord)_trecs.get(fringer);
// we better have a fringe record for the fringer
if (null != f1) {
// it had better have some types defined
if (f1.fringes.size() > 0) {
TileRecord f2 = (TileRecord)_trecs.get(fringed);
// and we only fringe if fringed doesn't have a fringe
// record or has a lower priority
if ((null == f2) || (f1.priority > f2.priority)) {
return f1.priority;
}
}
}
return -1;
}
/**
* Get a random FringeRecord from amongst the ones listed for the
* specified type.
*/
public FringeRecord getFringe (String type, int hashValue)
{
TileRecord t = (TileRecord)_trecs.get(type);
return (FringeRecord)t.fringes.get(hashValue % t.fringes.size());
}
/** The mapping from tile type to tile record. */
protected HashMap _trecs = new HashMap();
/** Increase this value when object's serialized state is impacted by
* a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 1;
}
@@ -0,0 +1,402 @@
//
// $Id: TileFringer.java 3177 2004-10-28 17:49:02Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 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
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.jme.tile;
import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import com.samskivert.util.QuickSort;
import com.threerings.media.image.ImageUtil;
import com.threerings.media.tile.TileUtil;
import com.threerings.jme.Log;
/**
* Computes fringe tile images according to the rules in an associated
* fringe configuration.
*/
public class TileFringer
{
public static interface TileSource
{
/** Returns the type of tile at the specified coordinates or -1 if
* there is no tile at this coordinate. */
public String getTileType (int x, int y);
/** Returns the tile type to use when a coordinate has no tile. */
public String getDefaultType ();
}
public static interface ImageSource
{
/** Creates a blank image into which various fringe images will be
* composited. */
public BufferedImage createImage (int width, int height);
/** Returns the source image for a tile of the specified type.
* This can be randomly selected and change from call to call. */
public BufferedImage getTileSource (String type);
/** Returns the named fringe source image (one long strip). */
public BufferedImage getFringeSource (String name);
}
/**
* Creates a fringer that will fringe according to the rules in the
* supplied configuration.
*/
public TileFringer (FringeConfiguration config, ImageSource isrc)
{
_config = config;
_isrc = isrc;
}
/**
* Compute and return the fringe tile to be inserted at the specified
* location.
*
* @param masks used to cache intermediate images of tiles cut out
* using a fringe mask.
*/
public BufferedImage getFringeTile (
TileSource tiles, int col, int row, HashMap masks)
{
// get the type of the tile we are considering
String baseType = tiles.getTileType(col, row);
// start with an empty fringer list
FringerRec fringers = null;
// walk through our influence tiles
for (int y = row - 1, maxy = row + 2; y < maxy; y++) {
for (int x = col - 1, maxx = col + 2; x < maxx; x++) {
// we sensibly do not consider ourselves
if ((x == col) && (y == row)) {
continue;
}
// determine the type of our fringing neighbor
String fringerType = tiles.getTileType(x, y);
if (fringerType == null) {
fringerType = tiles.getDefaultType();
}
// determine if it fringes on our tile
int pri = _config.fringesOn(fringerType, baseType);
if (pri == -1) {
continue;
}
FringerRec fringer = (fringers == null) ?
null : fringers.find(fringerType);
if (fringer == null) {
fringers = new FringerRec(fringerType, pri, fringers);
}
// now turn on the appropriate fringebits
fringer.bits |= FLAGMATRIX[y - row + 1][x - col + 1];
}
}
// if nothing fringed, we're done
if (fringers == null) {
return null;
}
// otherwise compose a fringe tile from the specified fringes
return composeFringeTile(
fringers.toArray(), masks, TileUtil.getTileHash(col, row));
}
/**
* Compose a fringe tile out of the various fringe images needed.
*/
protected BufferedImage composeFringeTile (
FringerRec[] fringers, HashMap masks, int hashValue)
{
// sort the array so that higher priority fringers get drawn first
QuickSort.sort(fringers);
BufferedImage ftimg = null;
for (int ii = 0; ii < fringers.length; ii++) {
int[] indexes = getFringeIndexes(fringers[ii].bits);
for (int jj = 0; jj < indexes.length; jj++) {
ftimg = getTileImage(ftimg, fringers[ii].fringerType,
indexes[jj], masks, hashValue);
}
}
return ftimg;
}
/**
* Retrieve or compose an image for the specified fringe.
*/
protected BufferedImage getTileImage (
BufferedImage ftimg, String fringerType, int index,
HashMap masks, int hashValue)
{
FringeConfiguration.FringeRecord frec =
_config.getFringe(fringerType, hashValue);
BufferedImage fsimg = (frec == null) ? null :
_isrc.getFringeSource(frec.name);
if (fsimg == null) {
Log.warning("Missing fringe source image [type=" + fringerType +
", hash=" + hashValue + ", frec=" + frec + "].");
return ftimg;
}
if (!frec.mask) {
// this is a non-mask image so just use the data from the
// fringe source image directly
BufferedImage stamp = getSubimage(fsimg, index);
return stampTileImage(stamp, ftimg, stamp.getWidth(),
stamp.getHeight());
}
// otherwise, it's a mask; look for it in the cache
String maskkey = fringerType + ":" + frec.name + ":" + index;
BufferedImage mimg = (BufferedImage)masks.get(maskkey);
if (mimg == null) {
BufferedImage fsrc = getSubimage(fsimg, index);
BufferedImage bsrc = _isrc.getTileSource(fringerType);
mimg = ImageUtil.composeMaskedImage(null, fsrc, bsrc); // TODO
masks.put(maskkey, mimg);
}
return stampTileImage(mimg, ftimg, mimg.getWidth(null),
mimg.getHeight(null));
}
/**
* Returns the <code>index</code>th tile image from the supplied
* source image. The source image is assumed to be a single strip of
* tile images, each with equal width and height.
*/
protected BufferedImage getSubimage (BufferedImage source, int index)
{
int size = source.getHeight(), x = size * index;
return source.getSubimage(x, 0, size, size);
}
/** Helper function for {@link #getTileImage}. */
protected BufferedImage stampTileImage (
BufferedImage stamp, BufferedImage ftimg, int width, int height)
{
// create the target image if necessary
if (ftimg == null) {
ftimg = _isrc.createImage(width, height);
}
Graphics2D gfx = (Graphics2D)ftimg.getGraphics();
try {
gfx.drawImage(stamp, 0, 0, null);
} finally {
gfx.dispose();
}
return ftimg;
}
/**
* Get the fringe index specified by the fringebits. If no index
* is available, try breaking down the bits into contiguous regions of
* bits and look for indexes for those.
*/
protected int[] getFringeIndexes (int bits)
{
int index = BITS_TO_INDEX[bits];
if (index != -1) {
int[] ret = new int[1];
ret[0] = index;
return ret;
}
// otherwise, split the bits into contiguous components
// look for a zero and start our first split
int start = 0;
while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) {
start++;
}
if (start == NUM_FRINGEBITS) {
// we never found an empty fringebit, and since index (above)
// was already -1, we have no fringe tile for these bits.. sad.
return new int[0];
}
ArrayList indexes = new ArrayList();
int weebits = 0;
for (int ii=(start + 1) % NUM_FRINGEBITS; ii != start;
ii = (ii + 1) % NUM_FRINGEBITS) {
if (((1 << ii) & bits) != 0) {
weebits |= (1 << ii);
} else if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.add(new Integer(index));
}
weebits = 0;
}
}
if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.add(new Integer(index));
}
}
int[] ret = new int[indexes.size()];
for (int ii=0; ii < ret.length; ii++) {
ret[ii] = ((Integer) indexes.get(ii)).intValue();
}
return ret;
}
/** A record for holding information about a particular fringe as
* we're computing what it will look like. */
protected static class FringerRec implements Comparable
{
public String fringerType;
public int priority;
public int bits;
public FringerRec next;
public FringerRec (String type, int pri, FringerRec next) {
fringerType = type;
priority = pri;
this.next = next;
}
public FringerRec find (String type)
{
if (fringerType.equals(type)) {
return this;
} else if (next != null) {
return next.find(type);
} else {
return null;
}
}
public FringerRec[] toArray ()
{
return toArray(0);
}
public int compareTo (Object o) {
return priority - ((FringerRec) o).priority;
}
public String toString () {
return "[type=" + fringerType + ", pri=" + priority +
", bits=" + Integer.toString(bits, 16) + "]";
}
protected FringerRec[] toArray (int index)
{
FringerRec[] array;
if (next == null) {
array = new FringerRec[index+1];
} else {
array = next.toArray(index+1);
}
array[index] = this;
return array;
}
}
// fringe bits: see docs/miso/fringebits.png
protected static final int NORTH = 1 << 0;
protected static final int NORTHEAST = 1 << 1;
protected static final int EAST = 1 << 2;
protected static final int SOUTHEAST = 1 << 3;
protected static final int SOUTH = 1 << 4;
protected static final int SOUTHWEST = 1 << 5;
protected static final int WEST = 1 << 6;
protected static final int NORTHWEST = 1 << 7;
protected static final int NUM_FRINGEBITS = 8;
/** A matrix mapping adjacent tiles to which fringe bits they affect.
* (x and y are offset by +1, since we can't have -1 as an array
* index) again, see docs/miso/fringebits.png */
protected static final int[][] FLAGMATRIX = {
{ NORTHEAST, (NORTHEAST | EAST | SOUTHEAST), SOUTHEAST },
{ (NORTHWEST | NORTH | NORTHEAST), 0, (SOUTHEAST | SOUTH | SOUTHWEST) },
{ NORTHWEST, (NORTHWEST | WEST | SOUTHWEST), SOUTHWEST }
};
/** The fringe tiles we use. These are the 17 possible tiles made up
* of continuous fringebits sections. Huh? see
* docs/miso/fringebits.png */
protected static final int[] FRINGETILES = {
SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST,
NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST,
NORTHEAST,
NORTHWEST | NORTH | NORTHEAST,
NORTHWEST,
SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST,
NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
// all the directions!
NORTH | NORTHEAST | EAST | SOUTHEAST | SOUTH | SOUTHWEST |
WEST | NORTHWEST
};
/** A reverse map of the {@link #FRINGETILES} array, for quickly
* looking up which tile we want. */
protected static final int[] BITS_TO_INDEX;
// Construct the BITS_TO_INDEX array.
static {
int num = (1 << NUM_FRINGEBITS);
BITS_TO_INDEX = new int[num];
// first clear everything to -1 (meaning there is no tile defined)
for (int ii=0; ii < num; ii++) {
BITS_TO_INDEX[ii] = -1;
}
// then fill in with the defined tiles.
for (int ii=0; ii < FRINGETILES.length; ii++) {
BITS_TO_INDEX[FRINGETILES[ii]] = ii;
}
}
protected ImageSource _isrc;
protected FringeConfiguration _config;
}
@@ -0,0 +1,76 @@
//
// $Id: CompileFringeConfigurationTask.java 3099 2004-08-27 02:21:06Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 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
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.jme.tile.tools;
import java.io.File;
import java.io.Serializable;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import com.samskivert.io.PersistenceException;
import com.threerings.util.CompiledConfig;
import com.threerings.jme.tile.tools.xml.FringeConfigurationParser;
/**
* Compiles a fringe configuration from XML format into binary format.
*/
public class CompileFringeConfigurationTask extends Task
{
public void setConfig (File config)
{
_config = config;
}
public void setTarget (File target)
{
_target = target;
}
public void execute () throws BuildException
{
// make sure the config file exists
if (!_config.exists()) {
throw new BuildException(
"Fringe configuration file not found [path=" + _config + "].");
}
FringeConfigurationParser parser = new FringeConfigurationParser();
Serializable config;
try {
config = parser.parseConfig(_config);
} catch (Exception e) {
throw new BuildException("Failure parsing fringe config", e);
}
try {
// and write it on out
CompiledConfig.saveConfig(_target, config);
} catch (Exception e) {
throw new BuildException("Failure writing serialized config", e);
}
}
protected File _config;
protected File _target;
}
@@ -0,0 +1,84 @@
//
// $Id: FringeConfigurationParser.java 3254 2004-11-30 20:03:47Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 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
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.jme.tile.tools.xml;
import java.io.Serializable;
import org.apache.commons.digester.Digester;
import com.samskivert.util.StringUtil;
import com.samskivert.xml.SetPropertyFieldsRule;
import com.threerings.tools.xml.CompiledConfigParser;
import com.threerings.jme.Log;
import com.threerings.jme.tile.FringeConfiguration.TileRecord;
import com.threerings.jme.tile.FringeConfiguration.FringeRecord;
import com.threerings.jme.tile.FringeConfiguration;
/**
* Parses fringe config definitions, which look like so (with angle
* brackets instead of square):
* <pre>
* [fringe]
* [tile type="water" priority="100"]
* [fringe name="water_fringe_1"/]
* [fringe name="water_fringe_2"/]
* [fringe name="water_fringe_3"/]
* [/tile]
* [tile type="dirt" priority="10"]
* [fringe name="dirt_fringe_1" mask="true"/]
* [/tile]
* [tile type="cobble" priority="100"]
* [fringe name="cobble_fringe_1" mask="false"/]
* [/tile]
* [/fringe]
* </pre>
*/
public class FringeConfigurationParser extends CompiledConfigParser
{
// documentation inherited
protected Serializable createConfigObject ()
{
return new FringeConfiguration();
}
// documentation inherited
protected void addRules (Digester digest)
{
// configure top-level constraints
String prefix = "fringe";
digest.addRule(prefix, new SetPropertyFieldsRule());
// create and configure fringe config instances
prefix += "/tile";
digest.addObjectCreate(prefix, TileRecord.class.getName());
digest.addRule(prefix, new SetPropertyFieldsRule());
digest.addSetNext(prefix, "addTileRecord");
// create the fringe type records in each tile record
prefix += "/fringe";
digest.addObjectCreate(prefix, FringeRecord.class.getName());
digest.addRule(prefix, new SetPropertyFieldsRule());
digest.addSetNext(prefix, "addFringe");
}
}