/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.worldpainter.layers; import org.pepsoft.minecraft.Chunk; import org.pepsoft.minecraft.Constants; import org.pepsoft.util.Box; import org.pepsoft.util.MathUtils; import org.pepsoft.util.ProgressReceiver; import org.pepsoft.worldpainter.*; import org.pepsoft.worldpainter.Dimension; import org.pepsoft.worldpainter.dynmap.DynMapPreviewer; import org.pepsoft.worldpainter.exporting.*; import org.pepsoft.worldpainter.layers.exporters.ExporterSettings; import org.pepsoft.worldpainter.layers.pockets.UndergroundPocketsLayer; import org.pepsoft.worldpainter.layers.tunnel.TunnelLayer; import org.pepsoft.worldpainter.objects.MinecraftWorldObject; import org.pepsoft.worldpainter.objects.WPObject; import org.pepsoft.worldpainter.plugins.WPPluginManager; import javax.imageio.ImageIO; import javax.vecmath.Point3i; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.*; import java.util.List; import static org.pepsoft.worldpainter.Constants.DIM_NORMAL; /** * A utility class for generating previews of layers. It renders a layer to a * small temporary world and generates a {@link MinecraftWorldObject} (an object * which implements both {@link MinecraftWorld} and {@link WPObject}) which can * be used to display it, for instance with {@link DynMapPreviewer}. * * @author SchmitzP */ public class LayerPreviewCreator { public MinecraftWorldObject renderPreview() { // Phase one: setup long timestamp = System.currentTimeMillis(); long seed = 0L; TileFactory tileFactory = subterranean ? TileFactoryFactory.createNoiseTileFactory(seed, Terrain.BARE_GRASS, previewHeight, 56, 62, false, true, 20f, 0.5) : TileFactoryFactory.createNoiseTileFactory(seed, Terrain.BARE_GRASS, previewHeight, 8, 14, false, true, 20f, 0.5); Dimension dimension = new Dimension(seed, tileFactory, DIM_NORMAL, previewHeight); dimension.setSubsurfaceMaterial(Terrain.STONE); MinecraftWorldObject minecraftWorldObject = new MinecraftWorldObject(layer.getName() + " Preview", new Box(-8, 136, -8, 136, 0, previewHeight), previewHeight, null, new Point3i(-64, -64, 0)); long now = System.currentTimeMillis(); if (logger.isDebugEnabled()) { logger.debug("Creating data structures took " + (now - timestamp) + " ms"); } // Phase two: apply layer to dimension timestamp = now; Tile tile = tileFactory.createTile(0, 0); switch (layer.getDataSize()) { case BIT: Random random = new Random(seed); for (int x = 0; x < 128; x++) { for (int y = 0; y < 128; y++) { if (random.nextFloat() < pattern.getStrength(x, y)) { tile.setBitLayerValue(layer, x, y, true); } } } break; case BIT_PER_CHUNK: random = new Random(seed); for (int x = 0; x < 128; x += 16) { for (int y = 0; y < 128; y += 16) { if (random.nextFloat() < pattern.getStrength(x, y)) { tile.setBitLayerValue(layer, x, y, true); } } } break; case BYTE: for (int x = 0; x < 128; x++) { for (int y = 0; y < 128; y++) { tile.setLayerValue(layer, x, y, Math.min((int) (pattern.getStrength(x, y) * 256), 255)); } } break; case NIBBLE: // If it's a CombinedLayer, also apply the terrain and biome, if // any if (layer instanceof CombinedLayer) { final Terrain terrain = ((CombinedLayer) layer).getTerrain(); final int biome = ((CombinedLayer) layer).getBiome(); final boolean terrainConfigured = terrain != null; final boolean biomeConfigured = biome != -1; for (int x = 0; x < 128; x++) { for (int y = 0; y < 128; y++) { float strength = pattern.getStrength(x, y); tile.setLayerValue(layer, x, y, Math.min((int) (strength * 16), 15)); // Double the strength so that 50% intensity results // in full coverage for terrain and biome, which is // inaccurate but probably more closely resembles // practical usage strength = Math.min(strength * 2, 1.0f); if (terrainConfigured && ((strength > 0.95f) || (Math.random() < strength))) { tile.setTerrain(x, y, terrain); } if (biomeConfigured && ((strength > 0.95f) || (Math.random() < strength))) { tile.setLayerValue(Biome.INSTANCE, x, y, biome); } } } } else { for (int x = 0; x < 128; x++) { for (int y = 0; y < 128; y++) { tile.setLayerValue(layer, x, y, Math.min((int) (pattern.getStrength(x, y) * 16), 15)); } } } break; default: throw new IllegalArgumentException("Unsupported data size " + layer.getDataSize() + " encountered"); } // If the layer is a combined layer, apply it recursively and collect // the added layers List<Layer> layers; if (layer instanceof CombinedLayer) { layers = new ArrayList<>(); layers.add(layer); while (true) { List<Layer> addedLayers = new ArrayList<>(); for (Iterator<Layer> i = layers.iterator(); i.hasNext(); ) { Layer tmpLayer = i.next(); if (tmpLayer instanceof CombinedLayer) { i.remove(); addedLayers.addAll(((CombinedLayer) tmpLayer).apply(tile)); } } if (! addedLayers.isEmpty()) { layers.addAll(addedLayers); } else { break; } } } else { layers = Collections.singletonList(layer); } dimension.addTile(tile); now = System.currentTimeMillis(); if (logger.isDebugEnabled()) { logger.debug("Applying layer(s) took " + (now - timestamp) + " ms"); } // Collect the exporters (could be multiple if the layer was a combined // layer) Map<Layer, LayerExporter> pass1Exporters = new HashMap<>(); Map<Layer, SecondPassLayerExporter> pass2Exporters = new HashMap<>(); for (Layer tmpLayer: layers) { LayerExporter exporter = tmpLayer.getExporter(); if (tmpLayer.equals(layer)) { exporter.setSettings(settings); } if (exporter instanceof FirstPassLayerExporter) { pass1Exporters.put(layer, exporter); } if (exporter instanceof SecondPassLayerExporter) { pass2Exporters.put(layer, (SecondPassLayerExporter) exporter); } } // Phase three: generate terrain and render first pass layers, if any timestamp = now; WorldPainterChunkFactory chunkFactory = new WorldPainterChunkFactory(dimension, pass1Exporters, Constants.SUPPORTED_VERSION_2, previewHeight); for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { Chunk chunk = chunkFactory.createChunk(x, y).chunk; minecraftWorldObject.addChunk(chunk); } } now = System.currentTimeMillis(); if (logger.isDebugEnabled()) { logger.debug("Generating terrain and rendering first pass layer(s) (if any) took " + (now - timestamp) + " ms"); } if (! pass2Exporters.isEmpty()) { // Phase four: render the second pass layers, if any timestamp = now; Rectangle area = new Rectangle(128, 128); for (SecondPassLayerExporter exporter: pass2Exporters.values()) { exporter.render(dimension, area, area, minecraftWorldObject); } now = System.currentTimeMillis(); if (logger.isDebugEnabled()) { logger.debug("Rendering second pass layer(s) took " + (now - timestamp) + " ms"); } } // Final phase: post processing timestamp = now; now = System.currentTimeMillis(); try { PostProcessor.postProcess(minecraftWorldObject, new Rectangle(-8, -8, 136, 136), null); } catch (ProgressReceiver.OperationCancelled e) { // Can't happen since we didn't pass in a progress receiver throw new InternalError(); } if (logger.isDebugEnabled()) { logger.debug("Post processing took " + (now - timestamp) + " ms"); } return minecraftWorldObject; } public Layer getLayer() { return layer; } public void setLayer(Layer layer) { this.layer = layer; } public int getPreviewHeight() { return previewHeight; } public void setPreviewHeight(int previewHeight) { this.previewHeight = previewHeight; } public ExporterSettings getSettings() { return settings; } public void setSettings(ExporterSettings settings) { this.settings = settings; } public boolean isSubterranean() { return subterranean; } public void setSubterranean(boolean subterranean) { this.subterranean = subterranean; } public Pattern getPattern() { return pattern; } public void setPattern(Pattern pattern) { this.pattern = pattern; } /** * Create a new <code>LayerPrevierCreator</code> configured with some * sensible defaults for a particular layer and dimension. * * @param layer The layer with which to configure the * <code>LayerPrevierCreator</code>. * @param dimension The dimension from which to take some defaults. * @return A <code>LayerPrevierCreator</code> configured with the * specified layer and some sensible defaults. */ public static LayerPreviewCreator createPreviewerForLayer(Layer layer, Dimension dimension) { LayerPreviewCreator previewer = new LayerPreviewCreator(); previewer.setLayer(layer); if ((layer instanceof UndergroundPocketsLayer) || (layer instanceof Caverns) || (layer instanceof Chasms) || (layer instanceof TunnelLayer) || layer.equals(Resources.INSTANCE)) { previewer.setSubterranean(true); previewer.setPattern(CONSTANT_HALF); } else { switch (layer.getDataSize()) { case BIT: case BIT_PER_CHUNK: previewer.setPattern(CONSTANT_FULL_PLUS_GRADIENT); break; default: previewer.setPattern(CONSTANT_HALF_PLUS_GRADIENT_PLUS_HIGHLIGHT); break; } } return previewer; } public static void main(String[] args) throws IOException, ClassNotFoundException { Configuration config = Configuration.load(); if (config == null) { config = new Configuration(); } Configuration.setInstance(config); WPPluginManager.initialise(config.getUuid()); Dimension dimension = WorldFactory.createDefaultWorldWithoutTiles(config, 0L).getDimension(DIM_NORMAL); for (Layer layer: LayerManager.getInstance().getLayers()) { // Layer layer = Caverns.INSTANCE; LayerPreviewCreator renderer = createPreviewerForLayer(layer, dimension); long start = System.currentTimeMillis(); MinecraftWorldObject preview = renderer.renderPreview(); System.out.println("Total: " + (System.currentTimeMillis() - start) + " ms"); // JFrame frame = new JFrame("LayerPreviewCreator Test"); // frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); DynMapPreviewer previewer = new DynMapPreviewer(); previewer.setZoom(-2); previewer.setInclination(30.0); previewer.setAzimuth(60.0); if ((layer instanceof Caverns) || (layer instanceof Chasms) || (layer instanceof TunnelLayer)) { previewer.setCaves(true); } previewer.setObject(preview); // frame.getContentPane().add(previewer, BorderLayout.CENTER); // frame.setSize(800, 600); // frame.setLocationRelativeTo(null); // Center on screen // frame.setVisible(true); start = System.currentTimeMillis(); BufferedImage image = previewer.createImage(); System.out.println("Creating image took " + (System.currentTimeMillis() - start) + " ms"); ImageIO.write(image, "png", new File(layer.getName().toLowerCase().replaceAll("\\s", "") + "-preview.png")); } } private Layer layer; private ExporterSettings settings; private int previewHeight = 128; private boolean subterranean; private Pattern pattern = CONSTANT_HALF_PLUS_GRADIENT_PLUS_HIGHLIGHT; private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LayerPreviewCreator.class); public static abstract class Pattern { protected Pattern(String description) { this.description = description; } abstract float getStrength(int x, int y); public String getDescription() { return description; } public BufferedImage getIcon() { BufferedImage icon = new BufferedImage(32, 32, BufferedImage.TYPE_BYTE_GRAY); for (int x = 1; x < 31; x++) { for (int y = 1; y < 31; y++) { int value = Math.min((int) (64.0f + getStrength((31 - x) << 2, (31 - y) << 2) * 192.0f), 255); icon.setRGB(x, y, (value << 16) | (value << 8) | value); } } return icon; } private final String description; } public static final Pattern CONSTANT_ONE_QUARTER = new Pattern("25%") { @Override float getStrength(int x, int y) { return 0.25f; } }; public static final Pattern CONSTANT_HALF = new Pattern("50%") { @Override float getStrength(int x, int y) { return 0.5f; } }; public static final Pattern CONSTANT_THREE_QUARTERS = new Pattern("75%") { @Override float getStrength(int x, int y) { return 0.75f; } }; public static final Pattern CONSTANT_FULL = new Pattern("100%") { @Override float getStrength(int x, int y) { return 1.0f; } }; public static final Pattern GRADIENT_ZERO_TO_FULL = new Pattern("100% - 0%") { @Override float getStrength(int x, int y) { return x / 127f; } }; public static final Pattern CIRCULAR_ZERO_TO_FULL = new Pattern("0% - 100% (circular)") { @Override float getStrength(int x, int y) { return 1f - MathUtils.getDistance(x - 64, y - 64) / MAX_DIST; } private final float MAX_DIST = (float) Math.sqrt(64 * 64 + 64 * 64); }; public static final Pattern CONSTANT_HALF_PLUS_GRADIENT_PLUS_HIGHLIGHT = new Pattern("50% - 50% - 0% (100% highlight)") { @Override float getStrength(int x, int y) { return Math.max(x < 64 ? x / 127f : 0.5f, 1f - MathUtils.getDistance(x - 64, 127 - y) / 64); } }; public static final Pattern CONSTANT_FULL_PLUS_GRADIENT = new Pattern("100% - 100% - 0%") { @Override float getStrength(int x, int y) { return x < 64 ? x / 63f : 1.0f; } }; public static final Pattern[] PATTERNS = {CONSTANT_ONE_QUARTER, CONSTANT_HALF, CONSTANT_THREE_QUARTERS, CONSTANT_FULL, CONSTANT_HALF_PLUS_GRADIENT_PLUS_HIGHLIGHT, CONSTANT_FULL_PLUS_GRADIENT, GRADIENT_ZERO_TO_FULL, CIRCULAR_ZERO_TO_FULL}; }