Java tutorial
/* 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.BufferedOutputStream; 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.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; 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 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.RenderState; import se.llbit.chunky.renderer.RenderStatusListener; import se.llbit.chunky.renderer.WorkerState; import se.llbit.chunky.renderer.projection.ProjectionMode; 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.chunky.world.entity.Entity; import se.llbit.chunky.world.entity.PaintingEntity; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.log.Log; import se.llbit.math.BVH; import se.llbit.math.Color; import se.llbit.math.Octree; import se.llbit.math.OctreeVisitor; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3d; import se.llbit.math.Vector3i; import se.llbit.math.primitive.Primitive; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.ListTag; 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 Font infoFont = new Font("Sans serif", Font.BOLD, 11); private static FontMetrics fontMetrics; 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 MIN_WATER_VISIBILITY = 0; //private static final double MAX_WATER_VISIBILITY = 62; /** * Default exposure */ public static final double DEFAULT_EXPOSURE = 1.0; /** * World */ private World loadedWorld; /** * Octree origin */ protected Vector3i origin = new Vector3i(); /** * Octree */ private Octree worldOctree; private List<Primitive> primitives = new LinkedList<Primitive>(); /** * Entities in the scene */ private Collection<Entity> entities = new LinkedList<Entity>(); private BVH bvh = new BVH(Collections.<Primitive>emptyList()); // 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 byte[] alphaChannel; private boolean finalized = false; private boolean finalizeBuffer = false; /** * Indicates if the render should be forced to reset. If false, the user may be * asked to confirm the render reset. */ private boolean resetRender = false; /** * Create an empty scene with default canvas width and height. */ public Scene() { worldOctree = new Octree(1); 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(); alphaChannel = new byte[width * height]; samples = new double[width * height * 3]; } /** * Clone other scene * @param other */ public Scene(Scene other) { set(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 worldOctree = other.worldOctree; primitives = other.primitives; entities = other.entities; bvh = other.bvh; grassTexture = other.grassTexture; foliageTexture = other.foliageTexture; origin.set(other.origin); chunks = other.chunks; exposure = other.exposure; name = other.name; stillWater = other.stillWater; waterOpacity = other.waterOpacity; waterVisibility = other.waterVisibility; useCustomWaterColor = other.useCustomWaterColor; waterColor.set(other.waterColor); biomeColors = other.biomeColors; sunEnabled = other.sunEnabled; emittersEnabled = other.emittersEnabled; emitterIntensity = other.emitterIntensity; atmosphereEnabled = other.atmosphereEnabled; transparentSky = other.transparentSky; 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; alphaChannel = other.alphaChannel; samples = other.samples; bufferData = other.bufferData; } } /** * 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); BufferedOutputStream out = new BufferedOutputStream(context.getSceneDescriptionOutputStream(name)); saveDescription(out); 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) { Log.warn("Old scene version detected! The scene may not have been loaded correctly."); } else if (sdfVersion > SDF_VERSION) { Log.warn( "This scene was created with a newer version of Chunky! The scene may not have been 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 { Log.info("Could not load world: " + worldPath); } } if (renderState == RenderState.RENDERING) { renderState = RenderState.PAUSED; } refresh = false; loadDump(context, renderListener); if (loadOctree(context, renderListener)) { 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) { Log.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 (renderState == RenderState.PREVIEW) { // don't interrupt the render if we are currently rendering 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.o.x -= origin.x; state.ray.o.y -= origin.y; state.ray.o.z -= origin.z; RayTracer.quickTrace(this, state); } /** * Path trace the ray in this scene * @param state */ public void pathTrace(WorkerState state) { state.ray.o.x -= origin.x; state.ray.o.y -= origin.y; state.ray.o.z -= origin.z; PathTracer.pathTrace(this, state); } /** * Find closest intersection between ray and scene * @param ray * @return <code>true</code> if an intersection was found */ public boolean intersect(Ray ray) { boolean hit = false; if (bvh.closestIntersection(ray)) { hit = true; } Ray oct = new Ray(ray); oct.setCurrentMat(ray.getPrevMaterial(), ray.getPrevData()); if (worldOctree.intersect(this, oct) && oct.distance < ray.t) { ray.t = oct.distance; ray.distance += oct.distance; ray.o.set(oct.o); ray.n.set(oct.n); ray.color.set(oct.color); ray.setPrevMat(oct.getPrevMaterial(), oct.getPrevData()); ray.setCurrentMat(oct.getCurrentMaterial(), oct.getCurrentData()); updateOpacity(ray); return true; } if (hit) { ray.distance += ray.t; ray.o.scaleAdd(ray.t, ray.d); updateOpacity(ray); return true; } return false; } public void updateOpacity(Ray ray) { if (ray.getCurrentMaterial() == Block.WATER || (ray.getCurrentMaterial() == Block.AIR && ray.getPrevMaterial() == Block.WATER)) { if (useCustomWaterColor) { ray.color.x = waterColor.x; ray.color.y = waterColor.y; ray.color.z = waterColor.z; } ray.color.w = waterOpacity; } } /** * Test if the ray should be killed (Russian Roulette) * @param depth * @param random * @return {@code true} if the ray needs to die now */ protected final boolean kill(int depth, Random random) { return depth >= rayDepth && random.nextDouble() < .5f; } /** * Reload all loaded chunks. * @param progressListener */ public synchronized void reloadChunks(ProgressListener progressListener) { if (loadedWorld == null) { Log.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 worldOctree = new Octree(requiredDepth); if (waterHeight > 0) { for (int x = 0; x < (1 << worldOctree.depth); ++x) { for (int z = 0; z < (1 << worldOctree.depth); ++z) { for (int y = -origin.y; y < (-origin.y) + waterHeight - 1; ++y) { worldOctree.set(Block.WATER_ID | (1 << WaterModel.FULL_BLOCK), x, y, z); } } } for (int x = 0; x < (1 << worldOctree.depth); ++x) { for (int z = 0; z < (1 << worldOctree.depth); ++z) { worldOctree.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.getRegion(region).parse(); } entities = new LinkedList<Entity>(); int ycutoff = PersistentSettings.getYCutoff(); ycutoff = Math.max(0, ycutoff); 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); Collection<CompoundTag> tileEnts = new LinkedList<CompoundTag>(); Collection<CompoundTag> ents = new LinkedList<CompoundTag>(); world.getChunk(cp).getBlockData(blocks, data, biomes, tileEnts, ents); nchunks += 1; int wx0 = cp.x * 16; int wz0 = cp.z * 16; for (int cz = 0; cz < 16; ++cz) { int wz = cz + wz0; for (int cx = 0; cx < 16; ++cx) { int wx = cx + wx0; int biomeId = 0xFF & biomes[Chunk.chunkXZIndex(cx, cz)]; biomeIdMap.set(biomeId, wx, wz); } } // load entities for (CompoundTag tag : ents) { if (tag.get("id").stringValue("").equals("Painting")) { ListTag pos = (ListTag) tag.get("Pos"); double x = pos.getItem(0).doubleValue(); double y = pos.getItem(1).doubleValue(); double z = pos.getItem(2).doubleValue(); ListTag rot = (ListTag) tag.get("Rotation"); double yaw = rot.getItem(0).floatValue(); //double pitch = rot.getItem(1).floatValue(); entities.add(new PaintingEntity(new Vector3d(x, y, z), tag.get("Motive").stringValue(), yaw)); } } for (int cy = ycutoff; 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) { worldOctree.set(Block.STONE_ID, x, cy - origin.y, z); continue; } } int metadata = 0xFF & data[index / 2]; metadata >>= (cx % 2) * 4; metadata &= 0xF; 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.STATIONARYWATER_ID: type = Block.WATER_ID; 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.STATIONARYLAVA_ID: type = Block.LAVA_ID; 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: case Block.SPRUCEDOOR_ID: case Block.BIRCHDOOR_ID: case Block.JUNGLEDOOR_ID: case Block.ACACIADOOR_ID: case Block.DARKOAKDOOR_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; worldOctree.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(worldOctree, origin, cp); } chunks = loadedChunks; camera.setWorldSize(1 << worldOctree.depth); buildBVH(); Log.info(String.format("Loaded %d chunks (%d emitters)", nchunks, emitters)); } private void buildBVH() { primitives = new LinkedList<Primitive>(); final List<Primitive> list = primitives; worldOctree.visit(new OctreeVisitor() { @Override public void visit(int data, int x, int y, int z, int size) { if ((data & 0xF) == Block.WATER_ID) { WaterModel.addPrimitives(list, data, x, y, z, 1 << size); } } }); Vector3d worldOffset = new Vector3d(-origin.x, -origin.y, -origin.z); for (Entity ent : entities) { primitives.addAll(ent.primitives(worldOffset)); } bvh = new BVH(primitives); } 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 = worldOctree.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; } /** * Start rendering */ public synchronized void startHeadlessRender() { renderState = RenderState.RENDERING; notifyAll(); } /** * @return {@code true} if the refresh happened * @throws InterruptedException */ public synchronized boolean waitOnRefreshOrStateChange() throws InterruptedException { while (renderState != RenderState.RENDERING && !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 (renderState == RenderState.PAUSED) { wait(); } } /** * Start rendering the scene. */ public synchronized void startRender() { if (renderState != RenderState.RENDERING) { renderState = RenderState.RENDERING; refresh(); } } /** * Pause the renderer. */ public synchronized void pauseRender() { renderState = RenderState.PAUSED; } /** * Resume a paused render. */ public synchronized void resumeRender() { renderState = RenderState.RENDERING; notifyAll(); } /** * Halt the rendering process. * Puts the renderer back in preview mode. */ public synchronized void haltRender() { if (renderState != RenderState.PREVIEW) { renderState = RenderState.PREVIEW; 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 * @return {@code true} if the ray hit something */ public boolean trace(Ray ray) { WorkerState state = new WorkerState(); state.ray = ray; if (isInWater(ray)) { ray.setCurrentMat(Block.WATER, 0); } else { ray.setCurrentMat(Block.AIR, 0); } ray.d.set(0, 0, 1); ray.o.set(camera.getPosition()); ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; camera.transform(ray.d); while (RayTracer.nextIntersection(this, ray, state)) { if (ray.getCurrentMaterial() != Block.AIR) { return true; } } return false; } /** * Perform auto focus */ public void autoFocus() { Ray ray = new Ray(); if (!trace(ray)) { camera.setDof(Double.POSITIVE_INFINITY); } else { camera.setSubjectDistance(ray.distance); camera.setDof(ray.distance * ray.distance); } } /** * @return The Octree object */ public Octree getOctree() { return worldOctree; } /** * @return World origin in the Octree */ public Vector3i getOrigin() { return origin; } /** * 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 (renderState == RenderState.PREVIEW) { // don't interrupt the render if we are currently rendering 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 transparent sky option. * @param value */ public void setTransparentSky(boolean value) { if (value != transparentSky) { transparentSky = 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} if transparent sky is enabled */ public boolean transparentSky() { return transparentSky; } /** * @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; renderState = other.renderState; } /** * @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) { Log.error("Fatal error: bad output directory!"); return; } String fileName = name + "-" + spp + ".png"; File targetFile = new File(directory, fileName); computeAlpha(progressListener); finalizeFrame(progressListener); writePNG(buffer, targetFile, progressListener); } /** * @param targetFile * @param progressListener * @throws IOException */ public synchronized void saveFrame(File targetFile, ProgressListener progressListener) throws IOException { computeAlpha(progressListener); finalizeFrame(progressListener); writePNG(backBuffer, targetFile, progressListener); } /** * Compute the alpha channel. * @param progressListener */ private void computeAlpha(ProgressListener progressListener) { if (transparentSky) { WorkerState state = new WorkerState(); state.ray = new Ray(); for (int x = 0; x < width; ++x) { progressListener.setProgress("Computing alpha channel", x + 1, 0, width); for (int y = 0; y < height; ++y) { computeAlpha(x, y, state); } } } } public void finalizeFrame(ProgressListener progressListener) { if (!finalized) { 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); } } } } /** * 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); if (transparentSky) { writer.write(buffer, alphaChannel, progressListener); } else { 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) { Log.warn("Failed to write PNG file: " + targetFile.getAbsolutePath(), e); } } private synchronized void saveOctree(RenderContext context, ProgressListener progressListener) { String fileName = name + ".octree"; DataOutputStream out = null; try { if (context.fileUnchangedSince(fileName, worldOctree.getTimestamp())) { Log.info("Skipping redundant Octree write"); return; } String task = "Saving octree"; progressListener.setProgress(task, 1, 0, 2); Log.info("Saving octree " + fileName); out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName))); worldOctree.store(out); out.close(); out = null; worldOctree.setTimestamp(context.fileTimestamp(fileName)); progressListener.setProgress(task, 2, 0, 2); Log.info("Octree saved"); } catch (IOException e) { Log.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 { if (context.fileUnchangedSince(fileName, grassTexture.getTimestamp())) { Log.info("Skipping redundant grass texture write"); return; } String task = "Saving grass texture"; progressListener.setProgress(task, 1, 0, 2); Log.info("Saving grass texture " + fileName); out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName))); grassTexture.store(out); out.close(); out = null; grassTexture.setTimestamp(context.fileTimestamp(fileName)); progressListener.setProgress(task, 2, 0, 2); Log.info("Grass texture saved"); } catch (IOException e) { Log.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 { if (context.fileUnchangedSince(fileName, foliageTexture.getTimestamp())) { Log.info("Skipping redundant foliage texture write"); return; } String task = "Saving foliage texture"; progressListener.setProgress(task, 1, 0, 2); Log.info("Saving foliage texture " + fileName); out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName))); foliageTexture.store(out); out.close(); out = null; foliageTexture.setTimestamp(context.fileTimestamp(fileName)); progressListener.setProgress(task, 2, 0, 2); Log.info("Foliage texture saved"); } catch (IOException e) { Log.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); Log.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]); } } Log.info("Render dump saved"); } catch (IOException e) { Log.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); Log.info("Loading octree " + fileName); in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName))); worldOctree = Octree.load(in); in.close(); in = null; worldOctree.setTimestamp(context.fileTimestamp(fileName)); renderListener.setProgress(task, 2, 0, 2); Log.info("Octree loaded"); calculateOctreeOrigin(chunks); camera.setWorldSize(1 << worldOctree.depth); buildBVH(); return true; } catch (IOException e) { Log.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); Log.info("Loading grass texture " + fileName); in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName))); grassTexture = WorldTexture.load(in); in.close(); in = null; grassTexture.setTimestamp(context.fileTimestamp(fileName)); renderListener.setProgress(task, 2, 0, 2); Log.info("Grass texture loaded"); return true; } catch (IOException e) { Log.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); Log.info("Loading foliage texture " + fileName); in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName))); foliageTexture = WorldTexture.load(in); in.close(); in = null; foliageTexture.setTimestamp(context.fileTimestamp(fileName)); renderListener.setProgress(task, 2, 0, 2); Log.info("Foliage texture loaded"); return true; } catch (IOException e) { Log.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); Log.info("Loading render dump " + fileName); int dumpWidth = in.readInt(); int dumpHeight = in.readInt(); if (dumpWidth != width || dumpHeight != height) { Log.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); } } Log.info("Render dump loaded"); } catch (IOException e) { Log.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 (renderState != RenderState.PREVIEW) { 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); } /** * Compute the alpha channel based on sky visibility. * @param x * @param y */ public void computeAlpha(int x, int y, WorkerState state) { Ray ray = state.ray; double halfWidth = width / (2.0 * height); double invHeight = 1.0 / height; // rotated grid supersampling camera.calcViewRay(ray, -halfWidth + (x - 3 / 8.0) * invHeight, -.5 + (y + 1 / 8.0) * invHeight); ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; double occlusion = RayTracer.skyOcclusion(this, state); camera.calcViewRay(ray, -halfWidth + (x + 1 / 8.0) * invHeight, -.5 + (y + 3 / 8.0) * invHeight); ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; occlusion += RayTracer.skyOcclusion(this, state); camera.calcViewRay(ray, -halfWidth + (x - 1 / 8.0) * invHeight, -.5 + (y - 3 / 8.0) * invHeight); ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; occlusion += RayTracer.skyOcclusion(this, state); camera.calcViewRay(ray, -halfWidth + (x + 3 / 8.0) * invHeight, -.5 + (y - 1 / 8.0) * invHeight); ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; occlusion += RayTracer.skyOcclusion(this, state); alphaChannel[y * width + x] = (byte) (255 * occlusion * 0.25 + 0.5); } /** * 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 (renderState == RenderState.PREVIEW) { 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); g.setFont(infoFont); Ray ray = new Ray(); if (trace(ray) && ray.getCurrentMaterial() instanceof Block) { Block block = (Block) ray.getCurrentMaterial(); g.drawString(String.format("target: %.2f m", ray.distance), 5, height - 18); g.drawString(String.format("[0x%08X] %s (%s)", ray.getCurrentData(), 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); } } g.dispose(); } catch (IllegalStateException e) { Log.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, 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 shouldFinalizeBuffer() { 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); Log.info("Loading render dump " + dumpFile.getAbsolutePath()); int dumpWidth = in.readInt(); int dumpHeight = in.readInt(); if (dumpWidth != width || dumpHeight != height) { Log.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); } } Log.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) { Log.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(); } public boolean isInWater(Ray ray) { if (worldOctree.isInside(ray.o)) { int x = (int) QuickMath.floor(ray.o.x); int y = (int) QuickMath.floor(ray.o.y); int z = (int) QuickMath.floor(ray.o.z); int block = worldOctree.get(x, y, z); if ((block & 0xF) != Block.WATER_ID) { return false; } return (ray.o.y - y) < 0.875 || block == (Block.WATER_ID | (1 << WaterModel.FULL_BLOCK)); } else { return waterHeight > 0 && ray.o.y < waterHeight - 0.125; } } public boolean isInsideOctree(Vector3d vec) { return worldOctree.isInside(vec); } public double getWaterOpacity() { return waterOpacity; } public void setWaterOpacity(double opacity) { if (opacity != waterOpacity) { this.waterOpacity = opacity; refresh(); } } public double getWaterVisibility() { return waterVisibility; } public void setWaterVisibility(double visibility) { if (visibility != waterVisibility) { this.waterVisibility = visibility; refresh(); } } public Vector3d getWaterColor() { return waterColor; } public void setWaterColor(Vector3d color) { waterColor.set(color); refresh(); } public boolean getUseCustomWaterColor() { return useCustomWaterColor; } public void setUseCustomWaterColor(boolean value) { if (value != useCustomWaterColor) { useCustomWaterColor = value; refresh(); } } @Override public synchronized JsonObject toJson() { JsonObject obj = super.toJson(); JsonArray entityArray = new JsonArray(); for (Entity entity : entities) { entityArray.add(entity.toJson()); } if (entityArray.getNumElement() > 0) { obj.add("entities", entityArray); } return obj; } @Override public synchronized void fromJson(JsonObject desc) { super.fromJson(desc); entities = new LinkedList<Entity>(); for (JsonValue element : desc.get("entities").array().getElementList()) { Entity entity = Entity.fromJson(element.object()); if (entity != null) { entities.add(entity); } } } }