All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.llbit.chunky.map.MapBuffer Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
/* Copyright (c) 2010-2016 Jesper Öqvist 
 *
 * This file is part of Chunky.
 *
 * Chunky is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Chunky 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with Chunky.  If not, see .
 */
package se.llbit.chunky.map;

import javafx.embed.swing.SwingFXUtils;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.paint.Color;
import se.llbit.chunky.ui.MapViewMode;
import se.llbit.chunky.world.Block;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.ChunkView;
import se.llbit.util.RingBuffer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * Keeps a buffered image of rendered map tiles. We only re-render chunks when
 * they are not buffered. The buffer contains all visible chunks, plus some
 * outside of the view. Chunks outside the view are rendered so that the
 * rendering and chunk loading delay when panning is minimized.
 *
 * @author Jesper Öqvist ([email protected])
 */
public class MapBuffer {
  private static final WritablePixelFormat PIXEL_FORMAT =
      PixelFormat.getIntArgbInstance();

  private int[] pixels;
  private int width;
  private int height;

  private WritableImage image = null;
  private boolean cached = false;

  private boolean highlightEnabled = false;
  private Block highlightBlock = Block.get(Block.DIAMONDORE_ID);
  private Color highlightColor = Color.CRIMSON;

  private ChunkView view = ChunkView.EMPTY;

  private RingBuffer tileCache = new RingBuffer<>(140);
  private Map activeTiles = new HashMap<>();

  public MapBuffer() {
    updateView(ChunkView.EMPTY, true);
  }

  /**
   * Called when this render buffer should buffer another view.
   */
  public synchronized void updateView(ChunkView newView, WorldMapLoader loader) {
    boolean rebuild = newView.scale != view.scale || newView.renderer != view.renderer
        || (newView.renderer == MapViewMode.LAYER && newView.layer != view.layer);
    if (newView.renderer == MapViewMode.LAYER) {
      if (loader.highlightEnabled() != highlightEnabled
          || loader.highlightBlock() != highlightBlock
          || !loader.highlightColor().equals(highlightColor)) {
        rebuild = true;
        highlightEnabled = loader.highlightEnabled();
        highlightBlock = loader.highlightBlock();
        highlightColor = loader.highlightColor();
      }
    }
    updateView(newView, rebuild);
  }

  private synchronized void updateView(ChunkView newView, boolean rebuild) {
    int newWidth = newView.chunkScale * (newView.px1 - newView.px0 + 1);
    int newHeight = newView.chunkScale * (newView.pz1 - newView.pz0 + 1);
    if (newWidth != width || newHeight != height || pixels == null) {
      width = newWidth;
      height = newHeight;
      pixels = new int[width * height];
    }
    updateActiveTiles(newView, rebuild);
    view = newView;
  }

  private synchronized void updateActiveTiles(ChunkView newView, boolean rebuild) {
    Collection discarded = new LinkedList<>();
    for (MapTile tile : activeTiles.values()) {
      if (!newView.shouldPreload(tile.pos)) {
        discarded.add(tile);
      } else if (rebuild) {
        tile.rebuild(tile.pos, newView);
      }
    }
    for (MapTile tile : discarded) {
      tileCache.append(tile);
      activeTiles.remove(tile.pos);
    }
    int x0, x1, z0, z1;
    if (newView.chunkScale >= 16) {
      x0 = newView.px0;
      x1 = newView.px1;
      z0 = newView.pz0;
      z1 = newView.pz1;
    } else {
      x0 = newView.prx0;
      x1 = newView.prx1;
      z0 = newView.prz0;
      z1 = newView.prz1;
    }
    for (int x = x0; x <= x1; ++x) {
      for (int z = z0; z <= z1; ++z) {
        ChunkPosition pos = ChunkPosition.get(x, z);
        if (!activeTiles.containsKey(pos)) {
          activeTiles.put(pos, newTile(pos, newView));
        }
      }
    }
  }

  /**
   * @return The buffered view
   */
  public ChunkView getView() {
    return view;
  }

  /**
   * Redraws the given tile.
   */
  public synchronized void drawTile(WorldMapLoader mapLoader, ChunkPosition chunk) {
    MapTile tile = activeTiles.get(chunk);
    if (tile != null) {
      tile.draw(this, mapLoader, view);
      cached = false;
    }
  }

  /**
   * Attempts to draw the tile using cached image.
   */
  public synchronized void drawTileCached(WorldMapLoader mapLoader, ChunkPosition chunk) {
    MapTile tile = activeTiles.get(chunk);
    if (tile != null) {
      tile.drawCached(this, mapLoader, view);
      cached = false;
    }
  }

  /**
   * Redraw all tiles in the current view.
   * This draws to the map buffer, it does not render to the map canvas.
   */
  public synchronized void redrawView(WorldMapLoader mapLoader) {
    int x0, x1, z0, z1;
    if (view.chunkScale >= 16) {
      x0 = view.px0;
      x1 = view.px1;
      z0 = view.pz0;
      z1 = view.pz1;
    } else {
      x0 = view.prx0;
      x1 = view.prx1;
      z0 = view.prz0;
      z1 = view.prz1;
    }
    for (int x = x0; x <= x1; ++x) {
      for (int z = z0; z <= z1; ++z) {
        drawTileCached(mapLoader, ChunkPosition.get(x, z));
      }
    }
  }

  /**
   * Create a new map tile to use in the map buffer.
   * This reuses existing map tiles when possible.
   */
  private MapTile newTile(ChunkPosition pos, ChunkView view) {
    if (tileCache.isEmpty()) {
      return new MapTile(pos, view);
    } else {
      MapTile tile = tileCache.remove();
      tile.rebuild(pos, view);
      return tile;
    }
  }

  /**
   * Copies a contiguous block of pixels into the buffer.
   */
  public void copyPixels(int[] data, int srcPos, int x, int z, int size) {
    System.arraycopy(data, srcPos, pixels, z * width + x, size);
  }

  /**
   * Draws the current buffered map to a map canvas (via a GraphicsContext).
   * We use a manual scaling implementation to avoid the blurry JavaFX upscaling.
   *
   * 

Drawing the image would be much simpler if we could rely on JavaFX * for scaling the image, unfortunately if JavaFX is used to scale the image * it will use a blurry upscaling algorithm which looks bad for the 2D map. * It is not possible to disable the JavaFX scaling interpolation, * so we do our own scaling here instead. */ public synchronized void drawBuffered(GraphicsContext gc) { if (!cached) { if (image == null || image.getWidth() != view.width || image.getHeight() != view.height) { image = new WritableImage(view.width, view.height); } // Here we make sure to scale only the part of the image that will be drawn. float scale = view.scale / (float) view.chunkScale; double x0 = view.chunkScale * (view.x0 - view.px0); double z0 = view.chunkScale * (view.z0 - view.pz0); float diffY = 0; int[] scaled = new int[view.width * view.height]; int index = 0; int sourceX = (int) (0.5 + x0); int sourceY = (int) (0.5 + z0); int destY = 0; for (int y = 0; y < (view.height / scale); ++y) { while (diffY < scale && destY < view.height) { float diffX = 0; int destX = 0; int pixelOffset = (sourceY + y) * width + sourceX; for (int x = 0; x < (view.width / scale); ++x) { int pixel = pixels[pixelOffset++]; while (diffX < scale && destX < view.width) { scaled[index++] = pixel; diffX += 1; destX += 1; } diffX -= scale; } diffY += 1; destY += 1; } diffY -= scale; } image.getPixelWriter() .setPixels(0, 0, view.width, view.height, PIXEL_FORMAT, scaled, 0, view.width); cached = true; } gc.clearRect(0, 0, view.width, view.height); gc.drawImage(image, 0, 0); } public boolean highlightEnabled() { return highlightEnabled; } public Block highlightBlock() { return highlightBlock; } public Color highlightColor() { return highlightColor; } /** * Forces all tiles to be redrawn on the next draw operation. */ public synchronized void clearBuffer() { updateActiveTiles(view, true); } public void renderPng(File targetFile) throws IOException { BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null); ImageIO.write(bufferedImage, "PNG", targetFile); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy