/* Copyright (c) 2012-2016 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 org.apache.commons.math3.util.FastMath;
import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.model.WaterModel;
import se.llbit.chunky.renderer.OutputMode;
import se.llbit.chunky.renderer.Postprocess;
import se.llbit.chunky.renderer.Refreshable;
import se.llbit.chunky.renderer.RenderContext;
import se.llbit.chunky.renderer.RenderMode;
import se.llbit.chunky.renderer.ResetReason;
import se.llbit.chunky.renderer.WorkerState;
import se.llbit.chunky.renderer.projection.ProjectionMode;
import se.llbit.chunky.resources.BitmapImage;
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.Material;
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.chunky.world.entity.PlayerEntity;
import se.llbit.chunky.world.entity.SignEntity;
import se.llbit.chunky.world.entity.SkullEntity;
import se.llbit.chunky.world.entity.WallSignEntity;
import se.llbit.json.JsonArray;
import se.llbit.json.JsonMember;
import se.llbit.json.JsonObject;
import se.llbit.json.JsonParser;
import se.llbit.json.JsonValue;
import se.llbit.json.PrettyPrinter;
import se.llbit.log.Log;
import se.llbit.math.BVH;
import se.llbit.math.ColorUtil;
import se.llbit.math.Octree;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import se.llbit.math.Vector3;
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;
import se.llbit.tiff.TiffFileWriter;
import se.llbit.util.JsonSerializable;
import se.llbit.util.MCDownloader;
import se.llbit.util.TaskTracker;
import se.llbit.util.ZipExport;
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.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Encapsulates scene and render state.
*
* <p>Render state is stored in a sample buffer. Two frame buffers
* are also kept for when a snapshot should be rendered.
*/
public class Scene implements JsonSerializable, Refreshable {
public static final int DEFAULT_DUMP_FREQUENCY = 500;
public static final String EXTENSION = ".json";
/** The current Scene Description Format (SDF) version. */
public static final int SDF_VERSION = 8;
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. */
public static final float SPECULAR_COEFF = 0.04f;
/**
* Default water specular reflection coefficient.
*/
public static final float WATER_SPECULAR = 0.12f;
/**
* 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;
public static final boolean DEFAULT_EMITTERS_ENABLED = false;
/**
* 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;
/**
* Default exposure.
*/
public static final double DEFAULT_EXPOSURE = 1.0;
/**
* Default fog density.
*/
public static final double DEFAULT_FOG_DENSITY = 0.0;
protected final Sky sky = new Sky(this);
protected final Camera camera = new Camera(this);
protected final Sun sun = new Sun(this);
protected final Vector3 waterColor =
new Vector3(PersistentSettings.getWaterColorRed(), PersistentSettings.getWaterColorGreen(),
PersistentSettings.getWaterColorBlue());
protected final Vector3 fogColor =
new Vector3(PersistentSettings.getFogColorRed(), PersistentSettings.getFogColorGreen(),
PersistentSettings.getFogColorBlue());
public int sdfVersion = -1;
public String name = "default";
/**
* Canvas width.
*/
public int width;
/**
* Canvas height.
*/
public int height;
public Postprocess postprocess = Postprocess.DEFAULT;
public OutputMode outputMode = OutputMode.DEFAULT;
public long renderTime;
/**
* Current SPP for the scene.
*/
public int spp = 0;
protected double exposure = DEFAULT_EXPOSURE;
/**
* Target SPP for the scene.
*/
protected int sppTarget = PersistentSettings.getSppTargetDefault();
/**
* Recursive ray depth limit (not including Russian Roulette).
*/
protected int rayDepth = PersistentSettings.getRayDepthDefault();
protected String worldPath = "";
protected int worldDimension = 0;
protected RenderMode mode = RenderMode.PREVIEW;
protected int dumpFrequency = DEFAULT_DUMP_FREQUENCY;
protected boolean saveSnapshots = false;
protected boolean emittersEnabled = DEFAULT_EMITTERS_ENABLED;
protected double emitterIntensity = DEFAULT_EMITTER_INTENSITY;
protected boolean sunEnabled = true;
/**
* Water opacity modifier.
*/
protected double waterOpacity = PersistentSettings.getWaterOpacity();
protected double waterVisibility = PersistentSettings.getWaterVisibility();
protected int waterHeight = PersistentSettings.getWaterHeight();
protected boolean stillWater = PersistentSettings.getStillWater();
protected boolean useCustomWaterColor = PersistentSettings.getUseCustomWaterColor();
/**
* Enables fast fog algorithm
*/
protected boolean fastFog = true;
/**
* Fog thickness.
*/
protected double fogDensity = DEFAULT_FOG_DENSITY;
protected boolean biomeColors = true;
protected boolean transparentSky = false;
protected boolean renderActors = true;
protected Collection<ChunkPosition> chunks = new ArrayList<>();
protected JsonObject cameraPresets = new JsonObject();
/**
* Indicates if the render should be forced to reset.
*/
protected ResetReason resetReason = ResetReason.NONE;
/**
* World reference.
*/
private World loadedWorld;
/**
* Octree origin.
*/
protected Vector3i origin = new Vector3i();
/**
* Octree
*/
private Octree worldOctree;
/**
* Entities in the scene.
*/
private Collection<Entity> entities = new LinkedList<>();
/**
* Poseable entities in the scene.
*/
private Collection<Entity> actors = new LinkedList<>();
/** Poseable entities in the scene. */
private Map<PlayerEntity, JsonObject> profiles = new HashMap<>();
/** Material properties for this scene. */
private Map<String, JsonValue> materials = Collections.emptyMap();
private BVH bvh = new BVH(Collections.emptyList());
private BVH actorBvh = new BVH(Collections.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();
/** This is the 8-bit channel frame buffer. */
protected BitmapImage frontBuffer;
private BitmapImage backBuffer;
/**
* HDR sample buffer for the render output.
*
* <p>Note: the sample buffer is initially null, it is only
* initialized if the scene will be used for rendering.
* This avoids allocating new sample buffers each time
* we want to copy the scene state to a temporary scene.
*
* <p>TODO: render buffers (sample buffer, alpha channel, etc.)
* should really be moved somewhere else and not be so tightly
* coupled to the scene settings.
*/
protected double[] samples;
private byte[] alphaChannel;
private boolean finalized = false;
private boolean finalizeBuffer = false;
private boolean forceReset = false;
/**
* Creates a scene with all default settings.
*
* <p>Note: this does not initialize the render buffers for the scene!
* Render buffers are initialized either by using loadDescription(),
* fromJson(), or importFromJson(), or by calling initBuffers().
*/
public Scene() {
width = PersistentSettings.get3DCanvasWidth();
height = PersistentSettings.get3DCanvasHeight();
sppTarget = PersistentSettings.getSppTargetDefault();
worldOctree = new Octree(1);
}
/**
* Delete all scene files from the scene directory, leaving only
* snapshots untouched.
*/
public static void delete(String name, File sceneDir) {
String[] extensions =
{".json", ".dump", ".octree", ".foliage", ".grass", ".json.backup", ".dump.backup",};
for (String extension : extensions) {
File file = new File(sceneDir, name + extension);
if (file.isFile()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
}
/**
* Export the scene to a zip file.
*/
public static void exportToZip(String name, File targetFile) {
String[] extensions = {".json", ".dump", ".octree", ".foliage", ".grass",};
ZipExport.zip(targetFile, PersistentSettings.getSceneDirectory(), name, extensions);
}
/**
* This initializes the render buffers when initializing the
* scene and after scene canvas size changes.
*/
public synchronized void initBuffers() {
frontBuffer = new BitmapImage(width, height);
backBuffer = new BitmapImage(width, height);
alphaChannel = new byte[width * height];
samples = new double[width * height * 3];
}
/**
* Creates a copy of another scene.
*/
public Scene(Scene other) {
copyState(other);
copyTransients(other);
}
/**
* Import scene state from another scene.
*/
public synchronized void copyState(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;
entities = other.entities;
actors = new LinkedList<>(other.actors); // Create a copy so that entity changes can be reset.
profiles = other.profiles;
bvh = other.bvh;
actorBvh = other.actorBvh;
renderActors = other.renderActors;
grassTexture = other.grassTexture;
foliageTexture = other.foliageTexture;
origin.set(other.origin);
// Copy material properties.
materials = other.materials;
chunks = other.chunks;
exposure = other.exposure;
name = other.name; // TODO: Safe to remove this? Name is copied in copyTransients().
stillWater = other.stillWater;
waterOpacity = other.waterOpacity;
waterVisibility = other.waterVisibility;
useCustomWaterColor = other.useCustomWaterColor;
waterColor.set(other.waterColor);
fogColor.set(other.fogColor);
biomeColors = other.biomeColors;
sunEnabled = other.sunEnabled;
emittersEnabled = other.emittersEnabled;
emitterIntensity = other.emitterIntensity;
transparentSky = other.transparentSky;
fogDensity = other.fogDensity;
fastFog = other.fastFog;
camera.set(other.camera);
sky.set(other.sky);
sun.set(other.sun);
waterHeight = other.waterHeight;
spp = other.spp;
renderTime = other.renderTime;
resetReason = other.resetReason;
finalized = false;
if (samples != other.samples) {
width = other.width;
height = other.height;
backBuffer = other.backBuffer;
frontBuffer = other.frontBuffer;
alphaChannel = other.alphaChannel;
samples = other.samples;
}
}
/**
* Save the scene description, render dump, and foliage
* and grass textures.
*
* @throws IOException
* @throws InterruptedException
*/
public synchronized void saveScene(RenderContext context, TaskTracker taskTracker)
throws IOException, InterruptedException {
try (TaskTracker.Task task = taskTracker.task("Saving scene", 2)) {
task.update(1);
try (BufferedOutputStream out = new BufferedOutputStream(context.getSceneDescriptionOutputStream(name))) {
saveDescription(out);
}
saveOctree(context, taskTracker);
saveGrassTexture(context, taskTracker);
saveFoliageTexture(context, taskTracker);
saveDump(context, taskTracker);
}
}
/**
* Load a stored scene by file name.
*
* @param sceneName file name of the scene to load
*/
public synchronized void loadScene(RenderContext context, String sceneName,
TaskTracker taskTracker) throws IOException, SceneLoadingError, InterruptedException {
loadDescription(context.getSceneDescriptionInputStream(sceneName));
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.");
}
// Load the configured skymap file.
sky.loadSkymap();
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 (loadDump(context, taskTracker)) {
postProcessFrame(taskTracker);
}
if (spp == 0) {
mode = RenderMode.PREVIEW;
} else if (mode == RenderMode.RENDERING) {
mode = RenderMode.PAUSED;
}
if (loadOctree(context, taskTracker)) {
boolean haveGrass = loadGrassTexture(context, taskTracker);
boolean haveFoliage = loadFoliageTexture(context, taskTracker);
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(taskTracker, loadedWorld, chunks);
}
}
notifyAll();
}
/**
* Set the exposure value
*/
public synchronized void setExposure(double value) {
exposure = value;
if (mode == RenderMode.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.
*/
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.
*/
public synchronized void setEmittersEnabled(boolean value) {
if (value != emittersEnabled) {
emittersEnabled = value;
refresh();
}
}
/**
* Set sunlight enable flag.
*/
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;
}
/**
* Trace a ray in this scene. This offsets the ray origin to
* move it into the scene coordinate space.
*/
public void rayTrace(RayTracer rayTracer, WorkerState state) {
state.ray.o.x -= origin.x;
state.ray.o.y -= origin.y;
state.ray.o.z -= origin.z;
rayTracer.trace(this, state);
}
/**
* Find closest intersection between ray and scene.
* This advances the ray by updating the ray origin if an intersection is found.
*
* @param ray ray to test against scene
* @return <code>true</code> if an intersection was found
*/
public boolean intersect(Ray ray) {
boolean hit = false;
if (bvh.closestIntersection(ray)) {
hit = true;
}
if (renderActors) {
if (actorBvh.closestIntersection(ray)) {
hit = true;
}
}
Ray oct = new Ray(ray);
oct.setCurrentMaterial(ray.getPrevMaterial(), ray.getPrevData());
if (worldOctree.intersect(this, oct) && oct.distance < ray.t) {
ray.distance += oct.distance;
ray.o.set(oct.o);
ray.n.set(oct.n);
ray.color.set(oct.color);
ray.setPrevMaterial(oct.getPrevMaterial(), oct.getPrevData());
ray.setCurrentMaterial(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().isWater() || (ray.getCurrentMaterial() == Block.AIR
&& ray.getPrevMaterial().isWater())) {
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 (using Russian Roulette).
*
* @return {@code true} if the ray needs to die now
*/
public final boolean kill(int depth, Random random) {
return depth >= rayDepth && random.nextDouble() < .5f;
}
/**
* Reload all loaded chunks.
*/
public synchronized void reloadChunks(TaskTracker progress) {
if (loadedWorld == null) {
Log.warn("Can not reload chunks for scene - world directory not found!");
return;
}
loadedWorld.setDimension(worldDimension);
loadedWorld.reload();
loadChunks(progress, loadedWorld, chunks);
refresh();
}
/**
* Load chunks into the Octree.
*/
public synchronized void loadChunks(TaskTracker progress, World world,
Collection<ChunkPosition> chunksToLoad) {
if (world == null) {
return;
}
Set<ChunkPosition> loadedChunks = new HashSet<>();
int numChunks = 0;
try (TaskTracker.Task task = progress.task("Loading regions")) {
task.update(2, 1);
loadedWorld = world;
worldPath = loadedWorld.getWorldDirectory().getAbsolutePath();
worldDimension = world.currentDimension();
if (chunksToLoad.isEmpty()) {
return;
}
int requiredDepth = calculateOctreeOrigin(chunksToLoad);
// Create new octree to fit all chunks.
worldOctree = new Octree(requiredDepth);
if (waterHeight > 0) {
// Water world mode enabled, fill in water in empty blocks.
// The water blocks are replaced later when the world chunks are loaded.
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<>();
for (ChunkPosition cp : chunksToLoad) {
regions.add(cp.getRegionPosition());
}
for (ChunkPosition region : regions) {
world.getRegion(region).parse();
}
}
try (TaskTracker.Task task = progress.task("Loading entities")) {
entities = new LinkedList<>();
if (actors.isEmpty() && PersistentSettings.getLoadPlayers()) {
// We don't load actor entities if some already exists. Loading actor entities
// risks resetting posed actors when reloading chunks for an existing scene.
actors = new LinkedList<>();
profiles = new HashMap<>();
Collection<PlayerEntity> players = world.playerEntities();
int done = 1;
int target = players.size();
for (PlayerEntity entity : players) {
entity.randomPose();
task.update(target, done);
done += 1;
JsonObject profile;
try {
profile = MCDownloader.fetchProfile(entity.uuid);
} catch (IOException e) {
Log.error(e);
profile = new JsonObject();
}
profiles.put(entity, profile);
actors.add(entity);
}
}
}
int ycutoff = PersistentSettings.getYCutoff();
ycutoff = Math.max(0, ycutoff);
Heightmap biomeIdMap = new Heightmap();
try (TaskTracker.Task task = progress.task("Loading chunks")) {
int done = 1;
int target = chunksToLoad.size();
for (ChunkPosition cp : chunksToLoad) {
task.update(target, done);
done += 1;
if (loadedChunks.contains(cp)) {
continue;
}
loadedChunks.add(cp);
Collection<CompoundTag> tileEntities = new LinkedList<>();
Collection<CompoundTag> chunkEntities = new LinkedList<>();
world.getChunk(cp).getBlockData(blocks, data, biomes, tileEntities, chunkEntities);
numChunks += 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 from the chunk:
for (CompoundTag tag : chunkEntities) {
if (tag.get("id").stringValue("").equals("Painting")) {
ListTag pos = (ListTag) tag.get("Pos");
double x = pos.get(0).doubleValue();
double y = pos.get(1).doubleValue();
double z = pos.get(2).doubleValue();
ListTag rot = (ListTag) tag.get("Rotation");
double yaw = rot.get(0).floatValue();
//double pitch = rot.getItem(1).floatValue();
entities.add(
new PaintingEntity(new Vector3(x, y, z), tag.get("Motive").stringValue(), yaw));
}
}
// The name tileEntities is confusing, because the entities are usually
// referred to as "block entities". However, we keep the name tileEntities
// to match the name of these entities in the Minecraft world format.
// Load tile entities.
for (CompoundTag entityTag : tileEntities) {
int x = entityTag.get("x").intValue(0) - wx0;
int y = entityTag.get("y").intValue(0);
int z = entityTag.get("z").intValue(0) - wz0;
int index = Chunk.chunkIndex(x, y, z);
int block = 0xFF & blocks[index];
// Metadata is the old block data (to be replaced in future Minecraft versions?).
int metadata = 0xFF & data[index / 2];
metadata >>= (x % 2) * 4;
metadata &= 0xF;
Vector3 position = new Vector3(x + wx0, y, z + wz0);
switch (block) {
case Block.WALLSIGN_ID:
entities.add(new WallSignEntity(position, entityTag, metadata));
break;
case Block.SIGNPOST_ID:
entities.add(new SignEntity(position, entityTag, metadata));
break;
case Block.HEAD_ID:
entities.add(new SkullEntity(position, entityTag, metadata));
break;
}
}
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);
int blockId = blocks[index];
Block block = Block.get(blockId);
if (cx > 0 && cx < 15 && cz > 0 && cz < 15 && cy > 0 && cy < 255 &&
blockId != Block.STONE_ID && block.isOpaque) {
// Set obscured blocks to stone. This makes adjacent obscured
// blocks be able to be merged into larger octree nodes
// even if they had different block types originally.
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.get(Block.LILY_PAD_ID)) {
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.FIRE_ID: {
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 = 0xF & (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.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<>(chunksToLoad);
try (TaskTracker.Task task = progress.task("Finalizing octree")) {
int done = 0;
int target = chunksToLoad.size();
for (ChunkPosition cp : chunksToLoad) {
// Finalize grass and foliage textures.
// 3x3 box blur.
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);
}
}
task.update(target, done);
done += 1;
OctreeFinalizer.finalizeChunk(worldOctree, origin, cp);
}
}
chunks = loadedChunks;
camera.setWorldSize(1 << worldOctree.depth);
buildBvh();
buildActorBvh();
Log.info(String.format("Loaded %d chunks", numChunks));
}
private void buildBvh() {
final List<Primitive> primitives = new LinkedList<>();
worldOctree.visit((data1, x, y, z, size) -> {
if ((data1 & 0xF) == Block.WATER_ID) {
WaterModel.addPrimitives(primitives, data1, x, y, z, 1 << size);
}
});
Vector3 worldOffset = new Vector3(-origin.x, -origin.y, -origin.z);
for (Entity entity : entities) {
primitives.addAll(entity.primitives(worldOffset));
}
bvh = new BVH(primitives);
}
private void buildActorBvh() {
final List<Primitive> actorPrimitives = new LinkedList<>();
Vector3 worldOffset = new Vector3(-origin.x, -origin.y, -origin.z);
for (Entity entity : actors) {
actorPrimitives.addAll(entity.primitives(worldOffset));
}
actorBvh = new BVH(actorPrimitives);
}
/**
* Rebuild the actors bounding volume hierarchy.
*/
public void rebuildActorBvh() {
buildActorBvh();
refresh();
}
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 <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 Vector3 calcCenterCamera() {
if (chunks.isEmpty()) {
return new Vector3(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 != Block.AIR_ID) {
return new Vector3(xcenter, y + 5, zcenter);
}
}
return new Vector3(xcenter, 128, zcenter);
}
/**
* Set the biome colors flag.
*/
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. This wakes up threads waiting on a scene
* state change, even if the scene state did not actually change.
*/
public synchronized void startHeadlessRender() {
mode = RenderMode.RENDERING;
notifyAll();
}
/**
* @return <code>true</code> if the rendering of this scene should be
* restarted
*/
public boolean shouldRefresh() {
return resetReason != ResetReason.NONE;
}
/**
* Start rendering the scene.
*/
public synchronized void startRender() {
if (mode == RenderMode.PAUSED) {
mode = RenderMode.RENDERING;
notifyAll();
} else if (mode != RenderMode.RENDERING) {
mode = RenderMode.RENDERING;
refresh();
}
}
/**
* Pause the renderer.
*/
public synchronized void pauseRender() {
mode = RenderMode.PAUSED;
// Wake up threads in awaitSceneStateChange().
notifyAll();
}
/**
* Halt the rendering process.
* Puts the renderer back in preview mode.
*/
public synchronized void haltRender() {
if (mode != RenderMode.PREVIEW) {
mode = RenderMode.PREVIEW;
resetReason = ResetReason.MODE_CHANGE;
forceReset = true;
refresh();
}
}
/**
* Move the camera to the player position, if available.
*/
public void moveCameraToPlayer() {
for (Entity entity : actors) {
if (entity instanceof PlayerEntity) {
camera.moveToPlayer((PlayerEntity) entity);
}
}
}
/**
* @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
*/
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 clearResetFlags() {
resetReason = ResetReason.NONE;
forceReset = false;
}
/**
* Trace a ray in the Octree towards the current view target.
* The ray is displaced to the target position if it hits something.
*
* <p>The view target is defined by the current camera state.
*
* @return {@code true} if the ray hit something
*/
public boolean traceTarget(Ray ray) {
WorkerState state = new WorkerState();
state.ray = ray;
if (isInWater(ray)) {
ray.setCurrentMaterial(Block.get(Block.WATER_ID), 0);
} else {
ray.setCurrentMaterial(Block.AIR, 0);
}
camera.getTargetDirection(ray);
ray.o.x -= origin.x;
ray.o.y -= origin.y;
ray.o.z -= origin.z;
while (PreviewRayTracer.nextIntersection(this, ray)) {
if (ray.getCurrentMaterial() != Block.AIR) {
return true;
}
}
return false;
}
/**
* Perform auto focus.
*/
public void autoFocus() {
Ray ray = new Ray();
if (!traceTarget(ray)) {
camera.setDof(Double.POSITIVE_INFINITY);
} else {
camera.setSubjectDistance(ray.distance);
camera.setDof(ray.distance * ray.distance);
}
}
/**
* Find the current camera target position.
*
* @return {@code null} if the camera is not aiming at some intersectable object
*/
public Vector3 getTargetPosition() {
Ray ray = new Ray();
if (!traceTarget(ray)) {
return null;
} else {
Vector3 target = new Vector3(ray.o);
target.add(origin.x, origin.y, origin.z);
return target;
}
}
/**
* @return World origin in the Octree
*/
public Vector3i getOrigin() {
return origin;
}
/**
* Set the scene name.
*/
public void setName(String newName) {
newName = AsynchronousSceneManager.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 (mode == RenderMode.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.
*/
public void setEmitterIntensity(double value) {
emitterIntensity = value;
refresh();
}
/**
* Set the transparent sky option.
*/
public void setTransparentSky(boolean value) {
if (value != transparentSky) {
transparentSky = value;
refresh();
}
}
/**
* @return {@code true} if transparent sky is enabled
*/
public boolean transparentSky() {
return transparentSky;
}
/**
* Set the ocean water height.
*
* @return {@code true} if the water height value was changed.
*/
public boolean setWaterHeight(int value) {
value = Math.max(0, value);
value = Math.min(256, value);
if (value != waterHeight) {
waterHeight = value;
refresh();
return true;
}
return false;
}
/**
* @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 scene state that does not require a render restart.
*
* @param other scene to copy transient state from.
*/
public synchronized void copyTransients(Scene other) {
name = other.name;
postprocess = other.postprocess;
exposure = other.exposure;
dumpFrequency = other.dumpFrequency;
saveSnapshots = other.saveSnapshots;
sppTarget = other.sppTarget;
rayDepth = other.rayDepth;
mode = other.mode;
outputMode = other.outputMode;
cameraPresets = other.cameraPresets;
camera.copyTransients(other.camera);
finalizeBuffer = other.finalizeBuffer;
}
/**
* @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 for this scene. This will refresh
* the scene and reinitialize the sample buffers if the
* new canvas size is not identical to the current canvas size.
*/
public synchronized void setCanvasSize(int canvasWidth, int canvasHeight) {
int newWidth = Math.max(MIN_CANVAS_WIDTH, canvasWidth);
int newHeight = Math.max(MIN_CANVAS_HEIGHT, canvasHeight);
if (newWidth != width || newHeight != height) {
width = newWidth;
height = newHeight;
initBuffers();
refresh();
}
}
/**
* @return Canvas width
*/
public int canvasWidth() {
return width;
}
/**
* @return Canvas height
*/
public int canvasHeight() {
return height;
}
/**
* Save a snapshot
*/
public void saveSnapshot(File directory, TaskTracker progress) {
if (directory == null) {
Log.error("Can't save snapshot: bad output directory!");
return;
}
String fileName = String.format("%s-%d%s", name, spp, outputMode.getExtension());
File targetFile = new File(directory, fileName);
computeAlpha(progress);
if (!finalized) {
postProcessFrame(progress);
}
writeImage(targetFile, progress);
}
/**
* Save the current frame as a PNG image.
* @throws IOException
*/
public synchronized void saveFrame(File targetFile, TaskTracker progress)
throws IOException {
computeAlpha(progress);
if (!finalized) {
postProcessFrame(progress);
}
writeImage(targetFile, progress);
}
/**
* Compute the alpha channel.
*/
private void computeAlpha(TaskTracker progress) {
if (transparentSky) {
if (outputMode == OutputMode.TIFF_32) {
Log.warn("Can not use transparent sky with TIFF output mode.");
} else {
try (TaskTracker.Task task = progress.task("Computing alpha channel")) {
WorkerState state = new WorkerState();
state.ray = new Ray();
for (int x = 0; x < width; ++x) {
task.update(width, x + 1);
for (int y = 0; y < height; ++y) {
computeAlpha(x, y, state);
}
}
}
}
}
}
/**
* Post-process all pixels in the current frame.
*
* <p>This is normally done by the render workers during rendering,
* but in some cases an separate post processing pass is needed.
*/
public void postProcessFrame(TaskTracker progress) {
try (TaskTracker.Task task = progress.task("Finalizing frame")) {
for (int x = 0; x < width; ++x) {
task.update(width, x + 1);
for (int y = 0; y < height; ++y) {
finalizePixel(x, y);
}
}
}
}
/**
* Write buffer data to image.
*
* @param targetFile file to write to.
*/
private void writeImage(File targetFile, TaskTracker progress) {
if (outputMode == OutputMode.PNG) {
writePng(targetFile, progress);
} else if (outputMode == OutputMode.TIFF_32) {
writeTiff(targetFile, progress);
}
}
/**
* Write PNG image.
*
* @param targetFile file to write to.
*/
private void writePng(File targetFile, TaskTracker progress) {
try (TaskTracker.Task task = progress.task("Writing PNG");
PngFileWriter writer = new PngFileWriter(targetFile)) {
if (transparentSky) {
writer.write(backBuffer.data, alphaChannel, width, height, task);
} else {
writer.write(backBuffer.data, width, height, task);
}
if (camera.getProjectionMode() == ProjectionMode.PANORAMIC && camera.getFov() >= 179
&& camera.getFov() <= 181) {
String xmp = "";
xmp += "<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n";
xmp += " <rdf:Description rdf:about=''\n";
xmp += " xmlns:GPano='http://ns.google.com/photos/1.0/panorama/'>\n";
xmp += " <GPano:CroppedAreaImageHeightPixels>";
xmp += height;
xmp += "</GPano:CroppedAreaImageHeightPixels>\n";
xmp += " <GPano:CroppedAreaImageWidthPixels>";
xmp += width;
xmp += "</GPano:CroppedAreaImageWidthPixels>\n";
xmp += " <GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>\n";
xmp += " <GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>\n";
xmp += " <GPano:FullPanoHeightPixels>";
xmp += height;
xmp += "</GPano:FullPanoHeightPixels>\n";
xmp += " <GPano:FullPanoWidthPixels>";
xmp += width;
xmp += "</GPano:FullPanoWidthPixels>\n";
xmp += " <GPano:ProjectionType>equirectangular</GPano:ProjectionType>\n";
xmp += " <GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>\n";
xmp += " </rdf:Description>\n";
xmp += " </rdf:RDF>";
ITXT iTXt = new ITXT("XML:com.adobe.xmp", xmp);
writer.writeChunk(iTXt);
}
writer.writeChunk(new IEND());
} catch (IOException e) {
Log.warn("Failed to write PNG file: " + targetFile.getAbsolutePath(), e);
}
}
/**
* Write TIFF image.
*
* @param targetFile file to write to.
*/
private void writeTiff(File targetFile, TaskTracker progress) {
try (TaskTracker.Task task = progress.task("Writing TIFF");
TiffFileWriter writer = new TiffFileWriter(targetFile)) {
writer.write32(this, task);
} catch (IOException e) {
Log.warn("Failed to write TIFF file: " + targetFile.getAbsolutePath(), e);
}
}
private synchronized void saveOctree(RenderContext context, TaskTracker progress) {
String fileName = name + ".octree";
if (context.fileUnchangedSince(fileName, worldOctree.getTimestamp())) {
Log.info("Skipping redundant Octree write");
return;
}
try (TaskTracker.Task task = progress.task("Saving octree", 2)) {
task.update(1);
Log.info("Saving octree " + fileName);
try (DataOutputStream out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName)))) {
worldOctree.store(out);
worldOctree.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Octree saved");
} catch (IOException e) {
Log.warn("IO exception while saving octree!", e);
}
}
}
private synchronized void saveGrassTexture(RenderContext context,
TaskTracker progress) {
String fileName = name + ".grass";
if (context.fileUnchangedSince(fileName, grassTexture.getTimestamp())) {
Log.info("Skipping redundant grass texture write");
return;
}
try (TaskTracker.Task task = progress.task("Saving grass texture", 2)) {
task.update(1);
Log.info("Saving grass texture " + fileName);
try (DataOutputStream out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName)))) {
grassTexture.store(out);
grassTexture.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Grass texture saved");
} catch (IOException e) {
Log.warn("IO exception while saving octree!", e);
}
}
}
private synchronized void saveFoliageTexture(RenderContext context, TaskTracker progress) {
String fileName = name + ".foliage";
if (context.fileUnchangedSince(fileName, foliageTexture.getTimestamp())) {
Log.info("Skipping redundant foliage texture write");
return;
}
try (TaskTracker.Task task = progress.task("Saving foliage texture", 2)) {
task.update(1);
Log.info("Saving foliage texture " + fileName);
try (DataOutputStream out = new DataOutputStream(new GZIPOutputStream(context.getSceneFileOutputStream(fileName)))) {
foliageTexture.store(out);
foliageTexture.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Foliage texture saved");
} catch (IOException e) {
Log.warn("IO exception while saving octree!", e);
}
}
}
public synchronized void saveDump(RenderContext context, TaskTracker progress) {
String fileName = name + ".dump";
try (TaskTracker.Task task = progress.task("Saving render dump", 2)) {
task.update(1);
Log.info("Saving render dump " + fileName);
try (DataOutputStream 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) {
task.update(width, x + 1);
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);
}
}
}
private synchronized boolean loadOctree(RenderContext context, TaskTracker progress) {
String fileName = name + ".octree";
try (TaskTracker.Task task = progress.task("Loading octree", 2)) {
task.update(1);
Log.info("Loading octree " + fileName);
try (DataInputStream in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName)))) {
worldOctree = Octree.load(in);
worldOctree.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Octree loaded");
calculateOctreeOrigin(chunks);
camera.setWorldSize(1 << worldOctree.depth);
buildBvh();
buildActorBvh();
return true;
} catch (IOException e) {
Log.info("Failed to load chunk octree: missing file or incorrect format!", e);
return false;
}
}
}
private synchronized boolean loadGrassTexture(RenderContext context, TaskTracker progress) {
String fileName = name + ".grass";
try (TaskTracker.Task task = progress.task("Loading grass texture", 2)) {
task.update(1);
Log.info("Loading grass texture " + fileName);
try (DataInputStream in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName)))) {
grassTexture = WorldTexture.load(in);
grassTexture.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Grass texture loaded");
return true;
} catch (IOException e) {
Log.info("Failed to load grass texture!");
return false;
}
}
}
private synchronized boolean loadFoliageTexture(RenderContext context, TaskTracker progress) {
String fileName = name + ".foliage";
try (TaskTracker.Task task = progress.task("Loading foliage texture", 2)) {
task.update(1);
Log.info("Loading foliage texture " + fileName);
try (DataInputStream in = new DataInputStream(new GZIPInputStream(context.getSceneFileInputStream(fileName)))) {
foliageTexture = WorldTexture.load(in);
foliageTexture.setTimestamp(context.fileTimestamp(fileName));
task.update(2);
Log.info("Foliage texture loaded");
return true;
} catch (IOException e) {
Log.info("Failed to load foliage texture!");
return false;
}
}
}
public synchronized boolean loadDump(RenderContext context, TaskTracker taskTracker) {
if (!tryLoadDump(context, name + ".dump", taskTracker)) {
// Failed to load the default render dump - try the backup file.
if (!tryLoadDump(context, name + ".dump.backup", taskTracker)) {
spp = 0; // Set spp = 0 because we don't have the old render state.
return false;
}
}
return true;
}
/**
* @return {@code true} if the render dump was successfully loaded
*/
private boolean tryLoadDump(RenderContext context, String fileName, TaskTracker taskTracker) {
File dumpFile = context.getSceneFile(fileName);
if (!dumpFile.isFile()) {
if (spp != 0) {
// The scene state says the render had some progress, so we should warn
// that the render dump does not exist.
Log.warn("Render dump not found: " + fileName);
}
return false;
}
try (DataInputStream in = new DataInputStream(new GZIPInputStream(new FileInputStream(dumpFile)));
TaskTracker.Task task = taskTracker.task("Loading render dump", 2)) {
task.update(1);
Log.info("Reading 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!");
return false;
}
spp = in.readInt();
renderTime = in.readLong();
for (int x = 0; x < width; ++x) {
task.update(width, x + 1);
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: " + fileName);
return true;
} catch (IOException e) {
// The render dump was possibly corrupt.
Log.warn("Failed to load render dump", e);
return false;
}
}
/**
* Finalize a pixel. Calculates the resulting RGB color values for
* the pixel and sets these in the bitmap image.
*/
public void finalizePixel(int x, int y) {
finalized = true;
double[] result = new double[3];
postProcessPixel(x, y, result);
backBuffer.data[y * width + x] = ColorUtil
.getRGB(QuickMath.min(1, result[0]), QuickMath.min(1, result[1]),
QuickMath.min(1, result[2]));
}
/**
* Postprocess a pixel. This applies gamma correction and clamps the color value to [0,1].
*
* @param result the resulting color values are written to this array
*/
public void postProcessPixel(int x, int y, double[] result) {
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 (mode != RenderMode.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 TONEMAP2:
// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
float aces_a = 2.51f;
float aces_b = 0.03f;
float aces_c = 2.43f;
float aces_d = 0.59f;
float aces_e = 0.14f;
r = QuickMath.max(QuickMath.min((r * (aces_a * r + aces_b)) / (r * (aces_c * r + aces_d) + aces_e), 1), 0);
g = QuickMath.max(QuickMath.min((g * (aces_a * g + aces_b)) / (r * (aces_c * g + aces_d) + aces_e), 1), 0);
b = QuickMath.max(QuickMath.min((b * (aces_a * b + aces_b)) / (r * (aces_c * b + aces_d) + aces_e), 1), 0);
break;
case TONEMAP3:
// http://filmicgames.com/archives/75
float hA = 0.15f;
float hB = 0.50f;
float hC = 0.10f;
float hD = 0.20f;
float hE = 0.02f;
float hF = 0.30f;
// This adjusts the exposure by a factor of 16 so that the resulting exposure approximately matches the other
// post-processing methods. Without this, the image would be very dark.
r *= 16;
g *= 16;
b *= 16;
r = ((r * (hA * r + hC * hB) + hD * hE) / (r * (hA * r + hB) + hD * hF)) - hE / hF;
g = ((g * (hA * g + hC * hB) + hD * hE) / (g * (hA * g + hB) + hD * hF)) - hE / hF;
b = ((b * (hA * b + hC * hB) + hD * hE) / (b * (hA * b + hB) + hD * hF)) - hE / hF;
float hW = 11.2f;
float whiteScale = 1.0f / (((hW * (hA * hW + hC * hB) + hD * hE) / (hW * (hA * hW + hB) + hD * hF)) - hE / hF);
r *= whiteScale;
g *= whiteScale;
b *= whiteScale;
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);
}
result[0] = r;
result[1] = g;
result[2] = b;
}
/**
* Compute the alpha channel based on sky visibility.
*/
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 = PreviewRayTracer.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 += PreviewRayTracer.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 += PreviewRayTracer.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 += PreviewRayTracer.skyOcclusion(this, state);
alphaChannel[y * width + x] = (byte) (255 * occlusion * 0.25 + 0.5);
}
/**
* Copies a pixel in-buffer.
*/
public void copyPixel(int jobId, int offset) {
backBuffer.data[jobId + offset] = backBuffer.data[jobId];
}
/**
* @return scene status text.
*/
public synchronized String sceneStatus() {
try {
if (!haveLoadedChunks()) {
return "No chunks loaded!";
} else {
StringBuilder buf = new StringBuilder();
Ray ray = new Ray();
if (traceTarget(ray) && ray.getCurrentMaterial() instanceof Block) {
Block block = (Block) ray.getCurrentMaterial();
buf.append(String.format("target: %.2f m\n", ray.distance));
buf.append(String.format("[0x%08X] %s (%s)\n", ray.getCurrentData(), block,
block.description(ray.getBlockData())));
}
Vector3 pos = camera.getPosition();
buf.append(String.format("pos: (%.1f, %.1f, %.1f)", pos.x, pos.y, pos.z));
return buf.toString();
}
} catch (IllegalStateException e) {
Log.error("Unexpected exception while rendering back buffer", e);
}
return "";
}
/**
* Prepare the front buffer for rendering by flipping the back and front buffer.
*/
public synchronized void swapBuffers() {
finalized = false;
BitmapImage tmp = frontBuffer;
frontBuffer = backBuffer;
backBuffer = tmp;
}
/**
* Call the consumer with the current front frame buffer.
*/
public synchronized void withBufferedImage(Consumer<BitmapImage> consumer) {
if (frontBuffer != null) {
consumer.accept(frontBuffer);
}
}
/**
* 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;
}
/**
* Set the buffer update flag. The buffer update flag decides whether the
* renderer should update the buffered image.
*/
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.
*/
public void mergeDump(File dumpFile, TaskTracker taskTracker) {
int dumpSpp;
long dumpTime;
try (TaskTracker.Task task = taskTracker.task("Merging render dump", 2);
DataInputStream in = new DataInputStream(
new GZIPInputStream(new FileInputStream(dumpFile)))) {
task.update(1);
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 width or height!");
return;
}
dumpSpp = in.readInt();
dumpTime = in.readLong();
double sa = spp / (double) (spp + dumpSpp);
double sb = 1 - sa;
for (int x = 0; x < width; ++x) {
task.update(width, x + 1);
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;
} catch (IOException e) {
Log.info("Render dump not loaded");
}
}
public void setSaveSnapshots(boolean value) {
saveSnapshots = value;
}
public boolean shouldSaveSnapshots() {
return saveSnapshots;
}
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);
return (block & 0xF) == Block.WATER_ID
&& ((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(Vector3 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 Vector3 getWaterColor() {
return waterColor;
}
public void setWaterColor(Vector3 color) {
waterColor.set(color);
refresh();
}
public Vector3 getFogColor() {
return fogColor;
}
public void setFogColor(Vector3 color) {
fogColor.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 json = new JsonObject();
json.add("sdfVersion", SDF_VERSION);
json.add("name", name);
json.add("width", width);
json.add("height", height);
json.add("exposure", exposure);
json.add("postprocess", postprocess.name());
json.add("outputMode", outputMode.name());
json.add("renderTime", renderTime);
json.add("spp", spp);
json.add("sppTarget", sppTarget);
json.add("rayDepth", rayDepth);
json.add("pathTrace", mode != RenderMode.PREVIEW);
json.add("dumpFrequency", dumpFrequency);
json.add("saveSnapshots", saveSnapshots);
json.add("emittersEnabled", emittersEnabled);
json.add("emitterIntensity", emitterIntensity);
json.add("sunEnabled", sunEnabled);
json.add("stillWater", stillWater);
json.add("waterOpacity", waterOpacity);
json.add("waterVisibility", waterVisibility);
json.add("useCustomWaterColor", useCustomWaterColor);
if (useCustomWaterColor) {
JsonObject colorObj = new JsonObject();
colorObj.add("red", waterColor.x);
colorObj.add("green", waterColor.y);
colorObj.add("blue", waterColor.z);
json.add("waterColor", colorObj);
}
JsonObject fogColorObj = new JsonObject();
fogColorObj.add("red", fogColor.x);
fogColorObj.add("green", fogColor.y);
fogColorObj.add("blue", fogColor.z);
json.add("fogColor", fogColorObj);
json.add("fastFog", fastFog);
json.add("biomeColorsEnabled", biomeColors);
json.add("transparentSky", transparentSky);
json.add("fogDensity", fogDensity);
json.add("waterHeight", waterHeight);
json.add("renderActors", renderActors);
if (!worldPath.isEmpty()) {
// Save world info.
JsonObject world = new JsonObject();
world.add("path", worldPath);
world.add("dimension", worldDimension);
json.add("world", world);
}
json.add("camera", camera.toJson());
json.add("sun", sun.toJson());
json.add("sky", sky.toJson());
json.add("cameraPresets", cameraPresets.copy());
JsonArray chunkList = new JsonArray();
for (ChunkPosition pos : chunks) {
JsonArray chunk = new JsonArray();
chunk.add(pos.x);
chunk.add(pos.z);
chunkList.add(chunk);
}
json.add("chunkList", chunkList);
JsonArray entityArray = new JsonArray();
for (Entity entity : entities) {
entityArray.add(entity.toJson());
}
if (!entityArray.isEmpty()) {
json.add("entities", entityArray);
}
JsonArray actorArray = new JsonArray();
for (Entity entity : actors) {
actorArray.add(entity.toJson());
}
if (!actorArray.isEmpty()) {
json.add("actors", actorArray);
}
return json;
}
/**
* Reset the scene settings and import from a JSON object.
*/
public synchronized void fromJson(JsonObject json) {
boolean finalizeBufferPrev = finalizeBuffer; // Remember the finalize setting.
Scene scene = new Scene();
scene.importFromJson(json);
copyState(scene);
copyTransients(scene);
finalizeBuffer = finalizeBufferPrev; // Restore the finalize setting.
setResetReason(ResetReason.SCENE_LOADED);
sdfVersion = json.get("sdfVersion").intValue(-1);
name = json.get("name").stringValue("default");
}
public Collection<Entity> getEntities() {
return entities;
}
public Collection<Entity> getActors() {
return actors;
}
public JsonObject getPlayerProfile(PlayerEntity entity) {
if (profiles.containsKey(entity)) {
return profiles.get(entity);
} else {
return new JsonObject();
}
}
public void removePlayer(PlayerEntity player) {
profiles.remove(player);
actors.remove(player);
rebuildActorBvh();
}
public void addPlayer(PlayerEntity player) {
if (!actors.contains(player)) {
profiles.put(player, new JsonObject());
actors.add(player);
rebuildActorBvh();
} else {
Log.warn("Failed to add player: entity already exists (" + player + ")");
}
}
/**
* Clears the scene, preparing to load fresh chunks.
*/
public void clear() {
cameraPresets = new JsonObject();
entities.clear();
actors.clear();
}
/** Create a backup of a scene file. */
public void backupFile(RenderContext context, String fileName) {
File renderDir = context.getSceneDirectory();
File file = new File(renderDir, fileName);
backupFile(context, file);
}
/** Create a backup of a scene file. */
public void backupFile(RenderContext context, File file) {
if (file.exists()) {
// Try to create backup. It is not a problem if we fail this.
String backupFileName = file.getName() + ".backup";
File renderDir = context.getSceneDirectory();
File backup = new File(renderDir, backupFileName);
if (backup.exists()) {
//noinspection ResultOfMethodCallIgnored
backup.delete();
}
if (!file.renameTo(new File(renderDir, backupFileName))) {
Log.info("Could not create backup " + backupFileName);
}
}
}
public boolean getForceReset() {
return forceReset;
}
public synchronized void setRenderMode(RenderMode renderMode) {
this.mode = renderMode;
}
public synchronized void forceReset() {
setResetReason(ResetReason.MODE_CHANGE);
forceReset = true;
// Wake up waiting threads.
notifyAll();
}
/**
* Resets the scene state to the default state.
*
* @param name sets the name for the scene
*/
public synchronized void resetScene(String name, SceneFactory sceneFactory) {
boolean finalizeBufferPrev = finalizeBuffer; // Remember the finalize setting.
Scene newScene = sceneFactory.newScene();
newScene.initBuffers();
newScene.setName(name);
copyState(newScene);
copyTransients(newScene);
forceReset = true;
resetReason = ResetReason.SETTINGS_CHANGED;
mode = RenderMode.PREVIEW;
finalizeBuffer = finalizeBufferPrev;
}
/**
* Parse the scene description from a JSON file.
*
* <p>This initializes the sample buffers.
*
* @param in Input stream to read the JSON data from. The stream will
* be closed when done.
*/
public void loadDescription(InputStream in) throws IOException {
try (JsonParser parser = new JsonParser(in)) {
JsonObject json = parser.parse().object();
fromJson(json);
} catch (JsonParser.SyntaxError e) {
throw new IOException("JSON syntax error");
}
}
/**
* Write the scene description as JSON.
*
* @param out Output stream to write the JSON data to.
* The stream will not be closed when done.
*/
public void saveDescription(OutputStream out) throws IOException {
PrettyPrinter pp = new PrettyPrinter(" ", new PrintStream(out));
JsonObject json = toJson();
json.prettyPrint(pp);
}
/**
* Replace the current settings from exported JSON settings.
*
* <p>This (re)initializes the sample buffers for the scene.
*/
public synchronized void importFromJson(JsonObject json) {
// The scene is refreshed so that any ongoing renders will restart.
// We do this in case some setting that requires restart changes.
// TODO: check if we actually need to reset the scene based on changed settings.
refresh();
int newWidth = json.get("width").intValue(width);
int newHeight = json.get("height").intValue(height);
if (width != newWidth || height != newHeight || samples == null) {
width = newWidth;
height = newHeight;
initBuffers();
}
exposure = json.get("exposure").doubleValue(exposure);
postprocess = Postprocess.get(json.get("postprocess").stringValue(postprocess.name()));
outputMode = OutputMode.get(json.get("outputMode").stringValue(outputMode.name()));
sppTarget = json.get("sppTarget").intValue(sppTarget);
rayDepth = json.get("rayDepth").intValue(rayDepth);
if (!json.get("pathTrace").isUnknown()) {
boolean pathTrace = json.get("pathTrace").boolValue(false);
if (pathTrace) {
mode = RenderMode.PAUSED;
} else {
mode = RenderMode.PREVIEW;
}
}
dumpFrequency = json.get("dumpFrequency").intValue(dumpFrequency);
saveSnapshots = json.get("saveSnapshots").boolValue(saveSnapshots);
emittersEnabled = json.get("emittersEnabled").boolValue(emittersEnabled);
emitterIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity);
sunEnabled = json.get("sunEnabled").boolValue(sunEnabled);
stillWater = json.get("stillWater").boolValue(stillWater);
waterOpacity = json.get("waterOpacity").doubleValue(waterOpacity);
waterVisibility = json.get("waterVisibility").doubleValue(waterVisibility);
useCustomWaterColor = json.get("useCustomWaterColor").boolValue(useCustomWaterColor);
if (useCustomWaterColor) {
JsonObject colorObj = json.get("waterColor").object();
waterColor.x = colorObj.get("red").doubleValue(waterColor.x);
waterColor.y = colorObj.get("green").doubleValue(waterColor.y);
waterColor.z = colorObj.get("blue").doubleValue(waterColor.z);
}
JsonObject fogColorObj = json.get("fogColor").object();
fogColor.x = fogColorObj.get("red").doubleValue(fogColor.x);
fogColor.y = fogColorObj.get("green").doubleValue(fogColor.y);
fogColor.z = fogColorObj.get("blue").doubleValue(fogColor.z);
fastFog = json.get("fastFog").boolValue(fastFog);
biomeColors = json.get("biomeColorsEnabled").boolValue(biomeColors);
transparentSky = json.get("transparentSky").boolValue(transparentSky);
fogDensity = json.get("fogDensity").doubleValue(fogDensity);
waterHeight = json.get("waterHeight").intValue(waterHeight);
renderActors = json.get("renderActors").boolValue(renderActors);
materials = json.get("materials").object().copy().toMap();
// Load world info.
if (json.get("world").isObject()) {
JsonObject world = json.get("world").object();
worldPath = world.get("path").stringValue(worldPath);
worldDimension = world.get("dimension").intValue(worldDimension);
}
if (json.get("camera").isObject()) {
camera.importFromJson(json.get("camera").object());
}
if (json.get("sun").isObject()) {
sun.importFromJson(json.get("sun").object());
}
if (json.get("sky").isObject()) {
sky.importFromJson(json.get("sky").object());
}
if (json.get("cameraPresets").isObject()) {
cameraPresets = json.get("cameraPresets").object();
}
// Current SPP and render time are read after loading
// other settings which can reset the render status.
spp = json.get("spp").intValue(spp);
renderTime = json.get("renderTime").longValue(renderTime);
if (json.get("chunkList").isArray()) {
JsonArray chunkList = json.get("chunkList").array();
chunks.clear();
for (JsonValue elem : chunkList) {
JsonArray chunk = elem.array();
int x = chunk.get(0).intValue(Integer.MAX_VALUE);
int z = chunk.get(1).intValue(Integer.MAX_VALUE);
if (x != Integer.MAX_VALUE && z != Integer.MAX_VALUE) {
chunks.add(ChunkPosition.get(x, z));
}
}
}
if (json.get("entities").isArray()) {
entities = new LinkedList<>();
actors = new LinkedList<>();
// Previously poseable entities were stored in the entities array
// rather than the actors array. In future versions only the actors
// array should contain poseable entities.
for (JsonValue element : json.get("entities").array()) {
Entity entity = Entity.fromJson(element.object());
if (entity != null) {
if (entity instanceof PlayerEntity) {
actors.add(entity);
} else {
entities.add(entity);
}
}
}
for (JsonValue element : json.get("actors").array()) {
Entity entity = Entity.fromJson(element.object());
actors.add(entity);
}
}
}
/**
* Called when the scene description has been altered in a way that
* forces the rendering to restart.
*/
@Override public synchronized void refresh() {
if (mode == RenderMode.PAUSED) {
mode = RenderMode.RENDERING;
}
spp = 0;
renderTime = 0;
setResetReason(ResetReason.SETTINGS_CHANGED);
notifyAll();
}
/**
* @return The sun state object.
*/
public Sun sun() {
return sun;
}
/**
* @return The sky state object.
*/
public Sky sky() {
return sky;
}
/**
* @return The camera state object.
*/
public Camera camera() {
return camera;
}
public void saveCameraPreset(String name) {
camera.name = name;
cameraPresets.set(name, camera.toJson());
}
public void loadCameraPreset(String name) {
JsonValue value = cameraPresets.get(name);
if (value.isObject()) {
camera.importFromJson(value.object());
refresh();
}
}
public void deleteCameraPreset(String name) {
for (int i = 0; i < cameraPresets.size(); ++i) {
if (cameraPresets.get(i).name.equals(name)) {
cameraPresets.remove(i);
return;
}
}
}
public JsonObject getCameraPresets() {
return cameraPresets;
}
public RenderMode getMode() {
return mode;
}
public void setFogDensity(double newValue) {
if (newValue != fogDensity) {
this.fogDensity = newValue;
refresh();
}
}
public double getFogDensity() {
return fogDensity;
}
public void setFastFog(boolean value) {
if (fastFog != value) {
fastFog = value;
refresh();
}
}
public boolean fastFog() {
return fastFog;
}
/**
* @return {@code true} if volumetric fog is enabled
*/
public boolean fogEnabled() {
return fogDensity > 0.0;
}
public OutputMode getOutputMode() {
return outputMode;
}
public void setOutputMode(OutputMode mode) {
outputMode = mode;
}
public int numberOfChunks() {
return chunks.size();
}
/**
* Clears the reset reason and returns the previous reason.
* @return the current reset reason
*/
public synchronized ResetReason getResetReason() {
return resetReason;
}
public void setResetReason(ResetReason resetReason) {
if (this.resetReason != ResetReason.SCENE_LOADED) {
this.resetReason = resetReason;
}
}
public void importMaterials() {
importMaterial(materials, "all:blocks", Block.blocks);
importMaterial(materials, "all:water", Block.WATER, Block.STATIONARYWATER);
importMaterial(materials, "block:stone", Block.STONE);
importMaterial(materials, "block:grass", Block.GRASS);
importMaterial(materials, "block:dirt", Block.DIRT);
importMaterial(materials, "block:cobblestone", Block.COBBLESTONE);
importMaterial(materials, "block:planks", Block.WOODENPLANKS);
importMaterial(materials, "block:sapling", Block.SAPLING);
importMaterial(materials, "block:bedrock", Block.BEDROCK);
importMaterial(materials, "block:flowing_water", Block.WATER);
importMaterial(materials, "block:water", Block.STATIONARYWATER);
importMaterial(materials, "block:flowing_lava", Block.LAVA);
importMaterial(materials, "block:lava", Block.STATIONARYLAVA);
importMaterial(materials, "block:sand", Block.SAND);
importMaterial(materials, "block:gravel", Block.GRAVEL);
importMaterial(materials, "block:gold_ore", Block.GOLDORE);
importMaterial(materials, "block:iron_ore", Block.IRONORE);
importMaterial(materials, "block:coal_ore", Block.COALORE);
importMaterial(materials, "block:log", Block.WOOD);
importMaterial(materials, "block:leaves", Block.LEAVES);
importMaterial(materials, "block:sponge", Block.SPONGE);
importMaterial(materials, "block:glass", Block.GLASS);
importMaterial(materials, "block:lapis_ore", Block.LAPIS_ORE);
importMaterial(materials, "block:lapis_block", Block.LAPIS_BLOCK);
importMaterial(materials, "block:dispenser", Block.DISPENSER);
importMaterial(materials, "block:sandstone", Block.SANDSTONE);
importMaterial(materials, "block:noteblock", Block.NOTEBLOCK);
importMaterial(materials, "block:bed", Block.BED);
importMaterial(materials, "block:golden_rail", Block.POWEREDRAIL);
importMaterial(materials, "block:detector_rail", Block.DETECTORRAIL);
importMaterial(materials, "block:sticky_piston", Block.STICKYPISTON);
importMaterial(materials, "block:web", Block.COBWEB);
importMaterial(materials, "block:tallgrass", Block.TALLGRASS);
importMaterial(materials, "block:deadbush", Block.DEADBUSH);
importMaterial(materials, "block:piston", Block.PISTON);
importMaterial(materials, "block:piston_head", Block.PISTON_HEAD);
importMaterial(materials, "block:wool", Block.WOOL);
importMaterial(materials, "block:piston_extension", Block.PISTON_EXTENSION);
importMaterial(materials, "block:yellow_flower", Block.DANDELION);
importMaterial(materials, "block:red_flower", Block.FLOWER);
importMaterial(materials, "block:brown_mushroom", Block.BROWNMUSHROOM);
importMaterial(materials, "block:red_mushroom", Block.REDMUSHROOM);
importMaterial(materials, "block:gold_block", Block.GOLDBLOCK);
importMaterial(materials, "block:iron_block", Block.IRONBLOCK);
importMaterial(materials, "block:double_stone_slab", Block.DOUBLESLAB);
importMaterial(materials, "block:stone_slab", Block.SLAB);
importMaterial(materials, "block:brick_block", Block.BRICKS);
importMaterial(materials, "block:tnt", Block.TNT);
importMaterial(materials, "block:bookshelf", Block.BOOKSHELF);
importMaterial(materials, "block:mossy_cobblestone", Block.MOSSSTONE);
importMaterial(materials, "block:obsidian", Block.OBSIDIAN);
importMaterial(materials, "block:torch", Block.TORCH);
importMaterial(materials, "block:fire", Block.FIRE);
importMaterial(materials, "block:mob_spawner", Block.MONSTERSPAWNER);
importMaterial(materials, "block:oak_stairs", Block.OAKWOODSTAIRS);
importMaterial(materials, "block:chest", Block.CHEST);
importMaterial(materials, "block:redstone_wire", Block.REDSTONEWIRE);
importMaterial(materials, "block:diamond_ore", Block.DIAMONDORE);
importMaterial(materials, "block:diamond_block", Block.DIAMONDBLOCK);
importMaterial(materials, "block:crafting_table", Block.WORKBENCH);
importMaterial(materials, "block:wheat", Block.CROPS);
importMaterial(materials, "block:farmland", Block.SOIL);
importMaterial(materials, "block:furnace", Block.FURNACEUNLIT);
importMaterial(materials, "block:lit_furnace", Block.FURNACELIT);
importMaterial(materials, "block:standing_sign", Block.SIGNPOST);
importMaterial(materials, "block:wooden_door", Block.WOODENDOOR);
importMaterial(materials, "block:ladder", Block.LADDER);
importMaterial(materials, "block:rail", Block.MINECARTTRACKS);
importMaterial(materials, "block:stone_stairs", Block.STONESTAIRS);
importMaterial(materials, "block:wall_sign", Block.WALLSIGN);
importMaterial(materials, "block:lever", Block.LEVER);
importMaterial(materials, "block:stone_pressure_plate", Block.STONEPRESSUREPLATE);
importMaterial(materials, "block:iron_door", Block.IRONDOOR);
importMaterial(materials, "block:wooden_pressure_plate", Block.WOODENPRESSUREPLATE);
importMaterial(materials, "block:redstone_ore", Block.REDSTONEORE);
importMaterial(materials, "block:lit_redstone_ore", Block.GLOWINGREDSTONEORE);
importMaterial(materials, "block:unlit_redstone_torch", Block.REDSTONETORCHOFF);
importMaterial(materials, "block:redstone_torch", Block.REDSTONETORCHON);
importMaterial(materials, "block:stone_button", Block.STONEBUTTON);
importMaterial(materials, "block:snow_layer", Block.SNOW);
importMaterial(materials, "block:ice", Block.ICE);
importMaterial(materials, "block:snow", Block.SNOWBLOCK);
importMaterial(materials, "block:cactus", Block.CACTUS);
importMaterial(materials, "block:clay", Block.CLAY);
importMaterial(materials, "block:reeds", Block.SUGARCANE);
importMaterial(materials, "block:jukebox", Block.JUKEBOX);
importMaterial(materials, "block:fence", Block.FENCE);
importMaterial(materials, "block:pumpkin", Block.PUMPKIN);
importMaterial(materials, "block:netherrack", Block.NETHERRACK);
importMaterial(materials, "block:soul_sand", Block.SOULSAND);
importMaterial(materials, "block:glowstone", Block.GLOWSTONE);
importMaterial(materials, "block:portal", Block.PORTAL);
importMaterial(materials, "block:lit_pumpkin", Block.JACKOLANTERN);
importMaterial(materials, "block:cake", Block.CAKE);
importMaterial(materials, "block:unpowered_repeater", Block.REDSTONEREPEATEROFF);
importMaterial(materials, "block:powered_repeater", Block.REDSTONEREPEATERON);
importMaterial(materials, "block:stained_glass", Block.STAINED_GLASS);
importMaterial(materials, "block:trapdoor", Block.TRAPDOOR);
importMaterial(materials, "block:monster_egg", Block.HIDDENSILVERFISH);
importMaterial(materials, "block:stonebrick", Block.STONEBRICKS);
importMaterial(materials, "block:brown_mushroom_block", Block.HUGEBROWNMUSHROOM);
importMaterial(materials, "block:red_mushroom_block", Block.HUGEREDMUSHROOM);
importMaterial(materials, "block:iron_bars", Block.IRONBARS);
importMaterial(materials, "block:glass_pane", Block.GLASSPANE);
importMaterial(materials, "block:melon_block", Block.MELON);
importMaterial(materials, "block:pumpkin_stem", Block.PUMPKINSTEM);
importMaterial(materials, "block:melon_stem", Block.MELONSTEM);
importMaterial(materials, "block:vine", Block.VINES);
importMaterial(materials, "block:fence_gate", Block.FENCEGATE);
importMaterial(materials, "block:brick_stairs", Block.BRICKSTAIRS);
importMaterial(materials, "block:stone_brick_stairs", Block.STONEBRICKSTAIRS);
importMaterial(materials, "block:mycelium", Block.MYCELIUM);
importMaterial(materials, "block:waterlily", Block.LILY_PAD);
importMaterial(materials, "block:nether_brick", Block.NETHERBRICK);
importMaterial(materials, "block:nether_brick_fence", Block.NETHERBRICKFENCE);
importMaterial(materials, "block:nether_brick_stairs", Block.NETHERBRICKSTAIRS);
importMaterial(materials, "block:nether_wart", Block.NETHERWART);
importMaterial(materials, "block:enchanting_table", Block.ENCHNATMENTTABLE);
importMaterial(materials, "block:brewing_stand", Block.BREWINGSTAND);
importMaterial(materials, "block:cauldron", Block.CAULDRON);
importMaterial(materials, "block:end_portal", Block.ENDPORTAL);
importMaterial(materials, "block:end_portal_frame", Block.ENDPORTALFRAME);
importMaterial(materials, "block:end_stone", Block.ENDSTONE);
importMaterial(materials, "block:dragon_egg", Block.DRAGONEGG);
importMaterial(materials, "block:redstone_lamp", Block.REDSTONELAMPOFF);
importMaterial(materials, "block:lit_redstone_lamp", Block.REDSTONELAMPON);
importMaterial(materials, "block:double_wooden_slab", Block.DOUBLEWOODENSLAB);
importMaterial(materials, "block:wooden_slab", Block.SINGLEWOODENSLAB);
importMaterial(materials, "block:cocoa", Block.COCOAPLANT);
importMaterial(materials, "block:sandstone_stairs", Block.SANDSTONESTAIRS);
importMaterial(materials, "block:emerald_ore", Block.EMERALDORE);
importMaterial(materials, "block:ender_chest", Block.ENDERCHEST);
importMaterial(materials, "block:tripwire_hook", Block.TRIPWIREHOOK);
importMaterial(materials, "block:tripwire", Block.TRIPWIRE);
importMaterial(materials, "block:emerald_block", Block.EMERALDBLOCK);
importMaterial(materials, "block:spruce_stairs", Block.SPRUCEWOODSTAIRS);
importMaterial(materials, "block:birch_stairs", Block.BIRCHWOODSTAIRS);
importMaterial(materials, "block:jungle_stairs", Block.JUNGLEWOODSTAIRS);
importMaterial(materials, "block:command_block", Block.COMMAND_BLOCK);
importMaterial(materials, "block:beacon", Block.BEACON);
importMaterial(materials, "block:cobblestone_wall", Block.STONEWALL);
importMaterial(materials, "block:flower_pot", Block.FLOWERPOT);
importMaterial(materials, "block:carrots", Block.CARROTS);
importMaterial(materials, "block:potatoes", Block.POTATOES);
importMaterial(materials, "block:wooden_button", Block.WOODENBUTTON);
importMaterial(materials, "block:skull", Block.HEAD);
importMaterial(materials, "block:anvil", Block.ANVIL);
importMaterial(materials, "block:trapped_chest", Block.TRAPPEDCHEST);
importMaterial(materials, "block:light_weighted_pressure_plate", Block.WEIGHTEDPRESSUREPLATELIGHT);
importMaterial(materials, "block:heavy_weighted_pressure_plate", Block.WEIGHTEDPRESSUREPLATEHEAVY);
importMaterial(materials, "block:unpowered_comparator", Block.COMPARATOR);
importMaterial(materials, "block:powered_comparator", Block.COMPARATOR_POWERED);
importMaterial(materials, "block:daylight_detector", Block.DAYLIGHTSENSOR);
importMaterial(materials, "block:redstone_block", Block.REDSTONEBLOCK);
importMaterial(materials, "block:quartz_ore", Block.NETHERQUARTZORE);
importMaterial(materials, "block:hopper", Block.HOPPER);
importMaterial(materials, "block:quartz_block", Block.QUARTZ);
importMaterial(materials, "block:quartz_stairs", Block.QUARTZSTAIRS);
importMaterial(materials, "block:activator_rail", Block.ACTIVATORRAIL);
importMaterial(materials, "block:dropper", Block.DROPPER);
importMaterial(materials, "block:stained_hardened_clay", Block.STAINED_CLAY);
importMaterial(materials, "block:stained_glass_pane", Block.STAINED_GLASSPANE);
importMaterial(materials, "block:leaves2", Block.LEAVES2);
importMaterial(materials, "block:log2", Block.WOOD2);
importMaterial(materials, "block:acacia_stairs", Block.ACACIASTAIRS);
importMaterial(materials, "block:dark_oak_stairs", Block.DARKOAKSTAIRS);
importMaterial(materials, "block:slime", Block.SLIMEBLOCK);
importMaterial(materials, "block:barrier", Block.BARRIER);
importMaterial(materials, "block:iron_trapdoor", Block.IRON_TRAPDOOR);
importMaterial(materials, "block:prismarine", Block.PRISMARINE);
importMaterial(materials, "block:sea_lantern", Block.SEALANTERN);
importMaterial(materials, "block:hay_block", Block.HAY_BLOCK);
importMaterial(materials, "block:carpet", Block.CARPET);
importMaterial(materials, "block:hardened_clay", Block.HARDENED_CLAY);
importMaterial(materials, "block:coal_block", Block.COAL_BLOCK);
importMaterial(materials, "block:packed_ice", Block.PACKED_ICE);
importMaterial(materials, "block:double_plant", Block.LARGE_FLOWER);
importMaterial(materials, "block:standing_banner", Block.STANDING_BANNER);
importMaterial(materials, "block:wall_banner", Block.WALL_BANNER);
importMaterial(materials, "block:daylight_detector_inverted", Block.INVERTED_DAYLIGHTSENSOR);
importMaterial(materials, "block:red_standstone", Block.REDSANDSTONE);
importMaterial(materials, "block:red_standstone_stairs", Block.REDSANDSTONESTAIRS);
importMaterial(materials, "block:double_stone_slab2", Block.DOUBLESLAB2);
importMaterial(materials, "block:stone_slab2", Block.SLAB2);
importMaterial(materials, "block:spruce_fence_gate", Block.SPRUCEFENCEGATE);
importMaterial(materials, "block:birch_fence_gate", Block.BIRCHFENCEGATE);
importMaterial(materials, "block:jungle_fence_gate", Block.JUNGLEFENCEGATE);
importMaterial(materials, "block:dark_oak_fence_gate", Block.DARKOAKFENCEGATE);
importMaterial(materials, "block:acacia_fence_gate", Block.ACACIAFENCEGATE);
importMaterial(materials, "block:spruce_fence", Block.SPRUCEFENCE);
importMaterial(materials, "block:birch_fence", Block.BIRCHFENCE);
importMaterial(materials, "block:jungle_fence", Block.JUNGLEFENCE);
importMaterial(materials, "block:dark_oak_fence", Block.DARKOAKFENCE);
importMaterial(materials, "block:acacia_fence", Block.ACACIAFENCE);
importMaterial(materials, "block:spruce_door", Block.SPRUCEDOOR);
importMaterial(materials, "block:birch_door", Block.BIRCHDOOR);
importMaterial(materials, "block:jungle_door", Block.JUNGLEDOOR);
importMaterial(materials, "block:acacia_door", Block.ACACIADOOR);
importMaterial(materials, "block:dark_oak_door", Block.DARKOAKDOOR);
importMaterial(materials, "block:end_rod", Block.ENDROD);
importMaterial(materials, "block:chorus_plant", Block.CHORUSPLANT);
importMaterial(materials, "block:chorus_flower", Block.CHORUSFLOWER);
importMaterial(materials, "block:purpur_block", Block.PURPURBLOCK);
importMaterial(materials, "block:purpur_pillar", Block.PURPURPILLAR);
importMaterial(materials, "block:purpur_stairs", Block.PURPURSTAIRS);
importMaterial(materials, "block:purpur_double_slab", Block.PURPURDOUBLESLAB);
importMaterial(materials, "block:purpur_slab", Block.PURPURSLAB);
importMaterial(materials, "block:end_bricks", Block.ENDBRICKS);
importMaterial(materials, "block:beetroots", Block.BEETROOTS);
importMaterial(materials, "block:grass_path", Block.GRASSPATH);
importMaterial(materials, "block:end_gateway", Block.END_GATEWAY);
importMaterial(materials, "block:repeating_command_block", Block.REPEATING_COMMAND_BLOCK);
importMaterial(materials, "block:chain_command_block", Block.CHAIN_COMMAND_BLOCK);
importMaterial(materials, "block:frosted_ice", Block.FROSTEDICE);
importMaterial(materials, "block:magma", Block.MAGMA);
importMaterial(materials, "block:nether_wart_block", Block.NETHER_WART_BLOCK);
importMaterial(materials, "block:red_nether_brick", Block.RED_NETHER_BRICK);
importMaterial(materials, "block:bone_block", Block.BONE);
importMaterial(materials, "block:observer", Block.OBSERVER);
importMaterial(materials, "block:white_shuler_box", Block.SHULKERBOX_WHITE);
importMaterial(materials, "block:orange_shuler_box", Block.SHULKERBOX_ORANGE);
importMaterial(materials, "block:magenta_shuler_box", Block.SHULKERBOX_MAGENTA);
importMaterial(materials, "block:ligth_blue_shuler_box", Block.SHULKERBOX_LIGHTBLUE);
importMaterial(materials, "block:yellow_shuler_box", Block.SHULKERBOX_YELLOW);
importMaterial(materials, "block:lime_shuler_box", Block.SHULKERBOX_LIME);
importMaterial(materials, "block:pink_shuler_box", Block.SHULKERBOX_PINK);
importMaterial(materials, "block:gray_shuler_box", Block.SHULKERBOX_GRAY);
importMaterial(materials, "block:light_gray_shuler_box", Block.SHULKERBOX_SILVER);
importMaterial(materials, "block:cyan_shuler_box", Block.SHULKERBOX_CYAN);
importMaterial(materials, "block:purple_shuler_box", Block.SHULKERBOX_PURPLE);
importMaterial(materials, "block:blue_shuler_box", Block.SHULKERBOX_BLUE);
importMaterial(materials, "block:brown_shuler_box", Block.SHULKERBOX_BROWN);
importMaterial(materials, "block:green_shuler_box", Block.SHULKERBOX_GREEN);
importMaterial(materials, "block:red_shuler_box", Block.SHULKERBOX_RED);
importMaterial(materials, "block:black_shuler_box", Block.SHULKERBOX_BLACK);
importMaterial(materials, "block:white_glazed_terracotta", Block.WHITE_TERRACOTTA);
importMaterial(materials, "block:orange_glazed_terracotta", Block.ORANGE_TERRACOTTA);
importMaterial(materials, "block:magenta_glazed_terracotta", Block.MAGENTA_TERRACOTTA);
importMaterial(materials, "block:light_blue_glazed_terracotta", Block.LIGHT_BLUE_TERRACOTTA);
importMaterial(materials, "block:yellow_glazed_terracotta", Block.YELLOW_TERRACOTTA);
importMaterial(materials, "block:lime_glazed_terracotta", Block.LIME_TERRACOTTA);
importMaterial(materials, "block:pink_glazed_terracotta", Block.PINK_TERRACOTTA);
importMaterial(materials, "block:gray_glazed_terracotta", Block.GRAY_TERRACOTTA);
importMaterial(materials, "block:light_gray_glazed_terracotta", Block.SILVER_TERRACOTTA);
importMaterial(materials, "block:cyan_glazed_terracotta", Block.CYAN_TERRACOTTA);
importMaterial(materials, "block:purple_glazed_terracotta", Block.PURPLE_TERRACOTTA);
importMaterial(materials, "block:blue_glazed_terracotta", Block.BLUE_TERRACOTTA);
importMaterial(materials, "block:brown_glazed_terracotta", Block.BROWN_TERRACOTTA);
importMaterial(materials, "block:green_glazed_terracotta", Block.GREEN_TERRACOTTA);
importMaterial(materials, "block:red_glazed_terracotta", Block.RED_TERRACOTTA);
importMaterial(materials, "block:black_glazed_terracotta", Block.BLACK_TERRACOTTA);
importMaterial(materials, "block:concrete", Block.CONCRETE);
importMaterial(materials, "block:concrete_powder", Block.CONCRETE_POWDER);
importMaterial(materials, "block:structure_block", Block.STRUCTURE_BLOCK);
}
private void importMaterial(Map<String, JsonValue> propertyMap, String name, Material material) {
JsonValue value = propertyMap.get(name);
if (value != null) {
JsonObject properties = value.object();
material.emittance = properties.get("emittance").floatValue(material.emittance);
material.specular = properties.get("specular").floatValue(material.specular);
material.ior = properties.get("ior").floatValue(material.ior);
}
}
private void importMaterial(Map<String, JsonValue> propertyMap, String name,
Material... materials) {
JsonValue value = propertyMap.get(name);
if (value != null) {
JsonObject properties = value.object();
for (Material material : materials) {
material.emittance = properties.get("emittance").floatValue(material.emittance);
material.specular = properties.get("specular").floatValue(material.specular);
material.ior = properties.get("ior").floatValue(material.ior);
}
}
}
}