/* * Copyright 2015 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.nui.layers.mainMenu.preview; import com.google.common.math.IntMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.math.Region3i; import org.terasology.math.geom.ImmutableVector2i; import org.terasology.math.geom.Rect2i; import org.terasology.math.geom.Vector3i; import org.terasology.module.ModuleEnvironment; import org.terasology.rendering.assets.texture.TextureData; import org.terasology.rendering.nui.layers.mainMenu.ProgressListener; import org.terasology.world.chunks.ChunkConstants; import org.terasology.world.generation.Region; import org.terasology.world.generation.World; import org.terasology.world.generation.WorldFacet; import org.terasology.world.generator.WorldGenerator; import org.terasology.world.viewer.TileThreadFactory; import org.terasology.world.viewer.color.ColorModels; import org.terasology.world.viewer.layers.FacetLayer; import org.terasology.world.viewer.layers.FacetLayers; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.awt.image.DirectColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.math.RoundingMode; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; /** * A world preview based on the facets that are generated by the {@link WorldGenerator} * and a collection of {@link FacetLayer}, each annotated with * {@link org.terasology.world.viewer.layers.Renders} to display them. */ public class FacetLayerPreview implements PreviewGenerator { private static final Logger logger = LoggerFactory.getLogger(FacetLayerPreview.class); private static final int TILE_SIZE_X = ChunkConstants.SIZE_X * 2; private static final int TILE_SIZE_Y = ChunkConstants.SIZE_Z * 2; private final DirectColorModel colorModel = ColorModels.RGBA; private final WorldGenerator worldGenerator; private final List<FacetLayer> facetLayers; private ExecutorService threadPool = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors(), new TileThreadFactory()); /** * @param environment * @param worldGenerator */ public FacetLayerPreview(ModuleEnvironment environment, WorldGenerator worldGenerator) { this.worldGenerator = worldGenerator; World world = worldGenerator.getWorld(); Set<Class<? extends WorldFacet>> facets = world.getAllFacets(); facetLayers = FacetLayers.createLayersFor(facets, environment); } @Override public ByteBuffer render(TextureData texData, int scale, ProgressListener progressListener) throws InterruptedException { int width = texData.getWidth(); int height = texData.getWidth(); final int offX = -width * scale / 2; final int offY = -height * scale / 2; worldGenerator.getWorld(); // trigger building the World now Rect2i worldArea = Rect2i.createFromMinAndSize(offX, offY, width * scale, height * scale); Rect2i tileArea = worldToTileArea(worldArea); AtomicInteger tilesComplete = new AtomicInteger(0); int tileCount = tileArea.area(); int[] masks = colorModel.getMasks(); DataBufferInt imageBuffer = new DataBufferInt(width * height); WritableRaster raster = Raster.createPackedRaster(imageBuffer, width, height, width, masks, null); BufferedImage view = new BufferedImage(colorModel, raster, false, null); Graphics2D g = view.createGraphics(); g.scale(1f / scale, 1f / scale); g.translate(-offX, -offY); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); Map<ImmutableVector2i, Future<BufferedImage>> imageFutures = new HashMap<>(tileCount); for (int z = tileArea.minY(); z < tileArea.maxY(); z++) { for (int x = tileArea.minX(); x < tileArea.maxX(); x++) { ImmutableVector2i pos = new ImmutableVector2i(x, z); imageFutures.put(pos, threadPool.submit(() -> { Region createRegion = createRegion(pos); BufferedImage image = rasterize(createRegion); if (progressListener != null) { progressListener.onProgress(tilesComplete.incrementAndGet() / (float) tileCount); } return image; })); } } for (int z = tileArea.minY(); z < tileArea.maxY(); z++) { for (int x = tileArea.minX(); x < tileArea.maxX(); x++) { ImmutableVector2i pos = new ImmutableVector2i(x, z); try { BufferedImage tileImage = imageFutures.get(pos).get(); g.drawImage(tileImage, x * TILE_SIZE_X, z * TILE_SIZE_Y, null); } catch (ExecutionException e) { logger.warn("Could not rasterize tile {}", pos, e); } if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } } } // draw coordinate lines through 0 / 0 g.setColor(Color.GRAY); g.drawLine(worldArea.minX(), 0, worldArea.maxX(), 0); g.drawLine(0, worldArea.minY(), 0, worldArea.maxY()); g.dispose(); int[] data = imageBuffer.getData(); ByteBuffer byteBuffer = texData.getBuffers()[0]; byteBuffer.asIntBuffer().put(data); return byteBuffer; } @Override public void close() { threadPool.shutdown(); } private Region createRegion(ImmutableVector2i chunkPos) { int vertChunks = 4; // 4 chunks high (relevant for trees, etc) int minX = chunkPos.getX() * TILE_SIZE_X; int minZ = chunkPos.getY() * TILE_SIZE_Y; int height = vertChunks * ChunkConstants.SIZE_Y; Region3i area3d = Region3i.createFromMinAndSize(new Vector3i(minX, 0, minZ), new Vector3i(TILE_SIZE_X, height, TILE_SIZE_Y)); World world = worldGenerator.getWorld(); Region region = world.getWorldData(area3d); return region; } private static Rect2i worldToTileArea(Rect2i area) { int chunkMinX = IntMath.divide(area.minX(), TILE_SIZE_X, RoundingMode.FLOOR); int chunkMinZ = IntMath.divide(area.minY(), TILE_SIZE_Y, RoundingMode.FLOOR); int chunkMaxX = IntMath.divide(area.maxX(), TILE_SIZE_X, RoundingMode.CEILING); int chunkMaxZ = IntMath.divide(area.maxY(), TILE_SIZE_Y, RoundingMode.CEILING); return Rect2i.createFromMinAndMax(chunkMinX, chunkMinZ, chunkMaxX, chunkMaxZ); } /** * Note: this method must be thread-safe! * @param region the thread-safe region * @return an image of that region */ private BufferedImage rasterize(Region region) { Vector3i extent = region.getRegion().size(); int width = extent.x; int height = extent.z; WritableRaster raster = colorModel.createCompatibleWritableRaster(width, height); BufferedImage image = new BufferedImage(colorModel, raster, false, null); Graphics2D g = image.createGraphics(); g.setColor(Color.BLACK); g.fillRect(0, 0, width, height); try { facetLayers.stream().filter(FacetLayer::isVisible).forEach(layer -> layer.render(image, region)); } finally { g.dispose(); } return image; } }