/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter;
import org.pepsoft.util.ColourUtils;
import org.pepsoft.util.IconUtils;
import org.pepsoft.worldpainter.biomeschemes.CustomBiomeManager;
import org.pepsoft.worldpainter.layers.*;
import org.pepsoft.worldpainter.layers.renderers.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.*;
import java.util.List;
import static org.pepsoft.minecraft.Constants.*;
import static org.pepsoft.worldpainter.Constants.*;
/**
* This class is <strong>not</strong> thread-safe!
*
* @author pepijn
*/
public final class TileRenderer {
public TileRenderer(TileProvider tileProvider, ColourScheme colourScheme, BiomeScheme biomeScheme, CustomBiomeManager customBiomeManager, int zoom) {
biomeRenderer = new BiomeRenderer(biomeScheme, customBiomeManager);
setTileProvider(tileProvider);
if ((tileProvider instanceof Dimension) && (((Dimension) tileProvider).getWorld() != null)) {
Dimension oppositeDimension = null;
switch (((Dimension) tileProvider).getDim()) {
case DIM_NORMAL:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_NORMAL_CEILING);
break;
case DIM_NORMAL_CEILING:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_NORMAL);
break;
case DIM_NETHER:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_NETHER_CEILING);
break;
case DIM_NETHER_CEILING:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_NETHER);
break;
case DIM_END:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_END_CEILING);
break;
case DIM_END_CEILING:
oppositeDimension = ((Dimension) tileProvider).getWorld().getDimension(DIM_END);
break;
}
if (oppositeDimension != null) {
setOppositeTileProvider(oppositeDimension);
}
}
setColourScheme(colourScheme);
this.zoom = zoom;
bufferedImage = new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB);
renderBuffer = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
}
public final TileProvider getTileProvider() {
return tileProvider;
}
public final void setTileProvider(TileProvider tileProvider) {
this.tileProvider = tileProvider;
if (tileProvider instanceof Dimension) {
seed = ((Dimension) tileProvider).getSeed();
bottomless = ((Dimension) tileProvider).isBottomless();
if (oppositeTileProvider instanceof Dimension) {
oppositesDelta = Math.abs(((Dimension) tileProvider).getCeilingHeight() - ((Dimension) oppositeTileProvider).getCeilingHeight());
} else {
noOpposites = true;
}
} else {
noOpposites = true;
}
}
public TileProvider getOppositeTileProvider() {
return oppositeTileProvider;
}
public void setOppositeTileProvider(TileProvider oppositeTileProvider) {
this.oppositeTileProvider = oppositeTileProvider;
if ((oppositeTileProvider instanceof Dimension) && (tileProvider instanceof Dimension)) {
oppositesDelta = Math.abs(((Dimension) tileProvider).getCeilingHeight() - ((Dimension) oppositeTileProvider).getCeilingHeight());
} else {
noOpposites = true;
}
}
public final ColourScheme getColourScheme() {
return colourScheme;
}
public final void setColourScheme(ColourScheme colourScheme) {
this.colourScheme = colourScheme;
waterColour = colourScheme.getColour(BLK_WATER);
lavaColour = colourScheme.getColour(BLK_LAVA);
bedrockColour = colourScheme.getColour(BLK_BEDROCK);
}
public void addHiddenLayers(Collection<Layer> hiddenLayers) {
this.hiddenLayers.addAll(hiddenLayers);
}
public void addHiddenLayer(Layer hiddenLayer) {
hiddenLayers.add(hiddenLayer);
}
public void setHiddenLayers(Set<Layer> hiddenLayers) {
this.hiddenLayers.clear();
// The FloodWithLava layer should *always* remain hidden
hiddenLayers.add(FloodWithLava.INSTANCE);
if (! hiddenLayers.isEmpty()) {
this.hiddenLayers.addAll(hiddenLayers);
}
}
public void removeHiddenLayer(Layer hiddenLayer) {
// The FloodWithLava layer should *always* remain hidden
if (! hiddenLayer.equals(FloodWithLava.INSTANCE)) {
hiddenLayers.remove(hiddenLayer);
}
}
public Set<Layer> getHiddenLayers() {
return Collections.unmodifiableSet(hiddenLayers);
}
public Tile getTile() {
return tile;
}
public void setTile(Tile tile) {
this.tile = tile;
for (int x = 0; x < TILE_SIZE; x++) {
for (int y = 0; y < TILE_SIZE; y++) {
final float height = tile.getHeight(x, y);
floatHeightCache[x | (y << TILE_SIZE_BITS)] = height;
intHeightCache[x | (y << TILE_SIZE_BITS)] = (int) (height + 0.5f);
intFluidHeightCache[x | (y << TILE_SIZE_BITS)] = tile.getWaterLevel(x, y);
}
}
// Determine which coordinates, if any, have heights which would
// intersect with the opposite tile, if any
if (oppositeTileProvider != null) {
if (! noOpposites) {
Arrays.fill(oppositesOverlap, false);
}
noOpposites = true;
final int maxHeight = tile.getMaxHeight();
Tile oppositeTile = oppositeTileProvider.getTile(tile.getX(), tile.getY());
if (oppositeTile != null) {
for (int x = 0; x < TILE_SIZE; x++) {
for (int y = 0; y < TILE_SIZE; y++) {
if ((oppositeTile.getIntHeight(x, y) + intHeightCache[x | (y << TILE_SIZE_BITS)] + oppositesDelta) >= maxHeight) {
oppositesOverlap[x | (y << TILE_SIZE_BITS)] = true;
noOpposites = false;
}
}
}
}
}
}
public int getZoom() {
return zoom;
}
public boolean isContourLines() {
return contourLines;
}
public void setContourLines(boolean contourLines) {
this.contourLines = contourLines;
}
public int getContourSeparation() {
return contourSeparation;
}
public void setContourSeparation(int contourSeparation) {
this.contourSeparation = contourSeparation;
}
public void renderTile(Image image, int dx, int dy) {
// TODO this deadlocks background painting. Find out why:
// synchronized (tile) {
final List<Layer> layerList = new ArrayList<>(tile.getLayers());
if (! layerList.contains(Biome.INSTANCE)) {
layerList.add(Biome.INSTANCE);
}
layerList.removeAll(hiddenLayers);
final boolean hideTerrain = hiddenLayers.contains(TERRAIN_AS_LAYER);
final boolean hideFluids = hiddenLayers.contains(FLUIDS_AS_LAYER);
final boolean _void = layerList.contains(org.pepsoft.worldpainter.layers.Void.INSTANCE), notAllChunksPresent = layerList.contains(NotPresent.INSTANCE);
final Layer[] layers = layerList.toArray(new Layer[layerList.size()]);
final LayerRenderer[] renderers = new LayerRenderer[layers.length];
// boolean renderBiomes = false;
for (int i = 0; i < layers.length; i++) {
if (layers[i] instanceof Biome) {
renderers[i] = biomeRenderer;
// renderBiomes = true;
} else {
renderers[i] = layers[i].getRenderer();
}
if (renderers[i] instanceof ColourSchemeRenderer) {
((ColourSchemeRenderer) renderers[i]).setColourScheme(colourScheme);
}
if ((renderers[i] instanceof DimensionAwareRenderer) && (tileProvider instanceof Dimension)) {
((DimensionAwareRenderer) renderers[i]).setDimension((Dimension) tileProvider);
}
}
// LayerRenderer[] voidRenderers = null;
// Layer[] voidLayers = null;
// if (_void) {
// if (renderBiomes) {
// voidLayers = new Layer[] {org.pepsoft.worldpainter.layers.Void.INSTANCE, Biome.INSTANCE};
// voidRenderers = new LayerRenderer[] {org.pepsoft.worldpainter.layers.Void.INSTANCE.getRenderer(), biomeRenderer};
// } else {
// voidLayers = new Layer[] {org.pepsoft.worldpainter.layers.Void.INSTANCE};
// voidRenderers = new LayerRenderer[] {org.pepsoft.worldpainter.layers.Void.INSTANCE.getRenderer()};
// }
// }
Arrays.fill(renderBuffer, 0);
final int tileX = tile.getX() * TILE_SIZE, tileY = tile.getY() * TILE_SIZE;
final int scale = 1 << -zoom;
if (zoom == 0) {
for (int x = 0; x < TILE_SIZE; x++) {
for (int y = 0; y < TILE_SIZE; y++) {
if (notAllChunksPresent && (tile.getBitLayerValue(NotPresent.INSTANCE, x, y))) {
// renderBuffer[x | (y << TILE_SIZE_BITS)] = 0x00000000;
} else if ((! noOpposites) && oppositesOverlap[x | (y << TILE_SIZE_BITS)] && CEILING_PATTERN[x & 0x7][y & 0x7]) {
renderBuffer[x | (y << TILE_SIZE_BITS)] = 0xff000000;
} else if (_void && tile.getBitLayerValue(org.pepsoft.worldpainter.layers.Void.INSTANCE, x, y)) {
// renderBuffer[x | (y << TILE_SIZE_BITS)] = 0xff000000 | getPixelColour(tileX, tileY, x, y, voidLayers, voidRenderers, false);
} else {
int colour = getPixelColour(tileX, tileY, x, y, layers, renderers, contourLines, hideTerrain, hideFluids);
colour = ColourUtils.multiply(colour, getTerrainBrightenAmount());
final int offset = x + y * TILE_SIZE;
if (intFluidHeightCache[offset] > intHeightCache[offset]) {
colour = ColourUtils.multiply(colour, getFluidBrightenAmount());
}
renderBuffer[x | (y << TILE_SIZE_BITS)] = 0xff000000 | colour;
}
}
}
Graphics2D g2 = (Graphics2D) image.getGraphics();
try {
g2.setComposite(AlphaComposite.Src);
g2.drawImage(bufferedImage, dx, dy, null);
} finally {
g2.dispose();
}
} else {
final int tileSize = TILE_SIZE / scale;
for (int x = 0; x < TILE_SIZE; x += scale) {
for (int y = 0; y < TILE_SIZE; y += scale) {
if (notAllChunksPresent && (tile.getBitLayerValue(NotPresent.INSTANCE, x, y))) {
// renderBuffer[x / scale + y * tileSize] = 0x00000000;
} else if ((! noOpposites) && oppositesOverlap[x | (y << TILE_SIZE_BITS)]) {
renderBuffer[x / scale + y * tileSize] = 0xff000000;
} else if (_void && tile.getBitLayerValue(org.pepsoft.worldpainter.layers.Void.INSTANCE, x, y)) {
// renderBuffer[x / scale + y * tileSize] = 0xff000000 | getPixelColour(tileX, tileY, x, y, voidLayers, voidRenderers, false);
} else {
int colour = getPixelColour(tileX, tileY, x, y, layers, renderers, contourLines, hideTerrain, hideFluids);
colour = ColourUtils.multiply(colour, getTerrainBrightenAmount());
final int offset = x + y * TILE_SIZE;
if (intFluidHeightCache[offset] > intHeightCache[offset]) {
colour = ColourUtils.multiply(colour, getFluidBrightenAmount());
}
renderBuffer[x / scale + y * tileSize] = 0xff000000 | colour;
}
}
}
Graphics2D g2 = (Graphics2D) image.getGraphics();
try {
g2.setComposite(AlphaComposite.Src);
g2.drawImage(bufferedImage, dx, dy, dx + tileSize, dy + tileSize, 0, 0, tileSize, tileSize, null);
} finally {
g2.dispose();
}
}
// }
}
public static TileRenderer forWorld(World2 world, int dim, ColourScheme colourScheme, BiomeScheme biomeScheme, CustomBiomeManager customBiomeManager, int zoom) {
Dimension dimension = world.getDimension(dim);
return new TileRenderer(dimension, colourScheme, biomeScheme, customBiomeManager, zoom);
}
/**
* Determine the brighten amount. This method assumes that the
* {@link #deltas} array has been filled by a previous call to
* {@link #getPixelColour(int, int, int, int, Layer[], LayerRenderer[], boolean, boolean, boolean)}.
*
* @return The amount by which to brighten the pixel for the specified
* block, out of 256; values below 256 darkening the pixel; values above
* brightening it and 256 resulting in no change.
*/
private int getTerrainBrightenAmount() {
return getBrightenAmount(deltas);
}
/**
* Determine the brighten amount for fluid. This method assumes that the
* {@link #deltas} array has been filled by a previous call to
* {@link #getPixelColour(int, int, int, int, Layer[], LayerRenderer[], boolean, boolean, boolean)}.
*
* @return The amount by which to brighten the pixel for the specified
* block, out of 256; values below 256 darkening the pixel; values above
* brightening it and 256 resulting in no change.
*/
private int getFluidBrightenAmount() {
return getBrightenAmount(fluidDeltas);
}
private int getBrightenAmount(int[][] deltas) {
switch (lightOrigin) {
case NORTHWEST:
return Math.max(0, ((deltas[2][1] - deltas[0][1] + deltas[1][2] - deltas[1][0]) << 5) + 256);
case NORTHEAST:
return Math.max(0, ((deltas[0][1] - deltas[2][1] + deltas[1][2] - deltas[1][0]) << 5) + 256);
case SOUTHEAST:
return Math.max(0, ((deltas[0][1] - deltas[2][1] + deltas[1][0] - deltas[1][2]) << 5) + 256);
case SOUTHWEST:
return Math.max(0, ((deltas[2][1] - deltas[0][1] + deltas[1][0] - deltas[1][2]) << 5) + 256);
case ABOVE:
return 256;
default:
throw new InternalError();
}
}
public LightOrigin getLightOrigin() {
return lightOrigin;
}
public void setLightOrigin(LightOrigin lightOrigin) {
if (lightOrigin == null) {
throw new NullPointerException();
}
this.lightOrigin = lightOrigin;
}
private int getPixelColour(int tileX, int tileY, int x, int y, Layer[] layers, LayerRenderer[] renderers, boolean contourLines, boolean hideTerrain, boolean hideFluids) {
final int offset = x + y * TILE_SIZE;
final int intHeight = intHeightCache[offset];
heights[1][0] = getNeighbourHeight(x, y, 0, -1);
deltas [1][0] = heights[1][0] - intHeight;
heights[0][1] = getNeighbourHeight(x, y, -1, 0);
deltas [0][1] = heights[0][1] - intHeight;
heights[2][1] = getNeighbourHeight(x, y, 1, 0);
deltas [2][1] = heights[2][1] - intHeight;
heights[1][2] = getNeighbourHeight(x, y, 0, 1);
deltas [1][2] = heights[1][2] - intHeight;
if (contourLines && ((intHeight % contourSeparation) == 0)
&& ((deltas[0][1] < 0)
|| (deltas[2][1] < 0)
|| (deltas[1][0] < 0)
|| (deltas[1][2] < 0))) {
return BLACK;
}
final int waterLevel = tile.getWaterLevel(x, y);
fluidHeights[1][0] = getNeighbourFluidHeight(x, y, 0, -1, waterLevel);
fluidDeltas [1][0] = fluidHeights[1][0] - waterLevel;
fluidHeights[0][1] = getNeighbourFluidHeight(x, y, -1, 0, waterLevel);
fluidDeltas [0][1] = fluidHeights[0][1] - waterLevel;
fluidHeights[2][1] = getNeighbourFluidHeight(x, y, 1, 0, waterLevel);
fluidDeltas [2][1] = fluidHeights[2][1] - waterLevel;
fluidHeights[1][2] = getNeighbourFluidHeight(x, y, 0, 1, waterLevel);
fluidDeltas [1][2] = fluidHeights[1][2] - waterLevel;
int colour;
final int worldX = tileX | x, worldY = tileY | y;
if ((! hideFluids) && (waterLevel > intHeight)) {
if (tile.getBitLayerValue(FloodWithLava.INSTANCE, x, y)) {
colour = lavaColour;
} else {
colour = waterColour;
}
} else if (! hideTerrain) {
final float height = floatHeightCache[offset];
colour = ((! bottomless) && (intHeight == 0)) ? bedrockColour : tile.getTerrain(x, y).getColour(seed, worldX, worldY, height, intHeight, colourScheme);
} else {
colour = LIGHT_GREY;
}
for (int i = 0; i < layers.length; i++) {
final Layer layer = layers[i];
switch (layer.getDataSize()) {
case BIT:
case BIT_PER_CHUNK:
if (hideFluids && (layer instanceof Frost) && (waterLevel > intHeightCache[offset])) {
continue;
}
boolean bitLayerValue = tile.getBitLayerValue(layer, x, y);
if (bitLayerValue) {
final BitLayerRenderer renderer = (BitLayerRenderer) renderers[i];
if (renderer == null) {
logger.error("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
if (! missingRendererReportedFor.contains(layer)) {
missingRendererReportedFor.add(layer);
throw new IllegalStateException("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
} else {
continue;
}
}
colour = renderer.getPixelColour(worldX, worldY, colour, true);
}
break;
case NIBBLE:
int layerValue = tile.getLayerValue(layer, x, y);
if (layerValue > 0) {
final NibbleLayerRenderer renderer = (NibbleLayerRenderer) renderers[i];
if (renderer == null) {
logger.error("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
if (! missingRendererReportedFor.contains(layer)) {
missingRendererReportedFor.add(layer);
throw new IllegalStateException("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
} else {
continue;
}
}
colour = renderer.getPixelColour(worldX, worldY, colour, layerValue);
}
break;
case BYTE:
final ByteLayerRenderer renderer = (ByteLayerRenderer) renderers[i];
if (renderer == null) {
logger.error("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
if (! missingRendererReportedFor.contains(layer)) {
missingRendererReportedFor.add(layer);
throw new IllegalStateException("Missing renderer for layer " + layer + " (type: " + layer.getClass().getSimpleName() + ")");
} else {
continue;
}
}
colour = renderer.getPixelColour(worldX, worldY, colour, tile.getLayerValue(layer, x, y));
break;
default:
throw new UnsupportedOperationException("Don't know how to render " + layer.getClass().getSimpleName());
}
}
return colour;
}
private int getNeighbourHeight(int x, int y, int dx, int dy) {
x = x + dx;
y = y + dy;
if ((x >= 0) && (x < TILE_SIZE) && (y >= 0) && (y < TILE_SIZE)) {
return intHeightCache[x + y * TILE_SIZE];
} else {
int tileDX = 0, tileDY = 0;
if (x < 0) {
tileDX = -1;
x += TILE_SIZE;
} else if (x >= TILE_SIZE) {
tileDX = 1;
x -= TILE_SIZE;
}
if (y < 0) {
tileDY = -1;
y += TILE_SIZE;
} else if (y >= TILE_SIZE) {
tileDY = 1;
y -= TILE_SIZE;
}
Tile neighborTile = tileProvider.getTile(tile.getX() + tileDX, tile.getY() + tileDY);
return (neighborTile != null) ? neighborTile.getIntHeight(x, y) : 62;
}
}
private int getNeighbourFluidHeight(int x, int y, int dx, int dy, int defaultHeight) {
x = x + dx;
y = y + dy;
if ((x >= 0) && (x < TILE_SIZE) && (y >= 0) && (y < TILE_SIZE)) {
int offset = x + y * TILE_SIZE;
return (intFluidHeightCache[offset] > intHeightCache[offset]) ? intFluidHeightCache[offset] : defaultHeight;
} else {
int tileDX = 0, tileDY = 0;
if (x < 0) {
tileDX = -1;
x += TILE_SIZE;
} else if (x >= TILE_SIZE) {
tileDX = 1;
x -= TILE_SIZE;
}
if (y < 0) {
tileDY = -1;
y += TILE_SIZE;
} else if (y >= TILE_SIZE) {
tileDY = 1;
y -= TILE_SIZE;
}
Tile neighborTile = tileProvider.getTile(tile.getX() + tileDX, tile.getY() + tileDY);
if (neighborTile != null) {
int waterLevel = neighborTile.getWaterLevel(x, y);
return (waterLevel > neighborTile.getIntHeight(x, y)) ? waterLevel : defaultHeight;
} else {
return defaultHeight;
}
}
}
private final BiomeRenderer biomeRenderer;
private final Set<Layer> hiddenLayers = new HashSet<>(Arrays.asList(FloodWithLava.INSTANCE));
private final int[] intHeightCache = new int[TILE_SIZE * TILE_SIZE], intFluidHeightCache = new int[TILE_SIZE * TILE_SIZE];
private final float[] floatHeightCache = new float[TILE_SIZE * TILE_SIZE];
private final BufferedImage bufferedImage;
private final int[] renderBuffer;
private final int[][] heights = new int[3][3], deltas = new int[3][3], fluidHeights = new int[3][3], fluidDeltas = new int[3][3];
private final Set<Layer> missingRendererReportedFor = new HashSet<>(); // TODO remove when no longer necessary!
private final boolean[] oppositesOverlap = new boolean[TILE_SIZE * TILE_SIZE];
private final int zoom;
private TileProvider tileProvider, oppositeTileProvider;
private long seed;
private Tile tile;
private ColourScheme colourScheme;
private boolean contourLines = true, bottomless, noOpposites;
private int contourSeparation = 10, waterColour, lavaColour, bedrockColour, oppositesDelta;
private LightOrigin lightOrigin = LightOrigin.NORTHWEST;
public static final Layer TERRAIN_AS_LAYER = new Layer("org.pepsoft.synthetic.Terrain", "Terrain", "The terrain type of the surface", Layer.DataSize.NONE, 0) {
@Override
public BufferedImage getIcon() {
return ICON;
}
private final BufferedImage ICON = IconUtils.scaleIcon(IconUtils.loadScaledImage("org/pepsoft/worldpainter/resources/terrain.png"), 16);
};
public static final Layer FLUIDS_AS_LAYER = new Layer("org.pepsoft.synthetic.Fluids", "Water/Lava", "Areas flooded with water or lava", Layer.DataSize.NONE, 0) {
@Override
public BufferedImage getIcon() {
return ICON;
}
private final BufferedImage ICON = IconUtils.loadScaledImage("org/pepsoft/worldpainter/resources/fluids.png");
};
private static final int BLACK = 0x000000, RED = 0xFF0000, LIGHT_GREY = 0xD0D0D0;
private static final boolean[][] CEILING_PATTERN = {
{ true, true, false, false, true, true, false, false},
{false, true, true, true, true, false, false, false},
{false, false, true, true, false, false, false, false},
{false, true, true, true, true, false, false, false},
{ true, true, false, false, true, true, false, false},
{ true, false, false, false, false, true, true, true},
{false, false, false, false, false, false, true, true},
{ true, false, false, false, false, true, true, true}};
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TileRenderer.class);
public enum LightOrigin {
NORTHWEST {
@Override
public LightOrigin left() {
return ABOVE;
}
@Override
public LightOrigin right() {
return NORTHEAST;
}
},
NORTHEAST {
@Override
public LightOrigin left() {
return NORTHWEST;
}
@Override
public LightOrigin right() {
return SOUTHEAST;
}
},
SOUTHEAST{
@Override
public LightOrigin left() {
return NORTHEAST;
}
@Override
public LightOrigin right() {
return SOUTHWEST;
}
},
SOUTHWEST{
@Override
public LightOrigin left() {
return SOUTHEAST;
}
@Override
public LightOrigin right() {
return ABOVE;
}
},
ABOVE{
@Override
public LightOrigin left() {
return SOUTHWEST;
}
@Override
public LightOrigin right() {
return NORTHWEST;
}
};
public abstract LightOrigin left();
public abstract LightOrigin right();
}
}