Package se.llbit.chunky.renderer.scene

Source Code of se.llbit.chunky.renderer.scene.Scene

/* Copyright (c) 2012-2013 Jesper Öqvist <jesper@llbit.se>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.renderer.scene;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.math3.util.FastMath;
import org.apache.log4j.Logger;

import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.model.WaterModel;
import se.llbit.chunky.renderer.Postprocess;
import se.llbit.chunky.renderer.ProgressListener;
import se.llbit.chunky.renderer.RenderContext;
import se.llbit.chunky.renderer.RenderStatusListener;
import se.llbit.chunky.renderer.WorkerState;
import se.llbit.chunky.world.Biomes;
import se.llbit.chunky.world.Block;
import se.llbit.chunky.world.BlockData;
import se.llbit.chunky.world.Chunk;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.Heightmap;
import se.llbit.chunky.world.World;
import se.llbit.chunky.world.WorldTexture;
import se.llbit.math.Color;
import se.llbit.math.Octree;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import se.llbit.math.Ray.RayPool;
import se.llbit.math.Vector3d;
import se.llbit.math.Vector3i;
import se.llbit.png.IEND;
import se.llbit.png.ITXT;
import se.llbit.png.PngFileWriter;

/**
* Scene description.
*/
public class Scene extends SceneDescription {

  private static final Logger logger =
      Logger.getLogger(Scene.class);

  private static final Font infoFont = new Font("Sans serif", Font.BOLD, 11);
  private static FontMetrics fontMetrics;

  private final RayPool rayPool = new RayPool();

  protected static final int DEFAULT_DUMP_FREQUENCY = 500;

  protected static final double fSubSurface = 0.3;

  /**
   * Minimum canvas width
   */
  public static final int MIN_CANVAS_WIDTH = 20;

  /**
   * Minimum canvas height
   */
  public static final int MIN_CANVAS_HEIGHT = 20;


  /**
   * Default specular reflection coefficient
   */
  protected static final float SPECULAR_COEFF = 0.31f;

  /**
   * Default water specular reflection coefficient
   */
  public static final float WATER_SPECULAR = 0.46f;

  /**
   * Minimum exposure
   */
  public static final double MIN_EXPOSURE = 0.001;

  /**
   * Maximum exposure
   */
  public static final double MAX_EXPOSURE = 1000.0;

  /**
   * Default gamma
   */
  public static final float DEFAULT_GAMMA = 2.2f;

  /**
   * One over gamma
   */
  public static final float DEFAULT_GAMMA_INV = 1 / DEFAULT_GAMMA;

  /**
   * Default emitter intensity
   */
  public static final double DEFAULT_EMITTER_INTENSITY = 13;

  /**
   * Minimum emitter intensity
   */
  public static final double MIN_EMITTER_INTENSITY = 0.01;

  /**
   * Maximum emitter intensity
   */
  public static final double MAX_EMITTER_INTENSITY = 1000;

  /**
   * Current CVF file format version
   */
  public static final int CVF_VERSION = 1;

  private static final double DEFAULT_WATER_VISIBILITY = 9;

  //private static final double MIN_WATER_VISIBILITY = 0;
  //private static final double MAX_WATER_VISIBILITY = 62;

  /**
   * Default exposure
   */
  public static final double DEFAULT_EXPOSURE = 1.0;

  protected double waterVisibility = DEFAULT_WATER_VISIBILITY;

  /**
   * World
   */
  private World loadedWorld;

  /**
   * Octree origin
   */
  protected Vector3i origin = new Vector3i();

  /**
   * Octree
   */
  protected Octree octree;


  // chunk loading buffers
  private final byte[] blocks = new byte[Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX];
  private final byte[] biomes = new byte[Chunk.X_MAX * Chunk.Z_MAX];
  private final byte[] data = new byte[(Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX) / 2];

  /**
   * Preview frame interlacing counter.
   */
  public int previewCount;

  private WorldTexture grassTexture = new WorldTexture();
  private WorldTexture foliageTexture = new WorldTexture();

  private BufferedImage buffer;

  private BufferedImage backBuffer;

  private double[] samples;

  private int[] bufferData;

  private boolean finalized = false;

  private boolean finalizeBuffer = false;

  private boolean resetRender = false;

  /**
   * Create an empty scene with default canvas width and height.
   */
  public Scene() {
    octree = new Octree(1);// empty octree

    width = PersistentSettings.get3DCanvasWidth();
    height = PersistentSettings.get3DCanvasHeight();

    sppTarget = PersistentSettings.getSppTargetDefault();

    initBuffers();
  }

  private synchronized void initBuffers() {
    buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    backBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    bufferData = ((DataBufferInt) backBuffer.getRaster().getDataBuffer()).getData();
    samples = new double[width*height*3];
  }

  /**
   * Clone other scene
   * @param other
   */
  public Scene(Scene other) {
    set(other);
    copyRenderState(other);
    copyTransients(other);
  }

  /**
   * Set scene equal to other
   * @param other
   */
  synchronized public void set(Scene other) {
    loadedWorld = other.loadedWorld;
    worldPath = other.worldPath;
    worldDimension = other.worldDimension;

    // the octree reference is overwritten to save time
    // when the other scene is changed it must create a new octree
    octree = other.octree;
    grassTexture = other.grassTexture;
    foliageTexture = other.foliageTexture;
    origin.set(other.origin);

    chunks = other.chunks;

    exposure = other.exposure;
    name = other.name;

    stillWater = other.stillWater;
    clearWater = other.clearWater;
    biomeColors = other.biomeColors;
    sunEnabled = other.sunEnabled;
    emittersEnabled = other.emittersEnabled;
    emitterIntensity = other.emitterIntensity;
    atmosphereEnabled = other.atmosphereEnabled;
    volumetricFogEnabled = other.volumetricFogEnabled;

    camera.set(other.camera);
    sky.set(other.sky);
    sun.set(other.sun);

    waterHeight = other.waterHeight;

    spp = other.spp;
    renderTime = other.renderTime;

    refresh = other.refresh;

    finalized = false;

    if (samples != other.samples) {
      width = other.width;
      height = other.height;
      backBuffer = other.backBuffer;
      buffer = other.buffer;
      samples = other.samples;
      bufferData = other.bufferData;
    }
  }

  /**
   * Copy the current rendering state.
   * @param other
   */
  public void copyRenderState(Scene other) {
    pathTrace = other.pathTrace;
    pauseRender = other.pauseRender;
  }

  /**
   * Save the scene description, render dump, and foliage
   * and grass textures.
   * @param context
   * @param progressListener
   * @throws IOException
   * @throws InterruptedException
   */
  public synchronized void saveScene(
      RenderContext context,
      RenderStatusListener progressListener)
    throws IOException, InterruptedException {

    String task = "Saving scene";
    progressListener.setProgress(task, 1, 0, 2);

    saveDescription(context.getSceneDescriptionOutputStream(name));

    saveOctree(context, progressListener);
    saveGrassTexture(context, progressListener);
    saveFoliageTexture(context, progressListener);

    saveDump(context, progressListener);

    progressListener.sceneSaved();
  }

  /**
   * Load a stored scene by file name.
   * @param context
   * @param renderListener
   * @param sceneName file name of the scene to load
   * @throws IOException
   * @throws SceneLoadingError
   * @throws InterruptedException
   */
  public synchronized void loadScene(
      RenderContext context,
      RenderStatusListener renderListener,
      String sceneName)
      throws IOException, SceneLoadingError, InterruptedException {

    loadDescription(context.getSceneDescriptionInputStream(sceneName));

    // load the configured skymap file
    sky.loadSkymap();

    if (sdfVersion < SDF_VERSION) {
      logger.warn("Old scene version detected! The scene may not have loaded correctly.");
    }

    setCanvasSize(width, height);

    if (!worldPath.isEmpty()) {
      File worldDirectory = new File(worldPath);
      if (World.isWorldDir(worldDirectory)) {
        if (loadedWorld == null ||
            loadedWorld.getWorldDirectory() == null ||
            !loadedWorld.getWorldDirectory().getAbsolutePath().equals(worldPath)) {

          loadedWorld = new World(worldDirectory, true);
          loadedWorld.setDimension(worldDimension);

        } else if (loadedWorld.currentDimension() != worldDimension) {

          loadedWorld.setDimension(worldDimension);

        }
      } else {
        logger.debug("Could not load world: " + worldPath);
      }
    }

    if (pathTrace) {
      pauseRender = true;
    }

    refresh = false;

    loadDump(context, renderListener);

    if (loadOctree(context, renderListener)) {
      calculateOctreeOrigin(chunks);
      camera.setWorldSize(1<<octree.depth);

      boolean haveGrass = loadGrassTexture(context, renderListener);
      boolean haveFoliage = loadFoliageTexture(context, renderListener);
      if (!haveGrass || !haveFoliage) {
        biomeColors = false;
      }
    } else {
      // Could not load stored octree
      // Load the chunks from the world
      if (loadedWorld == null) {
        logger.warn("Could not load chunks (no world found for scene)");
      } else {
        loadChunks(renderListener, loadedWorld, chunks);
      }
    }

    notifyAll();
  }

  /**
   * Set the exposure value
   * @param value
   */
  public synchronized void setExposure(double value) {
    exposure = value;
    if (!pathTrace)
      refresh();
  }

  /**
   * @return Current exposure value
   */
  public double getExposure() {
    return exposure;
  }

  /**
   * Set still water mode
   * @param value
   */
  public void setStillWater(boolean value) {
    if (value != stillWater) {
      stillWater = value;
      refresh();
    }
  }

  /**
   * @return <code>true</code> if sunlight is enabled
   */
  public boolean getDirectLight() {
    return sunEnabled;
  }

  /**
   * Set emitters enable flag
   * @param value
   */
  public synchronized void setEmittersEnabled(boolean value) {
    if (value != emittersEnabled) {
      emittersEnabled = value;
      refresh();
    }
  }

  /**
   * Set sunlight enable flag
   * @param value
   */
  public synchronized void setDirectLight(boolean value) {
    if (value != sunEnabled) {
      sunEnabled = value;
      refresh();
    }
  }

  /**
   * @return <code>true</code> if emitters are enabled
   */
  public boolean getEmittersEnabled() {
    return emittersEnabled;
  }

  /**
   * @param ray
   * @param rayPool
   */
  public void quickTrace(WorkerState state) {
    state.ray.x.x -= origin.x;
    state.ray.x.y -= origin.y;
    state.ray.x.z -= origin.z;

    RayTracer.quickTrace(this, state);
  }

  /**
   * Path trace the ray in this scene
   * @param state
   */
  public void pathTrace(WorkerState state) {

    state.ray.x.x -= origin.x;
    state.ray.x.y -= origin.y;
    state.ray.x.z -= origin.z;

    PathTracer.pathTrace(this, state);
  }

  /**
   * @param ray
   * @return <code>true</code> if an intersection was found
   */
  public boolean intersect(Ray ray) {
    return octree.intersect(this, ray);
  }

  protected final boolean kill(Ray ray, Random random) {
    return ray.depth >= rayDepth && random.nextDouble() < .5f;
  }

  /**
   * Reload all loaded chunks.
   * @param progressListener
   */
  public synchronized void reloadChunks(ProgressListener progressListener) {
    if (loadedWorld == null) {
      logger.warn("Can not reload chunks for scene - world directory not found!");
      return;
    }

    loadedWorld.setDimension(worldDimension);
    loadedWorld.reload();
    loadChunks(progressListener, loadedWorld, chunks);
    refresh();
  }

  /**
   * Load chunks into the Octree
   * @param progressListener
   * @param world
   * @param chunksToLoad
   */
  public synchronized void loadChunks(
      ProgressListener progressListener,
      World world,
      Collection<ChunkPosition> chunksToLoad) {

    if (world == null)
      return;

    String task = "Loading regions";
    progressListener.setProgress(task, 1, 0, 2);

    loadedWorld = world;
    worldPath = loadedWorld.getWorldDirectory().getAbsolutePath();
    worldDimension = world.currentDimension();

    int emitters = 0;
    int nchunks = 0;

    if (chunksToLoad.isEmpty()) {
      return;
    }

    Set<ChunkPosition> loadedChunks = new HashSet<ChunkPosition>();

    int requiredDepth = calculateOctreeOrigin(chunksToLoad);

    // create new octree to fit all chunks
    octree = new Octree(requiredDepth);

    if (waterHeight > 0) {
      for (int x = 0; x < (1<<octree.depth); ++x) {
        for (int z = 0; z < (1<<octree.depth); ++z) {
          for (int y = -origin.y; y < (-origin.y)+waterHeight-1; ++y) {
            octree.set(Block.WATER.id | (1<<WaterModel.FULL_BLOCK), x, y, z);
          }
        }
      }
      for (int x = 0; x < (1<<octree.depth); ++x) {
        for (int z = 0; z < (1<<octree.depth); ++z) {
          octree.set(Block.WATER.id, x, (-origin.y)+waterHeight-1, z);
        }
      }
    }

    // parse the regions first - force chunk lists to be populated!
    Set<ChunkPosition> regions = new HashSet<ChunkPosition>();
    for (ChunkPosition cp: chunksToLoad) {
      regions.add(cp.getRegionPosition());
    }

    for (ChunkPosition region: regions) {
      world.regionDiscovered(region);
      world.getRegion(region).parse();
    }

    Heightmap biomeIdMap = new Heightmap();
    task = "Loading chunks";
    int done = 1;
    int target = chunksToLoad.size();
    for (ChunkPosition cp : chunksToLoad) {

      progressListener.setProgress(task, done, 0, target);
      done += 1;

      if (loadedChunks.contains(cp))
        continue;

      loadedChunks.add(cp);

      world.getChunk(cp).getBlockData(blocks, data, biomes);
      nchunks += 1;

      for (int cz = 0; cz < 16; ++cz) {
        int wz = cz + cp.z*16;
        for (int cx = 0; cx < 16; ++cx) {
          int wx = cx + cp.x*16;
          int biomeId = 0xFF & biomes[Chunk.chunkXZIndex(cx, cz)];
          biomeIdMap.set(biomeId, wx, wz);
        }
      }

      for (int cy = 0; cy < 256; ++cy) {
        for (int cz = 0; cz < 16; ++cz) {
          int z = cz + cp.z*16 - origin.z;
          for (int cx = 0; cx < 16; ++cx) {
            int x = cx + cp.x*16 - origin.x;
            int index = Chunk.chunkIndex(cx, cy, cz);
            Block block = Block.get(blocks[index]);

            if (cx > 0 && cx < 15 && cz > 0 && cz < 15 && cy > 0 && cy < 255 &&
                block != Block.STONE && block.isOpaque) {

              // set obscured blocks to stone
              if (Block.get(blocks[index-1]).isOpaque &&
                  Block.get(blocks[index+1]).isOpaque &&
                  Block.get(blocks[index-Chunk.X_MAX]).isOpaque &&
                  Block.get(blocks[index+Chunk.X_MAX]).isOpaque &&
                  Block.get(blocks[index-Chunk.X_MAX*Chunk.Z_MAX]).isOpaque &&
                  Block.get(blocks[index+Chunk.X_MAX*Chunk.Z_MAX]).isOpaque) {
                octree.set(Block.STONE.id, x, cy - origin.y, z);
                continue;
              }
            }

            int metadata = 0xFF & data[index/2];
            metadata >>= (cx % 2) * 4;
            metadata &= 0xF;

            if (block == Block.STATIONARYWATER)
              block = Block.WATER;
            else if (block == Block.STATIONARYLAVA)
              block = Block.LAVA;

            int type = block.id;
            // store metadata
            switch (block.id) {
            case Block.VINES_ID:
              if (cy < 255) {
                // is this the top vine block?
                index = Chunk.chunkIndex(cx, cy+1, cz);
                Block above = Block.get(blocks[index]);
                if (above.isSolid) {
                  type = type | (1<<BlockData.VINE_TOP);
                }
              }
              break;

            case Block.WATER_ID:
              if (cy < 255) {
                // is there water above?
                index = Chunk.chunkIndex(cx, cy+1, cz);
                Block above = Block.get(blocks[index]);
                if (above.isWater()) {
                  type |= (1<<WaterModel.FULL_BLOCK);
                } else if (above == Block.LILY_PAD) {
                  type |= (1<<BlockData.LILY_PAD);
                  long wx = cp.x * 16L + cx;
                  long wy = cy + 1;
                  long wz = cp.z * 16L + cz;
                  long pr = (wx * 3129871L) ^ (wz * 116129781L) ^ (wy);
                  pr = pr * pr * 42317861L + pr * 11L;
                  int dir = 3 & (int)(pr >> 16);
                  type |= (dir<<BlockData.LILY_PAD_ROTATION);
                }
              }
              break;

            case Block.LAVA_ID:
              if (cy < 255) {
                // is there lava above?
                index = Chunk.chunkIndex(cx, cy+1, cz);
                Block above = Block.get(blocks[index]);
                if (above.isLava()) {
                  type = type | (1<<WaterModel.FULL_BLOCK);
                }
              }
              break;

            case Block.GRASS_ID:
              if (cy < 255) {
                // is it snow covered?
                index = Chunk.chunkIndex(cx, cy+1, cz);
                int blockAbove = 0xFF & blocks[index];
                if (blockAbove == Block.SNOW.id) {
                  type = type | (1 << 8);// 9th bit is the snow bit
                }
              }
              // fallthrough!

            case Block.WOODENDOOR_ID:
            case Block.IRONDOOR_ID:
            {
              int top = 0;
              int bottom = 0;
              if ((metadata & 8) != 0) {
                // this is the top part of the door
                top = metadata;
                if (cy > 0) {
                  bottom = 0xFF & data[Chunk.chunkIndex(cx, cy-1, cz)/2];
                  bottom >>= (cx % 2) * 4;// extract metadata
                  bottom &= 0xF;
                }
              } else {
                // this is the bottom part of the door
                bottom = metadata;
                if (cy < 255) {
                  top = 0xFF & data[Chunk.chunkIndex(cx, cy+1, cz)/2];
                  top >>= (cx % 2) * 4;// extract metadata
                  top &= 0xF;
                }
              }
              type |= (top << BlockData.DOOR_TOP);
              type |= (bottom << BlockData.DOOR_BOTTOM);
              break;
            }

            default:
              break;
            }
            type |= metadata << 8;
            if (block.isEmitter)
              emitters += 1;
            if (block.isInvisible)
              type = 0;
            octree.set(type, cx + cp.x*16 - origin.x,
                cy - origin.y, cz + cp.z*16 - origin.z);
          }
        }
      }
    }

    grassTexture = new WorldTexture();
    foliageTexture = new WorldTexture();

    Set<ChunkPosition> chunkSet = new HashSet<ChunkPosition>(chunksToLoad);

    task = "Finalizing octree";
    done = 0;
    for (ChunkPosition cp : chunksToLoad) {

      // finalize grass and foliage textures
      // box blur 3x3
      for (int x = 0; x < 16; ++x) {
        for (int z = 0; z < 16; ++z) {

          int nsum = 0;
          float[] grassMix = { 0, 0, 0 };
          float[] foliageMix = { 0, 0, 0 };
          for (int sx = x-1; sx <= x+1; ++sx) {
            int wx = cp.x*16 + sx;
            for (int sz = z-1; sz <= z+1; ++sz) {
              int wz = cp.z*16 + sz;

              ChunkPosition ccp = ChunkPosition.get(wx >> 4, wz >> 4);
              if (chunkSet.contains(ccp)) {
                nsum += 1;
                int biomeId = biomeIdMap.get(wx, wz);
                float[] grassColor = Biomes.getGrassColorLinear(biomeId);
                grassMix[0] += grassColor[0];
                grassMix[1] += grassColor[1];
                grassMix[2] += grassColor[2];
                float[] foliageColor = Biomes.getFoliageColorLinear(biomeId);
                foliageMix[0] += foliageColor[0];
                foliageMix[1] += foliageColor[1];
                foliageMix[2] += foliageColor[2];
              }
            }
          }

          grassMix[0] /= nsum;
          grassMix[1] /= nsum;
          grassMix[2] /= nsum;
          grassTexture.set(cp.x*16 + x - origin.x,
              cp.z*16 + z - origin.z, grassMix);

          foliageMix[0] /= nsum;
          foliageMix[1] /= nsum;
          foliageMix[2] /= nsum;
          foliageTexture.set(cp.x*16 + x - origin.x,
              cp.z*16 + z - origin.z, foliageMix);
        }
      }

      progressListener.setProgress(task, done, 0, target);
      done += 1;

      OctreeFinalizer.finalizeChunk(octree, origin, cp);
    }

    chunks = loadedChunks;

    camera.setWorldSize(1<<octree.depth);

    logger.info(String.format("Loaded %d chunks (%d emitters)",
        nchunks, emitters));
  }

  private int calculateOctreeOrigin(Collection<ChunkPosition> chunksToLoad) {

    int xmin = Integer.MAX_VALUE;
    int xmax = Integer.MIN_VALUE;
    int zmin = Integer.MAX_VALUE;
    int zmax = Integer.MIN_VALUE;
    for (ChunkPosition cp: chunksToLoad) {
      if (cp.x < xmin)
        xmin = cp.x;
      if (cp.x > xmax)
        xmax = cp.x;
      if (cp.z < zmin)
        zmin = cp.z;
      if (cp.z > zmax)
        zmax = cp.z;
    }

    xmax += 1;
    zmax += 1;
    xmin *= 16;
    xmax *= 16;
    zmin *= 16;
    zmax *= 16;

    int maxDimension = Math.max(Chunk.Y_MAX, Math.max(xmax-xmin, zmax-zmin));
    int requiredDepth = QuickMath.log2(QuickMath.nextPow2(maxDimension));

    int xroom = (1<<requiredDepth)-(xmax-xmin);
    int yroom = (1<<requiredDepth)-Chunk.Y_MAX;
    int zroom = (1<<requiredDepth)-(zmax-zmin);

    origin.set(xmin - xroom/2, -yroom/2, zmin - zroom/2);
    return requiredDepth;
  }

  /**
   * @return The currently loaded chunks
   */
  public Collection<ChunkPosition> loadedChunks() {
    return chunks;
  }

  /**
   * @return <code>true</code> if the scene has loaded chunks
   */
  public synchronized boolean haveLoadedChunks() {
    return !chunks.isEmpty();
  }

  /**
   * Calculate a camera position centered above all loaded chunks.
   * @return The calculated camera position
   */
  public Vector3d calcCenterCamera() {
    if (chunks.isEmpty())
      return new Vector3d(0, 128, 0);

    int xmin = Integer.MAX_VALUE;
    int xmax = Integer.MIN_VALUE;
    int zmin = Integer.MAX_VALUE;
    int zmax = Integer.MIN_VALUE;
    for (ChunkPosition cp: chunks) {
      if (cp.x < xmin)
        xmin = cp.x;
      if (cp.x > xmax)
        xmax = cp.x;
      if (cp.z < zmin)
        zmin = cp.z;
      if (cp.z > zmax)
        zmax = cp.z;
    }
    xmax += 1;
    zmax += 1;
    xmin *= 16;
    xmax *= 16;
    zmin *= 16;
    zmax *= 16;
    int xcenter = (xmax + xmin)/2;
    int zcenter = (zmax + zmin)/2;
    for (int y = Chunk.Y_MAX-1; y >= 0; --y) {
      int block = octree.get(xcenter - origin.x, y - origin.y,
          zcenter - origin.z);
      if (Block.get(block) != Block.AIR) {
        return new Vector3d(xcenter, y+5, zcenter);
      }
    }
    return new Vector3d(xcenter, 128, zcenter);
  }

  /**
   * Set the biome colors flag
   * @param value
   */
  public void setBiomeColorsEnabled(boolean value) {
    if (value != biomeColors) {
      biomeColors = value;
      refresh();
    }
  }

  /**
   * Center the camera over the loaded chunks
   */
  public synchronized void moveCameraToCenter() {
    camera.setPosition(calcCenterCamera());
  }

  /**
   * @return The name of this scene
   */
  public String name() {
    return name;
  }

  /**
   * @return <code>true</code> if this scene is to be Monte Carlo path traced
   */
  public boolean pathTrace() {
    return pathTrace;
  }

  /**
   * Toggle Monte Carlo path tracing.
   */
  public synchronized void toggleMonteCarlo() {
    pathTrace = !pathTrace;
    refresh();
  }

  /**
   * Start or resume path tracing
   */
  public synchronized void goHeadless() {
    pathTrace = true;
    pauseRender = false;
    notifyAll();
  }

  /**
   * @return {@code true} if the refresh happened
   * @throws InterruptedException
   */
  public synchronized boolean waitOnRefreshOrStateChange() throws InterruptedException {
    while ((!pathTrace || pauseRender) && !refresh)
      wait();
    if (refresh) {
      refresh = false;
      return true;
    }
    return false;
  }

  /**
   * @return <code>true</code> if the rendering of this scene should be
   * restarted
   */
  public boolean shouldRefresh() {
    return refresh;
  }

  /**
   * Wait while the rendering is paused
   * @throws InterruptedException
   */
  public synchronized void pauseWait() throws InterruptedException {
    while (pauseRender) {
      wait();
    }
  }

  /**
   * @return <code>true</code> if the rendering is paused
   */
  public synchronized boolean isPaused() {
    return pauseRender;
  }

  /**
   * Start rendering the scene.
   */
  public synchronized void startRender() {
    if (!pathTrace) {
      pathTrace = true;
      pauseRender = false;
      refresh();
    }
  }

  /**
   * Pause the renderer.
   */
  public synchronized void pauseRender() {
    pauseRender = true;
  }

  /**
   * Resume a paused render.
   */
  public synchronized void resumeRender() {
    pauseRender = false;
    notifyAll();
  }

  /**
   * Halt the rendering process.
   * Puts the renderer back in preview mode.
   */
  public synchronized void haltRender() {
    if (pathTrace) {
      pathTrace = false;
      pauseRender = false;
      resetRender = true;
      refresh();
    }
  }

  /**
   * Move the camera to the player position, if available.
   */
  public void moveCameraToPlayer() {
    camera.moveToPlayer(loadedWorld);
  }

  /**
   * @return <code>true</code> if still water is enabled
   */
  public boolean stillWaterEnabled() {
    return stillWater;
  }

  /**
   * @return <code>true</code> if biome colors are enabled
   */
  public boolean biomeColorsEnabled() {
    return biomeColors;
  }

  /**
   * Set the recursive ray depth limit
   * @param value
   */
  public synchronized void setRayDepth(int value) {
    value = Math.max(1, value);
    if (rayDepth != value) {
      rayDepth = value;
      PersistentSettings.setRayDepth(rayDepth);
    }
  }

  /**
   * @return Recursive ray depth limit
   */
  public int getRayDepth() {
    return rayDepth;
  }

  /**
   * Clear the scene refresh flag
   */
  synchronized public void setRefreshed() {
    refresh = false;
  }

  /**
   * Trace a ray in the Octree
   * @param ray
   */
  public void trace(Ray ray) {
    ray.d.set(0, 0, 1);
    ray.x.set(camera.getPosition());
    ray.x.x -= origin.x;
    ray.x.y -= origin.y;
    ray.x.z -= origin.z;
    camera.transform(ray.d);
    while (intersect(ray)) {
      if (ray.currentMaterial != 0) {
        ray.hit = true;
        break;
      }
    }
  }

  /**
   * Perform auto focus
   */
  public void autoFocus() {
    Ray ray = RayPool.getDefaultRayPool().get();
    trace(ray);
    if (!ray.hit) {
      camera.setDof(Double.POSITIVE_INFINITY);
    } else {
      camera.setSubjectDistance(ray.distance);
      camera.setDof(ray.distance*ray.distance);
    }
    RayPool.getDefaultRayPool().dispose(ray);
  }

  /**
   * @return The Octree object
   */
  public Octree getOctree() {
    return octree;
  }

  /**
   * @return World origin in the Octree
   */
  public Vector3i getOrigin() {
    return origin;
  }

  /**
   * Set the clear water flag
   * @param value
   */
  public void setClearWater(boolean value) {
    if (value != clearWater) {
      clearWater  = value;
      refresh();
    }
  }

  /**
   * @return <code>true</code> if clear water is enabled
   */
  public boolean getClearWater() {
    return clearWater;
  }

  /**
   * Set the scene name
   * @param newName
   */
  public void setName(String newName) {
    newName = SceneManager.sanitizedSceneName(newName);
    if (newName.length() > 0) {
      name = newName;
    }
  }

  /**
   * @return The current postprocessing mode
   */
  public Postprocess getPostprocess() {
    return postprocess;
  }

  /**
   * Change the postprocessing mode
   * @param p The new postprocessing mode
   */
  public synchronized void setPostprocess(Postprocess p) {
    postprocess = p;
    if (!pathTrace)
      refresh();
  }

  /**
   * @return The current emitter intensity
   */
  public double getEmitterIntensity() {
    return emitterIntensity;
  }

  /**
   * Set the emitter intensity
   * @param value
   */
  public void setEmitterIntensity(double value) {
    emitterIntensity = value;
    refresh();
  }

  /**
   * Set the atmospheric scattering flag
   * @param value
   */
  public void setAtmosphereEnabled(boolean value) {
    if (value != atmosphereEnabled) {
      atmosphereEnabled = value;
      refresh();
    }
  }

  /**
   * Set the volumetric fog flag
   * @param value
   */
  public void setVolumetricFogEnabled(boolean value) {
    if (value != volumetricFogEnabled) {
      volumetricFogEnabled = value;
      refresh();
    }
  }

  /**
   * @return <code>true</code> if atmospheric scattering is enabled
   */
  public boolean atmosphereEnabled() {
    return atmosphereEnabled;
  }

  /**
   * @return <code>true</code> if volumetric fog is enabled
   */
  public boolean volumetricFogEnabled() {
    return volumetricFogEnabled;
  }

  /**
   * Set the ocean water height
   * @param value
   */
  public void setWaterHeight(int value) {
    value = Math.max(0, value);
    value = Math.min(256, value);
    if (value != waterHeight) {
      waterHeight = value;
      refresh();
    }
  }

  /**
   * @return The ocean water height
   */
  public int getWaterHeight() {
    return waterHeight;
  }

  /**
   * @return the dumpFrequency
   */
  public int getDumpFrequency() {
    return dumpFrequency;
  }

  /**
   * @param value the dumpFrequency to set, if value is zero then render dumps
   * are disabled
   */
  public void setDumpFrequency(int value) {
    value = Math.max(0, value);
    if (value != dumpFrequency) {
      dumpFrequency = value;
    }
  }

  /**
   * @return the saveDumps
   */
  public boolean shouldSaveDumps() {
    return dumpFrequency > 0;
  }

  /**
   * Copy variables that do not require a render restart
   * @param other
   */
  public void copyTransients(Scene other) {
    name = other.name;
    postprocess = other.postprocess;
    exposure = other.exposure;
    dumpFrequency = other.dumpFrequency;
    saveSnapshots = other.saveSnapshots;
    sppTarget = other.sppTarget;
    cameraPresets = other.cameraPresets;
    rayDepth = other.rayDepth;
  }

  /**
   * @return The target SPP
   */
  public int getTargetSPP() {
    return sppTarget;
  }

  /**
   * @param value Target SPP value
   */
  public void setTargetSPP(int value) {
    sppTarget = value;
  }

  /**
   * Change the canvas size
   * @param canvasWidth
   * @param canvasHeight
   */
  public synchronized void setCanvasSize(int canvasWidth, int canvasHeight) {
    width = Math.max(MIN_CANVAS_WIDTH, canvasWidth);
    height = Math.max(MIN_CANVAS_HEIGHT, canvasHeight);
    initBuffers();
    refresh();
  }

  /**
   * @return Canvas width
   */
  public int canvasWidth() {
    return width;
  }

  /**
   * @return Canvas height
   */
  public int canvasHeight() {
    return height;
  }

  /**
   * Save a snapshot
   * @param directory
   * @param progressListener
   */
  public void saveSnapshot(File directory, ProgressListener progressListener) {

    if (directory == null) {
      logger.error("Fatal error: bad output directory!");
      return;
    }
    String fileName = name + "-" + spp + ".png";
    File targetFile = new File(directory, fileName);
    writePNG(buffer, targetFile, progressListener);
  }

  /**
   * @param targetFile
   * @param progressListener
   * @throws IOException
   */
  public synchronized void saveFrame(File targetFile,
      ProgressListener progressListener) throws IOException {

    for (int x = 0; x < width; ++x) {
      progressListener.setProgress("Finalizing frame", x+1, 0, width);
      for (int y = 0; y < height; ++y) {
        finalizePixel(x, y);
      }
    }

    writePNG(backBuffer, targetFile, progressListener);
  }

  /**
   * Write PNG image.
   * @param buffer
   * @param targetFile
   * @param progressListener
   */
  private void writePNG(BufferedImage buffer, File targetFile,
      ProgressListener progressListener) {
    try {
      progressListener.setProgress("Writing PNG", 0, 0, 1);
      PngFileWriter writer = new PngFileWriter(targetFile);
      writer.write(buffer, progressListener);
      if (camera.getProjectionMode() == ProjectionMode.PANORAMIC &&
          camera.getFoV() >= 179 && camera.getFoV() <= 181) {
        int height = buffer.getHeight();
        int width = buffer.getWidth();
        StringBuilder xmp = new StringBuilder();
        xmp.append("<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n");
        xmp.append(" <rdf:Description rdf:about=''\n");
        xmp.append("   xmlns:GPano='http://ns.google.com/photos/1.0/panorama/'>\n");
        xmp.append(" <GPano:CroppedAreaImageHeightPixels>");
        xmp.append(height);
        xmp.append("</GPano:CroppedAreaImageHeightPixels>\n");
        xmp.append(" <GPano:CroppedAreaImageWidthPixels>");
        xmp.append(width);
        xmp.append("</GPano:CroppedAreaImageWidthPixels>\n");
        xmp.append(" <GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>\n");
        xmp.append(" <GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>\n");
        xmp.append(" <GPano:FullPanoHeightPixels>");
        xmp.append(height);
        xmp.append("</GPano:FullPanoHeightPixels>\n");
        xmp.append(" <GPano:FullPanoWidthPixels>");
        xmp.append(width);
        xmp.append("</GPano:FullPanoWidthPixels>\n");
        xmp.append(" <GPano:ProjectionType>equirectangular</GPano:ProjectionType>\n");
        xmp.append(" <GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>\n");
        xmp.append(" </rdf:Description>\n");
        xmp.append(" </rdf:RDF>");
        ITXT iTXt = new ITXT("XML:com.adobe.xmp", xmp.toString());
        writer.writeChunk(iTXt);
      }
      writer.writeChunk(new IEND());
      writer.close();
    } catch (IOException e) {
      logger.warn("Failed to write PNG file: " + e.getMessage(), e);
    }

  }

  private synchronized void saveOctree(
      RenderContext context,
      ProgressListener progressListener) {

    String fileName = name + ".octree";
    DataOutputStream out = null;
    try {
      String task = "Saving octree";
      progressListener.setProgress(task, 1, 0, 2);
      logger.info("Saving octree " + fileName);
      out = new DataOutputStream(new GZIPOutputStream(
          context.getSceneFileOutputStream(fileName)));

      octree.store(out);

      progressListener.setProgress(task, 2, 0, 2);
      logger.info("Octree saved");
    } catch (IOException e) {
      logger.warn("IO exception while saving octree!", e);
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized void saveGrassTexture(
      RenderContext context,
      ProgressListener progressListener) {

    String fileName = name + ".grass";
    DataOutputStream out = null;
    try {
      String task = "Saving grass texture";
      progressListener.setProgress(task, 1, 0, 2);
      logger.info("Saving grass texture " + fileName);
      out = new DataOutputStream(new GZIPOutputStream(
          context.getSceneFileOutputStream(fileName)));

      grassTexture.store(out);

      progressListener.setProgress(task, 2, 0, 2);
      logger.info("Grass texture saved");
    } catch (IOException e) {
      logger.warn("IO exception while saving octree!", e);
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized void saveFoliageTexture(
      RenderContext context,
      ProgressListener progressListener) {

    String fileName = name + ".foliage";
    DataOutputStream out = null;
    try {
      String task = "Saving foliage texture";
      progressListener.setProgress(task, 1, 0, 2);
      logger.info("Saving foliage texture " + fileName);
      out = new DataOutputStream(new GZIPOutputStream(
          context.getSceneFileOutputStream(fileName)));

      foliageTexture.store(out);

      progressListener.setProgress(task, 2, 0, 2);
      logger.info("Foliage texture saved");
    } catch (IOException e) {
      logger.warn("IO exception while saving octree!", e);
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized void saveDump(
      RenderContext context,
      ProgressListener progressListener) {

    String fileName = name + ".dump";
    DataOutputStream out = null;
    try {
      String task = "Saving render dump";
      progressListener.setProgress(task, 1, 0, 2);
      logger.info("Saving render dump " + fileName);
      out = new DataOutputStream(new GZIPOutputStream(
          context.getSceneFileOutputStream(fileName)));
      out.writeInt(width);
      out.writeInt(height);
      out.writeInt(spp);
      out.writeLong(renderTime);
      for (int x = 0; x < width; ++x) {
        progressListener.setProgress(task, x+1, 0, width);
        for (int y = 0; y < height; ++y) {
          out.writeDouble(samples[(y*width+x)*3+0]);
          out.writeDouble(samples[(y*width+x)*3+1]);
          out.writeDouble(samples[(y*width+x)*3+2]);
        }
      }
      logger.info("Render dump saved");
    } catch (IOException e) {
      logger.warn("IO exception while saving render dump!", e);
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized boolean loadOctree(
      RenderContext context,
      RenderStatusListener renderListener) {

    String fileName = name + ".octree";

    DataInputStream in = null;
    try {
      String task = "Loading octree";
      renderListener.setProgress(task, 1, 0, 2);
      logger.info("Loading octree " + fileName);
      in = new DataInputStream(new GZIPInputStream(
          context.getSceneFileInputStream(fileName)));

      octree = Octree.load(in);

      renderListener.setProgress(task, 2, 0, 2);
      logger.info("Octree loaded");
      return true;
    } catch (IOException e) {
      logger.info("Failed to load chunk octree: Missing file or incorrect format!", e);
      return false;
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized boolean loadGrassTexture(
      RenderContext context,
      RenderStatusListener renderListener) {

    String fileName = name + ".grass";

    DataInputStream in = null;
    try {
      String task = "Loading grass texture";
      renderListener.setProgress(task, 1, 0, 2);
      logger.info("Loading grass texture " + fileName);
      in = new DataInputStream(new GZIPInputStream(
          context.getSceneFileInputStream(fileName)));

      grassTexture = WorldTexture.load(in);

      renderListener.setProgress(task, 2, 0, 2);
      logger.info("Grass texture loaded");
      return true;
    } catch (IOException e) {
      logger.info("Failed to load grass texture!");
      return false;
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private synchronized boolean loadFoliageTexture(
      RenderContext context,
      RenderStatusListener renderListener) {

    String fileName = name + ".foliage";

    DataInputStream in = null;
    try {
      String task = "Loading foliage texture";
      renderListener.setProgress(task, 1, 0, 2);
      logger.info("Loading foliage texture " + fileName);
      in = new DataInputStream(new GZIPInputStream(
          context.getSceneFileInputStream(fileName)));

      foliageTexture = WorldTexture.load(in);

      renderListener.setProgress(task, 2, 0, 2);
      logger.info("Foliage texture loaded");
      return true;
    } catch (IOException e) {
      logger.info("Failed to load foliage texture!");
      return false;
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
        }
      }
    }
  }

  public synchronized void loadDump(
      RenderContext context,
      RenderStatusListener renderListener) {

    String fileName = name + ".dump";

    DataInputStream in = null;
    try {
      in = new DataInputStream(new GZIPInputStream(
          context.getSceneFileInputStream(fileName)));

      String task = "Loading render dump";
      renderListener.setProgress(task, 1, 0, 2);
      logger.info("Loading render dump " + fileName);
      int dumpWidth = in.readInt();
      int dumpHeight= in.readInt();
      if (dumpWidth != width || dumpHeight != height) {
        logger.warn("Render dump discarded: incorrect width or height!");
        in.close();
        return;
      }
      spp = in.readInt();
      renderTime = in.readLong();

      // Update render status
      renderListener.setSPP(spp);
      renderListener.setRenderTime(renderTime);
      long totalSamples = spp * ((long) (width * height));
      renderListener.setSamplesPerSecond(
          (int) (totalSamples / (renderTime / 1000.0)));

      for (int x = 0; x < width; ++x) {
        renderListener.setProgress(task, x+1, 0, width);
        for (int y = 0; y < height; ++y) {
          samples[(y*width+x)*3+0] = in.readDouble();
          samples[(y*width+x)*3+1] = in.readDouble();
          samples[(y*width+x)*3+2] = in.readDouble();
          finalizePixel(x, y);
        }
      }
      logger.info("Render dump loaded");
    } catch (IOException e) {
      logger.info("Render dump not loaded");
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
        }
      }
    }
  }

  /**
   * Finalize a pixel. Calculates the resulting RGB color values for
   * the pixel and sets these in the bitmap image.
   * @param x
   * @param y
   */
  public void finalizePixel(int x, int y) {
    finalized = true;

    double r = samples[(y*width+x)*3+0];
    double g = samples[(y*width+x)*3+1];
    double b = samples[(y*width+x)*3+2];

    r *= exposure;
    g *= exposure;
    b *= exposure;

    if (pathTrace()) {
      switch (postprocess) {
      case NONE:
        break;
      case TONEMAP1:
        // http://filmicgames.com/archives/75
        r = QuickMath.max(0, r-0.004);
        r = (r*(6.2*r + .5)) / (r * (6.2*r + 1.7) + 0.06);
        g = QuickMath.max(0, g-0.004);
        g = (g*(6.2*g + .5)) / (g * (6.2*g + 1.7) + 0.06);
        b = QuickMath.max(0, b-0.004);
        b = (b*(6.2*b + .5)) / (b * (6.2*b + 1.7) + 0.06);
        break;
      case GAMMA:
        r = FastMath.pow(r, 1/DEFAULT_GAMMA);
        g = FastMath.pow(g, 1/DEFAULT_GAMMA);
        b = FastMath.pow(b, 1/DEFAULT_GAMMA);
        break;
      }
    } else {
      r = FastMath.sqrt(r);
      g = FastMath.sqrt(g);
      b = FastMath.sqrt(b);
    }

    r = QuickMath.min(1, r);
    g = QuickMath.min(1, g);
    b = QuickMath.min(1, b);

    bufferData[y*width + x] = Color.getRGB(r, g, b);
  }

  /**
   * Copies a pixel in-buffer
   * @param jobId
   * @param offset
   */
  public void copyPixel(int jobId, int offset) {
    bufferData[jobId + offset] = bufferData[jobId];
  }

  /**
   * Update the canvas - draw the latest rendered frame
   * @param warningText
   */
  public synchronized void updateCanvas(String warningText) {
    finalized = false;

    try {
      // flip buffers
      BufferedImage tmp = buffer;
      buffer = backBuffer;
      backBuffer = tmp;

      bufferData = ((DataBufferInt) backBuffer.getRaster().getDataBuffer()).getData();

      Graphics g = buffer.getGraphics();

      if (!warningText.isEmpty()) {
        g.setColor(java.awt.Color.red);
        int x0 = width/2;
        int y0 = height/2;
        g.setFont(infoFont);
        if (fontMetrics == null) {
          fontMetrics = g.getFontMetrics();
        }
        g.drawString(warningText,
            x0 - fontMetrics.stringWidth(warningText)/2, y0);
      } else {

        if (!pathTrace()) {
          int x0 = width/2;
          int y0 = height/2;
          g.setColor(java.awt.Color.white);
          g.drawLine(x0, y0-4, x0, y0+4);
          g.drawLine(x0-4, y0, x0+4, y0);
          Ray ray = rayPool.get();
          trace(ray);
          g.setFont(infoFont);
          if (ray.hit) {
            Block block = ray.getCurrentBlock();
            g.drawString(String.format("target: %.2f m", ray.distance), 5, height-18);
            g.drawString(String.format("[0x%08X] %s (%s)",
                ray.currentMaterial,
                block,
                block.description(ray.getBlockData())),
                5, height-5);
          }
          Vector3d pos = camera.getPosition();
          g.drawString(String.format("(%.1f, %.1f, %.1f)",
              pos.x, pos.y, pos.z), 5, 11);
          rayPool.dispose(ray);
        }
      }

      g.dispose();
    } catch (IllegalStateException e) {
      logger.error("Unexpected exception while rendering back buffer", e);
    }
  }

  /**
   * Prepare the front buffer for rendering by flipping the back and front buffer.
   * Draw status text on the front buffer.
   */
  public void updateCanvas() {
    if (!finalized)
      return;
    updateCanvas(haveLoadedChunks() ? "" : "No chunks loaded!");
  }

  /**
   * Draw the buffered image to a canvas
   * @param g The graphics object of the canvas to draw on
   * @param canvasWidth The canvas width
   * @param canvasHeight The canvas height
   */
  public synchronized void drawBufferedImage(Graphics g, int canvasWidth, int canvasHeight) {
    g.drawImage(buffer, 0, 0, canvasWidth, canvasHeight, null);
  }

  /**
   * Get direct access to the sample buffer
   * @return The sample buffer for this scene
   */
  public double[] getSampleBuffer() {
    return samples;
  }

  /**
   * @return <code>true</code> if the rendered buffer should be finalized
   */
  public boolean finalizeBuffer() {
    return finalizeBuffer;
  }

  /**
   * Enable or disable buffer finalization
   * @param value
   */
  public void setBufferFinalization(boolean value) {
    finalizeBuffer = value;
  }

  /**
   * @param x X coordinate in octree space
   * @param z Z coordinate in octree space
   * @return Foliage color for the given coordinates
   */
  public float[] getFoliageColor(int x, int z) {
    if (biomeColors) {
      return foliageTexture.get(x, z);
    } else {
      return Biomes.getFoliageColorLinear(0);
    }
  }

  /**
   * @param x X coordinate in octree space
   * @param z Z coordinate in octree space
   * @return Grass color for the given coordinates
   */
  public float[] getGrassColor(int x, int z) {
    if (biomeColors) {
      return grassTexture.get(x, z);
    } else {
      return Biomes.getGrassColorLinear(0);
    }
  }

  /**
   * Merge a render dump into this scene
   * @param dumpFile
   * @param renderListener
   */
  public void mergeDump(File dumpFile, RenderStatusListener renderListener) {
    int dumpSpp;
    long dumpTime;
    DataInputStream in = null;
    try {
      in = new DataInputStream(new GZIPInputStream(
          new FileInputStream(dumpFile)));

      String task = "Merging render dump";
      renderListener.setProgress(task, 1, 0, 2);
      logger.info("Loading render dump " + dumpFile.getAbsolutePath());
      int dumpWidth = in.readInt();
      int dumpHeight= in.readInt();
      if (dumpWidth != width || dumpHeight != height) {
        logger.warn("Render dump discarded: incorrect widht or height!");
        in.close();
        return;
      }
      dumpSpp = in.readInt();
      dumpTime = in.readLong();

      double sa = spp / (double) (spp + dumpSpp);
      double sb = 1 - sa;

      for (int x = 0; x < width; ++x) {
        renderListener.setProgress(task, x+1, 0, width);
        for (int y = 0; y < height; ++y) {
          samples[(y*width+x)*3+0] = samples[(y*width+x)*3+0] * sa
              + in.readDouble() * sb;
          samples[(y*width+x)*3+1] = samples[(y*width+x)*3+1] * sa
              + in.readDouble() * sb;
          samples[(y*width+x)*3+2] = samples[(y*width+x)*3+2] * sa
              + in.readDouble() * sb;
          finalizePixel(x, y);
        }
      }
      logger.info("Render dump loaded");

      // Update render status
      spp += dumpSpp;
      renderTime += dumpTime;
      renderListener.setSPP(spp);
      renderListener.setRenderTime(renderTime);
      long totalSamples = spp * ((long) (width * height));
      renderListener.setSamplesPerSecond(
          (int) (totalSamples / (renderTime / 1000.0)));

    } catch (IOException e) {
      logger.info("Render dump not loaded");
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
        }
      }
    }
  }

  public void setSaveSnapshots(boolean value) {
    saveSnapshots = value;
  }

  public boolean shouldSaveSnapshots() {
    return saveSnapshots;
  }

  public boolean shouldReset() {
    if (resetRender) {
      resetRender = false;
      return true;
    }
    return false;
  }

  synchronized public void resetRender() {
    resetRender = true;
    refresh();
  }

}
TOP

Related Classes of se.llbit.chunky.renderer.scene.Scene

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.