diff --git a/README.md b/README.md index f7b0157fb1..3d6159d9ff 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ For guides and more information please checkout the [Documentation][chunky-dev].
Which Minecraft versions are supported? -> Chunky 2.4.4 supports Minecraft 1.2-1.19.2 worlds and Cubic Chunks for Minecraft 1.10-1.12 worlds. +> Chunky 2.5.0 supports Minecraft 1.2-1.21.8 worlds and Cubic Chunks for Minecraft 1.10-1.12 worlds. > > We typically add new blocks shortly after a new Minecraft snapshot is released. Use the latest Chunky snapshot to render them until a new Chunky version is released. @@ -189,7 +189,7 @@ when in doubt. ## Copyright & License -Chunky is Copyright (c) 2010-2023, Jesper Öqvist and [Chunky Contributors][chunky-contributors]. +Chunky is Copyright (c) 2010-2025, Jesper Öqvist and [Chunky Contributors][chunky-contributors]. Permission to modify and redistribute is granted under the terms of the GPLv3 license. See the file `LICENSE` for the full license. @@ -221,6 +221,8 @@ Chunky uses the following 3rd party libraries: See the file `licenses/Apache-2.0.txt` for the full license text. See the file `licenses/lz4-java.txt` for the copyright notice. lz4-java uses LZ4, by Yann Collet, which is covered by the BSD 2-Clause license. See the file `licenses/lz4.txt` for the copyright notice and full license. +- **ControlsFX by the ControlsFX Project team** + The library is released under the BSD 3-Clause license. See the file `licenses/controlsfx.txt` for the copyright notice and license text. ## Special Thanks diff --git a/build.gradle b/build.gradle index 950a0d99b5..6f0762a19d 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,6 @@ subprojects { languageVersion = JavaLanguageVersion.of(21) } useJUnitPlatform() - // Always run tests, even when nothing changed. dependsOn 'cleanTest' diff --git a/chunky/build.gradle b/chunky/build.gradle index a71bd7026b..2f63733f6c 100644 --- a/chunky/build.gradle +++ b/chunky/build.gradle @@ -23,6 +23,7 @@ dependencies { implementation 'it.unimi.dsi:fastutil:8.4.4' implementation 'org.apache.commons:commons-math3:3.2' implementation 'com.google.code.gson:gson:2.9.0' + implementation 'org.controlsfx:controlsfx:11.2.1' implementation 'org.lz4:lz4-java:1.8.0' implementation 'org.apache.maven:maven-artifact:3.9.9' implementation project(':lib') @@ -31,7 +32,7 @@ dependencies { javafx { version = '17.0.11' configuration = 'implementation' - modules = ['javafx.base', 'javafx.controls', 'javafx.fxml'] + modules = ['javafx.base', 'javafx.controls', 'javafx.fxml', 'javafx.media'] } jar { diff --git a/chunky/src/java/se/llbit/chunky/block/AbstractModelBlock.java b/chunky/src/java/se/llbit/chunky/block/AbstractModelBlock.java index 6c7341c91c..a6b8b6749a 100644 --- a/chunky/src/java/se/llbit/chunky/block/AbstractModelBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/AbstractModelBlock.java @@ -1,11 +1,12 @@ package se.llbit.chunky.block; +import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.model.BlockModel; +import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.*; import java.util.Random; @@ -41,9 +42,188 @@ public BlockModel getModel() { return model; } + /** + * Moves {@code o} to the intersection point (specified by {@code d} and {@code distance}), + * and checks if it is at the edge of the block. + * @param o Origin point (which is updated) + * @param d Ray direction + * @param distance Distance to intersection + * @return Whether the intersection point is on the edge of the block. + */ + public static boolean onEdge(Vector3 o, Vector3 d, double distance) { + o.scaleAdd(distance, d); + double ix = o.x - QuickMath.floor(o.x); + double iy = o.y - QuickMath.floor(o.y); + double iz = o.z - QuickMath.floor(o.z); + return !(Constants.EPSILON < ix && ix < 1 - Constants.EPSILON && + Constants.EPSILON < iy && iy < 1 - Constants.EPSILON && + Constants.EPSILON < iz && iz < 1 - Constants.EPSILON); + } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + Intersectable waterModel = null; + + // TODO this shouldn't be checked at intersection time, but rather when loading chunks. + boolean isWaterloggedFull = false; + if (waterlogged) { + // If this block is waterlogged, we check if the block above it is either water or + // waterlogged (isWaterFilled()). + // If it is, then the water model to use should be the `Block.FULL_BLOCK` full block model. + // Otherwise, the `WaterModel.NOT_FULL_BLOCK` model for a full water block exposed to air + // will be used. + + int x = (int) QuickMath.floor(ray.o.x + ray.d.x * Constants.OFFSET); + int y = (int) QuickMath.floor(ray.o.y + ray.d.y * Constants.OFFSET); + int z = (int) QuickMath.floor(ray.o.z + ray.d.z * Constants.OFFSET); + isWaterloggedFull = scene.getWorldOctree().getMaterial(x, y + 1, z, scene.getPalette()).isWaterFilled(); + if (ray.getCurrentMedium().isWater()) { + if (!isWaterloggedFull) { + waterModel = WaterModel.WATER_TOP; + } + } else { + waterModel = (isWaterloggedFull) ? Block.FULL_BLOCK : WaterModel.NOT_FULL_BLOCK; + } + } + + IntersectionRecord modelIntersect = new IntersectionRecord(); + IntersectionRecord waterIntersect = new IntersectionRecord(); + + boolean modelHit = model.intersect(ray, modelIntersect, scene); + boolean waterHit = false; + if (waterModel != null) { + waterHit = waterModel.closestIntersection(ray, waterIntersect, scene); + } + + // Whether the ray hits the top of the water model when the water model is not a full block. + boolean hitTop = waterHit && !isWaterloggedFull && waterIntersect.n.y > 0 && ray.d.dot(waterIntersect.n) > 0; + + if (ray.getCurrentMedium() == this) { + // Ray is currently traversing this block. + if (modelHit) { + intersectionRecord.setNormal(modelIntersect); + if (ray.d.dot(intersectionRecord.n) > 0) { + // Ray is exiting the block. + Vector3 o = new Vector3(ray.o); + if (onEdge(o, ray.d, modelIntersect.distance)) { + return false; + } + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + + Block waterPlaneMaterial = scene.waterPlaneMaterial(ray.o.rScaleAdd(intersectionRecord.distance, ray.d)); + if (waterlogged) { + if (isWaterloggedFull || o.y < 1 - WaterModel.TOP_BLOCK_GAP) { + intersectionRecord.material = scene.getPalette().water; + Water.INSTANCE.getColor(intersectionRecord); + } else { + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + } + } else { + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + } + } else { + // Ray is not exiting the block, but it hit a surface inside the block. + intersectionRecord.color.set(modelIntersect.color); + } + intersectionRecord.distance = modelIntersect.distance; + intersectionRecord.flags = modelIntersect.flags; + return true; + } else { + return false; + } + } else if (ray.getCurrentMedium().isWater()) { + // Ray is currently traversing water, whether that be the water part of the block (if it be + // waterlogged), or water outside the block. + if (!waterlogged && (!modelHit || modelIntersect.distance > Constants.EPSILON)) { + // Ray is currently traversing water, and the block is not waterlogged. + // Therefore, if we don't hit the block model, or the distance to the model hit is greater + // than zero, we need to hit the "air part" of the block, to take the material properties + // into account. If the intersection point is below the water plane, the water plane + // material is hit instead of air. + intersectionRecord.distance = 0; + Block waterPlaneMaterial = scene.waterPlaneMaterial(ray.o); + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + return true; + } + if (modelHit && modelIntersect.distance < waterIntersect.distance - Constants.EPSILON) { + // The ray hit the block model, and the distance to the block model intersection is closer + // than the distance to the water model intersection. As such, we hit the block model. + intersectionRecord.distance = modelIntersect.distance; + intersectionRecord.setNormal(modelIntersect); + intersectionRecord.color.set(modelIntersect.color); + intersectionRecord.flags = modelIntersect.flags; + return true; + } else if (hitTop) { + // The ray is traversing water, did not hit the block model, and has hit the top of the + // water model (not full block). + intersectionRecord.distance = waterIntersect.distance; + intersectionRecord.setNormal(waterIntersect); + + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + intersectionRecord.material = scene.waterPlaneMaterial(testRay.o); + intersectionRecord.material.getColor(intersectionRecord); + + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + return true; + } else { + return false; + } + } else { + // The ray is currently traversing any other material, such as air, glass, WaterPlane, etc. + if (!waterlogged && (!modelHit || modelIntersect.distance > Constants.EPSILON)) { + // As before, if the block is not waterlogged, we need to make sure to take the "air part" + // into account, whether that be air, or the water plane. + Block waterPlaneMaterial = scene.waterPlaneMaterial(ray.o); + if (ray.getCurrentMedium() != waterPlaneMaterial) { + intersectionRecord.distance = 0; + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + return true; + } + } + if (modelHit && modelIntersect.distance < waterIntersect.distance + Constants.EPSILON) { + // The ray hit the block model, and the distance is closer than the distance to the water + // model intersection. + intersectionRecord.distance = modelIntersect.distance; + intersectionRecord.setNormal(modelIntersect); + intersectionRecord.color.set(modelIntersect.color); + intersectionRecord.flags = modelIntersect.flags; + return true; + } else if (waterHit) { + // The water model intersection is closer. + intersectionRecord.distance = waterIntersect.distance; + intersectionRecord.setNormal(waterIntersect); + Water.INSTANCE.getColor(intersectionRecord); + intersectionRecord.material = scene.getPalette().water; + intersectionRecord.flags = 0; + + if (intersectionRecord.n.y > 0) { + // Add water shading. + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + } + return true; + } else { + // The ray did not hit anything. + return false; + } + } + } + @Override - public boolean intersect(Ray ray, Scene scene) { - return model.intersect(ray, scene); + public boolean isInside(Ray ray) { + return model.isInside(ray); } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/Block.java b/chunky/src/java/se/llbit/chunky/block/Block.java index 3a8f3a7e71..02efff7640 100644 --- a/chunky/src/java/se/llbit/chunky/block/Block.java +++ b/chunky/src/java/se/llbit/chunky/block/Block.java @@ -7,21 +7,19 @@ import se.llbit.chunky.world.biome.Biome; import se.llbit.json.JsonString; import se.llbit.json.JsonValue; -import se.llbit.math.AABB; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.*; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.Tag; import java.util.Random; public abstract class Block extends Material { - private final static AABB block = new AABB(0, 1, 0, 1, 0, 1); + public static final AABB FULL_BLOCK = new AABB(0, 1, 0, 1,0, 1); /** * Set to true if there is a local intersection model for this block. If this is set to * false (default), this block is assumed to be an opaque cube block and {@link - * #intersect(Ray, Scene)} will never be called. + * #intersect(Ray, IntersectionRecord, Scene)} will never be called. */ public boolean localIntersect = false; @@ -50,14 +48,14 @@ public int faceCount() { * @param rand Random number source. */ public void sample(int face, Vector3 loc, Random rand) { - block.sampleFace(face, loc, rand); + FULL_BLOCK.sampleFace(face, loc, rand); } /** * Get the surface area of this face of the block. */ public double surfaceArea(int face) { - return block.faceSurfaceArea(face); + return FULL_BLOCK.faceSurfaceArea(face); } /** @@ -69,17 +67,7 @@ public double surfaceArea(int face) { * @param scene Scene * @return True if the ray hit this block, false if not */ - public boolean intersect(Ray ray, Scene scene) { - ray.t = Double.POSITIVE_INFINITY; - if (block.intersect(ray)) { - float[] color = texture.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.distance += ray.tNext; - ray.o.scaleAdd(ray.tNext, ray.d); - return true; - } - } + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { return false; } @@ -148,6 +136,15 @@ public Tag getNewTagWithBlockEntity(Tag blockTag, CompoundTag entityTag) { return null; } + /** + * Checks whether the given ray is inside this block. + */ + public boolean isInside(Ray ray) { + double ix = ray.o.x - QuickMath.floor(ray.o.x); + double iy = ray.o.y - QuickMath.floor(ray.o.y); + double iz = ray.o.z - QuickMath.floor(ray.o.z); + return FULL_BLOCK.inside(new Vector3(ix, iy, iz)); + } /** * Does this block use biome tint for its rendering */ diff --git a/chunky/src/java/se/llbit/chunky/block/LeavesBase.java b/chunky/src/java/se/llbit/chunky/block/LeavesBase.java new file mode 100644 index 0000000000..8d980b3c2a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/block/LeavesBase.java @@ -0,0 +1,106 @@ +package se.llbit.chunky.block; + +import se.llbit.chunky.block.minecraft.Water; +import se.llbit.chunky.model.minecraft.WaterModel; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.Intersectable; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.QuickMath; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +public abstract class LeavesBase extends AbstractModelBlock { + public LeavesBase(String name, Texture texture) { + super(name, texture); + } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + Intersectable waterModel = null; + boolean isWaterloggedFull = false; + if (waterlogged) { + int x = (int) QuickMath.floor(ray.o.x); + int y = (int) QuickMath.floor(ray.o.y); + int z = (int) QuickMath.floor(ray.o.z); + isWaterloggedFull = scene.getWorldOctree().getMaterial(x, y + 1, z, scene.getPalette()).isWaterFilled(); + if (ray.getCurrentMedium().isWater()) { + if (!isWaterloggedFull) { + waterModel = WaterModel.WATER_TOP; + } + } else { + waterModel = (isWaterloggedFull) ? Block.FULL_BLOCK : WaterModel.NOT_FULL_BLOCK; + } + } + + IntersectionRecord modelIntersect = new IntersectionRecord(); + IntersectionRecord waterIntersect = new IntersectionRecord(); + + boolean modelHit = model.intersect(ray, modelIntersect, scene); + boolean waterHit = false; + if (waterModel != null) { + waterHit = waterModel.closestIntersection(ray, waterIntersect, scene); + } + + boolean hitTop = waterHit && !isWaterloggedFull && waterIntersect.n.y > 0 && ray.d.dot(waterIntersect.n) > 0; + + if (ray.getCurrentMedium().isWater()) { + if (hitTop) { + intersectionRecord.distance = waterIntersect.distance; + intersectionRecord.setNormal(waterIntersect); + intersectionRecord.material = (scene.waterPlaneMaterial(ray.o.rScaleAdd(intersectionRecord.distance, ray.d))); + intersectionRecord.material.getColor(intersectionRecord); + + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + return true; + } else { + return false; + } + } else { + if (modelHit + && modelIntersect.distance < waterIntersect.distance + Constants.EPSILON + && modelIntersect.color.w > Constants.EPSILON) { + intersectionRecord.distance = modelIntersect.distance; + intersectionRecord.setNormal(modelIntersect); + intersectionRecord.color.set(modelIntersect.color); + intersectionRecord.flags = modelIntersect.flags; + return true; + } else if (waterHit) { + intersectionRecord.distance = waterIntersect.distance; + intersectionRecord.setNormal(waterIntersect); + Water.INSTANCE.getColor(intersectionRecord); + intersectionRecord.material = scene.getPalette().water; + intersectionRecord.flags = 0; + + if (intersectionRecord.n.y > 0) { + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + } + return true; + } else { + Block waterPlaneMaterial; + if (ray.getCurrentMedium() != (waterPlaneMaterial = scene.waterPlaneMaterial(ray.o))) { + intersectionRecord.distance = 0; + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + return true; + } + return false; + } + } + } + + @Override + public boolean isInside(Ray ray) { + return false; + } +} diff --git a/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java b/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java index 50b82082d6..f2334d944a 100644 --- a/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java +++ b/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java @@ -940,6 +940,7 @@ private static void addBlocks(Texture texture, String... names) { } static { + addBlock("void", (name, tag) -> Void.INSTANCE); addBlocks((name, tag) -> Air.INSTANCE, "air", "cave_air", "void_air", "structure_void"); addBlock("barrier", (name, tag) -> tag.get("Properties").get("waterlogged").stringValue("").equals("true") ? Water.INSTANCE : Air.INSTANCE); addBlocks(Texture.stone, "infested_stone", "stone"); @@ -1091,25 +1092,25 @@ private static void addBlocks(Texture texture, String... names) { addBlock("chiseled_tuff_bricks", Texture.chiseledTuffBricks); for (String s : new String[]{"", "waxed_"}) { addBlock(s + "chiseled_copper", Texture.chiseledCopper); - addBlock(s + "copper_grate", (name, tag) -> new SolidNonOpaqueBlock(name, Texture.copperGrate)); + addBlock(s + "copper_grate", (name, tag) -> new CopperGrate(name, Texture.copperGrate)); addBlock(s + "copper_bulb", (name, tag) -> new CopperBulb(name, tag.get("Properties").get("lit").stringValue().equals("true"), tag.get("Properties").get("powered").stringValue().equals("true"), Texture.copperBulbLitPowered, Texture.copperBulbLit, Texture.copperBulbPowered, Texture.copperBulb)); addBlock(s + "copper_door", (name, tag) -> door(tag, Texture.copperDoorTop, Texture.copperDoorBottom)); addBlock(s + "copper_trapdoor", (name, tag) -> trapdoor(tag, Texture.copperTrapdoor)); addBlock(s + "exposed_chiseled_copper", Texture.exposedChiseledCopper); - addBlock(s + "exposed_copper_grate", (name, tag) -> new SolidNonOpaqueBlock(name, Texture.exposedCopperGrate)); + addBlock(s + "exposed_copper_grate", (name, tag) -> new CopperGrate(name, Texture.exposedCopperGrate)); addBlock(s + "exposed_copper_bulb", (name, tag) -> new CopperBulb(name, tag.get("Properties").get("lit").stringValue().equals("true"), tag.get("Properties").get("powered").stringValue().equals("true"), Texture.exposedCopperBulbLitPowered, Texture.exposedCopperBulbLit, Texture.exposedCopperBulbPowered, Texture.exposedCopperBulb)); addBlock(s + "exposed_copper_door", (name, tag) -> door(tag, Texture.exposedCopperDoorTop, Texture.exposedCopperDoorBottom)); addBlock(s + "exposed_copper_trapdoor", (name, tag) -> trapdoor(tag, Texture.exposedCopperTrapdoor)); addBlock(s + "weathered_chiseled_copper", Texture.weatheredChiseledCopper); - addBlock(s + "weathered_copper_grate", (name, tag) -> new SolidNonOpaqueBlock(name, Texture.weatheredCopperGrate)); + addBlock(s + "weathered_copper_grate", (name, tag) -> new CopperGrate(name, Texture.weatheredCopperGrate)); addBlock(s + "weathered_copper_bulb", (name, tag) -> new CopperBulb(name, tag.get("Properties").get("lit").stringValue().equals("true"), tag.get("Properties").get("powered").stringValue().equals("true"), Texture.weatheredCopperBulbLitPowered, Texture.weatheredCopperBulbLit, Texture.weatheredCopperBulbPowered, Texture.weatheredCopperBulb)); addBlock(s + "weathered_copper_door", (name, tag) -> door(tag, Texture.weatheredCopperDoorTop, Texture.weatheredCopperDoorBottom)); addBlock(s + "weathered_copper_trapdoor", (name, tag) -> trapdoor(tag, Texture.weatheredCopperTrapdoor)); addBlock(s + "oxidized_chiseled_copper", Texture.oxidizedChiseledCopper); - addBlock(s + "oxidized_copper_grate", (name, tag) -> new SolidNonOpaqueBlock(name, Texture.oxidizedCopperGrate)); + addBlock(s + "oxidized_copper_grate", (name, tag) -> new CopperGrate(name, Texture.oxidizedCopperGrate)); addBlock(s + "oxidized_copper_bulb", (name, tag) -> new CopperBulb(name, tag.get("Properties").get("lit").stringValue().equals("true"), tag.get("Properties").get("powered").stringValue().equals("true"), Texture.oxidizedCopperBulbLitPowered, Texture.oxidizedCopperBulbLit, Texture.oxidizedCopperBulbPowered, Texture.oxidizedCopperBulb)); addBlock(s + "oxidized_copper_door", (name, tag) -> door(tag, Texture.oxidizedCopperDoorTop, Texture.oxidizedCopperDoorBottom)); @@ -1167,7 +1168,7 @@ private static void addBlocks(Texture texture, String... names) { addBlock("resin_bricks", Texture.resinBricks); addBlock("resin_brick_stairs", (name, tag) -> stairs(tag, Texture.resinBricks)); addBlock("resin_brick_slab", (name, tag) -> slab(tag, Texture.resinBricks)); - addBlock("resin_brick_walls", (name, tag) -> wall(tag, Texture.resinBricks)); + addBlock("resin_brick_wall", (name, tag) -> wall(tag, Texture.resinBricks)); addBlock("resin_clump", (name, tag) -> new ResinClump( tag.get("Properties").get("north").stringValue("false").equals("true"), tag.get("Properties").get("south").stringValue("false").equals("true"), @@ -1512,6 +1513,7 @@ public Block getBlockByTag(String namespacedName, Tag tag) { case "iron_block": return new MinecraftBlock(name, Texture.ironBlock); case "oak_slab": + case "petrified_oak_slab": return slab(tag, Texture.oakPlanks); case "spruce_slab": return slab(tag, Texture.sprucePlanks); @@ -1530,8 +1532,6 @@ public Block getBlockByTag(String namespacedName, Tag tag) { return slab(tag, Texture.smoothStoneSlabSide, Texture.smoothStone); case "sandstone_slab": return slab(tag, Texture.sandstoneSide, Texture.sandstoneTop); - case "petrified_oak_slab": - return slab(tag, Texture.oakPlanks); case "cobblestone_slab": return slab(tag, Texture.cobblestone); case "brick_slab": @@ -1667,7 +1667,7 @@ public Block getBlockByTag(String namespacedName, Tag tag) { case "ice": return new MinecraftBlockTranslucent(name, Texture.ice); case "snow_block": - return new MinecraftBlock(name, Texture.snowBlock); + return new SolidNonOpaqueBlock(name, Texture.snowBlock); case "cactus": return new Cactus(); case "clay": @@ -2053,9 +2053,7 @@ public Block getBlockByTag(String namespacedName, Tag tag) { return stairs( tag, Texture.redSandstoneSide, Texture.redSandstoneTop, Texture.redSandstoneBottom); case "magma_block": { - Block block = new MinecraftBlock(name, Texture.magma); - block.emittance = 0.6f; - return block; + return new MinecraftBlock(name, Texture.magma); } case "nether_wart_block": return new MinecraftBlock(name, Texture.netherWartBlock); diff --git a/chunky/src/java/se/llbit/chunky/block/ModelBlock.java b/chunky/src/java/se/llbit/chunky/block/ModelBlock.java index 5f9b88ae97..98ef3c39e3 100644 --- a/chunky/src/java/se/llbit/chunky/block/ModelBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/ModelBlock.java @@ -3,6 +3,7 @@ import se.llbit.chunky.model.BlockModel; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; @PluginApi @@ -11,7 +12,7 @@ public interface ModelBlock { @PluginApi BlockModel getModel(); - default boolean intersect(Ray ray, Scene scene) { - return getModel().intersect(ray, scene); + default boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return getModel().intersect(ray, intersectionRecord, scene); } } diff --git a/chunky/src/java/se/llbit/chunky/block/OctreeFinalizationState.java b/chunky/src/java/se/llbit/chunky/block/OctreeFinalizationState.java index 345e7d0983..f5c9b93d58 100644 --- a/chunky/src/java/se/llbit/chunky/block/OctreeFinalizationState.java +++ b/chunky/src/java/se/llbit/chunky/block/OctreeFinalizationState.java @@ -7,18 +7,16 @@ public class OctreeFinalizationState extends FinalizationState { private final Octree worldTree; - private final Octree waterTree; private final int yMin; private final int yMax; private int x; private int y; private int z; - public OctreeFinalizationState(Octree worldTree, Octree waterTree, + public OctreeFinalizationState(Octree worldTree, BlockPalette palette, int yMin, int yMax) { super(palette); this.worldTree = worldTree; - this.waterTree = waterTree; this.yMin = yMin; this.yMax = yMax; } diff --git a/chunky/src/java/se/llbit/chunky/block/SolidNonOpaqueBlock.java b/chunky/src/java/se/llbit/chunky/block/SolidNonOpaqueBlock.java index 1fab2c7711..18f322655b 100644 --- a/chunky/src/java/se/llbit/chunky/block/SolidNonOpaqueBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/SolidNonOpaqueBlock.java @@ -2,8 +2,7 @@ import se.llbit.chunky.resources.Texture; -public class SolidNonOpaqueBlock extends Block { - +public class SolidNonOpaqueBlock extends MinecraftBlock { public SolidNonOpaqueBlock(String name, Texture texture) { super(name, texture); solid = true; diff --git a/chunky/src/java/se/llbit/chunky/block/UntintedLeaves.java b/chunky/src/java/se/llbit/chunky/block/UntintedLeaves.java index 09e7b1e2e3..2cc5631653 100644 --- a/chunky/src/java/se/llbit/chunky/block/UntintedLeaves.java +++ b/chunky/src/java/se/llbit/chunky/block/UntintedLeaves.java @@ -1,9 +1,12 @@ package se.llbit.chunky.block; +import se.llbit.chunky.model.minecraft.UntintedLeafModel; import se.llbit.chunky.resources.Texture; -public class UntintedLeaves extends MinecraftBlockTranslucent { +public class UntintedLeaves extends LeavesBase { + public UntintedLeaves(String name, Texture texture) { super(name, texture); + this.model = new UntintedLeafModel(texture); } } diff --git a/chunky/src/java/se/llbit/chunky/block/Void.java b/chunky/src/java/se/llbit/chunky/block/Void.java new file mode 100644 index 0000000000..e16f90ca12 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/block/Void.java @@ -0,0 +1,14 @@ +package se.llbit.chunky.block; + +import se.llbit.chunky.resources.SolidColorTexture; + +public class Void extends MinecraftBlock { + public static final Void INSTANCE = new Void(); + + public Void() { + super("void", SolidColorTexture.EMPTY); + solid = false; + opaque = false; + invisible = true; + } +} diff --git a/chunky/src/java/se/llbit/chunky/block/legacy/LegacyBlocksFinalizer.java b/chunky/src/java/se/llbit/chunky/block/legacy/LegacyBlocksFinalizer.java index 99927e478c..e30c88a604 100644 --- a/chunky/src/java/se/llbit/chunky/block/legacy/LegacyBlocksFinalizer.java +++ b/chunky/src/java/se/llbit/chunky/block/legacy/LegacyBlocksFinalizer.java @@ -1,6 +1,5 @@ package se.llbit.chunky.block.legacy; -import se.llbit.chunky.block.FinalizationState; import se.llbit.chunky.block.OctreeFinalizationState; import se.llbit.chunky.chunk.BlockPalette; import se.llbit.chunky.world.ChunkPosition; @@ -23,9 +22,9 @@ public class LegacyBlocksFinalizer { * @param yMin Minimum y position to finalize * @param yMax Max y level to finalize (exclusive) */ - public static void finalizeChunk(Octree worldTree, Octree waterTree, BlockPalette palette, - Vector3i origin, ChunkPosition cp, int yMin, int yMax) { - OctreeFinalizationState finalizerState = new OctreeFinalizationState(worldTree, waterTree, + public static void finalizeChunk(Octree worldTree, BlockPalette palette, + Vector3i origin, ChunkPosition cp, int yMin, int yMax) { + OctreeFinalizationState finalizerState = new OctreeFinalizationState(worldTree, palette, yMin, yMax); for (int cy = yMin; cy < yMax; ++cy) { int y = cy - origin.y; diff --git a/chunky/src/java/se/llbit/chunky/block/legacy/UnfinalizedLegacyBlock.java b/chunky/src/java/se/llbit/chunky/block/legacy/UnfinalizedLegacyBlock.java index 49ebe10cac..069ed6798d 100644 --- a/chunky/src/java/se/llbit/chunky/block/legacy/UnfinalizedLegacyBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/legacy/UnfinalizedLegacyBlock.java @@ -5,6 +5,7 @@ import se.llbit.chunky.block.FinalizationState; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.log.Log; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; import se.llbit.nbt.CompoundTag; @@ -53,10 +54,10 @@ private UnfinalizedLegacyBlock(String name, Block block, CompoundTag tag) { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { Log.info("Intersecting a UnfinalizedLegacyBlock (" + block.name + "), which is supposed to be replaced"); - return block.intersect(ray, scene); + return block.intersect(ray, intersectionRecord, scene); } /** diff --git a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyBanner.java b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyBanner.java index 864734b46d..536add1808 100644 --- a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyBanner.java +++ b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyBanner.java @@ -1,14 +1,12 @@ package se.llbit.chunky.block.legacy.blocks; -import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.block.minecraft.EmptyModelBlock; import se.llbit.chunky.entity.BannerDesign; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.StandingBanner; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.ListTag; @@ -20,7 +18,7 @@ * The block itself is invisible and the banner is rendered as an entity so this block doesn't get * finalized but just creates the corresponding {@link StandingBanner}. */ -public class LegacyBanner extends MinecraftBlockTranslucent { +public class LegacyBanner extends EmptyModelBlock { private static final BannerDesign.Color[] COLOR_MAP = { BannerDesign.Color.BLACK, @@ -45,7 +43,6 @@ public class LegacyBanner extends MinecraftBlockTranslucent { public LegacyBanner(String name, CompoundTag tag) { super(name, Texture.whiteWool); - localIntersect = true; invisible = true; rotation = tag.get("Data").intValue(0); } @@ -60,11 +57,6 @@ public Entity toBlockEntity(Vector3 position, CompoundTag entityTag) { return new StandingBanner(position, rotation, parseDesign(entityTag)); } - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } - /** * Parse a banner design from the given Minecraft 1.12 or older banner entity tag and convert the * colors to 1.13+ values. diff --git a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacySkull.java b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacySkull.java index 540cc01da6..d7b55465ab 100644 --- a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacySkull.java +++ b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacySkull.java @@ -1,14 +1,14 @@ package se.llbit.chunky.block.legacy.blocks; -import se.llbit.chunky.block.MinecraftBlockTranslucent; +import static se.llbit.chunky.block.minecraft.Head.getTextureUrl; + +import se.llbit.chunky.block.minecraft.EmptyModelBlock; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.HeadEntity; import se.llbit.chunky.entity.SkullEntity; import se.llbit.chunky.entity.SkullEntity.Kind; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.log.Log; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; @@ -22,14 +22,13 @@ * The block itself is invisible and the skull is rendered as an entity so this block doesn't get * finalized but just creates the corresponding {@link HeadEntity} or {@link SkullEntity} instead. */ -public class LegacySkull extends MinecraftBlockTranslucent { +public class LegacySkull extends EmptyModelBlock { private final int placement; public LegacySkull(String name, CompoundTag tag) { super(name, Texture.steve); this.placement = tag.get("Data").intValue(0); - localIntersect = true; invisible = true; } @@ -55,11 +54,6 @@ public Entity toBlockEntity(Vector3 position, CompoundTag entityTag) { return new SkullEntity(position, kind, rotation, placement); } - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } - private static Kind getSkullKind(int skullType) { switch (skullType) { case 0: diff --git a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyWallBanner.java b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyWallBanner.java index ff46332ae8..0a5e41c8a3 100644 --- a/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyWallBanner.java +++ b/chunky/src/java/se/llbit/chunky/block/legacy/blocks/LegacyWallBanner.java @@ -1,11 +1,9 @@ package se.llbit.chunky.block.legacy.blocks; -import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.block.minecraft.EmptyModelBlock; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.WallBanner; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; @@ -15,13 +13,12 @@ * The block itself is invisible and the wall banner is rendered as an entity so this block doesn't * get finalized but just creates the corresponding {@link WallBanner}. */ -public class LegacyWallBanner extends MinecraftBlockTranslucent { +public class LegacyWallBanner extends EmptyModelBlock { private final int facing; public LegacyWallBanner(String name, CompoundTag tag) { super(name, Texture.whiteWool); - localIntersect = true; invisible = true; facing = tag.get("Data").intValue(2); } @@ -35,9 +32,4 @@ public boolean isBlockEntity() { public Entity toBlockEntity(Vector3 position, CompoundTag entityTag) { return new WallBanner(position, facing, LegacyBanner.parseDesign(entityTag)); } - - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Air.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Air.java index a0e291ed87..de83342b88 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Air.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Air.java @@ -19,13 +19,13 @@ package se.llbit.chunky.block.minecraft; import se.llbit.chunky.block.MinecraftBlock; -import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.resources.SolidColorTexture; public class Air extends MinecraftBlock { public static final Air INSTANCE = new Air(); private Air() { - super("air", Texture.air); + super("air", SolidColorTexture.EMPTY); solid = false; opaque = false; invisible = true; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Banner.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Banner.java index e9a204e6b2..8029d26c53 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Banner.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Banner.java @@ -18,38 +18,29 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.BannerDesign; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.StandingBanner; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.json.Json; import se.llbit.json.JsonObject; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; // Note: Mojang changed the ID values for banner colors in Minecraft 1.13, // for backward compatibility we need some way of mapping the old color IDs to the // new color IDs. This would require tracking the world format version somewhere. -public class Banner extends MinecraftBlockTranslucent { +public class Banner extends EmptyModelBlock { private final int rotation; private final BannerDesign.Color color; public Banner(String name, Texture texture, int rotation, BannerDesign.Color color) { super(name, texture); invisible = true; - opaque = false; - localIntersect = true; this.rotation = rotation % 16; this.color = color; } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isBlockEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Beacon.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Beacon.java index 3f6949024b..a15eb56b68 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Beacon.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Beacon.java @@ -22,7 +22,10 @@ import se.llbit.chunky.entity.BeaconBeam; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.model.minecraft.BeaconModel; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; @@ -52,4 +55,22 @@ public Entity toBlockEntity(Vector3 position, CompoundTag entityTag) { } return null; } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (model.intersect(ray, intersectionRecord, scene)) { + if (ray.getCurrentMedium() == this) { + if (ray.d.dot(intersectionRecord.n) > 0) { + Vector3 o = new Vector3(ray.o); + if (onEdge(o, ray.d, intersectionRecord.distance)) { + return false; + } + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + } + } + return true; + } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Campfire.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Campfire.java index 42c4d59499..47fab00c38 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Campfire.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Campfire.java @@ -18,17 +18,14 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import java.util.Random; -public class Campfire extends MinecraftBlockTranslucent { +public class Campfire extends EmptyModelBlock { private final se.llbit.chunky.entity.Campfire.Kind kind; private final String facing; public final boolean isLit; @@ -36,18 +33,11 @@ public class Campfire extends MinecraftBlockTranslucent { public Campfire(String name, se.llbit.chunky.entity.Campfire.Kind kind, String facing, boolean lit) { super(name, Texture.campfireLog); invisible = true; - opaque = false; - localIntersect = true; this.kind = kind; this.facing = facing; this.isLit = lit; } - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isBlockEntity() { return true; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Cauldron.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Cauldron.java index 6643e6db37..956489a044 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Cauldron.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Cauldron.java @@ -22,6 +22,7 @@ import se.llbit.chunky.model.minecraft.CauldronModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; public class Cauldron extends MinecraftBlockTranslucent { @@ -39,8 +40,8 @@ public int getLevel() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return CauldronModel.intersectWithWater(ray, scene, level); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return CauldronModel.intersectWithWater(ray, intersectionRecord, scene, level); } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/CopperGrate.java b/chunky/src/java/se/llbit/chunky/block/minecraft/CopperGrate.java new file mode 100644 index 0000000000..dad1251743 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/CopperGrate.java @@ -0,0 +1,13 @@ +package se.llbit.chunky.block.minecraft; + +import se.llbit.chunky.block.AbstractModelBlock; +import se.llbit.chunky.model.minecraft.CopperGrateModel; +import se.llbit.chunky.resources.Texture; + +public class CopperGrate extends AbstractModelBlock { + public CopperGrate(String name, Texture texture) { + super(name, texture); + solid = true; + this.model = new CopperGrateModel(texture); + } +} diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/CoralFan.java b/chunky/src/java/se/llbit/chunky/block/minecraft/CoralFan.java index d68a3111c7..00d52f3517 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/CoralFan.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/CoralFan.java @@ -18,23 +18,18 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.CoralFanEntity; import se.llbit.chunky.entity.Entity; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; -public class CoralFan extends MinecraftBlockTranslucent { +public class CoralFan extends EmptyModelBlock { private final String coralType; public CoralFan(String name, String coralType) { super(name, coralTexture(coralType)); this.coralType = coralType; - localIntersect = true; - solid = false; invisible = true; } @@ -64,10 +59,6 @@ public static Texture coralTexture(String coralType) { } } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/EmptyModelBlock.java b/chunky/src/java/se/llbit/chunky/block/minecraft/EmptyModelBlock.java new file mode 100644 index 0000000000..ae66c47670 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/EmptyModelBlock.java @@ -0,0 +1,12 @@ +package se.llbit.chunky.block.minecraft; + +import se.llbit.chunky.block.AbstractModelBlock; +import se.llbit.chunky.model.EmptyModel; +import se.llbit.chunky.resources.Texture; + +public class EmptyModelBlock extends AbstractModelBlock { + public EmptyModelBlock(String name, Texture texture) { + super(name, texture); + this.model = EmptyModel.INSTANCE; + } +} diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Glass.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Glass.java index b83715d8c2..bb4189fb62 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Glass.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Glass.java @@ -25,7 +25,6 @@ public class Glass extends MinecraftBlockTranslucent { public Glass(String name, Texture texture) { super(name, texture); - ior = 1.52f; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/HangingSign.java b/chunky/src/java/se/llbit/chunky/block/minecraft/HangingSign.java index f669c87a78..9ba7fe87a8 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/HangingSign.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/HangingSign.java @@ -18,15 +18,12 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.HangingSignEntity; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; -public class HangingSign extends MinecraftBlockTranslucent { +public class HangingSign extends EmptyModelBlock { private final String material; private final int rotation; private final boolean attached; @@ -37,13 +34,6 @@ public HangingSign(String name, String material, int rotation, boolean attached) this.rotation = rotation; this.attached = attached; invisible = true; - solid = false; - localIntersect = true; - } - - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Head.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Head.java index ff728b64f0..c78e49574e 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Head.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Head.java @@ -17,15 +17,12 @@ */ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.HeadEntity; import se.llbit.chunky.entity.SkullEntity; import se.llbit.chunky.entity.SkullEntity.Kind; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.log.Log; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.Tag; @@ -36,7 +33,7 @@ import java.io.IOException; import java.util.Optional; -public class Head extends MinecraftBlockTranslucent { +public class Head extends EmptyModelBlock { private final String description; private final int rotation; @@ -44,18 +41,12 @@ public class Head extends MinecraftBlockTranslucent { public Head(String name, Texture texture, SkullEntity.Kind type, int rotation) { super(name, texture); - localIntersect = true; invisible = true; description = "rotation=" + rotation; this.type = type; this.rotation = rotation; } - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public String description() { return description; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Honey.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Honey.java index 5fcbfc15bc..91421ebc77 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Honey.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Honey.java @@ -18,24 +18,35 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.block.AbstractModelBlock; import se.llbit.chunky.model.minecraft.HoneyBlockModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; +import se.llbit.math.Vector3; -public class Honey extends MinecraftBlockTranslucent { +public class Honey extends AbstractModelBlock { public Honey() { super("honey_block", Texture.honeyBlockSide); - localIntersect = true; - opaque = false; - ior = 1.474f; // according to https://study.com/academy/answer/what-is-the-refractive-index-of-honey.html - solid = false; - refractive = true; + model = new HoneyBlockModel(); } - @Override - public boolean intersect(Ray ray, Scene scene) { - return HoneyBlockModel.intersect(ray); + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (model.intersect(ray, intersectionRecord, scene)) { + if (ray.getCurrentMedium() == this) { + if (ray.d.dot(intersectionRecord.n) > 0) { + Vector3 o = new Vector3(ray.o); + if (onEdge(o, ray.d, intersectionRecord.distance)) { + return false; + } + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + } + } + return true; } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Lava.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Lava.java index 62e5099c36..95a004fe95 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Lava.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Lava.java @@ -19,15 +19,16 @@ package se.llbit.chunky.block.minecraft; import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.math.*; -import static se.llbit.chunky.block.minecraft.Water.CORNER_0; -import static se.llbit.chunky.block.minecraft.Water.CORNER_1; -import static se.llbit.chunky.block.minecraft.Water.CORNER_2; -import static se.llbit.chunky.block.minecraft.Water.CORNER_3; -import static se.llbit.chunky.block.minecraft.Water.FULL_BLOCK; +import static se.llbit.chunky.block.minecraft.Water.FULL_BLOCK_DATA; +import static se.llbit.chunky.model.minecraft.WaterModel.CORNER_0; +import static se.llbit.chunky.model.minecraft.WaterModel.CORNER_1; +import static se.llbit.chunky.model.minecraft.WaterModel.CORNER_2; +import static se.llbit.chunky.model.minecraft.WaterModel.CORNER_3; public class Lava extends MinecraftBlockTranslucent { private static final AABB fullBlock = new AABB(0, 1, 0, 1, 0, 1); @@ -45,34 +46,29 @@ public Lava(int level, int data) { this.data = data; solid = false; localIntersect = true; - emittance = 1.0f; } public Lava(int level) { - this(level, 1 << FULL_BLOCK); + this(level, 1 << FULL_BLOCK_DATA); } public boolean isFullBlock() { - return (this.data & (1 << FULL_BLOCK)) != 0; + return (this.data & (1 << FULL_BLOCK_DATA)) != 0; } - @Override public boolean intersect(Ray ray, Scene scene) { - ray.t = Double.POSITIVE_INFINITY; - + @Override public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { if (isFullBlock()) { - if (fullBlock.intersect(ray)) { - texture.getColor(ray); - ray.distance += ray.tNext; - ray.o.scaleAdd(ray.tNext, ray.d); + if (fullBlock.closestIntersection(ray, intersectionRecord)) { + texture.getColor(intersectionRecord); return true; } return false; } boolean hit = false; - if (bottom.intersect(ray)) { - ray.orientNormal(bottom.n); - ray.t = ray.tNext; + IntersectionRecord intersectionTest = new IntersectionRecord(); + if (bottom.closestIntersection(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, bottom.n)); hit = true; } @@ -80,121 +76,111 @@ public boolean isFullBlock() { int c1 = (0xF & (data >> CORNER_1)) % 8; int c2 = (0xF & (data >> CORNER_2)) % 8; int c3 = (0xF & (data >> CORNER_3)) % 8; - Triangle triangle = Water.t012[c0][c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + Triangle triangle = WaterModel.t012[c0][c1][c2]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); hit = true; } - triangle = Water.t230[c2][c3][c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + triangle = WaterModel.t230[c2][c3][c0]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + intersectionTest.uv.x = 1 - intersectionTest.uv.x; + intersectionTest.uv.y = 1 - intersectionTest.uv.y; hit = true; } - triangle = Water.westt[c0][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double y = ray.t * ray.d.y + ray.o.y; - double z = ray.t * ray.d.z + ray.o.z; + triangle = WaterModel.westt[c0][c3]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double y = intersectionTest.distance * ray.d.y + ray.o.y; + double z = intersectionTest.distance * ray.d.z + ray.o.z; y -= QuickMath.floor(y); z -= QuickMath.floor(z); - ray.u = z; - ray.v = y; + intersectionTest.uv.x = z; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.westb[c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double y = ray.t * ray.d.y + ray.o.y; - double z = ray.t * ray.d.z + ray.o.z; + triangle = WaterModel.westb[c0]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double y = intersectionTest.distance * ray.d.y + ray.o.y; + double z = intersectionTest.distance * ray.d.z + ray.o.z; y -= QuickMath.floor(y); z -= QuickMath.floor(z); - ray.u = z; - ray.v = y; + intersectionTest.uv.x = z; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.eastt[c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double y = ray.t * ray.d.y + ray.o.y; - double z = ray.t * ray.d.z + ray.o.z; + triangle = WaterModel.eastt[c1][c2]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double y = intersectionTest.distance * ray.d.y + ray.o.y; + double z = intersectionTest.distance * ray.d.z + ray.o.z; y -= QuickMath.floor(y); z -= QuickMath.floor(z); - ray.u = z; - ray.v = y; + intersectionTest.uv.x = z; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.eastb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double y = ray.t * ray.d.y + ray.o.y; - double z = ray.t * ray.d.z + ray.o.z; + triangle = WaterModel.eastb[c1]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double y = intersectionTest.distance * ray.d.y + ray.o.y; + double z = intersectionTest.distance * ray.d.z + ray.o.z; y -= QuickMath.floor(y); z -= QuickMath.floor(z); - ray.u = z; - ray.v = y; + intersectionTest.uv.x = z; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.southt[c0][c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double x = ray.t * ray.d.x + ray.o.x; - double y = ray.t * ray.d.y + ray.o.y; + triangle = WaterModel.southt[c0][c1]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double x = intersectionTest.distance * ray.d.x + ray.o.x; + double y = intersectionTest.distance * ray.d.y + ray.o.y; x -= QuickMath.floor(x); y -= QuickMath.floor(y); - ray.u = x; - ray.v = y; + intersectionTest.uv.x = x; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.southb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double x = ray.t * ray.d.x + ray.o.x; - double y = ray.t * ray.d.y + ray.o.y; + triangle = WaterModel.southb[c1]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double x = intersectionTest.distance * ray.d.x + ray.o.x; + double y = intersectionTest.distance * ray.d.y + ray.o.y; x -= QuickMath.floor(x); y -= QuickMath.floor(y); - ray.u = x; - ray.v = y; + intersectionTest.uv.x = x; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.northt[c2][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double x = ray.t * ray.d.x + ray.o.x; - double y = ray.t * ray.d.y + ray.o.y; + triangle = WaterModel.northt[c2][c3]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double x = intersectionTest.distance * ray.d.x + ray.o.x; + double y = intersectionTest.distance * ray.d.y + ray.o.y; x -= QuickMath.floor(x); y -= QuickMath.floor(y); - ray.u = 1 - x; - ray.v = y; + intersectionTest.uv.x = 1 - x; + intersectionTest.uv.y = y; hit = true; } - triangle = Water.northb[c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - double x = ray.t * ray.d.x + ray.o.x; - double y = ray.t * ray.d.y + ray.o.y; + triangle = WaterModel.northb[c2]; + if (triangle.intersect(ray, intersectionTest)) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, triangle.n)); + double x = intersectionTest.distance * ray.d.x + ray.o.x; + double y = intersectionTest.distance * ray.d.y + ray.o.y; x -= QuickMath.floor(x); y -= QuickMath.floor(y); - ray.u = 1 - x; - ray.v = y; + intersectionTest.uv.x = 1 - x; + intersectionTest.uv.y = y; hit = true; } if (hit) { - texture.getColor(ray); - ray.color.w = 1; - ray.distance += ray.tNext; - ray.o.scaleAdd(ray.tNext, ray.d); + texture.getColor(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.color.w = 1; + intersectionRecord.distance = intersectionTest.distance; return true; } return false; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/LavaCauldron.java b/chunky/src/java/se/llbit/chunky/block/minecraft/LavaCauldron.java index e2c96f39f4..f53a580b0b 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/LavaCauldron.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/LavaCauldron.java @@ -20,6 +20,7 @@ import se.llbit.chunky.model.minecraft.CauldronModel; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; public class LavaCauldron extends Cauldron { @@ -30,8 +31,8 @@ public LavaCauldron() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return CauldronModel.intersectWithLava(ray); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return CauldronModel.intersectWithLava(ray, intersectionRecord, scene); } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Leaves.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Leaves.java index 5d2aaca36e..8c7bd4109c 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Leaves.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Leaves.java @@ -18,24 +18,22 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.AbstractModelBlock; +import se.llbit.chunky.block.LeavesBase; import se.llbit.chunky.model.minecraft.LeafModel; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.biome.Biome; -public class Leaves extends AbstractModelBlock { +public class Leaves extends LeavesBase { private final int tint; public Leaves(String name, Texture texture) { super(name, texture); - solid = false; this.model = new LeafModel(texture); this.tint = -1; } public Leaves(String name, Texture texture, int tint) { super(name, texture); - solid = false; this.model = new LeafModel(texture, tint); this.tint = tint; } @@ -43,7 +41,7 @@ public Leaves(String name, Texture texture, int tint) { @Override public int getMapColor(Biome biome) { if (this.tint >= 0) { - // this leave type has a blending color that is independent from the biome (eg. spruce or birch leaves) + // this leaves type has a blending color that is independent of the biome (e.g. spruce or birch leaves) return this.tint | 0xFF000000; } return biome.foliageColor | 0xFF000000; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Lectern.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Lectern.java index ee8071214c..5582066bee 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Lectern.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Lectern.java @@ -18,14 +18,11 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; -public class Lectern extends MinecraftBlockTranslucent { +public class Lectern extends EmptyModelBlock { private final String facing; private final boolean hasBook; @@ -34,13 +31,6 @@ public Lectern(String facing, boolean hasBook) { this.facing = facing; this.hasBook = hasBook; invisible = true; - opaque = false; - localIntersect = true; - } - - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/LightBlock.java b/chunky/src/java/se/llbit/chunky/block/minecraft/LightBlock.java index acdaa126b3..f54f5dd05f 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/LightBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/LightBlock.java @@ -18,30 +18,29 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.AbstractModelBlock; -import se.llbit.chunky.model.minecraft.LightBlockModel; +import se.llbit.chunky.block.MinecraftBlock; import se.llbit.chunky.model.TexturedBlockModel; +import se.llbit.chunky.model.minecraft.LightBlockModel; import se.llbit.chunky.renderer.RenderMode; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; -import se.llbit.math.Vector4; -public class LightBlock extends AbstractModelBlock { +public class LightBlock extends MinecraftBlock { - private static final TexturedBlockModel previewBlockModel = new TexturedBlockModel( + private static final TexturedBlockModel PREVIEW_BLOCK_MODEL = new TexturedBlockModel( Texture.light, Texture.light, Texture.light, Texture.light, Texture.light, Texture.light ); - private final int level; + private static final LightBlockModel MODEL = new LightBlockModel(); - private final Vector4 color = new Vector4(1, 1, 1, 1); + private final int level; public LightBlock(String name, int level) { super(name, Texture.light); this.level = level; - this.model = new LightBlockModel(color); localIntersect = true; solid = false; } @@ -51,16 +50,11 @@ public int getLevel() { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { if (scene.getMode() == RenderMode.PREVIEW) { - return previewBlockModel.intersect(ray, scene); - } - if (scene.getMode() != RenderMode.PREVIEW && - (!scene.getEmittersEnabled() || emittance < Ray.EPSILON - || ray.depth >= scene.getRayDepth() - 1 || ray.specular)) { - return false; + return PREVIEW_BLOCK_MODEL.intersect(ray, intersectionRecord, scene); } - return this.model.intersect(ray, scene); + return MODEL.intersect(ray, intersectionRecord, scene); } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/LilyPad.java b/chunky/src/java/se/llbit/chunky/block/minecraft/LilyPad.java index 96d89236cd..a4462ac873 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/LilyPad.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/LilyPad.java @@ -18,24 +18,15 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.LilyPadEntity; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; -public class LilyPad extends MinecraftBlockTranslucent { +public class LilyPad extends EmptyModelBlock { public LilyPad() { super("lily_pad", Texture.lilyPad); invisible = true; - opaque = false; - localIntersect = true; - } - - @Override public boolean intersect(Ray ray, Scene scene) { - return false; } @Override public boolean isEntity() { diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/PowderSnowCauldron.java b/chunky/src/java/se/llbit/chunky/block/minecraft/PowderSnowCauldron.java index cc658fd386..3c37718e6f 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/PowderSnowCauldron.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/PowderSnowCauldron.java @@ -21,6 +21,7 @@ import se.llbit.chunky.model.minecraft.CauldronModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; public class PowderSnowCauldron extends Cauldron { @@ -30,7 +31,7 @@ public PowderSnowCauldron(int level) { } @Override - public boolean intersect(Ray ray, Scene scene) { - return CauldronModel.intersect(ray, getLevel(), Texture.powderSnow); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return CauldronModel.intersect(ray, intersectionRecord, scene, getLevel(), Texture.powderSnow, "powder_snow"); } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/RedstoneWallTorch.java b/chunky/src/java/se/llbit/chunky/block/minecraft/RedstoneWallTorch.java index bcbd5bf50d..2724d2d43a 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/RedstoneWallTorch.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/RedstoneWallTorch.java @@ -18,10 +18,17 @@ package se.llbit.chunky.block.minecraft; +import static se.llbit.chunky.block.minecraft.WallTorch.E0; +import static se.llbit.chunky.block.minecraft.WallTorch.E1; + import se.llbit.chunky.block.AbstractModelBlock; import se.llbit.chunky.model.minecraft.RedstoneWallTorchModel; import se.llbit.chunky.model.minecraft.TorchModel; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; public class RedstoneWallTorch extends AbstractModelBlock { private final boolean lit; @@ -44,4 +51,15 @@ public boolean isLit() { public String description() { return "facing=" + facing + ", lit=" + lit; } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (super.intersect(ray, intersectionRecord, scene)) { + double px = ray.o.x - Math.floor(ray.o.x + ray.d.x * Constants.OFFSET) + ray.d.x * intersectionRecord.distance; + double py = ray.o.y - Math.floor(ray.o.y + ray.d.y * Constants.OFFSET) + ray.d.y * intersectionRecord.distance; + double pz = ray.o.z - Math.floor(ray.o.z + ray.d.z * Constants.OFFSET) + ray.d.z * intersectionRecord.distance; + return !(px < E0) && !(px > E1) && !(py < E0) && !(py > E1) && !(pz < E0) && !(pz > E1); + } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Sign.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Sign.java index fd5ab16226..b5f6766881 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Sign.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Sign.java @@ -18,31 +18,22 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.SignEntity; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; -public class Sign extends MinecraftBlockTranslucent { +public class Sign extends EmptyModelBlock { private final int rotation; private final String material; public Sign(String name, String material, int rotation) { super(name, SignEntity.textureFromMaterial(material)); invisible = true; - solid = false; - localIntersect = true; this.rotation = rotation % 16; this.material = material; } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isBlockEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Slime.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Slime.java index 61301cf887..1ca35f3fd1 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Slime.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Slime.java @@ -18,24 +18,36 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.block.AbstractModelBlock; import se.llbit.chunky.model.minecraft.SlimeBlockModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; +import se.llbit.math.Vector3; -public class Slime extends MinecraftBlockTranslucent { +public class Slime extends AbstractModelBlock { public Slime() { super("slime_block", Texture.slime); - localIntersect = true; - opaque = false; - ior = 1.516f; // gelatin, according to https://study.com/academy/answer/what-is-the-refractive-index-of-gelatin.html solid = true; - refractive = true; + model = new SlimeBlockModel(); } - @Override - public boolean intersect(Ray ray, Scene scene) { - return SlimeBlockModel.intersect(ray); + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (model.intersect(ray, intersectionRecord, scene)) { + if (ray.getCurrentMedium() == this) { + if (ray.d.dot(intersectionRecord.n) > 0) { + Vector3 o = new Vector3(ray.o); + if (onEdge(o, ray.d, intersectionRecord.distance)) { + return false; + } + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + } + } + return true; } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Snow.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Snow.java index 52a5752c78..ecc62b2414 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Snow.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Snow.java @@ -30,10 +30,8 @@ public Snow(int layers) { super("snow", Texture.snowBlock); this.layers = layers; localIntersect = layers < 8; - opaque = layers == 8; + opaque = false; this.model = new SnowModel(layers); - localIntersect = layers < 8; - opaque = layers == 8; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/SporeBlossom.java b/chunky/src/java/se/llbit/chunky/block/minecraft/SporeBlossom.java index c9a8ea0e0d..ac0c1d0d44 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/SporeBlossom.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/SporeBlossom.java @@ -18,25 +18,15 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.Block; import se.llbit.chunky.entity.Entity; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.Ray; import se.llbit.math.Vector3; -public class SporeBlossom extends Block { +public class SporeBlossom extends EmptyModelBlock { public SporeBlossom() { super("spore_blossom", Texture.sporeBlossom); invisible = true; - opaque = false; - localIntersect = true; - } - - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/TintedGlass.java b/chunky/src/java/se/llbit/chunky/block/minecraft/TintedGlass.java index 78e9a31d4f..168a96ff05 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/TintedGlass.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/TintedGlass.java @@ -25,6 +25,5 @@ public class TintedGlass extends MinecraftBlockTranslucent { public TintedGlass() { super("tinted_glass", Texture.tintedGlass); - ior = 1.52f; } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/UnknownBlock.java b/chunky/src/java/se/llbit/chunky/block/minecraft/UnknownBlock.java index e0434cf392..b9f80f6a69 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/UnknownBlock.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/UnknownBlock.java @@ -20,6 +20,7 @@ import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; public class UnknownBlock extends SpriteBlock { @@ -30,10 +31,10 @@ public UnknownBlock(String name) { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { if (scene.getHideUnknownBlocks()) { return false; } - return super.intersect(ray, scene); + return super.intersect(ray, intersectionRecord, scene); } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallBanner.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallBanner.java index 816a926185..0e226932ed 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallBanner.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallBanner.java @@ -18,27 +18,22 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.BannerDesign; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.StandingBanner; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.json.Json; import se.llbit.json.JsonObject; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; -public class WallBanner extends MinecraftBlockTranslucent { +public class WallBanner extends EmptyModelBlock { private final int facing; private final BannerDesign.Color color; public WallBanner(String name, Texture texture, String facing, BannerDesign.Color color) { super(name, texture); invisible = true; - opaque = false; - localIntersect = true; switch (facing) { default: case "north": @@ -57,10 +52,6 @@ public WallBanner(String name, Texture texture, String facing, BannerDesign.Colo this.color = color; } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isBlockEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallCoralFan.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallCoralFan.java index c1c1f1f600..188003103f 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallCoralFan.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallCoralFan.java @@ -18,14 +18,11 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.WallCoralFanEntity; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import se.llbit.math.Vector3; -public class WallCoralFan extends MinecraftBlockTranslucent { +public class WallCoralFan extends EmptyModelBlock { private final String coralType; private final String facing; @@ -34,15 +31,9 @@ public WallCoralFan(String name, String coralType, String facing) { super(name, CoralFan.coralTexture(coralType)); this.coralType = coralType; this.facing = facing; - localIntersect = true; - solid = false; invisible = true; } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallHangingSign.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallHangingSign.java index 1afccff9fc..ef8147682b 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallHangingSign.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallHangingSign.java @@ -18,16 +18,13 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.HangingSignEntity; import se.llbit.chunky.entity.WallHangingSignEntity; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; -public class WallHangingSign extends MinecraftBlockTranslucent { +public class WallHangingSign extends EmptyModelBlock { private final String material; private final Facing facing; @@ -36,13 +33,6 @@ public WallHangingSign(String name, String material, String facing) { this.material = material; this.facing = Facing.fromString(facing); invisible = true; - solid = false; - localIntersect = true; - } - - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; } @Override diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallHead.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallHead.java index 8cd02a59c5..6212c57161 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallHead.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallHead.java @@ -18,21 +18,18 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.HeadEntity; import se.llbit.chunky.entity.SkullEntity; import se.llbit.chunky.entity.SkullEntity.Kind; -import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.log.Log; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import java.io.IOException; -public class WallHead extends MinecraftBlockTranslucent { +public class WallHead extends EmptyModelBlock { private final String description; private final int facing; @@ -40,7 +37,6 @@ public class WallHead extends MinecraftBlockTranslucent { public WallHead(String name, Texture texture, SkullEntity.Kind type, String facing) { super(name, texture); - localIntersect = true; invisible = true; description = "facing=" + facing; this.type = type; @@ -61,11 +57,6 @@ public WallHead(String name, Texture texture, SkullEntity.Kind type, String faci } } - @Override - public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public String description() { return description; diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallSign.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallSign.java index dd833d102e..ec141e096d 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallSign.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallSign.java @@ -18,24 +18,19 @@ package se.llbit.chunky.block.minecraft; -import se.llbit.chunky.block.MinecraftBlockTranslucent; import se.llbit.chunky.entity.Entity; import se.llbit.chunky.entity.SignEntity; import se.llbit.chunky.entity.WallSignEntity; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; -public class WallSign extends MinecraftBlockTranslucent { +public class WallSign extends EmptyModelBlock { private final int facing; private final String material; public WallSign(String name, String material, String facing) { super(name, SignEntity.textureFromMaterial(material)); invisible = true; - solid = false; - localIntersect = true; this.material = material; switch (facing) { default: @@ -54,10 +49,6 @@ public WallSign(String name, String material, String facing) { } } - @Override public boolean intersect(Ray ray, Scene scene) { - return false; - } - @Override public boolean isBlockEntity() { return true; } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/WallTorch.java b/chunky/src/java/se/llbit/chunky/block/minecraft/WallTorch.java index a0a1d1ae87..3ed8d9e363 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/WallTorch.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/WallTorch.java @@ -20,12 +20,20 @@ import se.llbit.chunky.block.AbstractModelBlock; import se.llbit.chunky.model.minecraft.TorchModel; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; /** * A torch attached to a wall. */ public class WallTorch extends AbstractModelBlock { + // Epsilons to clip ray intersections to the current block. + public static final double E0 = -Constants.EPSILON; + public static final double E1 = 1 + Constants.EPSILON; + protected final String facing; public WallTorch(String name, Texture texture, String facing) { @@ -39,4 +47,15 @@ public WallTorch(String name, Texture texture, String facing) { public String description() { return "facing=" + facing; } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (super.intersect(ray, intersectionRecord, scene)) { + double px = ray.o.x - Math.floor(ray.o.x + ray.d.x * Constants.OFFSET) + ray.d.x * intersectionRecord.distance; + double py = ray.o.y - Math.floor(ray.o.y + ray.d.y * Constants.OFFSET) + ray.d.y * intersectionRecord.distance; + double pz = ray.o.z - Math.floor(ray.o.z + ray.d.z * Constants.OFFSET) + ray.d.z * intersectionRecord.distance; + return !(px < E0) && !(px > E1) && !(py < E0) && !(py > E1) && !(pz < E0) && !(pz > E1); + } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/block/minecraft/Water.java b/chunky/src/java/se/llbit/chunky/block/minecraft/Water.java index 1f819f50ff..1712d2aedb 100644 --- a/chunky/src/java/se/llbit/chunky/block/minecraft/Water.java +++ b/chunky/src/java/se/llbit/chunky/block/minecraft/Water.java @@ -19,33 +19,27 @@ package se.llbit.chunky.block.minecraft; import se.llbit.chunky.block.MinecraftBlockTranslucent; +import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.resources.SolidColorTexture; import se.llbit.chunky.world.Material; -import se.llbit.math.Quad; -import se.llbit.math.Ray; -import se.llbit.math.Triangle; -import se.llbit.math.Vector3; -import se.llbit.math.Vector4; +import se.llbit.math.*; public class Water extends MinecraftBlockTranslucent { public static final Water INSTANCE = new Water(0); - public static final double TOP_BLOCK_GAP = 0.125; + public static final int FULL_BLOCK_DATA = 16; public final int level; public final int data; public Water(int level, int data) { - super("water", Texture.water); + super("water", SolidColorTexture.EMPTY); this.level = level % 8; this.data = data; solid = false; - localIntersect = true; - specular = 0.12f; - ior = 1.333f; - refractive = true; + localIntersect = !isFullBlock(); } public Water(int level) { @@ -53,7 +47,7 @@ public Water(int level) { } public boolean isFullBlock() { - return (this.data & (1 << FULL_BLOCK)) != 0; + return (this.data & (1 << FULL_BLOCK_DATA)) != 0; } @Override public boolean isWater() { @@ -64,244 +58,39 @@ public boolean isFullBlock() { return other.isWater(); } - private static final Quad[] fullBlock = { - // Bottom. - new Quad(new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true), - // Top. - new Quad(new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 1), - new Vector4(0, 1, 0, 1), true), - // West. - new Quad(new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true), - // East. - new Quad(new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(1, 0, 1), - new Vector4(0, 1, 0, 1), true), - // North. - new Quad(new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(0, 0, 0), - new Vector4(0, 1, 0, 0), true), - // South. - new Quad(new Vector3(0, 1, 1), new Vector3(1, 1, 1), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true), - }; - - private static final Quad bottom = - new Quad(new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true); - static final Triangle[][][] t012 = new Triangle[8][8][8]; - static final Triangle[][][] t230 = new Triangle[8][8][8]; - static final Triangle[][] westt = new Triangle[8][8]; - static final Triangle[] westb = new Triangle[8]; - static final Triangle[][] northt = new Triangle[8][8]; - static final Triangle[] northb = new Triangle[8]; - static final Triangle[][] eastt = new Triangle[8][8]; - static final Triangle[] eastb = new Triangle[8]; - static final Triangle[][] southt = new Triangle[8][8]; - static final Triangle[] southb = new Triangle[8]; - - /** Water height levels. */ - static final double[] height = { - 14 / 16., 12.25 / 16., 10.5 / 16, 8.75 / 16, 7. / 16, 5.25 / 16, 3.5 / 16, 1.75 / 16 - }; - - /** - * Block data offset for water above flag. - */ - public static final int FULL_BLOCK = 16; - public static final int CORNER_0 = 0; - public static final int CORNER_1 = 4; - public static final int CORNER_2 = 8; - public static final int CORNER_3 = 12; - - static { - // Precompute water triangles. - for (int i = 0; i < 8; ++i) { - double c0 = height[i]; - for (int j = 0; j < 8; ++j) { - double c1 = height[j]; - for (int k = 0; k < 8; ++k) { - double c2 = height[k]; - t012[i][j][k] = - new Triangle(new Vector3(1, c1, 1), new Vector3(1, c2, 0), new Vector3(0, c0, 1)); - } - } - } - for (int i = 0; i < 8; ++i) { - double c2 = height[i]; - for (int j = 0; j < 8; ++j) { - double c3 = height[j]; - for (int k = 0; k < 8; ++k) { - double c0 = height[k]; - t230[i][j][k] = - new Triangle(new Vector3(0, c3, 0), new Vector3(0, c0, 1), new Vector3(1, c2, 0)); - } - } - } - for (int i = 0; i < 8; ++i) { - double c0 = height[i]; - for (int j = 0; j < 8; ++j) { - double c3 = height[j]; - westt[i][j] = - new Triangle(new Vector3(0, c3, 0), new Vector3(0, 0, 0), new Vector3(0, c0, 1)); - } - } - for (int i = 0; i < 8; ++i) { - double c0 = height[i]; - westb[i] = new Triangle(new Vector3(0, 0, 1), new Vector3(0, c0, 1), new Vector3(0, 0, 0)); - } - for (int i = 0; i < 8; ++i) { - double c1 = height[i]; - for (int j = 0; j < 8; ++j) { - double c2 = height[j]; - eastt[i][j] = - new Triangle(new Vector3(1, c2, 0), new Vector3(1, c1, 1), new Vector3(1, 0, 0)); - } - } - for (int i = 0; i < 8; ++i) { - double c1 = height[i]; - eastb[i] = new Triangle(new Vector3(1, c1, 1), new Vector3(1, 0, 1), new Vector3(1, 0, 0)); - } - for (int i = 0; i < 8; ++i) { - double c2 = height[i]; - for (int j = 0; j < 8; ++j) { - double c3 = height[j]; - northt[i][j] = - new Triangle(new Vector3(0, c3, 0), new Vector3(1, c2, 0), new Vector3(0, 0, 0)); - } - } - for (int i = 0; i < 8; ++i) { - double c2 = height[i]; - northb[i] = - new Triangle(new Vector3(1, 0, 0), new Vector3(0, 0, 0), new Vector3(1, c2, 0)); - } - for (int i = 0; i < 8; ++i) { - double c0 = height[i]; - for (int j = 0; j < 8; ++j) { - double c1 = height[j]; - southt[i][j] = - new Triangle(new Vector3(0, c0, 1), new Vector3(0, 0, 1), new Vector3(1, c1, 1)); - } - } - for (int i = 0; i < 8; ++i) { - double c1 = height[i]; - southb[i] = - new Triangle(new Vector3(1, 0, 1), new Vector3(1, c1, 1), new Vector3(0, 0, 1)); + @Override public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + boolean hit = WaterModel.intersect(ray, intersectionRecord, scene, data); + if (hit) { + intersectionRecord.material.getColor(intersectionRecord); } + return hit; } - @Override public boolean intersect(Ray ray, Scene scene) { - ray.t = Double.POSITIVE_INFINITY; - - int data = ray.getCurrentData(); - int isFull = (data >> FULL_BLOCK) & 1; + @Override public String description() { + return String.format("level=%d", level); + } - if (isFull != 0) { - boolean hit = false; - for (Quad quad : fullBlock) { - if (quad.intersect(ray)) { - texture.getAvgColorLinear(ray.color); - ray.t = ray.tNext; - ray.orientNormal(quad.n); - hit = true; - } + @Override + public boolean isInside(Ray ray) { + if (isFullBlock()) { + return true; + } else { + double ix = ray.o.x - QuickMath.floor(ray.o.x); + double iy = ray.o.y - QuickMath.floor(ray.o.y); + double iz = ray.o.z - QuickMath.floor(ray.o.z); + + if (iy > 1 - WaterModel.TOP_BLOCK_GAP) { + return false; } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } - return hit; - } - - boolean hit = false; - if (bottom.intersect(ray)) { - ray.orientNormal(bottom.n); - ray.t = ray.tNext; - hit = true; - } - - int c0 = (0xF & (data >> CORNER_0)) % 8; - int c1 = (0xF & (data >> CORNER_1)) % 8; - int c2 = (0xF & (data >> CORNER_2)) % 8; - int c3 = (0xF & (data >> CORNER_3)) % 8; - Triangle triangle = t012[c0][c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; - } - triangle = t230[c2][c3][c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; - } - triangle = westt[c0][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; - } - triangle = westb[c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; + Ray testRay = new Ray(new Vector3(ix, iy, iz), ray.d); + IntersectionRecord intersectionRecord = new IntersectionRecord(); + return WaterModel.isInside(testRay, intersectionRecord, data); } - triangle = eastt[c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; - } - triangle = eastb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; - } - triangle = southt[c0][c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; - } - triangle = southb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; - } - triangle = northt[c2][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; - } - triangle = northb[c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; - } - if (hit) { - texture.getAvgColorLinear(ray.color); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } - return hit; } - @Override public String description() { - return String.format("level=%d", level); + public boolean isInside(double x, double y, double z) { + Ray ray = new Ray(); + ray.o.set(x, y, z); + return isInside(ray); } } diff --git a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java index 069a990c0d..6929517300 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java +++ b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java @@ -19,7 +19,7 @@ import se.llbit.chunky.block.*; import se.llbit.chunky.block.minecraft.*; import se.llbit.chunky.plugin.PluginApi; -import se.llbit.chunky.resources.Texture; +import se.llbit.json.JsonValue; import se.llbit.math.Octree; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.IntTag; @@ -52,10 +52,11 @@ */ public class BlockPalette { private static final int BLOCK_PALETTE_VERSION = 4; - public final int airId, stoneId, waterId; + public final int voidId, airId, stoneId, waterId; public static final int ANY_ID = Octree.ANY_TYPE; private final Map> materialProperties; + public static final Map> DEFAULT_MATERIAL_PROPERTIES = getDefaultMaterialProperties(); /** * Stone blocks are used for filling invisible regions in the Octree. @@ -70,13 +71,16 @@ public class BlockPalette { public BlockPalette(Map initialMap, List initialList) { this.blockMap = initialMap; this.palette = initialList; - this.materialProperties = getDefaultMaterialProperties(); + this.materialProperties = new HashMap<>(); + CompoundTag voidTag = new CompoundTag(); + voidTag.add("Name", new StringTag("minecraft:void")); CompoundTag airTag = new CompoundTag(); airTag.add("Name", new StringTag("minecraft:air")); CompoundTag stoneTag = new CompoundTag(); stoneTag.add("Name", new StringTag("minecraft:stone")); CompoundTag waterTag = new CompoundTag(); waterTag.add("Name", new StringTag("minecraft:water")); + voidId = put(voidTag); airId = put(airTag); stoneId = put(stoneTag); waterId = put(waterTag); @@ -88,6 +92,15 @@ public BlockPalette() { this(new ConcurrentHashMap<>(), new CopyOnWriteArrayList<>()); } + public BlockPalette(Map materials) { + this(); + materials.forEach((name, properties) -> { + materialProperties.put(name, block -> { + block.loadMaterialProperties(properties.asObject()); + }); + }); + } + /** * This method should be called when no threads are acting on the palette anymore. *

@@ -226,6 +239,10 @@ public void updateProperties(String name, Consumer properties) { * @param block Block to apply the material configuration to */ public void applyMaterial(Block block) { + Consumer defaultProperties = DEFAULT_MATERIAL_PROPERTIES.get(block.name); + if (defaultProperties != null) { + defaultProperties.accept(block); + } Consumer properties = materialProperties.get(block.name); if (properties != null) { properties.accept(block); @@ -246,166 +263,215 @@ public void applyMaterials() { public static Map> getDefaultMaterialProperties() { Map> materialProperties = new HashMap<>(); materialProperties.put( - "minecraft:water", - block -> { - block.specular = 0.255f; - block.ior = 1.333f; - block.refractive = true; - }); + "minecraft:water", + block -> { + block.ior = 1.333f; + block.alpha = 0; + }); materialProperties.put( - "minecraft:lava", + "minecraft:air", block -> { - block.emittance = 1.0f; - }); + block.alpha = 0; + } + ); + materialProperties.put( + "minecraft:void", + block -> { + block.alpha = 0; + } + ); + materialProperties.put( + "minecraft:lava", + block -> { + block.setLightLevel(15); + }); Consumer glassConfig = + block -> { + block.ior = 1.52f; + }; + Consumer stainedGlassConfig = block -> { block.ior = 1.52f; - block.refractive = true; + block.transmissionMetalness = 0.95f; + float[] color = block.texture.getAvgColorFlat(); + block.absorptionColor.x = color[0]; + block.absorptionColor.y = color[1]; + block.absorptionColor.z = color[2]; + block.absorption = 1.0f; + block.volumeDensity = 0.3f; }; materialProperties.put("minecraft:glass", glassConfig); materialProperties.put("minecraft:glass_pane", glassConfig); - materialProperties.put("minecraft:white_stained_glass", glassConfig); - materialProperties.put("minecraft:orange_stained_glass", glassConfig); - materialProperties.put("minecraft:magenta_stained_glass", glassConfig); - materialProperties.put("minecraft:light_blue_stained_glass", glassConfig); - materialProperties.put("minecraft:yellow_stained_glass", glassConfig); - materialProperties.put("minecraft:lime_stained_glass", glassConfig); - materialProperties.put("minecraft:pink_stained_glass", glassConfig); - materialProperties.put("minecraft:gray_stained_glass", glassConfig); - materialProperties.put("minecraft:light_gray_stained_glass", glassConfig); - materialProperties.put("minecraft:cyan_stained_glass", glassConfig); - materialProperties.put("minecraft:purple_stained_glass", glassConfig); - materialProperties.put("minecraft:blue_stained_glass", glassConfig); - materialProperties.put("minecraft:brown_stained_glass", glassConfig); - materialProperties.put("minecraft:green_stained_glass", glassConfig); - materialProperties.put("minecraft:red_stained_glass", glassConfig); - materialProperties.put("minecraft:black_stained_glass", glassConfig); - materialProperties.put("minecraft:white_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:orange_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:magenta_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:light_blue_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:yellow_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:lime_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:pink_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:gray_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:light_gray_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:cyan_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:purple_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:blue_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:brown_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:green_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:red_stained_glass_pane", glassConfig); - materialProperties.put("minecraft:black_stained_glass_pane", glassConfig); + materialProperties.put("minecraft:white_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:orange_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:magenta_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:light_blue_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:yellow_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:lime_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:pink_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:gray_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:light_gray_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:cyan_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:purple_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:blue_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:brown_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:green_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:red_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:black_stained_glass", stainedGlassConfig); + materialProperties.put("minecraft:white_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:orange_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:magenta_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:light_blue_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:yellow_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:lime_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:pink_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:gray_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:light_gray_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:cyan_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:purple_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:blue_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:brown_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:green_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:red_stained_glass_pane", stainedGlassConfig); + materialProperties.put("minecraft:black_stained_glass_pane", stainedGlassConfig); + + // IoR for glossy surface + materialProperties.put("minecraft:white_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:orange_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:magenta_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:light_blue_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:yellow_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:lime_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:pink_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:gray_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:light_gray_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:cyan_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:purple_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:blue_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:brown_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:green_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:red_glazed_terracotta", glassConfig); + materialProperties.put("minecraft:black_glazed_terracotta", glassConfig); materialProperties.put("minecraft:gold_block", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; + block.specular = 1.0f; + block.metalness = 0.96f; block.setPerceptualSmoothness(0.9); }); materialProperties.put("minecraft:raw_gold_block", block -> { - block.metalness = 0.8f; + block.specular = 0.8f; + block.metalness = 1.0f; block.setPerceptualSmoothness(0.5); }); materialProperties.put("minecraft:diamond_block", block -> { - block.specular = 0.04f; + block.ior = 2.418f; }); - materialProperties.put("minecraft:iron_block", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; + Consumer ironConfig = block -> { + block.specular = 1.0f; + block.metalness = 0.96f; block.setPerceptualSmoothness(0.9); - }); + }; + materialProperties.put("minecraft:iron_block", ironConfig); materialProperties.put("minecraft:raw_iron_block", block -> { - block.metalness = 0.66f; - block.setPerceptualSmoothness(0.3); - }); - materialProperties.put("minecraft:iron_bars", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; - block.setPerceptualSmoothness(0.9); - }); - materialProperties.put("minecraft:iron_door", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; - block.setPerceptualSmoothness(0.8); - }); - materialProperties.put("minecraft:iron_trapdoor", block -> { - block.specular = 0.04f; + block.specular = 0.66f; block.metalness = 1.0f; - block.setPerceptualSmoothness(0.8); - }); - materialProperties.put("minecraft:cauldron", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; - block.setPerceptualSmoothness(0.7); + block.setPerceptualSmoothness(0.3); }); + materialProperties.put("minecraft:iron_bars", ironConfig); + materialProperties.put("minecraft:iron_door", ironConfig); + materialProperties.put("minecraft:iron_trapdoor", ironConfig); + Consumer cauldronConfig = block -> { + block.specular = 1.0f; + block.metalness = 0.96f; + block.setPerceptualSmoothness(0.5); + }; + materialProperties.put("minecraft:cauldron", cauldronConfig); + materialProperties.put("minecraft:lava_cauldron", cauldronConfig); + materialProperties.put("minecraft:powder_snow_cauldron", cauldronConfig); + materialProperties.put("minecraft:water_cauldron", cauldronConfig); materialProperties.put("minecraft:hopper", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; + block.specular = 1.0f; + block.metalness = 0.96f; block.setPerceptualSmoothness(0.7); }); - materialProperties.put("minecraft:iron_chain", block -> { - block.specular = 0.04f; - block.metalness = 1.0f; - block.setPerceptualSmoothness(0.9); - }); - materialProperties.put("minecraft:redstone_torch", block -> { + materialProperties.put("minecraft:iron_chain", ironConfig); + Consumer redstoneTorchConfig = block -> { + block.useReferenceColors = true; + block.addRefColorGammaCorrected(255, 255, 210, 0.35f); + block.addRefColorGammaCorrected(255, 185, 0, 0.25f); + block.addRefColorGammaCorrected(221, 0, 0, 0.3f); if (block instanceof RedstoneTorch && ((RedstoneTorch) block).isLit()) { - block.emittance = 1.0f; - } - }); - materialProperties.put("minecraft:redstone_wall_torch", block -> { - if (block instanceof RedstoneWallTorch && ((RedstoneWallTorch) block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(7); } - }); - materialProperties.put("minecraft:torch", block -> { - block.emittance = 1.0f; - }); - materialProperties.put("minecraft:wall_torch", block -> { - block.emittance = 1.0f; - }); + }; + materialProperties.put("minecraft:redstone_torch", redstoneTorchConfig); + materialProperties.put("minecraft:redstone_wall_torch", redstoneTorchConfig); + materialProperties.put("minecraft:redstone_ore", block -> { + block.useReferenceColors = true; + block.addRefColorGammaCorrected(254, 118, 118, 0.2f); + block.addRefColorGammaCorrected(210, 3, 3, 0.2f); + }); + materialProperties.put("minecraft:deepslate_redstone_ore", block -> { + block.useReferenceColors = true; + block.addRefColorGammaCorrected(254, 118, 118, 0.2f); + block.addRefColorGammaCorrected(210, 3, 3, 0.35f); + }); + Consumer torchConfig = block -> { + block.useReferenceColors = true; + block.addRefColorGammaCorrected(255, 255, 210, 0.35f); + block.addRefColorGammaCorrected(255, 185, 0, 0.25f); + block.setLightLevel(14); + }; + materialProperties.put("minecraft:torch", torchConfig); + materialProperties.put("minecraft:wall_torch", torchConfig); materialProperties.put("minecraft:fire", block -> { - block.emittance = 1.0f; - }); - materialProperties.put("minecraft:ice", block -> { - block.ior = 1.31f; - block.refractive = true; + block.setLightLevel(15); + block.emitterMappingOffset = -0.5f; }); - materialProperties.put("minecraft:frosted_ice", block -> { + Consumer iceConfig = block -> { block.ior = 1.31f; - block.refractive = true; - }); + }; + materialProperties.put("minecraft:ice", iceConfig); + materialProperties.put("minecraft:frosted_ice", iceConfig); + materialProperties.put("minecraft:packed_ice", iceConfig); + materialProperties.put("minecraft:blue_ice", iceConfig); materialProperties.put("minecraft:glowstone", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); materialProperties.put("minecraft:portal", block -> { // MC <1.13 - block.emittance = 0.4f; + block.setLightLevel(11); }); materialProperties.put("minecraft:nether_portal", block -> { // MC >=1.13 - block.emittance = 0.4f; + block.setLightLevel(11); }); materialProperties.put("minecraft:jack_o_lantern", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 0.5f; }); materialProperties.put("minecraft:beacon", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); block.ior = 1.52f; }); materialProperties.put("minecraft:redstone_lamp", block -> { if (block instanceof RedstoneLamp && ((RedstoneLamp) block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(15); } }); materialProperties.put("minecraft:emerald_block", block -> { - block.specular = 0.04f; + block.ior = 1.5825f; }); materialProperties.put("minecraft:sea_lantern", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); materialProperties.put("minecraft:magma", block -> { - block.emittance = 0.6f; + block.setLightLevel(3); + }); + materialProperties.put("minecraft:magma_block", block -> { + block.setLightLevel(3); }); materialProperties.put("minecraft:end_rod", block -> { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(248, 236, 219, 0.3f); + block.setLightLevel(14); }); materialProperties.put("minecraft:kelp", block -> { block.waterlogged = true; @@ -415,92 +481,110 @@ public static Map> getDefaultMaterialProperties() { }); materialProperties.put("minecraft:seagrass", block -> { block.waterlogged = true; + block.subSurfaceScattering = 0.3f; }); materialProperties.put("minecraft:tall_seagrass", block -> { block.waterlogged = true; + block.subSurfaceScattering = 0.3f; }); materialProperties.put("minecraft:sea_pickle", block -> { if (block instanceof SeaPickle) { if (((SeaPickle) block).live) { - block.emittance = 1.0f / 15f * (3 * ((SeaPickle) block).pickles + 1); + block.setLightLevel(3 * ((SeaPickle) block).pickles + 1); } } }); - materialProperties.put("minecraft:campfire", block -> { - if (block instanceof Campfire && ((Campfire) block).isLit) { - block.emittance = 1.0f; - } - }); materialProperties.put("minecraft:furnace", block -> { - if (block instanceof Furnace && ((Furnace) block).isLit()) { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(255, 255, 215, 0.38f); + block.addRefColorGammaCorrected(230, 171, 16, 0.38f); + if(block instanceof Furnace && ((Furnace)block).isLit()) { + block.setLightLevel(13); } }); materialProperties.put("minecraft:smoker", block -> { - if (block instanceof Smoker && ((Smoker) block).isLit()) { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(228, 169, 17, 0.32f); + if(block instanceof Smoker && ((Smoker)block).isLit()) { + block.setLightLevel(13); } }); materialProperties.put("minecraft:blast_furnace", block -> { - if (block instanceof BlastFurnace && ((BlastFurnace) block).isLit()) { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(224, 128, 46, 0.25f); + if(block instanceof BlastFurnace && ((BlastFurnace)block).isLit()) { + block.setLightLevel(13); } }); materialProperties.put("minecraft:lantern", block -> { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(254, 254, 179, 0.25f); + block.addRefColorGammaCorrected(253, 158, 76, 0.45f); + block.addRefColorGammaCorrected(134, 73, 42, 0.05f); + block.setLightLevel(15); }); materialProperties.put("minecraft:shroomlight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); materialProperties.put("minecraft:soul_fire_lantern", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(220, 252, 255, 0.5f); + block.addRefColorGammaCorrected(76, 198, 202, 0.3f); + block.setLightLevel(10); }); materialProperties.put("minecraft:soul_lantern", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_fire_torch", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_torch", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_fire_wall_torch", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_wall_torch", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); + block.useReferenceColors = true; + block.addRefColorGammaCorrected(220, 252, 255, 0.5f); + block.addRefColorGammaCorrected(76, 198, 202, 0.3f); + block.setLightLevel(10); + }); + Consumer soulTorchConfig = block -> { + block.useReferenceColors = true; + block.addRefColorGammaCorrected(199, 252, 254, 0.45f); + block.addRefColorGammaCorrected(35, 204, 209, 0.25f); + block.setLightLevel(10); + }; + materialProperties.put("minecraft:soul_fire_torch", soulTorchConfig); + materialProperties.put("minecraft:soul_torch", soulTorchConfig); + materialProperties.put("minecraft:soul_fire_wall_torch", soulTorchConfig); + materialProperties.put("minecraft:soul_wall_torch", soulTorchConfig); materialProperties.put("minecraft:soul_fire", block -> { - block.emittance = 0.6f; + block.setLightLevel(10); + block.emitterMappingOffset = -0.5f; }); materialProperties.put("minecraft:crying_obsidian", block -> { - block.emittance = 0.6f; + block.setLightLevel(10); + block.ior = 1.493f; }); materialProperties.put("minecraft:enchanting_table", block -> { - block.emittance = 0.5f; + block.setLightLevel(7); }); materialProperties.put("minecraft:respawn_anchor", block -> { if (block instanceof RespawnAnchor) { int charges = ((RespawnAnchor) block).charges; if (charges > 0) { - block.emittance = 1.0f / 15 * (charges * 4 - 2); + block.setLightLevel(charges * 4 - 2); } } }); Consumer copperConfig = block -> { + block.specular = 1.0f; block.metalness = 1.0f; block.setPerceptualSmoothness(0.75); }; Consumer exposedCopperConfig = block -> { - block.metalness = 0.66f; + block.specular = 0.66f; + block.metalness = 1.0f; block.setPerceptualSmoothness(0.75); }; Consumer weatheredCopperConfig = block -> { - block.metalness = 0.66f; + block.specular = 0.66f; + block.metalness = 1.0f; block.setPerceptualSmoothness(0.75); }; materialProperties.put("minecraft:raw_copper_block", block -> { - block.metalness = 0.66f; + block.specular = 0.66f; + block.metalness = 1.0f; block.setPerceptualSmoothness(0.5); }); for (String s : new String[]{"minecraft:", "minecraft:waxed_"}) { @@ -577,79 +661,223 @@ public static Map> getDefaultMaterialProperties() { }); } materialProperties.put("minecraft:small_amethyst_bud", block -> { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); + block.ior = 1.543f; }); materialProperties.put("minecraft:medium_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 2; + block.setLightLevel(2); + block.ior = 1.543f; }); materialProperties.put("minecraft:large_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 4; + block.setLightLevel(4); + block.ior = 1.543f; }); materialProperties.put("minecraft:amethyst_cluster", block -> { - block.emittance = 1.0f / 15f * 5; + block.setLightLevel(5); + block.ior = 1.543f; + }); + materialProperties.put("minecraft:budding_amethyst", block -> { + block.ior = 1.543f; + }); + materialProperties.put("minecraft:amethyst_block", block -> { + block.ior = 1.543f; }); materialProperties.put("minecraft:tinted_glass", glassConfig); materialProperties.put("minecraft:sculk_sensor", block -> { if (block instanceof SculkSensor && ((SculkSensor) block).isActive()) { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); } }); materialProperties.put("minecraft:calibrated_sculk_sensor", block -> { if (block instanceof CalibratedSculkSensor && ((CalibratedSculkSensor) block).isActive()) { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); } }); + Consumer foliageConfig = block -> { + block.subSurfaceScattering = 0.3f; + }; + materialProperties.put("minecraft:grass", foliageConfig); + materialProperties.put("minecraft:tall_grass", foliageConfig); + materialProperties.put("minecraft:short_grass", foliageConfig); + materialProperties.put("minecraft:sugar_cane", foliageConfig); + materialProperties.put("minecraft:beetroots", foliageConfig); + materialProperties.put("minecraft:carrots", foliageConfig); + materialProperties.put("minecraft:potatoes", foliageConfig); + materialProperties.put("minecraft:melon_stem", foliageConfig); + materialProperties.put("minecraft:attached_melon_stem", foliageConfig); + materialProperties.put("minecraft:pumpkin_stem", foliageConfig); + materialProperties.put("minecraft:attached_pumpkin_stem", foliageConfig); + materialProperties.put("minecraft:wheat", foliageConfig); + materialProperties.put("minecraft:big_dripleaf", foliageConfig); + materialProperties.put("minecraft:big_dripleaf_stem", foliageConfig); + materialProperties.put("minecraft:small_dripleaf", foliageConfig); + materialProperties.put("minecraft:fern", foliageConfig); + materialProperties.put("minecraft:large_fern", foliageConfig); + materialProperties.put("minecraft:allium", foliageConfig); + materialProperties.put("minecraft:azure_bluet", foliageConfig); + materialProperties.put("minecraft:blue_orchid", foliageConfig); + materialProperties.put("minecraft:cornflower", foliageConfig); + materialProperties.put("minecraft:dandelion", foliageConfig); + materialProperties.put("minecraft:lily_of_the_valley", foliageConfig); + materialProperties.put("minecraft:oxeye_daisy", foliageConfig); + materialProperties.put("minecraft:poppy", foliageConfig); + materialProperties.put("minecraft:torchflower", foliageConfig); + materialProperties.put("minecraft:orange_tulip", foliageConfig); + materialProperties.put("minecraft:pink_tulip", foliageConfig); + materialProperties.put("minecraft:red_tulip", foliageConfig); + materialProperties.put("minecraft:white_tulip", foliageConfig); + materialProperties.put("minecraft:wither_rose", foliageConfig); + materialProperties.put("minecraft:lilac", foliageConfig); + materialProperties.put("minecraft:peony", foliageConfig); + materialProperties.put("minecraft:pitcher_plant", foliageConfig); + materialProperties.put("minecraft:rose_bush", foliageConfig); + materialProperties.put("minecraft:sunflower", foliageConfig); + materialProperties.put("minecraft:mangrove_propagule", foliageConfig); + materialProperties.put("minecraft:pink_petals", foliageConfig); + materialProperties.put("minecraft:oak_sapling", foliageConfig); + materialProperties.put("minecraft:spruce_sapling", foliageConfig); + materialProperties.put("minecraft:birch_sapling", foliageConfig); + materialProperties.put("minecraft:jungle_sapling", foliageConfig); + materialProperties.put("minecraft:acacia_sapling", foliageConfig); + materialProperties.put("minecraft:dark_oak_sapling", foliageConfig); + materialProperties.put("minecraft:cherry_sapling", foliageConfig); + materialProperties.put("minecraft:oak_leaves", foliageConfig); + materialProperties.put("minecraft:spruce_leaves", foliageConfig); + materialProperties.put("minecraft:birch_leaves", foliageConfig); + materialProperties.put("minecraft:jungle_leaves", foliageConfig); + materialProperties.put("minecraft:acacia_acacia", foliageConfig); + materialProperties.put("minecraft:dark_oak_leaves", foliageConfig); + materialProperties.put("minecraft:mangrove_leaves", foliageConfig); + materialProperties.put("minecraft:cherry_leaves", foliageConfig); + materialProperties.put("minecraft:azalea_leaves", foliageConfig); + materialProperties.put("minecraft:flowering_azalea_leaves", foliageConfig); + materialProperties.put("minecraft:sweet_berry_bush", foliageConfig); materialProperties.put("minecraft:glow_lichen", block -> { - block.emittance = 1.0f / 15f * 7; - }); - materialProperties.put("minecraft:cave_vines_plant", block -> { + block.setLightLevel(1); + block.subSurfaceScattering = 0.3f; + }); + Consumer caveVinesConfig = block -> { + block.subSurfaceScattering = 0.3f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(241, 189, 85, 0.3f); + block.addRefColorGammaCorrected(164, 100, 34, 0.05f); if (block instanceof CaveVines && ((CaveVines) block).hasBerries()) { - block.emittance = 1.0f / 15f * 14; + block.setLightLevel(14); } - }); - materialProperties.put("minecraft:cave_vines", block -> { - if (block instanceof CaveVines && ((CaveVines) block).hasBerries()) { - block.emittance = 1.0f / 15f * 14; - } - }); + }; + materialProperties.put("minecraft:cave_vines_plant", caveVinesConfig); + materialProperties.put("minecraft:cave_vines", caveVinesConfig); + materialProperties.put("minecraft:vines", foliageConfig); materialProperties.put("minecraft:light", block -> { if (block instanceof LightBlock) { block.emittance = 1.0f / 15f * 4 * ((LightBlock) block).getLevel(); } }); materialProperties.put("minecraft:ochre_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:verdant_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:pearlescent_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:sculk_catalyst", block -> { - block.emittance = 1.0f / 15f * 6; + block.setLightLevel(6); }); - for (String s : new String[]{"minecraft:", "minecraft:waxed_"}) { + Consumer copperBulbRedLight = block -> { + block.addRefColorGammaCorrected(217, 35, 35, 0.05f); + block.addRefColorGammaCorrected(176, 23, 23, 0.05f); + block.addRefColorGammaCorrected(163, 24, 24, 0.05f); + block.addRefColorGammaCorrected(138, 24, 24, 0.05f); + }; + for(String s : new String[]{"minecraft:", "minecraft:waxed_"}) { materialProperties.put(s + "copper_bulb", block -> { - if (block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 1.0f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(255, 235, 186, 0.25f); + block.addRefColorGammaCorrected(251, 184, 96, 0.25f); + copperBulbRedLight.accept(block); + copperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(15); } }); materialProperties.put(s + "exposed_copper_bulb", block -> { - if (block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 12 / 15f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(253, 202, 138, 0.25f); + block.addRefColorGammaCorrected(223, 139, 41, 0.2f); + copperBulbRedLight.accept(block); + exposedCopperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(12); } }); materialProperties.put(s + "weathered_copper_bulb", block -> { - if (block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 8 / 15f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(234, 184, 91, 0.25f); + block.addRefColorGammaCorrected(224, 151, 53, 0.25f); + copperBulbRedLight.accept(block); + weatheredCopperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(8); } }); materialProperties.put(s + "oxidized_copper_bulb", block -> { - if (block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 4 / 15f; + block.useReferenceColors = true; + block.addRefColorGammaCorrected(212, 153, 67, 0.25f); + block.addRefColorGammaCorrected(191, 113, 65, 0.25f); + copperBulbRedLight.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(4); } }); + materialProperties.put("minecraft:calcite", block -> { + block.ior = 1.486f; + }); + materialProperties.put("minecraft:lapis_block", block -> { + block.ior = 1.525f; + }); + materialProperties.put("minecraft:obsidian", block -> { + block.ior = 1.493f; + }); + materialProperties.put("minecraft:quartz_block", block -> { + block.ior = 1.594f; + }); + materialProperties.put("minecraft:quartz_pillar", block -> { + block.ior = 1.594f; + }); + materialProperties.put("minecraft:chiseled_quartz_block", block -> { + block.ior = 1.594f; + }); + materialProperties.put("minecraft:smooth_quartz", block -> { + block.ior = 1.594f; + }); + materialProperties.put("minecraft:quartz_bricks", block -> { + block.ior = 1.594f; + }); + materialProperties.put("minecraft:honey_block", block -> { + block.ior = 1.474f; // according to https://study.com/academy/answer/what-is-the-refractive-index-of-honey.html + block.transmissionMetalness = 0.95f; + float[] color = block.texture.getAvgColorFlat(); + block.absorptionColor.x = color[0]; + block.absorptionColor.y = color[1]; + block.absorptionColor.z = color[2]; + block.absorption = 1.0f; + block.volumeDensity = 0.3f; + }); + materialProperties.put("minecraft:slime_block", block -> { + block.ior = 1.516f; // gelatin, according to https://study.com/academy/answer/what-is-the-refractive-index-of-gelatin.html + block.transmissionMetalness = 0.95f; + float[] color = block.texture.getAvgColorFlat(); + block.absorptionColor.x = color[0]; + block.absorptionColor.y = color[1]; + block.absorptionColor.z = color[2]; + block.absorption = 1.0f; + block.volumeDensity = 0.3f; + }); } materialProperties.put("minecraft:copper_wall_torch", block -> { block.emittance = 1.0f; diff --git a/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java b/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java index 17a6ae945f..57f29666e5 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java +++ b/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java @@ -36,8 +36,12 @@ public class GenericChunkData implements ChunkData { @Override public int getBlockAt(int x, int y, int z) { SectionData sectionData = sections.get(y >> 4); if (sectionData == null) - return 0; - return sectionData.blocks[chunkIndex(x & (X_MAX - 1), y & (SECTION_Y_MAX - 1), z & (Z_MAX - 1))]; + return 1; + int block = sectionData.blocks[chunkIndex(x & (X_MAX - 1), y & (SECTION_Y_MAX - 1), z & (Z_MAX - 1))]; + if (block == 0) { + return 1; + } + return block; } @Override public void setBlockAt(int x, int y, int z, int block) { diff --git a/chunky/src/java/se/llbit/chunky/chunk/SimpleChunkData.java b/chunky/src/java/se/llbit/chunky/chunk/SimpleChunkData.java index cd708a1549..af01c26452 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/SimpleChunkData.java +++ b/chunky/src/java/se/llbit/chunky/chunk/SimpleChunkData.java @@ -38,6 +38,10 @@ public class SimpleChunkData implements ChunkData { if(y < 0 || y > 255) { return 0; } + int block = blocks[chunkIndex(x & (X_MAX - 1), y, z & (Z_MAX - 1))]; + if (block == 0) { + return 1; + } return blocks[chunkIndex(x & (X_MAX - 1), y, z & (Z_MAX - 1))]; } diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java index 84bd5be37b..c98ee85187 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java @@ -1,19 +1,10 @@ package se.llbit.chunky.chunk.biome; import se.llbit.chunky.chunk.ChunkData; -import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; -import se.llbit.chunky.world.biome.Biomes; -import se.llbit.log.Log; -import se.llbit.math.QuickMath; -import se.llbit.nbt.ListTag; -import se.llbit.nbt.SpecificTag; -import se.llbit.nbt.StringTag; import se.llbit.nbt.Tag; -import se.llbit.util.BitBuffer; import static se.llbit.chunky.world.Chunk.*; -import static se.llbit.util.NbtUtil.getTagFromNames; public class BiomeDataFactory { //TODO: Ideally we would have registered factory impls with an isValidFor(Tag chunkTag), but this messy if chain works for now diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java b/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java index e633d3a2a4..101d53f6ef 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java @@ -13,7 +13,6 @@ import se.llbit.util.BitBuffer; import static se.llbit.chunky.world.Chunk.*; -import static se.llbit.chunky.world.Chunk.Z_MAX; import static se.llbit.util.NbtUtil.getTagFromNames; /** diff --git a/chunky/src/java/se/llbit/chunky/entity/ArmorStand.java b/chunky/src/java/se/llbit/chunky/entity/ArmorStand.java index a4d8c76c55..04b6eb5a1d 100644 --- a/chunky/src/java/se/llbit/chunky/entity/ArmorStand.java +++ b/chunky/src/java/se/llbit/chunky/entity/ArmorStand.java @@ -17,7 +17,6 @@ */ package se.llbit.chunky.entity; -import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.Material; import se.llbit.chunky.world.material.TextureMaterial; diff --git a/chunky/src/java/se/llbit/chunky/entity/BeaconBeam.java b/chunky/src/java/se/llbit/chunky/entity/BeaconBeam.java index 9df60b648d..8bae0b2c6c 100644 --- a/chunky/src/java/se/llbit/chunky/entity/BeaconBeam.java +++ b/chunky/src/java/se/llbit/chunky/entity/BeaconBeam.java @@ -1,11 +1,25 @@ package se.llbit.chunky.entity; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import se.llbit.chunky.block.minecraft.Beacon; import se.llbit.chunky.block.Block; import se.llbit.chunky.chunk.BlockPalette; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.IntegerTextField; +import se.llbit.chunky.ui.dialogs.EditMaterialDialog; +import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.Material; import se.llbit.chunky.world.material.BeaconBeamMaterial; +import se.llbit.fx.LuxColorPicker; import se.llbit.json.JsonMember; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; @@ -217,8 +231,7 @@ public JsonValue toJson() { JsonObject materialsList = new JsonObject(); for (int i : materials.keySet()) { BeaconBeamMaterial material = materials.get(i); - JsonObject object = new JsonObject(materials.size()); - material.saveMaterialProperties(object); + JsonObject object = material.saveMaterialProperties(); materialsList.add(String.valueOf(i), object); } @@ -266,4 +279,91 @@ public void setHeight(int height) { public Map getMaterials() { return materials; } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + VBox controls = new VBox(); + + IntegerAdjuster height = new IntegerAdjuster(); + height.setName("Height"); + height.setTooltip("Modifies the height of the beam. Useful if your scene is taller than the world height."); + height.set(getHeight()); + height.setRange(1, 512); + height.onValueChange(value -> { + setHeight(value); + scene.rebuildActorBvh(); + }); + controls.getChildren().add(height); + + HBox beamColor = new HBox(); + VBox listControls = new VBox(); + + listControls.setMaxWidth(200); + beamColor.setPadding(new Insets(10)); + beamColor.setSpacing(15); + + LuxColorPicker beamColorPicker = new LuxColorPicker(); + + Button editMaterial = new Button("Edit material"); + + ObservableList colorHeights = FXCollections.observableArrayList(); + colorHeights.addAll(getMaterials().keySet()); + ListView colorHeightList = new ListView<>(colorHeights); + colorHeightList.setMaxHeight(150.0); + colorHeightList.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, heightIndex) -> { + + BeaconBeamMaterial beamMat = getMaterials().get(heightIndex); + editMaterial.setOnAction(e -> { + EditMaterialDialog dialog = new EditMaterialDialog(beamMat, scene); + dialog.showAndWait(); + dialog = null; + }); + beamColorPicker.setColor(ColorUtil.toFx(beamMat.getColorInt())); + } + ); + beamColorPicker.colorProperty().addListener( + (observableColor, oldColorValue, newColorValue) -> { + Integer index = colorHeightList.getSelectionModel().getSelectedItem(); + if (index != null) { + getMaterials().get(index).updateColor(ColorUtil.getRGB(ColorUtil.fromFx(newColorValue))); + scene.rebuildActorBvh(); + } + } + ); + + HBox listButtons = new HBox(); + listButtons.setPadding(new Insets(10)); + listButtons.setSpacing(15); + Button deleteButton = new Button("Delete"); + deleteButton.setOnAction(e -> { + Integer index = colorHeightList.getSelectionModel().getSelectedItem(); + if (index != null && index != 0) { //Prevent removal of the bottom layer + getMaterials().remove(index); + colorHeightList.getItems().removeAll(index); + scene.rebuildActorBvh(); + } + }); + IntegerTextField layerInput = new IntegerTextField(); + layerInput.setMaxWidth(50); + Button addButton = new Button("Add"); + addButton.setOnAction(e -> { + if (!getMaterials().containsKey(layerInput.valueProperty().get())) { //Don't allow duplicate indices + getMaterials().put(layerInput.valueProperty().get(), new BeaconBeamMaterial(BeaconBeamMaterial.DEFAULT_COLOR)); + colorHeightList.getItems().add(layerInput.valueProperty().get()); + scene.rebuildActorBvh(); + } + }); + + listButtons.getChildren().addAll(deleteButton, layerInput, addButton); + listControls.getChildren().addAll(new Label("Start Height:"), colorHeightList, listButtons); + beamColor.getChildren().addAll(listControls, beamColorPicker, editMaterial); + controls.getChildren().add(beamColor); + + controls.setSpacing(6); + + return controls; + } } diff --git a/chunky/src/java/se/llbit/chunky/entity/Book.java b/chunky/src/java/se/llbit/chunky/entity/Book.java index 5179d9f797..c656fe6618 100644 --- a/chunky/src/java/se/llbit/chunky/entity/Book.java +++ b/chunky/src/java/se/llbit/chunky/entity/Book.java @@ -3,15 +3,18 @@ import java.util.Collection; import java.util.LinkedList; -import se.llbit.chunky.PersistentSettings; +import javafx.scene.layout.VBox; import se.llbit.chunky.model.Model; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.json.Json; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; +import se.llbit.math.Constants; import se.llbit.math.Quad; -import se.llbit.math.Ray; import se.llbit.math.Transform; import se.llbit.math.Vector3; import se.llbit.math.Vector4; @@ -234,7 +237,7 @@ public Collection primitives(Transform transform) { double pagesDistance = (1 - Math.sin(Math.PI / 2 - pageAngle)) / 16.0; for (int i = 0; i < leftPages.length; i++) { - if (i == 5 && openAngle < Ray.EPSILON) { + if (i == 5 && openAngle < Constants.EPSILON) { continue; // the cover would overlay the pages if the book is closed } if (i == 4 && (pageAngleA >= (Math.PI + openAngle) / 2 @@ -247,7 +250,7 @@ public Collection primitives(Transform transform) { } for (int i = 0; i < rightPages.length; i++) { - if (i == 5 && openAngle < Ray.EPSILON) { + if (i == 5 && openAngle < Constants.EPSILON) { continue; // the cover would overlay the pages if the book is closed } if (i == 4 && (pageAngleA <= (Math.PI - openAngle) / 2 @@ -355,4 +358,48 @@ public double getPageAngleB() { public void setPageAngleB(double pageAngleB) { this.pageAngleB = pageAngleB; } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + VBox controls = new VBox(); + + DoubleAdjuster openingAngle = new DoubleAdjuster(); + openingAngle.setName("Opening angle"); + openingAngle.setTooltip("Modifies the book's opening angle."); + openingAngle.set(Math.toDegrees(getOpenAngle())); + openingAngle.setRange(0, 180); + openingAngle.onValueChange(value -> { + setOpenAngle(Math.toRadians(value)); + scene.rebuildActorBvh(); + }); + controls.getChildren().add(openingAngle); + + DoubleAdjuster page1Angle = new DoubleAdjuster(); + page1Angle.setName("Page 1 angle"); + page1Angle.setTooltip("Modifies the book's first visible page's angle."); + page1Angle.set(Math.toDegrees(getPageAngleA())); + page1Angle.setRange(0, 180); + page1Angle.onValueChange(value -> { + setPageAngleA(Math.toRadians(value)); + scene.rebuildActorBvh(); + }); + controls.getChildren().add(page1Angle); + + DoubleAdjuster page2Angle = new DoubleAdjuster(); + page2Angle.setName("Page 2 angle"); + page2Angle.setTooltip("Modifies the book's second visible page's angle."); + page2Angle.set(Math.toDegrees(getPageAngleB())); + page2Angle.setRange(0, 180); + page2Angle.onValueChange(value -> { + setPageAngleB(Math.toRadians(value)); + scene.rebuildActorBvh(); + }); + controls.getChildren().add(page2Angle); + + controls.setSpacing(6); + + return controls; + } } diff --git a/chunky/src/java/se/llbit/chunky/entity/ChickenEntity.java b/chunky/src/java/se/llbit/chunky/entity/ChickenEntity.java index dd3c563574..df07c0ed55 100644 --- a/chunky/src/java/se/llbit/chunky/entity/ChickenEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/ChickenEntity.java @@ -6,6 +6,7 @@ import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; +import se.llbit.math.Constants; import se.llbit.math.Quad; import se.llbit.math.QuickMath; import se.llbit.math.Ray; @@ -38,7 +39,7 @@ public class ChickenEntity extends Entity implements Poseable, Variant { .addBox(new Vector3(0 / 16.0, -5 / 16.0, 0 / 16.0), new Vector3(3 / 16.0, 0, 3 / 16.0), box -> box.forTextureSize(Texture.chicken, 64, 32).atUVCoordinates(26, 0).flipX().doubleSided() .addTopFace().addBottomFace(UVMapHelper.Side::flipY).addLeftFace().addRightFace().addFrontFace().addBackFace() - .transform(Transform.NONE.translate(0, Ray.OFFSET, 0)) // Prevent Z-Fighting with the block the Chicken is standing on + .transform(Transform.NONE.translate(0, Constants.OFFSET, 0)) // Prevent Z-Fighting with the block the Chicken is standing on ).toQuads(); private static final Quad[] wing = new BoxModelBuilder() @@ -72,7 +73,7 @@ public class ChickenEntity extends Entity implements Poseable, Variant { ).addBox(new Vector3(-3 / 16.0, 4 / 16.0, -3 / 16.0), new Vector3(3 / 16.0, 7 / 16.0, 1 / 16.0), box -> box.forTextureSize(Texture.chicken, 64, 32).atUVCoordinates(44, 0).flipX() .addTopFace().addBottomFace().addLeftFace().addRightFace().addFrontFace().addBackFace() - .transform(Transform.NONE.translate(0, 0, -Ray.OFFSET)).doubleSided() + .transform(Transform.NONE.translate(0, 0, -Constants.OFFSET)).doubleSided() ).toQuads(); private static final Quad[] cold_tail = { diff --git a/chunky/src/java/se/llbit/chunky/entity/Entity.java b/chunky/src/java/se/llbit/chunky/entity/Entity.java index 291e454fa4..9bffcb6edc 100644 --- a/chunky/src/java/se/llbit/chunky/entity/Entity.java +++ b/chunky/src/java/se/llbit/chunky/entity/Entity.java @@ -19,22 +19,21 @@ import se.llbit.chunky.chunk.BlockPalette; import se.llbit.chunky.model.minecraft.DecoratedPotModel; +import se.llbit.chunky.renderer.HasPrimitives; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.math.Grid; import se.llbit.math.Octree; import se.llbit.math.Vector3; import se.llbit.math.Vector3i; -import se.llbit.math.primitive.Primitive; - -import java.util.Collection; +import se.llbit.util.HasControls; /** * Represents Minecraft entities that are not stored in the octree. * * @author Jesper Öqvist */ -abstract public class Entity { +public abstract class Entity implements HasPrimitives, HasControls { public final Vector3 position; @@ -42,8 +41,6 @@ protected Entity(Vector3 position) { this.position = new Vector3(position); } - abstract public Collection primitives(Vector3 offset); - public Grid.EmitterPosition[] getEmitterPosition() { return new Grid.EmitterPosition[0]; } @@ -71,7 +68,7 @@ public void loadDataFromOctree(Octree octree, BlockPalette palette, Vector3i ori * @param json json data. * @return unmarshalled entity, or {@code null} if it was not a valid entity. */ - public static Entity fromJson(JsonObject json) { + public static Entity loadFromJson(JsonObject json) { String kind = json.get("kind").stringValue(""); switch (kind) { case "painting": @@ -118,6 +115,8 @@ public static Entity fromJson(JsonObject json) { return HangingSignEntity.fromJson(json); case "wallHangingSign": return WallHangingSignEntity.fromJson(json); + case "sphere": + return SphereEntity.fromJson(json); case "sheep": return SheepEntity.fromJson(json); case "cow": diff --git a/chunky/src/java/se/llbit/chunky/entity/HeadEntity.java b/chunky/src/java/se/llbit/chunky/entity/HeadEntity.java index deec5532b3..b0291247f7 100644 --- a/chunky/src/java/se/llbit/chunky/entity/HeadEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/HeadEntity.java @@ -202,6 +202,4 @@ public static Entity fromJson(JsonObject json) { String skin = json.get("skin").stringValue(""); return new HeadEntity(position, skin, rotation, placement); } - - } diff --git a/chunky/src/java/se/llbit/chunky/entity/MooshroomEntity.java b/chunky/src/java/se/llbit/chunky/entity/MooshroomEntity.java index 7c03e6ac99..545c63d489 100644 --- a/chunky/src/java/se/llbit/chunky/entity/MooshroomEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/MooshroomEntity.java @@ -1,10 +1,14 @@ package se.llbit.chunky.entity; +import javafx.scene.control.CheckBox; +import javafx.scene.layout.VBox; import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.model.builder.BoxModelBuilder; import se.llbit.chunky.model.builder.UVMapHelper; import se.llbit.chunky.model.minecraft.SpriteModel; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; @@ -257,6 +261,20 @@ public JsonValue toJson() { return json; } + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + CheckBox showMushrooms = new CheckBox("Show mushrooms"); + showMushrooms.setSelected(this.showMushrooms); + showMushrooms.selectedProperty().addListener(((observable, oldValue, newValue) -> { + this.showMushrooms = newValue; + scene.rebuildActorBvh(); + })); + + return new VBox(6, showMushrooms); + } + public static MooshroomEntity fromJson(JsonObject json) { return new MooshroomEntity(json); } diff --git a/chunky/src/java/se/llbit/chunky/entity/PlayerEntity.java b/chunky/src/java/se/llbit/chunky/entity/PlayerEntity.java index 5bd97a02f4..4c0a65c543 100644 --- a/chunky/src/java/se/llbit/chunky/entity/PlayerEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/PlayerEntity.java @@ -18,16 +18,25 @@ */ package se.llbit.chunky.entity; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import se.llbit.chunky.block.minecraft.Head; import se.llbit.chunky.entity.SkullEntity.Kind; import se.llbit.chunky.renderer.scene.PlayerModel; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.*; import se.llbit.chunky.resources.PlayerTexture.ExtendedUVMap; import se.llbit.chunky.resources.texturepack.*; +import se.llbit.chunky.ui.dialogs.ValidatingTextInputDialog; +import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.PlayerEntityData; import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.chunky.world.model.CubeModel; import se.llbit.chunky.world.model.JsonModel; +import se.llbit.fxutil.Dialogs; import se.llbit.json.Json; import se.llbit.json.JsonObject; import se.llbit.json.JsonParser; @@ -999,4 +1008,119 @@ public String[] gearSlots() { public JsonObject getGear() { return gear; } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + ChoiceBox playerModel = new ChoiceBox<>(); + playerModel.getSelectionModel().select(this.model); + playerModel.getItems().addAll(PlayerModel.values()); + playerModel.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + this.model = newValue; + scene.rebuildActorBvh(); + }); + HBox modelBox = new HBox(); + modelBox.setSpacing(10.0); + modelBox.setAlignment(Pos.CENTER_LEFT); + modelBox.getChildren().addAll(new Label("Player model:"), playerModel); + + HBox skinBox = new HBox(); + skinBox.setSpacing(10.0); + skinBox.setAlignment(Pos.CENTER_LEFT); + TextField skinField = new TextField(); + skinField.setText(skin); + Button selectSkin = new Button("Select skin..."); + selectSkin.setOnAction(e -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Load Skin"); + fileChooser + .getExtensionFilters() + .add(new FileChooser.ExtensionFilter("Minecraft skin", "*.png")); + File skinFile = fileChooser.showOpenDialog(parent.getScene().getWindow()); + if (skinFile != null) { + setTexture(skinFile.getAbsolutePath()); + skinField.setText(skinFile.getAbsolutePath()); + scene.rebuildActorBvh(); + } + }); + Button downloadSkin = new Button("Download skin..."); + downloadSkin.setOnAction(e -> { + TextInputDialog playerIdentifierInput = new ValidatingTextInputDialog(input -> input != null && !input.isEmpty()); + playerIdentifierInput.setTitle("Input player identifier"); + playerIdentifierInput.setHeaderText("Please enter the UUID or name of the player."); + playerIdentifierInput.setContentText("UUID / player name:"); + Dialogs.setupDialogDesign(playerIdentifierInput, parent.getScene()); + playerIdentifierInput.showAndWait().map(playerIdentifier -> { + try { + // TODO: refactor this (deduplicate code, check UUID format, trim input, better error handling) + MinecraftProfile profile = MojangApi.fetchProfile(playerIdentifier); //Search by uuid + Optional skin = profile.getSkin(); + if (skin.isPresent()) { // If it found a skin, pass it back to caller + downloadAndApplySkinForPlayer( + skin.get(), + playerModel, + skinField, + scene + ); + return true; + } else { // Otherwise, search by Username + String uuid = MojangApi.usernameToUUID(playerIdentifier); + profile = MojangApi.fetchProfile(uuid); + skin = profile.getSkin(); + if (skin.isPresent()) { + downloadAndApplySkinForPlayer( + skin.get(), + playerModel, + skinField, + scene + ); + return true; + } else { //If still not found, warn user. + Log.warn("Could not find player with that identifier"); + } + } + } catch (IOException ex) { + Log.warn("Could not download skin", ex); + } + return false; + }); + }); + skinBox.getChildren().addAll(new Label("Skin:"), skinField, selectSkin, downloadSkin); + + CheckBox showOuterLayer = new CheckBox("Show second layer"); + showOuterLayer.setSelected(this.showOuterLayer); + showOuterLayer.selectedProperty().addListener(((observable, oldValue, newValue) -> { + this.showOuterLayer = newValue; + scene.rebuildActorBvh(); + })); + HBox layerBox = new HBox(); + layerBox.setSpacing(10.0); + layerBox.setAlignment(Pos.CENTER_LEFT); + layerBox.getChildren().addAll(showOuterLayer); + + VBox controls = new VBox(); + controls.getChildren().addAll(modelBox, skinBox, layerBox); + + controls.setSpacing(6); + + return controls; + } + + private void downloadAndApplySkinForPlayer( + MinecraftSkin skin, + ChoiceBox playerModelSelector, + TextField skinField, + Scene scene + ) throws IOException { + if (skin != null) { + String filePath = MojangApi.downloadSkin(skin.getSkinUrl()).getAbsolutePath(); + setTexture(filePath); + playerModelSelector.getSelectionModel().select(skin.getPlayerModel()); + skinField.setText(filePath); + Log.info("Successfully set skin"); + scene.rebuildActorBvh(); + } + } } diff --git a/chunky/src/java/se/llbit/chunky/entity/Poseable.java b/chunky/src/java/se/llbit/chunky/entity/Poseable.java index 090bbbe52c..b4f15414ee 100644 --- a/chunky/src/java/se/llbit/chunky/entity/Poseable.java +++ b/chunky/src/java/se/llbit/chunky/entity/Poseable.java @@ -19,6 +19,7 @@ import org.apache.commons.math3.util.FastMath; import se.llbit.json.Json; +import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.math.Vector3; import se.llbit.util.JsonUtil; @@ -50,9 +51,17 @@ default void lookAt(Vector3 target) { dir.sub(face); dir.normalize(); double headYaw = getPose("head").y; - getPose().set("rotation", Json.of(FastMath.atan2(dir.x, dir.z) + Math.PI - headYaw)); double pitch = Math.asin(dir.y); - getPose().add("head", JsonUtil.vec3ToJson(new Vector3(pitch, headYaw, 0))); + + JsonArray headPose = getPose().get("head").array(); + headPose.set(0, Json.of(pitch)); + headPose.set(1, Json.of(0)); + headPose.set(2, Json.of(0)); + + JsonArray rotation = getPose().get("all").array(); + rotation.set(0, Json.of(0)); + rotation.set(1, Json.of(FastMath.atan2(dir.x, dir.z) + Math.PI - headYaw)); + rotation.set(2, Json.of(0)); } diff --git a/chunky/src/java/se/llbit/chunky/entity/SheepEntity.java b/chunky/src/java/se/llbit/chunky/entity/SheepEntity.java index 62c9899952..db476127a3 100644 --- a/chunky/src/java/se/llbit/chunky/entity/SheepEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/SheepEntity.java @@ -8,6 +8,7 @@ import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; +import se.llbit.math.Constants; import se.llbit.math.Quad; import se.llbit.math.QuickMath; import se.llbit.math.Ray; @@ -213,7 +214,7 @@ public Collection primitives(Vector3 offset) { quad.addTriangles(faces, skinMaterial, transform); } - double inflateOffset = 1.0 + Ray.OFFSET; + double inflateOffset = 1.0 + Constants.OFFSET; if (sheared) { // The sheared overlay needs some specific translations and scaling to prevent z-fighting because of their rotation points. @@ -229,7 +230,7 @@ public Collection primitives(Vector3 offset) { } transform = Transform.NONE - .translate(0, Ray.OFFSET, 0) + .translate(0, Constants.OFFSET, 0) .scale(inflateOffset) .rotateX(frontRightLegPose.x) .rotateY(frontRightLegPose.y) @@ -241,7 +242,7 @@ public Collection primitives(Vector3 offset) { } transform = Transform.NONE - .translate(0, Ray.OFFSET, 0) + .translate(0, Constants.OFFSET, 0) .scale(inflateOffset) .rotateX(frontLeftLegPose.x) .rotateY(frontLeftLegPose.y) @@ -253,7 +254,7 @@ public Collection primitives(Vector3 offset) { } transform = Transform.NONE - .translate(0, Ray.OFFSET, 0) + .translate(0, Constants.OFFSET, 0) .scale(inflateOffset) .rotateX(backRightLegPose.x) .rotateY(backRightLegPose.y) @@ -265,7 +266,7 @@ public Collection primitives(Vector3 offset) { } transform = Transform.NONE - .translate(0, Ray.OFFSET, 0) + .translate(0, Constants.OFFSET, 0) .scale(inflateOffset) .rotateX(backLeftLegPose.x) .rotateY(backLeftLegPose.y) @@ -277,7 +278,7 @@ public Collection primitives(Vector3 offset) { } transform = Transform.NONE - .translate(0, 0, Ray.OFFSET) + .translate(0, 0, Constants.OFFSET) .scale(inflateOffset) .rotateX(headPose.x) .rotateY(headPose.y) @@ -363,10 +364,7 @@ public JsonValue toJson() { json.add("headScale", headScale); json.add("pose", pose); - JsonObject furMatData = new JsonObject(); - materialFur.saveMaterialProperties(furMatData); - - json.add("furMaterial", furMatData); + json.add("furMaterial", materialFur.saveMaterialProperties()); json.add("sheared", sheared); return json; } diff --git a/chunky/src/java/se/llbit/chunky/entity/SphereEntity.java b/chunky/src/java/se/llbit/chunky/entity/SphereEntity.java new file mode 100644 index 0000000000..4720dad28a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/entity/SphereEntity.java @@ -0,0 +1,105 @@ +package se.llbit.chunky.entity; + +import javafx.scene.control.TitledPane; +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.SolidColorTexture; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.material.TextureMaterial; +import se.llbit.json.JsonObject; +import se.llbit.json.JsonValue; +import se.llbit.math.ColorUtil; +import se.llbit.math.Vector3; +import se.llbit.math.Vector4; +import se.llbit.math.primitive.Primitive; +import se.llbit.math.primitive.Sphere; + +import java.util.Collection; +import java.util.LinkedList; + +public class SphereEntity extends Entity { + private final Material material; + private double radius; + + public SphereEntity(Vector3 position, double radius) { + super(position); + this.material = new TextureMaterial(new SolidColorTexture(new Vector4(1, 1, 1, 1))); + this.radius = radius; + } + + public Material getMaterial() { + return this.material; + } + + public double getRadius() { + return this.radius; + } + + public void setRadius(double radius) { + this.radius = radius; + } + + @Override + public Collection primitives(Vector3 offset) { + Sphere sphere = new Sphere(position.rAdd(offset), radius, material); + Collection primitives = new LinkedList<>(); + primitives.add(sphere); + return primitives; + } + + @Override + public JsonValue toJson() { + JsonObject json = new JsonObject(); + json.add("kind", "sphere"); + json.add("position", position.toJson()); + json.add("radius", radius); + json.add("materialProperties", material.saveMaterialProperties()); + return json; + } + + /** + * Deserialize entity from JSON. + * + * @return deserialized entity, or {@code null} if it was not a valid entity + */ + public static Entity fromJson(JsonObject json) { + Vector3 position = new Vector3(); + position.fromJson(json.get("position").object()); + Vector3 color = ColorUtil.jsonToRGB(json.get("color").asObject()); + double radius = json.get("radius").doubleValue(0.5); + + SphereEntity sphereEntity = new SphereEntity(position, radius); + sphereEntity.getMaterial().loadMaterialProperties(json.get("materialProperties").asObject()); + + return sphereEntity; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + VBox controls = new VBox(6); + + DoubleAdjuster radiusAdjuster = new DoubleAdjuster(); + radiusAdjuster.setName("Radius"); + radiusAdjuster.setTooltip("Set the radius of the sphere."); + radiusAdjuster.setRange(0.001, 50); + radiusAdjuster.clampMin(); + radiusAdjuster.set(this.radius); + radiusAdjuster.onValueChange(value -> { + this.setRadius(value); + scene.rebuildBvh(); + }); + controls.getChildren().add(radiusAdjuster); + + TitledPane materialPropertiesPane = new TitledPane(); + materialPropertiesPane.setText("Material Properties"); + materialPropertiesPane.setContent(Material.getControls(this.material, scene)); + + controls.getChildren().add(materialPropertiesPane); + + return controls; + } +} diff --git a/chunky/src/java/se/llbit/chunky/entity/SquidEntity.java b/chunky/src/java/se/llbit/chunky/entity/SquidEntity.java index 1d879da282..d5fba38f3c 100644 --- a/chunky/src/java/se/llbit/chunky/entity/SquidEntity.java +++ b/chunky/src/java/se/llbit/chunky/entity/SquidEntity.java @@ -6,6 +6,7 @@ import se.llbit.chunky.world.material.TextureMaterial; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; +import se.llbit.math.Constants; import se.llbit.math.Quad; import se.llbit.math.QuickMath; import se.llbit.math.Ray; @@ -98,7 +99,7 @@ public Collection primitives(Vector3 offset) { .rotateX(frontTentacle.x) .rotateY(frontTentacle.y) .rotateZ(frontTentacle.z) - .translate(0 / 16.0, -3 / 16.0, -5 / 16.0 + Ray.OFFSET) // Prevent Z-fighting + .translate(0 / 16.0, -3 / 16.0, -5 / 16.0 + Constants.OFFSET) // Prevent Z-fighting .chain(worldTransform); for (Quad quad : tentacle) { quad.addTriangles(faces, skinMaterial, transform); @@ -118,7 +119,7 @@ public Collection primitives(Vector3 offset) { .rotateX(leftTentacle.x) .rotateY(leftTentacle.y) .rotateZ(leftTentacle.z) - .translate(-5 / 16.0 + Ray.OFFSET, -3 / 16.0, 0 / 16.0) + .translate(-5 / 16.0 + Constants.OFFSET, -3 / 16.0, 0 / 16.0) .chain(worldTransform); for (Quad quad : tentacle) { quad.addTriangles(faces, skinMaterial, transform); @@ -138,7 +139,7 @@ public Collection primitives(Vector3 offset) { .rotateX(backTentacle.x) .rotateY(backTentacle.y) .rotateZ(backTentacle.z) - .translate(0 / 16.0, -3 / 16.0, 5 / 16.0 - Ray.OFFSET) + .translate(0 / 16.0, -3 / 16.0, 5 / 16.0 - Constants.OFFSET) .chain(worldTransform); for (Quad quad : tentacle) { quad.addTriangles(faces, skinMaterial, transform); @@ -158,7 +159,7 @@ public Collection primitives(Vector3 offset) { .rotateX(rightTentacle.x) .rotateY(rightTentacle.y) .rotateZ(rightTentacle.z) - .translate(5 / 16.0 - Ray.OFFSET, -3 / 16.0, 0 / 16.0) + .translate(5 / 16.0 - Constants.OFFSET, -3 / 16.0, 0 / 16.0) .chain(worldTransform); for (Quad quad : tentacle) { quad.addTriangles(faces, skinMaterial, transform); diff --git a/chunky/src/java/se/llbit/chunky/main/Chunky.java b/chunky/src/java/se/llbit/chunky/main/Chunky.java index 13a6965357..071ee107ca 100644 --- a/chunky/src/java/se/llbit/chunky/main/Chunky.java +++ b/chunky/src/java/se/llbit/chunky/main/Chunky.java @@ -28,7 +28,6 @@ import se.llbit.chunky.plugin.loader.JarPluginLoader; import se.llbit.chunky.plugin.manifest.PluginManifest; import se.llbit.chunky.renderer.*; -import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.export.PictureExportFormat; import se.llbit.chunky.renderer.scene.AsynchronousSceneManager; import se.llbit.chunky.renderer.scene.Scene; @@ -59,9 +58,9 @@ import java.util.stream.Collectors; /** - * Chunky is a Minecraft mapping and rendering tool created byJesper Öqvist (jesper@llbit.se). + * Chunky is a Minecraft mapping and rendering tool created by Jesper Öqvist (jesper@llbit.se). * - *

Read more about Chunky at https://chunky.llbit.se. + *

Read more about Chunky at https://chunky-dev.github.io/docs/. */ public class Chunky { static { diff --git a/chunky/src/java/se/llbit/chunky/map/SurfaceLayer.java b/chunky/src/java/se/llbit/chunky/map/SurfaceLayer.java index d8e571cf71..7d2976b50e 100644 --- a/chunky/src/java/se/llbit/chunky/map/SurfaceLayer.java +++ b/chunky/src/java/se/llbit/chunky/map/SurfaceLayer.java @@ -17,6 +17,7 @@ package se.llbit.chunky.map; import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.block.Void; import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.Block; import se.llbit.chunky.block.legacy.UnfinalizedLegacyBlock; @@ -57,7 +58,8 @@ public SurfaceLayer(int dim, ChunkData chunkData, BlockPalette palette, BiomePal int y = Math.min(Math.min(chunkData.maxY() - 1, yMax), heightmapData[z*Chunk.X_MAX + x]); int minY = Math.max(chunkData.minY(), yMin); for (; y > minY; --y) { - if (palette.get(chunkData.getBlockAt(x, y, z)) != Air.INSTANCE) { + Block block = palette.get(chunkData.getBlockAt(x, y, z)); + if (block != Air.INSTANCE && block != Void.INSTANCE) { break; } } diff --git a/chunky/src/java/se/llbit/chunky/model/AABBModel.java b/chunky/src/java/se/llbit/chunky/model/AABBModel.java index 90ca47f55e..dcc6dd3d7e 100644 --- a/chunky/src/java/se/llbit/chunky/model/AABBModel.java +++ b/chunky/src/java/se/llbit/chunky/model/AABBModel.java @@ -3,12 +3,9 @@ import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.AABB; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.*; import java.util.Arrays; -import java.util.List; import java.util.Objects; import java.util.Random; @@ -70,7 +67,7 @@ public double faceSurfaceArea(int face) { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { AABB[] boxes = getBoxes(); Texture[][] textures = getTextures(); UVMapping[][] mapping = getUVMapping(); @@ -78,107 +75,120 @@ public boolean intersect(Ray ray, Scene scene) { boolean hit = false; Tint tint = Tint.NONE; - ray.t = Double.POSITIVE_INFINITY; for (int i = 0; i < boxes.length; ++i) { - if (boxes[i].intersect(ray)) { + if (boxes[i].closestIntersection(ray, intersectionRecord)) { Tint[] tintedFacesBox = tintedFaces != null ? tintedFaces[i] : null; - Vector3 n = ray.getNormal(); + Vector3 n = intersectionRecord.shadeN; if (n.y > 0) { // top - ray.v = 1 - ray.v; - if (intersectFace(ray, scene, textures[i][4], + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; + if (intersectFace(intersectionRecord, scene, textures[i][4], mapping != null ? mapping[i][4] : null )) { tint = tintedFacesBox != null ? tintedFacesBox[4] : Tint.NONE; hit = true; } } else if (n.y < 0) { // bottom - if (intersectFace(ray, scene, textures[i][5], - mapping != null ? mapping[i][5] : null)) { + if (intersectFace(intersectionRecord, scene, textures[i][5], + mapping != null ? mapping[i][5] : null + )) { hit = true; tint = tintedFacesBox != null ? tintedFacesBox[5] : Tint.NONE; } } else if (n.z < 0) { // north - if (intersectFace(ray, scene, textures[i][0], + if (intersectFace(intersectionRecord, scene, textures[i][0], mapping != null ? mapping[i][0] : null )) { hit = true; tint = tintedFacesBox != null ? tintedFacesBox[0] : Tint.NONE; } } else if (n.z > 0) { // south - if (intersectFace(ray, scene, textures[i][2], + if (intersectFace(intersectionRecord, scene, textures[i][2], mapping != null ? mapping[i][2] : null )) { hit = true; tint = tintedFacesBox != null ? tintedFacesBox[2] : Tint.NONE; } } else if (n.x < 0) { // west - if (intersectFace(ray, scene, textures[i][3], - mapping != null ? mapping[i][3] : null)) { + if (intersectFace(intersectionRecord, scene, textures[i][3], + mapping != null ? mapping[i][3] : null + )) { hit = true; tint = tintedFacesBox != null ? tintedFacesBox[3] : Tint.NONE; } } else if (n.x > 0) { // east - if (intersectFace(ray, scene, textures[i][1], - mapping != null ? mapping[i][1] : null)) { + if (intersectFace(intersectionRecord, scene, textures[i][1], + mapping != null ? mapping[i][1] : null + )) { hit = true; tint = tintedFacesBox != null ? tintedFacesBox[1] : Tint.NONE; } } - if (hit) { - ray.t = ray.tNext; - } } } if (hit) { - if (ray.getCurrentMaterial().opaque) { - ray.color.w = 1; - } - - tint.tint(ray.color, ray, scene); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + tint.tint(intersectionRecord.color, ray, scene); } return hit; } - private boolean intersectFace(Ray ray, Scene scene, Texture texture, UVMapping mapping) { + public boolean intersectFace(IntersectionRecord intersectionRecord, Scene scene, Texture texture, UVMapping mapping) { // This is the method that handles intersecting faces of all AABB-based models. // Do normal mapping, parallax occlusion mapping, specular maps and all the good stuff here! if (texture == null) { - return false; + intersectionRecord.color.set(1, 1, 1, 0); + return true; } double tmp; if (mapping != null) { switch (mapping) { case ROTATE_90: - tmp = ray.u; - ray.u = 1 - ray.v; - ray.v = tmp; + tmp = intersectionRecord.uv.x; + intersectionRecord.uv.x = 1 - intersectionRecord.uv.y; + intersectionRecord.uv.y = tmp; break; case ROTATE_180: - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.y; break; case ROTATE_270: - tmp = ray.v; - ray.v = 1 - ray.u; - ray.u = tmp; + tmp = intersectionRecord.uv.y; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.x; + intersectionRecord.uv.x = tmp; break; case FLIP_U: - ray.u = 1 - ray.u; + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; break; case FLIP_V: - ray.v = 1 - ray.v; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.y; break; } } - float[] color = texture.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - return true; + float[] color = texture.getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (color[3] > Constants.EPSILON) { + intersectionRecord.color.set(color); + } else { + intersectionRecord.color.set(1, 1, 1, 0); + } + return true; + } + + @Override + public boolean isInside(Ray ray) { + return isInside(ray.o); + } + + public boolean isInside(Vector3 p) { + double ix = p.x - QuickMath.floor(p.x); + double iy = p.y - QuickMath.floor(p.y); + double iz = p.z - QuickMath.floor(p.z); + AABB[] boxes = getBoxes(); + for (AABB box: boxes) { + if (box.inside(ix, iy, iz)) { + return true; + } } return false; } diff --git a/chunky/src/java/se/llbit/chunky/model/AnimatedQuadModel.java b/chunky/src/java/se/llbit/chunky/model/AnimatedQuadModel.java index 48946e75cf..37f6c81029 100644 --- a/chunky/src/java/se/llbit/chunky/model/AnimatedQuadModel.java +++ b/chunky/src/java/se/llbit/chunky/model/AnimatedQuadModel.java @@ -3,6 +3,8 @@ import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.AnimatedTexture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -35,9 +37,8 @@ public AnimationMode getAnimationMode() { public abstract AnimatedTexture[] getTextures(); @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; Quad[] quads = getQuads(); AnimatedTexture[] textures = getTextures(); @@ -47,43 +48,59 @@ public boolean intersect(Ray ray, Scene scene) { int j = (int) (scene.getAnimationTime() * animationMode.framerate); if (animationMode.positional) { Vector3 position = new Vector3(ray.o); - position.scaleAdd(Ray.OFFSET, ray.d); + position.scaleAdd(Constants.OFFSET, ray.d); j += (int) MinecraftPRNG.rand((long) position.x, (long) position.y, (long) position.z); } float[] color = null; Tint tint = Tint.NONE; - for (int i = 0; i < quads.length; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] c = textures[i].getColor(ray.u, ray.v, j); - if (c[3] > Ray.EPSILON) { - tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; - color = c; - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); + if (refractive) { + for (int i = 0; i < quads.length; ++i) { + Quad quad = quads[i]; + if (quad.closestIntersection(ray, intersectionRecord)) { + if (ray.d.dot(quad.n) < 0) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y, j); + if (c[3] > Constants.EPSILON) { + tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; + color = c; + } else { + tint = Tint.NONE; + color = new float[] {1, 1, 1, 0}; + } + } else { + tint = Tint.NONE; + color = new float[] {1, 1, 1, 0}; + } hit = true; + intersectionRecord.setNormal(quad.n); + } + } + } else { + for (int i = 0; i < quads.length; ++i) { + Quad quad = quads[i]; + double distance = intersectionRecord.distance; + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y, j); + if (c[3] > Constants.EPSILON) { + tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; + color = c; + if (quad.doubleSided) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, quad.n)); + } else { + intersectionRecord.setNormal(quad.n); + } + hit = true; + } else { + intersectionRecord.distance = distance; + } } } } if (hit) { - double px = ray.o.x - Math.floor(ray.o.x + ray.d.x * Ray.OFFSET) + ray.d.x * ray.tNext; - double py = ray.o.y - Math.floor(ray.o.y + ray.d.y * Ray.OFFSET) + ray.d.y * ray.tNext; - double pz = ray.o.z - Math.floor(ray.o.z + ray.d.z * Ray.OFFSET) + ray.d.z * ray.tNext; - if (px < E0 || px > E1 || py < E0 || py > E1 || pz < E0 || pz > E1) { - // TODO this check is only really needed for wall torches - return false; - } - - ray.color.set(color); - tint.tint(ray.color, ray, scene); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + intersectionRecord.color.set(color); + tint.tint(intersectionRecord.color, ray, scene); } return hit; } diff --git a/chunky/src/java/se/llbit/chunky/model/BlockModel.java b/chunky/src/java/se/llbit/chunky/model/BlockModel.java index 5fd4786a83..91423e8c3e 100644 --- a/chunky/src/java/se/llbit/chunky/model/BlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/BlockModel.java @@ -2,16 +2,14 @@ import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.*; -import java.util.List; import java.util.Random; @PluginApi public interface BlockModel { - boolean intersect(Ray ray, Scene scene); + boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene); int faceCount(); @@ -19,5 +17,7 @@ public interface BlockModel { double faceSurfaceArea(int face); + boolean isInside(Ray ray); + boolean isBiomeDependant(); } diff --git a/chunky/src/java/se/llbit/chunky/model/EmptyModel.java b/chunky/src/java/se/llbit/chunky/model/EmptyModel.java new file mode 100644 index 0000000000..b9d786083e --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/model/EmptyModel.java @@ -0,0 +1,42 @@ +package se.llbit.chunky.model; + +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class EmptyModel implements BlockModel { + public static final EmptyModel INSTANCE = new EmptyModel(); + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return false; + } + + @Override + public int faceCount() { + return 0; + } + + @Override + public void sample(int face, Vector3 loc, Random rand) { + + } + + @Override + public double faceSurfaceArea(int face) { + return 0; + } + + @Override + public boolean isInside(Ray ray) { + return false; + } + + @Override + public boolean isBiomeDependant() { + return false; + } +} diff --git a/chunky/src/java/se/llbit/chunky/model/QuadModel.java b/chunky/src/java/se/llbit/chunky/model/QuadModel.java index e8bdcfb9c4..9380fc57f1 100644 --- a/chunky/src/java/se/llbit/chunky/model/QuadModel.java +++ b/chunky/src/java/se/llbit/chunky/model/QuadModel.java @@ -18,18 +18,17 @@ package se.llbit.chunky.model; -import se.llbit.chunky.model.BlockModel; -import se.llbit.chunky.model.Tint; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.math.Vector4; import java.util.Arrays; -import java.util.Objects; import java.util.Random; /** @@ -76,9 +75,11 @@ public abstract class QuadModel implements BlockModel { FULL_BLOCK_TOP_SIDE, FULL_BLOCK_BOTTOM_SIDE }; - // Epsilons to clip ray intersections to the current block. - protected static final double E0 = -Ray.EPSILON; - protected static final double E1 = 1 + Ray.EPSILON; + /** + * Whether this block model will allow intersecting rays to update their mediums when traversing + * the model. + */ + public boolean refractive = false; @PluginApi public abstract Quad[] getQuads(); @@ -107,9 +108,8 @@ public double faceSurfaceArea(int face) { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; Quad[] quads = getQuads(); Texture[] textures = getTextures(); @@ -117,41 +117,78 @@ public boolean intersect(Ray ray, Scene scene) { float[] color = null; Tint tint = Tint.NONE; - for (int i = 0; i < quads.length; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] c = textures[i].getColor(ray.u, ray.v); - if (c[3] > Ray.EPSILON) { - tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; - color = c; - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); + if (refractive) { + for (int i = 0; i < quads.length; ++i) { + Quad quad = quads[i]; + if (quad.closestIntersection(ray, intersectionRecord)) { + if (ray.d.dot(quad.n) < 0) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (c[3] > Constants.EPSILON) { + tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; + color = c; + } else { + tint = Tint.NONE; + color = new float[] {1, 1, 1, 0}; + } + } else { + tint = Tint.NONE; + color = new float[] {1, 1, 1, 0}; + } hit = true; + intersectionRecord.setNormal(quad.n); + } + } + } else { + for (int i = 0; i < quads.length; ++i) { + Quad quad = quads[i]; + double distance = intersectionRecord.distance; + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (c[3] > Constants.EPSILON) { + tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; + color = c; + if (quad.doubleSided) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, quad.n)); + } else { + intersectionRecord.setNormal(quad.n); + } + intersectionRecord.setNoMediumChange(true); + hit = true; + } else { + intersectionRecord.distance = distance; + } } } } if (hit) { - double px = ray.o.x - Math.floor(ray.o.x + ray.d.x * Ray.OFFSET) + ray.d.x * ray.tNext; - double py = ray.o.y - Math.floor(ray.o.y + ray.d.y * Ray.OFFSET) + ray.d.y * ray.tNext; - double pz = ray.o.z - Math.floor(ray.o.z + ray.d.z * Ray.OFFSET) + ray.d.z * ray.tNext; - if (px < E0 || px > E1 || py < E0 || py > E1 || pz < E0 || pz > E1) { - // TODO this check is only really needed for wall torches - return false; - } - - ray.color.set(color); - tint.tint(ray.color, ray, scene); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + intersectionRecord.color.set(color); + tint.tint(intersectionRecord.color, ray, scene); } return hit; } @Override + public boolean isInside(Ray ray) { + if (!refractive) { + return false; + } + + IntersectionRecord intersectionTest = new IntersectionRecord(); + + Quad[] quads = getQuads(); + boolean hit = false; + for (Quad quad : quads) { + if (quad.closestIntersection(ray, intersectionTest)) { + hit = true; + } + } + if (hit) { + return ray.d.dot(intersectionTest.n) > 0; + } + return false; + } + public boolean isBiomeDependant() { Tint[] tints = getTints(); if(tints == null) diff --git a/chunky/src/java/se/llbit/chunky/model/RotatableBlockModel.java b/chunky/src/java/se/llbit/chunky/model/RotatableBlockModel.java index a397387a13..b2e7486512 100644 --- a/chunky/src/java/se/llbit/chunky/model/RotatableBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/RotatableBlockModel.java @@ -19,8 +19,6 @@ import se.llbit.chunky.resources.Texture; import se.llbit.math.Quad; -import se.llbit.math.Vector3; -import se.llbit.math.Vector4; /** * This block model is used to render blocks which can face east, west, north, south, up and down. diff --git a/chunky/src/java/se/llbit/chunky/model/TexturedBlockModel.java b/chunky/src/java/se/llbit/chunky/model/TexturedBlockModel.java index fdb3568dff..fce0431d04 100644 --- a/chunky/src/java/se/llbit/chunky/model/TexturedBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/TexturedBlockModel.java @@ -19,6 +19,7 @@ import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.resources.Texture; import se.llbit.math.AABB; +import se.llbit.math.IntersectionRecord; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -54,16 +55,16 @@ public Texture[][] getTextures() { * * @param ray ray to test */ - public static void getIntersectionColor(Ray ray) { - if (ray.getCurrentMaterial() == Air.INSTANCE) { - ray.color.x = 1; - ray.color.y = 1; - ray.color.z = 1; - ray.color.w = 0; + public static void getIntersectionColor(Ray ray, IntersectionRecord intersectionRecord) { + if (intersectionRecord.material == Air.INSTANCE) { + intersectionRecord.color.x = 1; + intersectionRecord.color.y = 1; + intersectionRecord.color.z = 1; + intersectionRecord.color.w = 0; return; } - getTextureCoordinates(ray); - ray.getCurrentMaterial().getColor(ray); + getTextureCoordinates(ray, intersectionRecord); + intersectionRecord.material.getColor(intersectionRecord); } /** @@ -71,26 +72,26 @@ public static void getIntersectionColor(Ray ray) { * * @param ray ray to test */ - private static void getTextureCoordinates(Ray ray) { + private static void getTextureCoordinates(Ray ray, IntersectionRecord intersectionRecord) { int bx = (int) QuickMath.floor(ray.o.x); int by = (int) QuickMath.floor(ray.o.y); int bz = (int) QuickMath.floor(ray.o.z); - Vector3 n = ray.getNormal(); + Vector3 n = intersectionRecord.n; if (n.y != 0) { - ray.u = ray.o.x - bx; - ray.v = ray.o.z - bz; + intersectionRecord.uv.x = ray.o.x - bx; + intersectionRecord.uv.y = ray.o.z - bz; } else if (n.x != 0) { - ray.u = ray.o.z - bz; - ray.v = ray.o.y - by; + intersectionRecord.uv.x = ray.o.z - bz; + intersectionRecord.uv.y = ray.o.y - by; } else { - ray.u = ray.o.x - bx; - ray.v = ray.o.y - by; + intersectionRecord.uv.x = ray.o.x - bx; + intersectionRecord.uv.y = ray.o.y - by; } if (n.x > 0 || n.z < 0) { - ray.u = 1 - ray.u; + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; } if (n.y > 0) { - ray.v = 1 - ray.v; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.y; } } } diff --git a/chunky/src/java/se/llbit/chunky/model/Tint.java b/chunky/src/java/se/llbit/chunky/model/Tint.java index a6c187c4e9..a2bab6ad51 100644 --- a/chunky/src/java/se/llbit/chunky/model/Tint.java +++ b/chunky/src/java/se/llbit/chunky/model/Tint.java @@ -3,6 +3,7 @@ import se.llbit.chunky.renderer.scene.Scene; import se.llbit.log.Log; import se.llbit.math.ColorUtil; +import se.llbit.math.Constants; import se.llbit.math.Ray; import se.llbit.math.Vector4; @@ -66,13 +67,29 @@ private float[] getTintColor(Ray ray, Scene scene) { case CONSTANT: return this.tint; case BIOME_FOLIAGE: - return ray.getBiomeFoliageColor(scene); + return scene.getFoliageColor( + (int) (ray.o.x + ray.d.x * Constants.OFFSET), + (int) (ray.o.y + ray.d.y * Constants.OFFSET), + (int) (ray.o.z + ray.d.z * Constants.OFFSET) + ); case BIOME_DRY_FOLIAGE: - return ray.getBiomeDryFoliageColor(scene); + return scene.getDryFoliageColor( + (int) (ray.o.x + ray.d.x * Constants.OFFSET), + (int) (ray.o.y + ray.d.y * Constants.OFFSET), + (int) (ray.o.z + ray.d.z * Constants.OFFSET) + ); case BIOME_GRASS: - return ray.getBiomeGrassColor(scene); + return scene.getGrassColor( + (int) (ray.o.x + ray.d.x * Constants.OFFSET), + (int) (ray.o.y + ray.d.y * Constants.OFFSET), + (int) (ray.o.z + ray.d.z * Constants.OFFSET) + ); case BIOME_WATER: - return ray.getBiomeWaterColor(scene); + return scene.getWaterColor( + (int) (ray.o.x + ray.d.x * Constants.OFFSET), + (int) (ray.o.y + ray.d.y * Constants.OFFSET), + (int) (ray.o.z + ray.d.z * Constants.OFFSET) + ); default: Log.warn("Unsupported tint type " + type); return null; diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/BambooModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/BambooModel.java index 9ef08c97f2..8aba9cbbeb 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/BambooModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/BambooModel.java @@ -129,25 +129,29 @@ public class BambooModel extends QuadModel { new Vector3(15.2 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(0.8 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(15.2 / 16.0, 16 / 16.0, 8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(0.8 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(15.2 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(0.8 / 16.0, 16 / 16.0, 8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(8 / 16.0, 0 / 16.0, 0.8 / 16.0), new Vector3(8 / 16.0, 0 / 16.0, 15.2 / 16.0), new Vector3(8 / 16.0, 16 / 16.0, 0.8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(8 / 16.0, 0 / 16.0, 15.2 / 16.0), new Vector3(8 / 16.0, 0 / 16.0, 0.8 / 16.0), new Vector3(8 / 16.0, 16 / 16.0, 15.2 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ) }; //endregion @@ -163,25 +167,29 @@ public class BambooModel extends QuadModel { new Vector3(15.2 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(0.8 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(15.2 / 16.0, 16 / 16.0, 8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(0.8 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(15.2 / 16.0, 0 / 16.0, 8 / 16.0), new Vector3(0.8 / 16.0, 16 / 16.0, 8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(8 / 16.0, 0 / 16.0, 0.8 / 16.0), new Vector3(8 / 16.0, 0 / 16.0, 15.2 / 16.0), new Vector3(8 / 16.0, 16 / 16.0, 0.8 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ), new Quad( new Vector3(8 / 16.0, 0 / 16.0, 15.2 / 16.0), new Vector3(8 / 16.0, 0 / 16.0, 0.8 / 16.0), new Vector3(8 / 16.0, 16 / 16.0, 15.2 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true ) }; //endregion diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/BarrelModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/BarrelModel.java index ffe39c462e..3312e592a0 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/BarrelModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/BarrelModel.java @@ -56,6 +56,7 @@ public class BarrelModel extends QuadModel { private final Texture[] textures; public BarrelModel(String facing, String open) { + refractive = true; textures = new Texture[] {Texture.barrelSide, Texture.barrelSide, Texture.barrelSide, Texture.barrelSide, open.equals("true") ? Texture.barrelOpen : Texture.barrelTop, Texture.barrelBottom}; switch (facing) { diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/ButtonModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/ButtonModel.java index 7700865158..151ab8af32 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/ButtonModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/ButtonModel.java @@ -57,6 +57,8 @@ public class ButtonModel extends QuadModel { private final Texture[] textures; public ButtonModel(String face, String facing, Texture tex) { + refractive = true; + textures = new Texture[attachedSouth.length]; Arrays.fill(textures, tex); diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/CakeModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/CakeModel.java index ecafdafeeb..ba70251d3c 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/CakeModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/CakeModel.java @@ -27,33 +27,33 @@ public class CakeModel extends QuadModel { private static final Quad[][] cake = new Quad[7][]; static { - int[] fromX = new int[] {1, 3, 5, 7, 9, 11, 13}; - for (int i = 0; i < 7; i++) { - double xMin = fromX[i] / 16.0; - cake[i] = new Quad[]{ - // front - new Quad(new Vector3(.9375, 0, .0625), new Vector3(xMin, 0, .0625), - new Vector3(.9375, .5, .0625), new Vector4(.9375, xMin, 0, .5)), + int[] fromX = new int[] {1, 3, 5, 7, 9, 11, 13}; + for (int i = 0; i < 7; i++) { + double xMin = fromX[i] / 16.0; + cake[i] = new Quad[]{ + // front + new Quad(new Vector3(.9375, 0, .0625), new Vector3(xMin, 0, .0625), + new Vector3(.9375, .5, .0625), new Vector4(.9375, xMin, 0, .5)), - // back - new Quad(new Vector3(xMin, 0, .9375), new Vector3(.9375, 0, .9375), - new Vector3(xMin, .5, .9375), new Vector4(xMin, .9375, 0, .5)), + // back + new Quad(new Vector3(xMin, 0, .9375), new Vector3(.9375, 0, .9375), + new Vector3(xMin, .5, .9375), new Vector4(xMin, .9375, 0, .5)), - // right - new Quad(new Vector3(xMin, 0, .0625), new Vector3(xMin, 0, .9375), - new Vector3(xMin, .5, .0625), new Vector4(0.0625, .9375, 0, .5)), + // right + new Quad(new Vector3(xMin, 0, .0625), new Vector3(xMin, 0, .9375), + new Vector3(xMin, .5, .0625), new Vector4(0.0625, .9375, 0, .5)), - // left - new Quad(new Vector3(.9375, 0, .9375), new Vector3(.9375, 0, .0625), - new Vector3(.9375, .5, .9375), new Vector4(.9375, 0.0625, 0, .5)), + // left + new Quad(new Vector3(.9375, 0, .9375), new Vector3(.9375, 0, .0625), + new Vector3(.9375, .5, .9375), new Vector4(.9375, 0.0625, 0, .5)), - // top - new Quad(new Vector3(.9375, .5, .0625), new Vector3(xMin, .5, .0625), - new Vector3(.9375, .5, .9375), new Vector4(.9375, xMin, .9375, .0625)), + // top + new Quad(new Vector3(.9375, .5, .0625), new Vector3(xMin, .5, .0625), + new Vector3(.9375, .5, .9375), new Vector4(.9375, xMin, .9375, .0625)), - // bottom - new Quad(new Vector3(xMin, 0, .0625), new Vector3(.9375, 0, .0625), - new Vector3(xMin, 0, .9375), new Vector4(xMin, .9375, .0625, .9375)) + // bottom + new Quad(new Vector3(xMin, 0, .0625), new Vector3(.9375, 0, .0625), + new Vector3(xMin, 0, .9375), new Vector4(xMin, .9375, .0625, .9375)) }; } } @@ -62,22 +62,23 @@ public class CakeModel extends QuadModel { private final Texture[] textures; public CakeModel(int bites) { - this.quads = cake[bites]; + refractive = true; - Texture top = Texture.cakeTop; - Texture side = Texture.cakeSide; - Texture bottom = Texture.cakeBottom; - Texture inside = Texture.cakeInside; - textures = new Texture[]{side, side, bites == 0 ? side : inside, side, top, bottom}; + this.quads = cake[bites]; + Texture top = Texture.cakeTop; + Texture side = Texture.cakeSide; + Texture bottom = Texture.cakeBottom; + Texture inside = Texture.cakeInside; + textures = new Texture[]{side, side, bites == 0 ? side : inside, side, top, bottom}; } @Override public Quad[] getQuads() { - return quads; + return quads; } @Override public Texture[] getTextures() { - return textures; + return textures; } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java index 485e42fe2f..13b497d73d 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java @@ -17,11 +17,14 @@ */ package se.llbit.chunky.model.minecraft; +import se.llbit.chunky.block.MinecraftBlock; +import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.minecraft.Lava; import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.renderer.scene.StillWaterShader; import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -335,17 +338,20 @@ public class CauldronModel { new Vector3(2 / 16.0, 9 / 16.0, 14 / 16.0), new Vector3(14 / 16.0, 9 / 16.0, 14 / 16.0), new Vector3(2 / 16.0, 9 / 16.0, 2 / 16.0), - new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0)), + new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0), + true), new Quad( new Vector3(2 / 16.0, 12 / 16.0, 14 / 16.0), new Vector3(14 / 16.0, 12 / 16.0, 14 / 16.0), new Vector3(2 / 16.0, 12 / 16.0, 2 / 16.0), - new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0)), + new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0), + true), new Quad( new Vector3(2 / 16.0, 15 / 16.0, 14 / 16.0), new Vector3(14 / 16.0, 15 / 16.0, 14 / 16.0), new Vector3(2 / 16.0, 15 / 16.0, 2 / 16.0), - new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0)) + new Vector4(2 / 16.0, 14 / 16.0, 1 - 14 / 16.0, 1 - 2 / 16.0), + true) }; private static final Texture top = Texture.cauldronTop; @@ -361,111 +367,105 @@ public class CauldronModel { side, side }; - public static boolean intersect(Ray ray, int level, Texture contentTexture) { + public static boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene, int level, Texture contentTexture, String materialName) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - ray.setNormal(quad.n); + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] color = tex[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (color[3] > Constants.EPSILON) { + intersectionRecord.color.set(color); + intersectionRecord.setNormal(quad.n); hit = true; } } } Quad water = waterLevels[level]; - if (water != null && water.intersect(ray)) { - float[] color = contentTexture.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - ray.setNormal(water.n); - hit = true; + if (water != null && water.closestIntersection(ray, intersectionRecord)) { + hit = true; + intersectionRecord.setNormal(water.n); + if (ray.d.dot(water.n) > 0) { + intersectionRecord.material = scene.waterPlaneMaterial(ray.o.rScaleAdd(intersectionRecord.distance, ray.d)); + intersectionRecord.material.getColor(intersectionRecord); + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + } else { + contentTexture.getColor(intersectionRecord); + MinecraftBlock mat = new MinecraftBlock(materialName, Texture.air); + scene.getPalette().applyMaterial(mat); + intersectionRecord.material = mat; } } - - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } return hit; } - public static boolean intersectWithWater(Ray ray, Scene scene, int level) { + public static boolean intersectWithWater(Ray ray, IntersectionRecord intersectionRecord, Scene scene, int level) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - ray.setNormal(quad.n); + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] color = tex[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (color[3] > Constants.EPSILON) { + intersectionRecord.color.set(color); + intersectionRecord.setNormal(quad.n); hit = true; } } } - // TODO since this water is the same block, refraction is not taken into account – still better than no water Quad water = waterLevels[level]; - if (water != null && water.intersect(ray)) { - if (!(scene.getCurrentWaterShader() instanceof StillWaterShader)) { - scene.getCurrentWaterShader().doWaterShading(ray, scene.getAnimationTime()); + if (water != null && water.closestIntersection(ray, intersectionRecord)) { + hit = true; + intersectionRecord.setNormal(water.n); + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + if (ray.d.dot(water.n) > 0) { + intersectionRecord.material = scene.waterPlaneMaterial(testRay.o); + intersectionRecord.material.getColor(intersectionRecord); + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); } else { - ray.setNormal(water.n); + intersectionRecord.material = scene.getPalette().water; + Water.INSTANCE.getColor(intersectionRecord); } - ray.setPrevMaterial(ray.getCurrentMaterial(), ray.getCurrentData()); - ray.setCurrentMaterial(Water.INSTANCE); - ray.t = ray.tNext; - } - - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); } return hit; } - public static boolean intersectWithLava(Ray ray) { + public static boolean intersectWithLava(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - ray.setNormal(quad.n); + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] color = tex[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (color[3] > Constants.EPSILON) { + intersectionRecord.color.set(color); + intersectionRecord.setNormal(quad.n); hit = true; } } } Quad lava = waterLevels[3]; - if (lava.intersect(ray)) { - float[] color = Texture.lava.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - ray.setNormal(lava.n); - hit = true; - + if (lava.closestIntersection(ray, intersectionRecord)) { + hit = true; + intersectionRecord.setNormal(lava.n); + if (ray.d.dot(lava.n) > 0) { + intersectionRecord.material = scene.waterPlaneMaterial(ray.o.rScaleAdd(intersectionRecord.distance, ray.d)); + intersectionRecord.material.getColor(intersectionRecord); + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + } else { + Texture.lava.getColor(intersectionRecord); // set the current material to lava so that only the lava is emissive and not the cauldron - ray.setPrevMaterial(ray.getCurrentMaterial(), ray.getCurrentData()); - ray.setCurrentMaterial(new Lava(7)); + MinecraftBlock lavaMat = new Lava(7); + scene.getPalette().applyMaterial(lavaMat); + intersectionRecord.material = lavaMat; } } - - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } return hit; } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/ComparatorModel1212.java b/chunky/src/java/se/llbit/chunky/model/minecraft/ComparatorModel1212.java index 43495f4f5b..a0b71b0ef4 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/ComparatorModel1212.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/ComparatorModel1212.java @@ -21,6 +21,7 @@ import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -814,7 +815,7 @@ public Texture[] getTextures() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return RedstoneTorchModel.intersectWithGlow(ray, scene, this); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return RedstoneTorchModel.intersectWithGlow(ray, intersectionRecord, this); } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/ConduitModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/ConduitModel.java index 6f0296be17..5f0654b68a 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/ConduitModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/ConduitModel.java @@ -69,6 +69,10 @@ public class ConduitModel extends QuadModel { Texture.conduit, Texture.conduit, Texture.conduit }; + public ConduitModel() { + refractive = true; + } + @Override public Quad[] getQuads() { return quads; diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/CopperGrateModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/CopperGrateModel.java new file mode 100644 index 0000000000..33e271cac2 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/CopperGrateModel.java @@ -0,0 +1,25 @@ +package se.llbit.chunky.model.minecraft; + +import java.util.Arrays; +import se.llbit.chunky.model.QuadModel; +import se.llbit.chunky.resources.Texture; +import se.llbit.math.Quad; + +public class CopperGrateModel extends QuadModel { + private final Texture[] textures; + + public CopperGrateModel(Texture texture) { + textures = new Texture[6]; + Arrays.fill(textures, texture); + } + + @Override + public Quad[] getQuads() { + return FULL_BLOCK_QUADS; + } + + @Override + public Texture[] getTextures() { + return textures; + } +} diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/DecoratedPotModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/DecoratedPotModel.java index 63bcfadc69..917536fe78 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/DecoratedPotModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/DecoratedPotModel.java @@ -245,14 +245,4 @@ private static Texture getTextureForsherd(String sherd) { return Texture.decoratedPotSide; } } - - @Override - public Quad[] getQuads() { - return quads; - } - - @Override - public Texture[] getTextures() { - return textures; - } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/EmissiveSpriteModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/EmissiveSpriteModel.java index 03ca20116d..e2172c9e54 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/EmissiveSpriteModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/EmissiveSpriteModel.java @@ -6,8 +6,11 @@ import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.material.TextureMaterial; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; +import se.llbit.math.Vector3; public class EmissiveSpriteModel extends QuadModel { private static final Quad[] quads; @@ -40,9 +43,8 @@ public Texture[] getTextures() { } @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; Quad[] quads = getQuads(); Texture[] textures = getTextures(); @@ -52,29 +54,31 @@ public boolean intersect(Ray ray, Scene scene) { Tint tint = Tint.NONE; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] c = textures[i].getColor(ray.u, ray.v); - if (c[3] > Ray.EPSILON) { + double distance = intersectionRecord.distance; + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (c[3] > Constants.EPSILON) { tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; color = c; - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); + if (quad.doubleSided) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, quad.n)); + } else { + intersectionRecord.setNormal(quad.n); + } + intersectionRecord.setNoMediumChange(true); hit = true; if (i < quads.length / 2) { - ray.setCurrentMaterial(emissiveMaterial); + intersectionRecord.material = emissiveMaterial; } + } else { + intersectionRecord.distance = distance; } } } if (hit) { - ray.color.set(color); - tint.tint(ray.color, ray, scene); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + intersectionRecord.color.set(color); + tint.tint(intersectionRecord.color, ray, scene); } return hit; } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/FlowerPotModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/FlowerPotModel.java index 771d0bb912..fba103d242 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/FlowerPotModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/FlowerPotModel.java @@ -24,6 +24,8 @@ import se.llbit.chunky.model.Tint; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -661,9 +663,8 @@ public static BlockModel forKind(Kind kind) { if (kind == Kind.OPEN_EYEBLOSSOM) { return new FlowerPotModel(Kind.OPEN_EYEBLOSSOM) { @Override - public boolean intersect(Ray ray, Scene scene) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; Quad[] quads = getQuads(); Texture[] textures = getTextures(); @@ -673,29 +674,32 @@ public boolean intersect(Ray ray, Scene scene) { Tint tint = Tint.NONE; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] c = textures[i].getColor(ray.u, ray.v); - if (c[3] > Ray.EPSILON) { + double distance = intersectionRecord.distance; + if (quad.closestIntersection(ray, intersectionRecord)) { + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (c[3] > Constants.EPSILON) { tint = tintedQuads == null ? Tint.NONE : tintedQuads[i]; color = c; - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); + if (quad.doubleSided) { + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, quad.n)); + } + else { + intersectionRecord.setNormal(quad.n); + } + intersectionRecord.setNoMediumChange(true); hit = true; if (textures[i] == Texture.openEyeblossomEmissive) { - ray.setCurrentMaterial(OpenEyeblossom.emissiveMaterial); + intersectionRecord.material = OpenEyeblossom.emissiveMaterial; } + } else { + intersectionRecord.distance = distance; } } } if (hit) { - ray.color.set(color); - tint.tint(ray.color, ray, scene); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + intersectionRecord.color.set(color); + tint.tint(intersectionRecord.color, ray, scene); } return hit; } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/GlassPaneModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/GlassPaneModel.java index 70fb1ad887..0fcbd181b7 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/GlassPaneModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/GlassPaneModel.java @@ -34,7 +34,8 @@ public class GlassPaneModel extends QuadModel { new Vector3(7 / 16.0, 16 / 16.0, 9 / 16.0), new Vector3(9 / 16.0, 16 / 16.0, 9 / 16.0), new Vector3(7 / 16.0, 16 / 16.0, 7 / 16.0), - new Vector4(7 / 16.0, 9 / 16.0, 7 / 16.0, 9 / 16.0) + new Vector4(7 / 16.0, 9 / 16.0, 7 / 16.0, 9 / 16.0), + true ), // Bottom @@ -42,7 +43,8 @@ public class GlassPaneModel extends QuadModel { new Vector3(7 / 16.0, 0 / 16.0, 7 / 16.0), new Vector3(9 / 16.0, 0 / 16.0, 7 / 16.0), new Vector3(7 / 16.0, 0 / 16.0, 9 / 16.0), - new Vector4(7 / 16.0, 9 / 16.0, 7 / 16.0, 9 / 16.0) + new Vector4(7 / 16.0, 9 / 16.0, 7 / 16.0, 9 / 16.0), + true ), // North @@ -50,7 +52,8 @@ public class GlassPaneModel extends QuadModel { new Vector3(7 / 16.0, 16 / 16.0, 7 / 16.0), new Vector3(9 / 16.0, 16 / 16.0, 7 / 16.0), new Vector3(7 / 16.0, 0 / 16.0, 7 / 16.0), - new Vector4(7 / 16.0, 9 / 16.0, 16 / 16.0, 0 / 16.0) + new Vector4(7 / 16.0, 9 / 16.0, 16 / 16.0, 0 / 16.0), + true ), null, // East null, // South @@ -67,39 +70,87 @@ public class GlassPaneModel extends QuadModel { // Front side. { // Left face. - new Quad(new Vector3(7 / 16., 1, 7 / 16.), new Vector3(7 / 16., 1, 0), - new Vector3(7 / 16., 0, 7 / 16.), new Vector4(7 / 16., 0, 1, 0)), + new Quad( + new Vector3(7 / 16., 1, 7 / 16.), + new Vector3(7 / 16., 1, 0), + new Vector3(7 / 16., 0, 7 / 16.), + new Vector4(7 / 16., 0, 1, 0) + ), // Right face. - new Quad(new Vector3(9 / 16., 1, 0), new Vector3(9 / 16., 1, 7 / 16.), - new Vector3(9 / 16., 0, 0), new Vector4(0, 7 / 16., 1, 0)), + new Quad( + new Vector3(9 / 16., 1, 0), + new Vector3(9 / 16., 1, 7 / 16.), + new Vector3(9 / 16., 0, 0), + new Vector4(0, 7 / 16., 1, 0) + ), // Top face. - new Quad(new Vector3(9 / 16., 1, 0), new Vector3(7 / 16., 1, 0), - new Vector3(9 / 16., 1, 7 / 16.), new Vector4(9 / 16., 7 / 16., 0, 7 / 16.)), + new Quad( + new Vector3(9 / 16., 1, 0), + new Vector3(7 / 16., 1, 0), + new Vector3(9 / 16., 1, 7 / 16.), + new Vector4(9 / 16., 7 / 16., 0, 7 / 16.) + ), // Bottom face. - new Quad(new Vector3(7 / 16., 0, 0), new Vector3(9 / 16., 0, 0), - new Vector3(7 / 16., 0, 7 / 16.), new Vector4(7 / 16., 9 / 16., 0, 7 / 16.)), + new Quad( + new Vector3(7 / 16., 0, 0), + new Vector3(9 / 16., 0, 0), + new Vector3(7 / 16., 0, 7 / 16.), + new Vector4(7 / 16., 9 / 16., 0, 7 / 16.) + ), + + // Outside face. + new Quad( + new Vector3(9 / 16., 1, 0), + new Vector3(7 / 16., 1, 0), + new Vector3(9 / 16., 0, 0), + new Vector4(9 / 16., 1, 7 / 16., 0) + ) }, // Back side. { // Left face. - new Quad(new Vector3(7 / 16., 1, 1), new Vector3(7 / 16., 1, 9 / 16.), - new Vector3(7 / 16., 0, 1), new Vector4(1, 9 / 16., 1, 0)), + new Quad( + new Vector3(7 / 16., 1, 1), + new Vector3(7 / 16., 1, 9 / 16.), + new Vector3(7 / 16., 0, 1), + new Vector4(1, 9 / 16., 1, 0) + ), // Right face. - new Quad(new Vector3(9 / 16., 1, 9 / 16.), new Vector3(9 / 16., 1, 1), - new Vector3(9 / 16., 0, 9 / 16.), new Vector4(9 / 16., 1, 1, 0)), + new Quad( + new Vector3(9 / 16., 1, 9 / 16.), + new Vector3(9 / 16., 1, 1), + new Vector3(9 / 16., 0, 9 / 16.), + new Vector4(9 / 16., 1, 1, 0) + ), // Top face. - new Quad(new Vector3(9 / 16., 1, 9 / 16.), new Vector3(7 / 16., 1, 9 / 16.), - new Vector3(9 / 16., 1, 1), new Vector4(9 / 16., 7 / 16., 9 / 16., 1)), + new Quad( + new Vector3(9 / 16., 1, 9 / 16.), + new Vector3(7 / 16., 1, 9 / 16.), + new Vector3(9 / 16., 1, 1), + new Vector4(9 / 16., 7 / 16., 9 / 16., 1) + ), // Bottom face. - new Quad(new Vector3(7 / 16., 0, 9 / 16.), new Vector3(9 / 16., 0, 9 / 16.), - new Vector3(7 / 16., 0, 1), new Vector4(7 / 16., 9 / 16., 9 / 16., 1)), + new Quad( + new Vector3(7 / 16., 0, 9 / 16.), + new Vector3(9 / 16., 0, 9 / 16.), + new Vector3(7 / 16., 0, 1), + new Vector4(7 / 16., 9 / 16., 9 / 16., 1) + ), + + // Outside face. + new Quad( + new Vector3(7 / 16., 1, 1), + new Vector3(9 / 16., 1, 1), + new Vector3(7 / 16., 0, 1), + new Vector4(7 / 16., 0, 9 / 16., 1) + ) }, }; @@ -123,7 +174,7 @@ public GlassPaneModel(Texture top, Texture side, boolean north, boolean south, b Consumer addConnector = qs -> { quads.addAll(Arrays.asList(qs)); - textures.addAll(Arrays.asList(side, side, top, top)); + textures.addAll(Arrays.asList(side, side, top, top, Texture.air)); }; // Top and bottom diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/GrassBlockModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/GrassBlockModel.java index 22872581bc..e8c5632f45 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/GrassBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/GrassBlockModel.java @@ -23,14 +23,15 @@ import se.llbit.chunky.model.AABBModel; import se.llbit.chunky.model.Tint; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; -import se.llbit.math.AABB; +import se.llbit.math.*; public class GrassBlockModel extends AABBModel { private final static Tint[][] tints = new Tint[][] { - {BIOME_GRASS, BIOME_GRASS, BIOME_GRASS, BIOME_GRASS, NONE, NONE}, - {NONE, NONE, NONE, NONE, BIOME_GRASS, NONE} + {NONE, NONE, NONE, NONE, BIOME_GRASS, NONE}, + {BIOME_GRASS, BIOME_GRASS, BIOME_GRASS, BIOME_GRASS, NONE, NONE} }; private final static AABB[] boxes = new AABB[]{ @@ -39,15 +40,15 @@ public class GrassBlockModel extends AABBModel { }; private static final Texture[][] textures = new Texture[][]{ - { - Texture.grassSide, Texture.grassSide, - Texture.grassSide, Texture.grassSide, - null, null - }, { Texture.grassSideSaturated, Texture.grassSideSaturated, Texture.grassSideSaturated, Texture.grassSideSaturated, Texture.grassTop, Texture.dirt + }, + { + Texture.grassSide, Texture.grassSide, + Texture.grassSide, Texture.grassSide, + null, null } }; @@ -65,4 +66,47 @@ public Texture[][] getTextures() { public Tint[][] getTints() { return tints; } + + @Override + public boolean intersectFace(IntersectionRecord intersectionRecord, Scene scene, Texture texture, UVMapping mapping) { + // This is the method that handles intersecting faces of all AABB-based models. + // Do normal mapping, parallax occlusion mapping, specular maps and all the good stuff here! + + if (texture == null) { + return false; + } + + double tmp; + if (mapping != null) { + switch (mapping) { + case ROTATE_90: + tmp = intersectionRecord.uv.x; + intersectionRecord.uv.x = 1 - intersectionRecord.uv.y; + intersectionRecord.uv.y = tmp; + break; + case ROTATE_180: + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.y; + break; + case ROTATE_270: + tmp = intersectionRecord.uv.y; + intersectionRecord.uv.y = 1 - intersectionRecord.uv.x; + intersectionRecord.uv.x = tmp; + break; + case FLIP_U: + intersectionRecord.uv.x = 1 - intersectionRecord.uv.x; + break; + case FLIP_V: + intersectionRecord.uv.y = 1 - intersectionRecord.uv.y; + break; + } + } + + float[] color = texture.getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (color[3] > Constants.EPSILON) { + intersectionRecord.color.set(color); + return true; + } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/HoneyBlockModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/HoneyBlockModel.java index a3b647d1c6..f2cc9b27fc 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/HoneyBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/HoneyBlockModel.java @@ -18,86 +18,99 @@ package se.llbit.chunky.model.minecraft; +import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.resources.Texture; import se.llbit.math.*; -public class HoneyBlockModel { +public class HoneyBlockModel extends QuadModel { private static final Quad[] quads = { - new Quad( - new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0, 1, 0, 1) - ), - new Quad( - new Vector3(15 / 16.0, 15 / 16.0, 1 / 16.0), - new Vector3(1 / 16.0, 15 / 16.0, 1 / 16.0), - new Vector3(15 / 16.0, 15 / 16.0, 15 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), - new Quad( - new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), - new Quad( - new Vector3(15 / 16.0, 1 / 16.0, 15 / 16.0), - new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(15 / 16.0, 15 / 16.0, 15 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), - new Quad( - new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), - new Vector3(1 / 16.0, 15 / 16.0, 1 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), - new Quad( - new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), - new Vector3(15 / 16.0, 15 / 16.0, 1 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), - new Quad( - new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), - new Vector3(15 / 16.0, 1 / 16.0, 15 / 16.0), - new Vector3(1 / 16.0, 15 / 16.0, 15 / 16.0), - new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0) - ), + new Quad( + new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0, 1, 0, 1), + true + ), + new Quad( + new Vector3(15 / 16.0, 15 / 16.0, 1 / 16.0), + new Vector3(1 / 16.0, 15 / 16.0, 1 / 16.0), + new Vector3(15 / 16.0, 15 / 16.0, 15 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), + new Quad( + new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), + new Quad( + new Vector3(15 / 16.0, 1 / 16.0, 15 / 16.0), + new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(15 / 16.0, 15 / 16.0, 15 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), + new Quad( + new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), + new Vector3(1 / 16.0, 15 / 16.0, 1 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), + new Quad( + new Vector3(15 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(1 / 16.0, 1 / 16.0, 1 / 16.0), + new Vector3(15 / 16.0, 15 / 16.0, 1 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), + new Quad( + new Vector3(1 / 16.0, 1 / 16.0, 15 / 16.0), + new Vector3(15 / 16.0, 1 / 16.0, 15 / 16.0), + new Vector3(1 / 16.0, 15 / 16.0, 15 / 16.0), + new Vector4(1 / 16.0, 15 / 16.0, 1 / 16.0, 15 / 16.0), + true + ), }; - private static final Texture[] tex = { + private static final Texture[] textures = { Texture.honeyBlockBottom, Texture.honeyBlockBottom, Texture.honeyBlockBottom, Texture.honeyBlockBottom, Texture.honeyBlockBottom, Texture.honeyBlockBottom, @@ -105,47 +118,22 @@ public class HoneyBlockModel { Texture.honeyBlockSide, Texture.honeyBlockSide, Texture.honeyBlockSide }; - public static boolean intersect(Ray ray) { - ray.t = Double.POSITIVE_INFINITY; - boolean hit = false; - Vector4 oldColor = new Vector4(ray.color); - for (int i = 6; i < quads.length; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ColorUtil.overlayColor(ray.color, color); - ray.setNormal(quad.n); - ray.t = ray.tNext; - hit = true; - } - } - } - boolean innerHit = hit; - Vector4 innerColor = hit ? new Vector4(ray.color) : null; + public HoneyBlockModel() { + refractive = true; + } - ray.color.set(oldColor); - hit = false; + @Override + public Quad[] getQuads() { + return quads; + } - for (int i = 0; i < 6; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ColorUtil.overlayColor(ray.color, color); - ray.setNormal(quad.n); - ray.t = ray.tNext; - hit = true; - } - } - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - if (innerHit) { - ColorUtil.overlayColor(ray.color, innerColor); - } - } - return hit; - } + @Override + public Texture[] getTextures() { + return textures; + } + + @Override + public boolean isInside(Ray ray) { + return true; + } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/JigsawModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/JigsawModel.java index cfc46cdf5d..c8e25904dc 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/JigsawModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/JigsawModel.java @@ -100,6 +100,7 @@ public class JigsawModel extends QuadModel { private final Texture[] textures; public JigsawModel(String orientation) { + refractive = true; switch (orientation) { case "up": textures = textureNoLock; diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/LeafModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/LeafModel.java index ac7e9f82bb..bcb481eaa3 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/LeafModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/LeafModel.java @@ -19,8 +19,11 @@ import se.llbit.chunky.model.AABBModel; import se.llbit.chunky.model.Tint; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.math.AABB; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; public class LeafModel extends AABBModel { private static final AABB[] boxes = { new AABB(0, 1, 0, 1, 0, 1) }; @@ -29,18 +32,27 @@ public class LeafModel extends AABBModel { private final Tint[][] tints; public LeafModel(Texture texture) { + this.textures = new Texture[][] { + {texture, texture, texture, texture, texture, texture} + }; + this.tints = new Tint[][] {{ + Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, + Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE + }}; + } + + protected LeafModel(Texture texture, Tint tint) { this.textures = new Texture[][] { {texture, texture, texture, texture, texture, texture} }; this.tints = new Tint[][] {{ - Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, - Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE, Tint.BIOME_FOLIAGE + tint, tint, tint, tint, tint, tint }}; } public LeafModel(Texture texture, int tint) { this.textures = new Texture[][] { - {texture, texture, texture, texture, texture, texture} + {texture, texture, texture, texture, texture, texture} }; Tint t = new Tint(tint); this.tints = new Tint[][] {{t, t, t, t, t, t}}; @@ -60,4 +72,16 @@ public Texture[][] getTextures() { public Tint[][] getTints() { return tints; } + + @Override + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (super.intersect(ray, intersectionRecord, scene)) { + if (ray.d.dot(intersectionRecord.n) > 0) { + return false; + } + intersectionRecord.setNoMediumChange(true); + return true; + } + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/LightBlockModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/LightBlockModel.java index 4fbfad8d9b..91eb6d89a0 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/LightBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/LightBlockModel.java @@ -20,10 +20,11 @@ import se.llbit.chunky.model.AABBModel; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.SolidColorTexture; import se.llbit.chunky.resources.Texture; import se.llbit.math.AABB; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; -import se.llbit.math.Vector4; public class LightBlockModel extends AABBModel { public static final AABB[] aabb = { new AABB(0.125, 0.875, 0.125, 0.875, 0.125, 0.875) }; @@ -35,28 +36,20 @@ public class LightBlockModel extends AABBModel { Texture.light, Texture.light, Texture.light }}; - private final Vector4 color; - - public LightBlockModel(Vector4 color) { - this.color = color; - } - @Override public AABB[] getBoxes() { return box; } @Override - public boolean intersect(Ray ray, Scene scene) { - boolean hit = false; - AABB[] boxes = getBoxes(); - ray.t = Double.POSITIVE_INFINITY; - if (boxes[0].intersect(ray)) { - ray.color.set(color); - hit = true; - ray.t = ray.tNext; + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + if (ray.isIndirect()) { + if (getBoxes()[0].closestIntersection(ray, intersectionRecord)) { + SolidColorTexture.EMPTY.getColor(intersectionRecord); + return true; + } } - return hit; + return false; } @Override diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/LogModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/LogModel.java index ab5bad7eed..8d7785de73 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/LogModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/LogModel.java @@ -56,6 +56,7 @@ public class LogModel extends QuadModel { private final Texture[] textures; public LogModel(String facing, Texture side, Texture top) { + refractive = true; switch (facing) { case "x": quads = Model.rotateZ(sides); diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/ObserverModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/ObserverModel.java index c6a001a472..918cbf445a 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/ObserverModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/ObserverModel.java @@ -91,6 +91,7 @@ public class ObserverModel extends QuadModel { private final Texture[] textures; public ObserverModel(int facing, boolean powered) { + refractive = true; quads = faces[facing]; textures = powered ? texturesOn : texturesOff; } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/PressurePlateModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/PressurePlateModel.java index e2edbcae90..2abda0a4bb 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/PressurePlateModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/PressurePlateModel.java @@ -57,6 +57,7 @@ public class PressurePlateModel extends QuadModel { private final Texture[] textures = new Texture[quads.length]; public PressurePlateModel(Texture texture) { + refractive = true; Arrays.fill(textures, texture); } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneRepeaterModel1212.java b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneRepeaterModel1212.java index faf31514c4..57eb19e387 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneRepeaterModel1212.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneRepeaterModel1212.java @@ -21,6 +21,7 @@ import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -262,7 +263,7 @@ public Texture[] getTextures() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return RedstoneTorchModel.intersectWithGlow(ray, scene, this); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return RedstoneTorchModel.intersectWithGlow(ray, intersectionRecord, this); } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneTorchModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneTorchModel.java index a4cbe01bcc..cd860ba4bc 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneTorchModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneTorchModel.java @@ -101,14 +101,13 @@ public Texture[] getTextures() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return intersectWithGlow(ray, scene, this); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return intersectWithGlow(ray, intersectionRecord, this); } - static boolean intersectWithGlow(Ray ray, Scene scene, QuadModel model) { + static boolean intersectWithGlow(Ray ray, IntersectionRecord intersectionRecord, QuadModel model) { boolean hit = false; Quad lastFrontHitQuad = null; - ray.t = Double.POSITIVE_INFINITY; Quad[] quads = model.getQuads(); Texture[] textures = model.getTextures(); @@ -117,7 +116,8 @@ static boolean intersectWithGlow(Ray ray, Scene scene, QuadModel model) { int hitCount = 0; for (int i = 0; i < quads.length; ++i) { Quad quad = quads[i]; - if (quad.intersect(ray)) { + double distance = intersectionRecord.distance; + if (quad.closestIntersection(ray, intersectionRecord)) { if (quad instanceof GlowQuad) { // hitCount++; if (ray.d.dot(quad.n) < 0) { @@ -126,31 +126,21 @@ static boolean intersectWithGlow(Ray ray, Scene scene, QuadModel model) { hitCount--; } } - float[] c = textures[i].getColor(ray.u, ray.v); - if (c[3] > Ray.EPSILON) { - if (ray.d.dot(quad.n) < 0) { - color = c; - ray.t = ray.tNext; - ray.setNormal(quad.n); - hit = true; - lastFrontHitQuad = quad; - } + float[] c = textures[i].getColor(intersectionRecord.uv.x, intersectionRecord.uv.y); + if (c[3] > Constants.EPSILON && ray.d.dot(quad.n) < 0) { + color = c; + intersectionRecord.setNormal(quad.n); + intersectionRecord.setNoMediumChange(true); + hit = true; + lastFrontHitQuad = quad; + } else { + intersectionRecord.distance = distance; } } } if (hit && (hitCount % 2 == 0 || !(lastFrontHitQuad instanceof GlowQuad))) { - double px = ray.o.x - Math.floor(ray.o.x + ray.d.x * Ray.OFFSET) + ray.d.x * ray.tNext; - double py = ray.o.y - Math.floor(ray.o.y + ray.d.y * Ray.OFFSET) + ray.d.y * ray.tNext; - double pz = ray.o.z - Math.floor(ray.o.z + ray.d.z * Ray.OFFSET) + ray.d.z * ray.tNext; - if (px < E0 || px > E1 || py < E0 || py > E1 || pz < E0 || pz > E1) { - // TODO this check is only really needed for wall torches - return false; - } - - ray.color.set(color); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + intersectionRecord.color.set(color); return true; } return false; diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneWallTorchModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneWallTorchModel.java index de65716d4c..eb83577331 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneWallTorchModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/RedstoneWallTorchModel.java @@ -4,6 +4,7 @@ import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Quad; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -124,7 +125,7 @@ public Texture[] getTextures() { } @Override - public boolean intersect(Ray ray, Scene scene) { - return RedstoneTorchModel.intersectWithGlow(ray, scene, this); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return RedstoneTorchModel.intersectWithGlow(ray, intersectionRecord, this); } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/SlimeBlockModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/SlimeBlockModel.java index d7873dd7c0..1d55ec2605 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/SlimeBlockModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/SlimeBlockModel.java @@ -18,132 +18,120 @@ package se.llbit.chunky.model.minecraft; +import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.resources.Texture; import se.llbit.math.*; -public class SlimeBlockModel { +public class SlimeBlockModel extends QuadModel { private static final Quad[] quads = { - new Quad( - new Vector3(13 / 16.0, 13 / 16.0, 3 / 16.0), - new Vector3(3 / 16.0, 13 / 16.0, 3 / 16.0), - new Vector3(13 / 16.0, 13 / 16.0, 13 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(13 / 16.0, 3 / 16.0, 13 / 16.0), - new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(13 / 16.0, 13 / 16.0, 13 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), - new Vector3(3 / 16.0, 13 / 16.0, 3 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), - new Vector3(13 / 16.0, 13 / 16.0, 3 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), - new Vector3(13 / 16.0, 3 / 16.0, 13 / 16.0), - new Vector3(3 / 16.0, 13 / 16.0, 13 / 16.0), - new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0) - ), - new Quad( - new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), - new Quad( - new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), - new Quad( - new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), - new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), - new Quad( - new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), - new Vector3(0 / 16.0, 16 / 16.0, 16 / 16.0), - new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0) - ), + new Quad( + new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(16 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(0 / 16.0, 0 / 16.0, 0 / 16.0), + new Vector3(16 / 16.0, 16 / 16.0, 0 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(0 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(16 / 16.0, 0 / 16.0, 16 / 16.0), + new Vector3(0 / 16.0, 16 / 16.0, 16 / 16.0), + new Vector4(0 / 16.0, 16 / 16.0, 0 / 16.0, 16 / 16.0), + true + ), + new Quad( + new Vector3(13 / 16.0, 13 / 16.0, 3 / 16.0), + new Vector3(3 / 16.0, 13 / 16.0, 3 / 16.0), + new Vector3(13 / 16.0, 13 / 16.0, 13 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ), + new Quad( + new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ), + new Quad( + new Vector3(13 / 16.0, 3 / 16.0, 13 / 16.0), + new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(13 / 16.0, 13 / 16.0, 13 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ), + new Quad( + new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), + new Vector3(3 / 16.0, 13 / 16.0, 3 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ), + new Quad( + new Vector3(13 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(3 / 16.0, 3 / 16.0, 3 / 16.0), + new Vector3(13 / 16.0, 13 / 16.0, 3 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ), + new Quad( + new Vector3(3 / 16.0, 3 / 16.0, 13 / 16.0), + new Vector3(13 / 16.0, 3 / 16.0, 13 / 16.0), + new Vector3(3 / 16.0, 13 / 16.0, 13 / 16.0), + new Vector4(3 / 16.0, 13 / 16.0, 3 / 16.0, 13 / 16.0), + true + ) }; - private static final Texture[] tex = { - Texture.slime, Texture.slime, Texture.slime, Texture.slime, Texture.slime, Texture.slime, - - Texture.slime, Texture.slime, Texture.slime, Texture.slime, Texture.slime, Texture.slime + private static final Texture[] textures = new Texture[] { + Texture.slime, Texture.slime, Texture.slime, Texture.slime, + Texture.slime, Texture.slime, Texture.slime, Texture.slime, + Texture.slime, Texture.slime, Texture.slime, Texture.slime }; - public static boolean intersect(Ray ray) { - ray.t = Double.POSITIVE_INFINITY; - boolean hit = false; - Vector4 oldColor = new Vector4(ray.color); - for (int i = 0; i < 6; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ColorUtil.overlayColor(ray.color, color); - ray.setNormal(quad.n); - ray.t = ray.tNext; - hit = true; - } - } - } - boolean innerHit = hit; - Vector4 innerColor = hit ? new Vector4(ray.color) : null; + public SlimeBlockModel() { + refractive = true; + } - ray.color.set(oldColor); - hit = false; + @Override + public Quad[] getQuads() { + return quads; + } + + @Override + public Texture[] getTextures() { + return textures; + } - for (int i = 6; i < quads.length; ++i) { - Quad quad = quads[i]; - if (quad.intersect(ray)) { - float[] color = tex[i].getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ColorUtil.overlayColor(ray.color, color); - ray.setNormal(quad.n); - ray.t = ray.tNext; - hit = true; - } - } - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - if (innerHit) { - ColorUtil.overlayColor(ray.color, innerColor); - } - } - return hit; + @Override + public boolean isInside(Ray ray) { + return true; } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/SnifferEggModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/SnifferEggModel.java index 496078ead7..c5670d0bb8 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/SnifferEggModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/SnifferEggModel.java @@ -90,6 +90,7 @@ public class SnifferEggModel extends QuadModel { private final int age; public SnifferEggModel(int age) { + refractive = true; this.age = QuickMath.clamp(age, 0, 2); } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/SpriteModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/SpriteModel.java index 0ad41c0903..dd90e9201e 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/SpriteModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/SpriteModel.java @@ -21,7 +21,6 @@ import se.llbit.chunky.model.QuadModel; import se.llbit.chunky.resources.Texture; import se.llbit.math.Quad; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.math.Vector4; @@ -75,54 +74,6 @@ public Texture[] getTextures() { return textures; } - public static boolean intersect(Ray ray, Texture material) { - boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; - for (Quad quad : quads) { - if (quad.intersect(ray)) { - float[] color = material.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); - hit = true; - } - } - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } - return hit; - } - - public static boolean intersect(Ray ray, Texture material, String facing) { - boolean hit = false; - ray.t = Double.POSITIVE_INFINITY; - for (Quad quad : orientedQuads[getOrientationIndex(facing)]) { - if (quad.intersect(ray)) { - float[] color = material.getColor(ray.u, ray.v); - if (color[3] > Ray.EPSILON) { - ray.color.set(color); - ray.t = ray.tNext; - if (quad.doubleSided) - ray.orientNormal(quad.n); - else - ray.setNormal(quad.n); - hit = true; - } - } - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } - return hit; - } - private static int getOrientationIndex(String facing) { switch (facing) { case "down": diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/TerracottaModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/TerracottaModel.java index abdbe60310..6cfa789b9d 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/TerracottaModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/TerracottaModel.java @@ -78,6 +78,7 @@ public class TerracottaModel extends QuadModel { private final Texture[] textures; public TerracottaModel(Texture texture, int direction) { + refractive = true; quads = faces[direction]; textures = new Texture[quads.length]; Arrays.fill(textures, texture); diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/TurtleEggModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/TurtleEggModel.java index 14b099489d..faf93f0ae6 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/TurtleEggModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/TurtleEggModel.java @@ -192,6 +192,7 @@ public class TurtleEggModel extends QuadModel { private final Texture[] textures; public TurtleEggModel(int eggs, int hatch) { + refractive = true; eggs = Math.max(1, Math.min(egg_models.length, eggs)); hatch = Math.max(0, Math.min(rot.length, hatch)); ArrayList quads = new ArrayList<>(); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java b/chunky/src/java/se/llbit/chunky/model/minecraft/UntintedLeafModel.java similarity index 54% rename from chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java rename to chunky/src/java/se/llbit/chunky/model/minecraft/UntintedLeafModel.java index deff6a0ff5..e619369326 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/UntintedLeafModel.java @@ -1,4 +1,5 @@ -/* Copyright (c) 2012-2021 Chunky contributors +/* + * Copyright (c) 2012-2023 Chunky contributors * * This file is part of Chunky. * @@ -14,28 +15,30 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ -package se.llbit.chunky.renderer.scene; +package se.llbit.chunky.model.minecraft; -import se.llbit.chunky.model.minecraft.WaterModel; -import se.llbit.json.JsonObject; +import se.llbit.chunky.model.AABBModel; +import se.llbit.chunky.model.Tint; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.Texture; +import se.llbit.math.AABB; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; -public class LegacyWaterShader implements WaterShader { - @Override - public void doWaterShading(Ray ray, double animationTime) { - WaterModel.doWaterDisplacement(ray); - } +public class UntintedLeafModel extends LeafModel { + private static final AABB[] boxes = { new AABB(0, 1, 0, 1, 0, 1) }; - @Override - public WaterShader clone() { - return new LegacyWaterShader(); + public UntintedLeafModel(Texture texture) { + super(texture, Tint.NONE); } @Override - public void save(JsonObject json) { + public AABB[] getBoxes() { + return boxes; } @Override - public void load(JsonObject json) { + public Tint[][] getTints() { + return null; } } diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/WaterModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/WaterModel.java index 3d96c8edfd..7019fa89a2 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/WaterModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/WaterModel.java @@ -17,13 +17,11 @@ */ package se.llbit.chunky.model.minecraft; -import se.llbit.chunky.resources.Texture; -import se.llbit.math.Quad; -import se.llbit.math.QuickMath; -import se.llbit.math.Ray; -import se.llbit.math.Triangle; -import se.llbit.math.Vector3; -import se.llbit.math.Vector4; +import se.llbit.chunky.block.Block; +import se.llbit.chunky.block.minecraft.Air; +import se.llbit.chunky.model.QuadModel; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.*; /** * A water block. The height of the top water block is slightly @@ -32,40 +30,29 @@ * @author Jesper Öqvist */ public class WaterModel { + public static final double TOP_BLOCK_GAP = 0.125; + public static final Quad WATER_TOP = new Quad( + new Vector3(1, 1 - TOP_BLOCK_GAP, 0), + new Vector3(0, 1 - TOP_BLOCK_GAP, 0), + new Vector3(1, 1 - TOP_BLOCK_GAP, 1), + new Vector4(1, 0, 1, 0), + true); - private static final Quad[] fullBlock = { - // bottom - new Quad(new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true), - // top - new Quad(new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 1), - new Vector4(0, 1, 0, 1), true), - // west - new Quad(new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true), - // east - new Quad(new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(1, 0, 1), - new Vector4(0, 1, 0, 1), true), - // north - new Quad(new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(0, 0, 0), - new Vector4(0, 1, 0, 0), true), - // south - new Quad(new Vector3(0, 1, 1), new Vector3(1, 1, 1), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true),}; + public static final AABB NOT_FULL_BLOCK = new AABB(0, 1, 0, 1 - TOP_BLOCK_GAP, 0, 1); - static final Quad bot = - new Quad(new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1), - new Vector4(0, 1, 0, 1), true); - static final Triangle[][][] t012 = new Triangle[8][8][8]; - static final Triangle[][][] t230 = new Triangle[8][8][8]; - static final Triangle[][] westt = new Triangle[8][8]; - static final Triangle[] westb = new Triangle[8]; - static final Triangle[][] northt = new Triangle[8][8]; - static final Triangle[] northb = new Triangle[8]; - static final Triangle[][] eastt = new Triangle[8][8]; - static final Triangle[] eastb = new Triangle[8]; - static final Triangle[][] southt = new Triangle[8][8]; - static final Triangle[] southb = new Triangle[8]; + // Top triangles + public static final Triangle[][][] t012 = new Triangle[8][8][8]; + public static final Triangle[][][] t230 = new Triangle[8][8][8]; + + // Side top and bottom triangles + public static final Triangle[][] westt = new Triangle[8][8]; + public static final Triangle[] westb = new Triangle[8]; + public static final Triangle[][] northt = new Triangle[8][8]; + public static final Triangle[] northb = new Triangle[8]; + public static final Triangle[][] eastt = new Triangle[8][8]; + public static final Triangle[] eastb = new Triangle[8]; + public static final Triangle[][] southt = new Triangle[8][8]; + public static final Triangle[] southb = new Triangle[8]; /** * Water height levels @@ -73,31 +60,18 @@ public class WaterModel { static final double[] height = {14 / 16., 12.25 / 16., 10.5 / 16, 8.75 / 16, 7. / 16, 5.25 / 16, 3.5 / 16, 1.75 / 16}; - private static final float[] normalMap; - private static final int normalMapW; + public static final int CORNER_0 = 0; + public static final int CORNER_1 = 4; + public static final int CORNER_2 = 8; + public static final int CORNER_3 = 12; - /** - * Block data offset for water above flag - */ - private static final int FULL_BLOCK = 12; + public static final Quad FULL_BLOCK_BOTTOM_SIDE = new Quad( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(0, 0, 1), + new Vector4(0, 1, 0, 1), true); static { - // precompute normal map - Texture waterHeight = new Texture("water-height"); - normalMapW = waterHeight.getWidth(); - normalMap = new float[normalMapW*normalMapW*2]; - for (int u = 0; u < normalMapW; ++u) { - for (int v = 0; v < normalMapW; ++v) { - - float hx0 = (waterHeight.getColorWrapped(u, v) & 0xFF) / 255.f; - float hx1 = (waterHeight.getColorWrapped(u + 1, v) & 0xFF) / 255.f; - float hz0 = (waterHeight.getColorWrapped(u, v) & 0xFF) / 255.f; - float hz1 = (waterHeight.getColorWrapped(u, v + 1) & 0xFF) / 255.f; - normalMap[(u*normalMapW + v) * 2] = hx1 - hx0; - normalMap[(u*normalMapW + v) * 2 + 1] = hz1 - hz0; - } - } - // precompute water triangles for (int i = 0; i < 8; ++i) { double c0 = height[i]; @@ -173,169 +147,186 @@ public class WaterModel { } } - public static boolean intersect(Ray ray) { - ray.t = Double.POSITIVE_INFINITY; - - int data = ray.getCurrentData(); - int isFull = (data >> FULL_BLOCK) & 1; - //int level = data >> 8; - - if (isFull != 0) { - boolean hit = false; - for (Quad quad : fullBlock) { - if (quad.intersect(ray)) { - Texture.water.getAvgColorLinear(ray.color); - ray.t = ray.tNext; - ray.orientNormal(quad.n); - hit = true; - } - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - } - return hit; - } - + public static boolean quickIntersect(Ray ray, IntersectionRecord intersectionRecord, int data) { boolean hit = false; - if (bot.intersect(ray)) { - ray.orientNormal(bot.n); - ray.t = ray.tNext; - hit = true; - } - int c0 = (0xF & (data >> 16)) % 8; - int c1 = (0xF & (data >> 20)) % 8; - int c2 = (0xF & (data >> 24)) % 8; - int c3 = (0xF & (data >> 28)) % 8; - Triangle triangle = t012[c0][c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + if (FULL_BLOCK_BOTTOM_SIDE.closestIntersection(ray, intersectionRecord)) { + intersectionRecord.setNormal(QuadModel.FULL_BLOCK_BOTTOM_SIDE.n); hit = true; } - triangle = t230[c2][c3][c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; - } - triangle = westt[c0][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + + int c0 = (0xF & (data >> CORNER_0)) % 8; + int c1 = (0xF & (data >> CORNER_1)) % 8; + int c2 = (0xF & (data >> CORNER_2)) % 8; + int c3 = (0xF & (data >> CORNER_3)) % 8; + + Triangle triangle = westt[c0][c3]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = westb[c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = eastt[c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = eastb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = southt[c0][c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = southb[c1]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = northt[c2][c3]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } triangle = northb[c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = t012[c0][c1][c2]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); hit = true; } - if (hit) { - Texture.water.getAvgColorLinear(ray.color); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + triangle = t230[c2][c3][c0]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; } + return hit; } - public static boolean intersectTop(Ray ray) { - ray.t = Double.POSITIVE_INFINITY; + public static boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Scene scene, int data) { + boolean hit = false; + + int c0 = (0xF & (data >> CORNER_0)) % 8; + int c1 = (0xF & (data >> CORNER_1)) % 8; + int c2 = (0xF & (data >> CORNER_2)) % 8; + int c3 = (0xF & (data >> CORNER_3)) % 8; - int data = ray.getCurrentData(); + Vector3 n = new Vector3(intersectionRecord.n); + + if (!ray.getCurrentMedium().isWater()) { + if (QuadModel.FULL_BLOCK_BOTTOM_SIDE.closestIntersection(ray, intersectionRecord)) { + intersectionRecord.setNormal(QuadModel.FULL_BLOCK_BOTTOM_SIDE.n); + hit = true; + } + + Triangle triangle = westt[c0][c3]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = westb[c0]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = eastt[c1][c2]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = eastb[c1]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = southt[c0][c1]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = southb[c1]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = northt[c2][c3]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + triangle = northb[c2]; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hit = true; + } + } + + boolean hitTop = false; - boolean hit = false; - int c0 = (0xF & (data >> 16)) % 8; - int c1 = (0xF & (data >> 20)) % 8; - int c2 = (0xF & (data >> 24)) % 8; - int c3 = (0xF & (data >> 28)) % 8; Triangle triangle = t012[c0][c1][c2]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - hit = true; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hitTop = true; } triangle = t230[c2][c3][c0]; - if (triangle.intersect(ray)) { - ray.orientNormal(triangle.n); - ray.t = ray.tNext; - ray.u = 1 - ray.u; - ray.v = 1 - ray.v; - hit = true; + if (triangle.intersect(ray, intersectionRecord)) { + intersectionRecord.setNormal(triangle.n); + hitTop = true; } - if (hit) { - Texture.water.getAvgColorLinear(ray.color); - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); + + Block waterPlaneMaterial; + if (hitTop) { + if (intersectionRecord.distance > Constants.EPSILON && + ray.getCurrentMedium() != (waterPlaneMaterial = scene.waterPlaneMaterial(ray.o)) && + !ray.getCurrentMedium().isWater()) { + intersectionRecord.distance = 0; + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + intersectionRecord.setNormal(n); + return true; + } + // Create a new ray at the intersection position to get the normal. + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = scene.getCurrentWaterShader().doWaterShading(testRay, intersectionRecord, scene.getAnimationTime()); + intersectionRecord.shadeN.set(shadeNormal); + + if (ray.d.dot(intersectionRecord.n) > 0) { + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + intersectionRecord.material = Air.INSTANCE; + } + + return true; + + } else if (hit) { + return ray.d.dot(intersectionRecord.n) < 0; + } else if (ray.getCurrentMedium() != (waterPlaneMaterial = scene.waterPlaneMaterial(ray.o)) && + !ray.getCurrentMedium().isWater()) { + intersectionRecord.distance = 0; + intersectionRecord.material = waterPlaneMaterial; + waterPlaneMaterial.getColor(intersectionRecord); + return true; + } else { + return false; } - return hit; } - /** - * Displace the normal using the water displacement map. - */ - public static void doWaterDisplacement(Ray ray) { - int w = (1 << 4); - double x = ray.o.x / w - QuickMath.floor(ray.o.x / w); - double z = ray.o.z / w - QuickMath.floor(ray.o.z / w); - int u = (int) (x * normalMapW - Ray.EPSILON); - int v = (int) ((1 - z) * normalMapW - Ray.EPSILON); - Vector3 n = new Vector3(normalMap[(u*normalMapW + v) * 2], .15f, normalMap[(u*normalMapW + v) * 2 + 1]); - w = (1 << 1); - x = ray.o.x / w - QuickMath.floor(ray.o.x / w); - z = ray.o.z / w - QuickMath.floor(ray.o.z / w); - u = (int) (x * normalMapW - Ray.EPSILON); - v = (int) ((1 - z) * normalMapW - Ray.EPSILON); - n.x += normalMap[(u*normalMapW + v) * 2] / 2; - n.z += normalMap[(u*normalMapW + v) * 2 + 1] / 2; - n.normalize(); - ray.setShadingNormal(n.x, n.y, n.z); + public static boolean isInside(Ray ray, IntersectionRecord intersectionRecord, int data) { + if (quickIntersect(ray, intersectionRecord, data)) { + return ray.d.dot(intersectionRecord.n) > 0; + } + return false; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/DefaultRenderManager.java b/chunky/src/java/se/llbit/chunky/renderer/DefaultRenderManager.java index 92aa5507f1..92611d987e 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/DefaultRenderManager.java +++ b/chunky/src/java/se/llbit/chunky/renderer/DefaultRenderManager.java @@ -17,6 +17,7 @@ */ package se.llbit.chunky.renderer; +import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.postprocessing.PixelPostProcessingFilter; @@ -415,52 +416,96 @@ private void updateRenderProgress() { */ protected void finalizeFrame(boolean force) { if (force || snapshotControl.saveSnapshot(bufferedScene, bufferedScene.spp)) { - PostProcessingFilter filter = bufferedScene.getPostProcessingFilter(); - if (mode == RenderMode.PREVIEW) filter = PreviewFilter.INSTANCE; - if (filter instanceof PixelPostProcessingFilter) { - PixelPostProcessingFilter pixelFilter = (PixelPostProcessingFilter) filter; + List filters; + if (mode == RenderMode.PREVIEW) { + filters = Collections.singletonList(PreviewFilter.INSTANCE); + } else { + filters = bufferedScene.getPostprocessingFilters(); + } + + int width = bufferedScene.canvasConfig.getWidth(); + int height = bufferedScene.canvasConfig.getHeight(); + double exposure = FastMath.pow(2, bufferedScene.getExposure()); + double[] sampleBuffer = bufferedScene.getSampleBuffer(); + double[] intermediate = new double[sampleBuffer.length]; + for (int i = 0; i < intermediate.length; i++) { + intermediate[i] = sampleBuffer[i] * exposure; + } - int width = bufferedScene.canvasConfig.getWidth(); - int height = bufferedScene.canvasConfig.getHeight(); - int totalPixelCount = bufferedScene.canvasConfig.getPixelCount(); + int numThreads = pool.getThreadCount(); - double[] sampleBuffer = bufferedScene.getSampleBuffer(); - double exposure = bufferedScene.getExposure(); + for (PostProcessingFilter filter : filters) { + if (filter instanceof PixelPostProcessingFilter) { + PixelPostProcessingFilter pixelFilter = (PixelPostProcessingFilter) filter; - // Split up to 10 tasks per thread - int tasksPerThread = 10; - int pixelsPerTask = totalPixelCount / (pool.getThreadCount() * tasksPerThread - 1); - ArrayList jobs = new ArrayList<>(pool.getThreadCount() * tasksPerThread); + // Split up to 10 tasks per thread + int tasksPerThread = 10; + int pixelsPerTask = (width * height) / (numThreads * tasksPerThread - 1); + ArrayList jobs = new ArrayList<>(numThreads * tasksPerThread); - for (int i = 0; i < totalPixelCount; i += pixelsPerTask) { - int start = i; - int end = Math.min(totalPixelCount, i + pixelsPerTask); - jobs.add(pool.submit(worker -> { - double[] pixelbuffer = new double[3]; + for (int i = 0; i < width * height; i += pixelsPerTask) { + int start = i; + int end = Math.min(width * height, i + pixelsPerTask); + jobs.add(pool.submit(worker -> { + double[] pixelBuffer = new double[3]; - for (int j = start; j < end; j++) { - int x = j % width; - int y = j / width; + for (int j = start; j < end; j++) { + int x = j % width; + int y = j / width; + + int index = (y * width + x) * 3; + System.arraycopy(intermediate, index, pixelBuffer, 0, 3); + pixelFilter.processPixel(pixelBuffer); + System.arraycopy(pixelBuffer, 0, intermediate, index, 3); + + // TODO: extract clamping into own interface + } + })); + } - pixelFilter.processPixel(width, height, sampleBuffer, x, y, exposure, pixelbuffer); - Arrays.setAll(pixelbuffer, k -> Math.min(1, pixelbuffer[k])); - bufferedScene.getBackBuffer().setPixel(x, y, ColorUtil.getRGB(pixelbuffer)); + try { + for (RenderWorkerPool.RenderJobFuture job : jobs) { + job.awaitFinish(); } - })); + } catch (InterruptedException e) { + // Interrupted + } + } else { + filter.processFrame(width, height, intermediate); } + } - try { - for (RenderWorkerPool.RenderJobFuture job : jobs) { - job.awaitFinish(); + // Split up to 10 tasks per thread + int tasksPerThread = 10; + int pixelsPerTask = (width * height) / (numThreads * tasksPerThread - 1); + ArrayList jobs = new ArrayList<>(numThreads * tasksPerThread); + for (int i = 0; i < width * height; i += pixelsPerTask) { + int start = i; + int end = Math.min(width * height, i + pixelsPerTask); + jobs.add(pool.submit(worker -> { + double[] pixelBuffer = new double[3]; + + for (int j = start; j < end; j++) { + int x = j % width; + int y = j / width; + + int index = (y * width + x) * 3; + System.arraycopy(intermediate, index, pixelBuffer, 0, 3); + + // TODO: extract clamping into own interface + Arrays.setAll(pixelBuffer, k -> Math.min(1, pixelBuffer[k])); + bufferedScene.getBackBuffer().setPixel(x, y, ColorUtil.getRGB(pixelBuffer)); } - } catch (InterruptedException e) { - // Interrupted + })); + } + try { + for (RenderWorkerPool.RenderJobFuture job : jobs) { + job.awaitFinish(); } - } else { - bufferedScene.postProcessFrame(TaskTracker.Task.NONE); + } catch (InterruptedException e) { + // Interrupted } - redrawScreen(); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/HasPrimitives.java b/chunky/src/java/se/llbit/chunky/renderer/HasPrimitives.java new file mode 100644 index 0000000000..ecaa9e4062 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/HasPrimitives.java @@ -0,0 +1,10 @@ +package se.llbit.chunky.renderer; + +import se.llbit.math.Vector3; +import se.llbit.math.primitive.Primitive; + +import java.util.Collection; + +public interface HasPrimitives { + Collection primitives(Vector3 offset); +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/PathTracingRenderer.java b/chunky/src/java/se/llbit/chunky/renderer/PathTracingRenderer.java index 149f007aac..786d144ee5 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/PathTracingRenderer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/PathTracingRenderer.java @@ -90,13 +90,13 @@ public void render(DefaultRenderManager manager) throws InterruptedException { -0.5 + (y + oy + cropY) * invHeight); scene.rayTrace(tracer, state); - sr += state.ray.color.x * branchCount; - sg += state.ray.color.y * branchCount; - sb += state.ray.color.z * branchCount; + sr += state.color.x * branchCount; + sg += state.color.y * branchCount; + sb += state.color.z * branchCount; } int offset = 3 * (y*width + x); - sampleBuffer[offset + 0] = (sampleBuffer[offset + 0] * spp + sr) * sinv; + sampleBuffer[offset] = (sampleBuffer[offset] * spp + sr) * sinv; sampleBuffer[offset + 1] = (sampleBuffer[offset + 1] * spp + sg) * sinv; sampleBuffer[offset + 2] = (sampleBuffer[offset + 2] * spp + sb) * sinv; }); diff --git a/chunky/src/java/se/llbit/chunky/renderer/PreviewRenderer.java b/chunky/src/java/se/llbit/chunky/renderer/PreviewRenderer.java index 94355daa95..209d0a1028 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/PreviewRenderer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/PreviewRenderer.java @@ -20,6 +20,8 @@ import se.llbit.chunky.renderer.scene.Camera; import se.llbit.chunky.renderer.scene.RayTracer; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; import se.llbit.util.TaskTracker; @@ -70,10 +72,11 @@ public void render(DefaultRenderManager manager) throws InterruptedException { double invHeight = 1.0 / fullHeight; Ray target = new Ray(); - boolean hit = scene.traceTarget(target); - int tx = (int) Math.floor(target.o.x + target.d.x * Ray.OFFSET); - int ty = (int) Math.floor(target.o.y + target.d.y * Ray.OFFSET); - int tz = (int) Math.floor(target.o.z + target.d.z * Ray.OFFSET); + IntersectionRecord intersectionRecord = new IntersectionRecord(); + boolean hit = scene.traceTarget(target, intersectionRecord); + int tx = (int) Math.floor(target.o.x + intersectionRecord.n.x * -2 * Constants.OFFSET); + int ty = (int) Math.floor(target.o.y + intersectionRecord.n.y * -2 * Constants.OFFSET); + int tz = (int) Math.floor(target.o.z + intersectionRecord.n.z * -2 * Constants.OFFSET); double[] sampleBuffer = scene.getSampleBuffer(); @@ -94,7 +97,7 @@ public void render(DefaultRenderManager manager) throws InterruptedException { // Draw crosshairs if (x == fullWidth / 2 && (y >= fullHeight / 2 - 5 && y <= fullHeight / 2 + 5) || y == fullHeight / 2 && ( x >= fullWidth / 2 - 5 && x <= fullWidth / 2 + 5)) { - sampleBuffer[offset + 0] = 0xFF; + sampleBuffer[offset] = 0xFF; sampleBuffer[offset + 1] = 0xFF; sampleBuffer[offset + 2] = 0xFF; return; @@ -106,24 +109,24 @@ public void render(DefaultRenderManager manager) throws InterruptedException { scene.rayTrace(tracer, state); // Target highlighting. - int rx = (int) Math.floor(state.ray.o.x + state.ray.d.x * Ray.OFFSET); - int ry = (int) Math.floor(state.ray.o.y + state.ray.d.y * Ray.OFFSET); - int rz = (int) Math.floor(state.ray.o.z + state.ray.d.z * Ray.OFFSET); + int rx = (int) Math.floor(state.ray.o.x + state.intersectionRecord.n.x * -2 * Constants.OFFSET); + int ry = (int) Math.floor(state.ray.o.y + state.intersectionRecord.n.y * -2 * Constants.OFFSET); + int rz = (int) Math.floor(state.ray.o.z + state.intersectionRecord.n.z * -2 * Constants.OFFSET); if (hit && tx == rx && ty == ry && tz == rz) { - state.ray.color.x = 1 - state.ray.color.x; - state.ray.color.y = 1 - state.ray.color.y; - state.ray.color.z = 1 - state.ray.color.z; - state.ray.color.w = 1; + state.color.x = 1 - state.color.x; + state.color.y = 1 - state.color.y; + state.color.z = 1 - state.color.z; + state.color.w = 1; } - sampleBuffer[offset + 0] = state.ray.color.x; - sampleBuffer[offset + 1] = state.ray.color.y; - sampleBuffer[offset + 2] = state.ray.color.z; + sampleBuffer[offset + 0] = state.color.x; + sampleBuffer[offset + 1] = state.color.y; + sampleBuffer[offset + 2] = state.color.z; if (sampleNum == 0 && x < (width - 1)) { - sampleBuffer[offset + 3] = state.ray.color.x; - sampleBuffer[offset + 4] = state.ray.color.y; - sampleBuffer[offset + 5] = state.ray.color.z; + sampleBuffer[offset + 3] = state.color.x; + sampleBuffer[offset + 4] = state.color.y; + sampleBuffer[offset + 5] = state.color.z; } }); diff --git a/chunky/src/java/se/llbit/chunky/renderer/RenderContextFactory.java b/chunky/src/java/se/llbit/chunky/renderer/RenderContextFactory.java index d951319b2b..be984267b7 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/RenderContextFactory.java +++ b/chunky/src/java/se/llbit/chunky/renderer/RenderContextFactory.java @@ -18,7 +18,6 @@ package se.llbit.chunky.renderer; import se.llbit.chunky.main.Chunky; -import se.llbit.chunky.main.ChunkyOptions; public interface RenderContextFactory { RenderContext newRenderContext(Chunky chunky); diff --git a/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java deleted file mode 100644 index a8fd916bac..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright (c) 2022 Chunky Contributors - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ -package se.llbit.chunky.renderer; - -import se.llbit.util.Registerable; - -public enum SunSamplingStrategy implements Registerable { - OFF("Off", "Sun is not sampled with next event estimation.", false, true, false, true, false), - NON_LUMINOUS("Non-Luminous", "Sun is drawn on the skybox but it does not contribute to the lighting of the scene.", false, false, false, false, false), - FAST("Fast", "Fast sun sampling algorithm. Lower noise but does not correctly model some visual effects.", true, false, false, false, false), - IMPORTANCE("Importance", "Sun is sampled on a certain percentage of diffuse reflections. Correctly models visual effects while reducing noise for direct and diffuse illumination.", false, true, false, true, true), - HIGH_QUALITY("High Quality", "High quality sun sampling. More noise but correctly models visual effects such as caustics.", true, true, true, true, false); - - private final String displayName; - private final String description; - - private final boolean sunSampling; - private final boolean diffuseSun; - private final boolean strictDirectLight; - private final boolean sunLuminosity; - private final boolean importanceSampling; - - SunSamplingStrategy(String displayName, String description, boolean sunSampling, boolean diffuseSun, boolean strictDirectLight, boolean sunLuminosity, boolean importanceSampling) { - this.displayName = displayName; - this.description = description; - - this.sunSampling = sunSampling; - this.diffuseSun = diffuseSun; - this.strictDirectLight = strictDirectLight; - this.sunLuminosity = sunLuminosity; - this.importanceSampling = importanceSampling; - } - - @Override - public String getName() { - return this.displayName; - } - - @Override - public String getDescription() { - return this.description; - } - - @Override - public String getId() { - return this.name(); - } - - public boolean doSunSampling() { - return sunSampling; - } - - public boolean isDiffuseSun() { - return diffuseSun; - } - - public boolean isStrictDirectLight() { - return strictDirectLight; - } - - public boolean isSunLuminosity() { - return sunLuminosity; - } - - public boolean isImportanceSampling() { - return importanceSampling; - } - - @Override - public DeprecationStatus getDeprecationStatus() { - if (this == HIGH_QUALITY) { - return DeprecationStatus.HIDDEN; - } - return DeprecationStatus.ACTIVE; - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/TileBasedRenderer.java b/chunky/src/java/se/llbit/chunky/renderer/TileBasedRenderer.java index f87039e1ae..aeb98cd711 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/TileBasedRenderer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/TileBasedRenderer.java @@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.ints.IntIntPair; import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.Ray; import java.util.ArrayList; import java.util.function.BiConsumer; @@ -70,8 +69,6 @@ protected void submitTiles(DefaultRenderManager manager, BiConsumer manager.pool.submit(worker -> { WorkerState state = new WorkerState(); - state.ray = new Ray(); - state.ray.setNormal(0, 0, -1); state.random = worker.random; IntIntMutablePair pair = new IntIntMutablePair(0, 0); @@ -80,6 +77,15 @@ protected void submitTiles(DefaultRenderManager manager, BiConsumer. - */ -package se.llbit.chunky.renderer; - -import se.llbit.util.Registerable; - -public enum WaterShadingStrategy implements Registerable { - SIMPLEX("Simplex", "Uses configurable noise to shade the water, which prevents tiling at great distances."), - TILED_NORMALMAP("Tiled normal map", "Uses a built-in tiled normal map to shade the water"), - STILL("Still", "Renders the water surface as flat."); - - private final String displayName; - private final String description; - - WaterShadingStrategy(String displayName, String description) { - this.displayName = displayName; - this.description = description; - } - - @Override - public String getName() { - return this.displayName; - } - - @Override - public String getDescription() { - return this.description; - } - - @Override - public String getId() { - return this.name(); - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/WorkerState.java b/chunky/src/java/se/llbit/chunky/renderer/WorkerState.java index 380ffa28af..63ee0a290f 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/WorkerState.java +++ b/chunky/src/java/se/llbit/chunky/renderer/WorkerState.java @@ -16,7 +16,9 @@ */ package se.llbit.chunky.renderer; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; +import se.llbit.math.Vector3; import se.llbit.math.Vector4; import java.util.Random; @@ -25,7 +27,14 @@ * State for a render worker. */ public class WorkerState { - public Ray ray; + public Ray ray = new Ray(); + public IntersectionRecord intersectionRecord = new IntersectionRecord(); + public Ray sampleRay = new Ray(); + public IntersectionRecord sampleRecord = new IntersectionRecord(); + public Vector3 throughput = new Vector3(1); + public Vector4 color = new Vector4(); + public Vector3 emittance = new Vector3(); + public Vector4 sampleColor = new Vector4(); public Vector4 attenuation = new Vector4(); public Random random; } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/ACESFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/ACESFilmicFilter.java deleted file mode 100644 index fc03828250..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/ACESFilmicFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package se.llbit.chunky.renderer.postprocessing; - -import org.apache.commons.math3.util.FastMath; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.math.QuickMath; - -/** - * Implementation of ACES filmic tone mapping - * @link https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ - */ -public class ACESFilmicFilter extends SimplePixelPostProcessingFilter { - private static final float aces_a = 2.51f; - private static final float aces_b = 0.03f; - private static final float aces_c = 2.43f; - private static final float aces_d = 0.59f; - private static final float aces_e = 0.14f; - - @Override - public void processPixel(double[] pixel) { - for(int i = 0; i < 3; ++i) { - pixel[i] = QuickMath.max(QuickMath.min((pixel[i] * (aces_a * pixel[i] + aces_b)) / (pixel[i] * (aces_c * pixel[i] + aces_d) + aces_e), 1), 0); - pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); - } - } - - @Override - public String getName() { - return "ACES filmic tone mapping"; - } - - @Override - public String getId() { - return "TONEMAP2"; - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AgXFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AgXFilter.java new file mode 100644 index 0000000000..d567415cf6 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AgXFilter.java @@ -0,0 +1,401 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.beans.value.ChangeListener; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.fx.LuxColorPicker; +import se.llbit.json.JsonObject; +import se.llbit.math.ColorUtil; +import se.llbit.math.Matrix3; +import se.llbit.math.QuickMath; +import se.llbit.math.Vector3; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +public class AgXFilter extends SimplePixelPostProcessingFilter { + private enum Preset { + DEFAULT, GOLDEN, PUNCHY + } + + private static void matrixMultiplyByVector(Matrix3 m, Vector3 v) { + v.set( + m.m11 * v.x + m.m21 * v.y + m.m31 * v.z, + m.m12 * v.x + m.m22 * v.y + m.m32 * v.z, + m.m13 * v.x + m.m23 * v.y + m.m33 * v.z + ); + } + + private static final Matrix3 LINEAR_REC2020_TO_LINEAR_SRGB = new Matrix3( + 1.6605, -0.1246, -0.0182, + -0.5876, 1.1329, -0.1006, + -0.0728, -0.0083, 1.1187 + ); + + private static final Matrix3 LINEAR_SRGB_TO_LINEAR_REC2020 = new Matrix3( + 0.6274, 0.0691, 0.0164, + 0.3293, 0.9195, 0.0880, + 0.0433, 0.0113, 0.8956 + ); + + /** + * Converted to column major from blender: ... + */ + private static final Matrix3 AGX_INSET_MATRIX = new Matrix3( + 0.856627153315983, 0.137318972929847, 0.11189821299995, + 0.0951212405381588, 0.761241990602591, 0.0767994186031903, + 0.0482516061458583, 0.101439036467562, 0.811302368396859 + ); + + /** + * Converted to column major and inverted from ... + * ... + */ + private static final Matrix3 AGX_OUTSET_MATRIX = new Matrix3( + 1.1271005818144368, -0.1413297634984383, -0.14132976349843826, + -0.11060664309660323, 1.157823702216272, -0.11060664309660294, + -0.016493938717834573, -0.016493938717834257, 1.2519364065950405 + ); + + private static final double AGX_MIN_EV = -12.47393; + private static final double AGX_MAX_EV = 4.026069; + + private final Vector3 offset = new Vector3(1); + private double offsetMagnitude = 0; + private final Vector3 slope = new Vector3(1); + private double slopeMagnitude = 1; + private final Vector3 power = new Vector3(1); + private double powerMagnitude = 1; + private double saturation = 1; + private double gamma = Scene.DEFAULT_GAMMA; + + public AgXFilter() { + reset(); + } + + private void agxAscCdl(Vector3 color) { + final Vector3 lw = new Vector3(0.2126, 0.7152, 0.0722); + double luma = color.dot(lw); + Vector3 c = new Vector3( + FastMath.pow(color.x * slope.x * slopeMagnitude + offset.x * offsetMagnitude, power.x * powerMagnitude), + FastMath.pow(color.y * slope.y * slopeMagnitude + offset.y * offsetMagnitude, power.y * powerMagnitude), + FastMath.pow(color.z * slope.z * slopeMagnitude + offset.z * offsetMagnitude, power.z * powerMagnitude) + ); + color.set( + luma + saturation * (c.x - luma), + luma + saturation * (c.y - luma), + luma + saturation * (c.z - luma) + ); + } + + @Override + public void processPixel(double[] pixel) { + Vector3 color = new Vector3(pixel[0], pixel[1], pixel[2]); + + matrixMultiplyByVector(LINEAR_SRGB_TO_LINEAR_REC2020, color); + + matrixMultiplyByVector(AGX_INSET_MATRIX, color); + + color.x = FastMath.max(color.x, 1e-10); + color.y = FastMath.max(color.y, 1e-10); + color.z = FastMath.max(color.z, 1e-10); + + color.x = QuickMath.clamp(FastMath.log(2, color.x), AGX_MIN_EV, AGX_MAX_EV); + color.y = QuickMath.clamp(FastMath.log(2, color.y), AGX_MIN_EV, AGX_MAX_EV); + color.z = QuickMath.clamp(FastMath.log(2, color.z), AGX_MIN_EV, AGX_MAX_EV); + + color.x = (color.x - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV); + color.y = (color.y - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV); + color.z = (color.z - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV); + + color.x = QuickMath.clamp(color.x, 0, 1); + color.y = QuickMath.clamp(color.y, 0, 1); + color.z = QuickMath.clamp(color.z, 0, 1); + + Vector3 x2 = color.rMultiplyEntrywise(color); + Vector3 x4 = x2.rMultiplyEntrywise(x2); + + color.x = + 15.5 * x4.x * x2.x + - 40.14 * x4.x * color.x + + 31.96 * x4.x + - 6.868 * x2.x * color.x + + 0.4298 * x2.x + + 0.1191 * color.x + - 0.00232; + color.y = + 15.5 * x4.y * x2.y + - 40.14 * x4.y * color.y + + 31.96 * x4.y + - 6.868 * x2.y * color.y + + 0.4298 * x2.y + + 0.1191 * color.y + - 0.00232; + color.z = + 15.5 * x4.z * x2.z + - 40.14 * x4.z * color.z + + 31.96 * x4.z + - 6.868 * x2.z * color.z + + 0.4298 * x2.z + + 0.1191 * color.z + - 0.00232; + + agxAscCdl(color); + + matrixMultiplyByVector(AGX_OUTSET_MATRIX, color); + + color.x = FastMath.pow(FastMath.max(0, color.x), gamma); + color.y = FastMath.pow(FastMath.max(0, color.y), gamma); + color.z = FastMath.pow(FastMath.max(0, color.z), gamma); + + matrixMultiplyByVector(LINEAR_REC2020_TO_LINEAR_SRGB, color); + + pixel[0] = color.x; + pixel[1] = color.y; + pixel[2] = color.z; + } + + private void applyPreset(Preset preset) { + switch (preset) { + case GOLDEN: + slope.set(1.0, 0.9, 0.5); + slopeMagnitude = 1; + offset.set(1); + offsetMagnitude = 0; + power.set(1); + powerMagnitude = 0.8; + saturation = 1.3; + gamma = Scene.DEFAULT_GAMMA; + break; + case PUNCHY: + slope.set(1); + slopeMagnitude = 1; + offset.set(1); + offsetMagnitude = 0; + power.set(1); + powerMagnitude = 1.35; + saturation = 1.4; + gamma = Scene.DEFAULT_GAMMA; + break; + case DEFAULT: + default: + slope.set(1); + slopeMagnitude = 1; + offset.set(1); + offsetMagnitude = 0; + power.set(1); + powerMagnitude = 1; + saturation = 1; + gamma = Scene.DEFAULT_GAMMA; + break; + } + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + RenderControlsFxController controller = parent.getController(); + + MenuButton presetChooser = new MenuButton("Load preset"); + LuxColorPicker slopePicker = new LuxColorPicker(); + ChangeListener slopePickerListener = (observable, oldValue, newValue) -> { + slope.set(ColorUtil.fromFx(newValue)); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }; + DoubleAdjuster slopeMagnitudeAdjuster = new DoubleAdjuster(); + LuxColorPicker offsetPicker = new LuxColorPicker(); + ChangeListener offsetPickerListener = (observable, oldValue, newValue) -> { + offset.set(ColorUtil.fromFx(newValue)); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }; + DoubleAdjuster offsetMagnitudeAdjuster = new DoubleAdjuster(); + LuxColorPicker powerPicker = new LuxColorPicker(); + ChangeListener powerPickerListener = (observable, oldValue, newValue) -> { + power.set(ColorUtil.fromFx(newValue)); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }; + DoubleAdjuster powerMagnitudeAdjuster = new DoubleAdjuster(); + DoubleAdjuster saturationAdjuster = new DoubleAdjuster(); + DoubleAdjuster gammaAdjuster = new DoubleAdjuster(); + + for (AgXFilter.Preset preset : AgXFilter.Preset.values()) { + MenuItem menuItem = new MenuItem(preset.toString()); + menuItem.setOnAction(e -> { + applyPreset(preset); + slopePicker.colorProperty().removeListener(slopePickerListener); + slopePicker.setColor(ColorUtil.toFx(slope)); + slopePicker.colorProperty().addListener(slopePickerListener); + slopeMagnitudeAdjuster.set(slopeMagnitude); + offsetPicker.colorProperty().removeListener(offsetPickerListener); + offsetPicker.setColor(ColorUtil.toFx(offset)); + offsetPicker.colorProperty().addListener(offsetPickerListener); + offsetMagnitudeAdjuster.set(offsetMagnitude); + powerPicker.colorProperty().removeListener(powerPickerListener); + powerPicker.setColor(ColorUtil.toFx(power)); + powerPicker.colorProperty().addListener(powerPickerListener); + powerMagnitudeAdjuster.set(powerMagnitude); + saturationAdjuster.set(saturation); + gammaAdjuster.set(gamma); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + presetChooser.getItems().add(menuItem); + } + + slopePicker.setText("Slope"); + slopePicker.setColor(ColorUtil.toFx(slope)); + slopePicker.colorProperty().addListener(slopePickerListener); + + slopeMagnitudeAdjuster.setName("Slope magnitude"); + slopeMagnitudeAdjuster.setRange(0, 10); + slopeMagnitudeAdjuster.clampMin(); + slopeMagnitudeAdjuster.makeLogarithmic(); + slopeMagnitudeAdjuster.set(slopeMagnitude); + slopeMagnitudeAdjuster.onValueChange(value -> { + slopeMagnitude = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + offsetPicker.setText("Offset"); + offsetPicker.setColor(ColorUtil.toFx(offset)); + offsetPicker.colorProperty().addListener(offsetPickerListener); + + offsetMagnitudeAdjuster.setName("Offset magnitude"); + offsetMagnitudeAdjuster.setRange(0, 10); + offsetMagnitudeAdjuster.clampMin(); + offsetMagnitudeAdjuster.makeLogarithmic(); + offsetMagnitudeAdjuster.set(offsetMagnitude); + offsetMagnitudeAdjuster.onValueChange(value -> { + offsetMagnitude = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + powerPicker.setText("Power"); + powerPicker.setColor(ColorUtil.toFx(power)); + powerPicker.colorProperty().addListener(powerPickerListener); + + powerMagnitudeAdjuster.setName("Power magnitude"); + powerMagnitudeAdjuster.setRange(0, 10); + powerMagnitudeAdjuster.clampMin(); + powerMagnitudeAdjuster.makeLogarithmic(); + powerMagnitudeAdjuster.set(powerMagnitude); + powerMagnitudeAdjuster.onValueChange(value -> { + powerMagnitude = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + saturationAdjuster.setName("Saturation"); + saturationAdjuster.setRange(0, 10); + saturationAdjuster.clampMin(); + saturationAdjuster.makeLogarithmic(); + saturationAdjuster.set(saturation); + saturationAdjuster.onValueChange(value -> { + saturation = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + gammaAdjuster.setName("Gamma"); + gammaAdjuster.setRange(0.001, 5); + gammaAdjuster.clampMin(); + gammaAdjuster.set(gamma); + gammaAdjuster.onValueChange(value -> { + gamma = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, presetChooser, slopePicker, slopeMagnitudeAdjuster, offsetPicker, offsetMagnitudeAdjuster, powerPicker, powerMagnitudeAdjuster, saturationAdjuster, gammaAdjuster); + } + + @Override + public String getName() { + return "AgX Tone mapping"; + } + + @Override + public String getId() { + return "AGX"; + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("offset", offset.toJson()); + json.add("offsetMagnitude", offsetMagnitude); + json.add("slope", slope.toJson()); + json.add("slopeMagnitude", slopeMagnitude); + json.add("power", power.toJson()); + json.add("powerMagnitude", powerMagnitude); + json.add("saturation", saturation); + json.add("gamma", gamma); + } + + @Override + public void fromJson(JsonObject json) { + offset.fromJson(json.get("offset").asObject()); + offsetMagnitude = json.get("offsetMagnitude").doubleValue(0); + slope.fromJson(json.get("slope").asObject()); + slopeMagnitude = json.get("slopeMagnitude").doubleValue(1); + power.fromJson(json.get("power").asObject()); + powerMagnitude = json.get("powerMagnitude").doubleValue(1); + saturation = json.get("saturation").doubleValue(1); + gamma = json.get("gamma").doubleValue(Scene.DEFAULT_GAMMA); + } + + @Override + public void reset() { + applyPreset(Preset.DEFAULT); + } + + @Override + public String getDescription() { + return ""; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AldridgeFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AldridgeFilmicFilter.java new file mode 100644 index 0000000000..af37e5f74b --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/AldridgeFilmicFilter.java @@ -0,0 +1,88 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Graham Aldridge's variation of the Hejl Burgess-Dawson curve. + * + * link + */ +public class AldridgeFilmicFilter extends SimplePixelPostProcessingFilter { + + private float cutoff = 0.025f; + + @Override + public void processPixel(double[] pixel) { + for (int i = 0; i < 3; ++i) { + double tmp = 2.0 * cutoff; + double x = + pixel[i] + (tmp - pixel[i]) * QuickMath.clamp(tmp - pixel[i], 0d, 1d) * (0.25 / cutoff) + - cutoff; + pixel[i] = x * (0.5 + 6.2 * x) / (0.06 + x * (1.7 + 6.2 * x)); + } + } + + @Override + public void fromJson(JsonObject json) { + cutoff = json.get("cutoff").floatValue(cutoff); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("cutoff", cutoff); + } + + @Override + public void reset() { + cutoff = 0.025f; + } + + @Override + public String getName() { + return "Aldridge Filmic"; + } + + @Override + public String getId() { + return "ALDRIDGE_FILMIC"; + } + + @Override + public String getDescription() { + return "Graham Aldridge's variation of the Hejl Burgess-Dawson filmic tonemapping curve.\n" + + "https://iwasbeingirony.blogspot.com/2010/04/approximating-film-with-tonemapping.html"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster cutoff = new DoubleAdjuster(); + cutoff.setName("Cutoff"); + cutoff.setTooltip("Transition into compressed blacks"); + cutoff.setRange(0.001, 0.5); + cutoff.clampMin(); + cutoff.set(this.cutoff); + cutoff.onValueChange(value -> { + this.cutoff = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, cutoff); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/DayFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/DayFilmicFilter.java new file mode 100644 index 0000000000..e2fed5c3fd --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/DayFilmicFilter.java @@ -0,0 +1,233 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Mike Day's filmic tonemapping curve. + * link + */ +public class DayFilmicFilter extends PostProcessingFilter { + + private float gamma = Scene.DEFAULT_GAMMA; + private float w = 10f; + private float b = 0.1f; + private float t = 0.7f; + private float s = 0.8f; + private float c = 2f; + private boolean autoExposure = true; + + private static double getMeanLuminance(int width, int height, double[] sampleBuffer) { + double meanLuminance = 0; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int pixelIndex = (y * width + x) * 3; + meanLuminance += luminance(sampleBuffer[pixelIndex], + sampleBuffer[pixelIndex + 1], sampleBuffer[pixelIndex + 2]); + } + } + return meanLuminance / (width * height); + } + + private static double luminance(double r, double g, double b) { + return r * 0.212671 + g * 0.715160 + b * 0.072169; + } + + private double curve(double x, float k) { + if (x < c) { + return k * (1 - t) * (x - b) / (c - (1 - t) * b - t * x); + } else { + return (1 - k) * (x - c) / (s * x + (1 - s) * w - c) + k; + } + } + + @Override + public void processFrame(int width, int height, double[] input) { + // Code adapted from Tizian Zeltner's implementation of the tonemapping curve. + // https://github.com/tizian/tonemapper/blob/fe100b9052e91d034927779d22a5afed9bedc1e3/src/operators/DayFilmicOperator.cpp#L71 + + double lAvg = autoExposure ? getMeanLuminance(width, height, input) : 0.5; + float k = (1f - t) * (c - b) / ((1f - s) * (w - c) + (1f - t) * (c - b)); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int pixelIndex = (y * width + x) * 3; + for (int i = 0; i < 3; i++) { + input[pixelIndex + i] /= lAvg; + input[pixelIndex + i] = curve(input[pixelIndex + i], k); + input[pixelIndex + i] = FastMath.pow(input[pixelIndex + i], 1f / gamma); + } + } + } + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").floatValue(gamma); + w = json.get("w").floatValue(w); + b = json.get("b").floatValue(b); + t = json.get("t").floatValue(t); + s = json.get("s").floatValue(s); + c = json.get("c").floatValue(c); + autoExposure = json.get("autoExposure").boolValue(autoExposure); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + json.add("w", w); + json.add("b", b); + json.add("t", t); + json.add("s", s); + json.add("c", c); + json.add("autoExposure", autoExposure); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + w = 10f; + b = 0.1f; + t = 0.7f; + s = 0.8f; + c = 2f; + autoExposure = true; + } + + @Override + public String getName() { + return "Day Filmic"; + } + + @Override + public String getId() { + return "DAY_FILMIC"; + } + + @Override + public String getDescription() { + return "Mike Day's filmic tonemapping curve.\n" + + "https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/09/an-efficient-and-user-friendly-tone-mapping-operator.pdf"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster w = new DoubleAdjuster(); + DoubleAdjuster b = new DoubleAdjuster(); + DoubleAdjuster t = new DoubleAdjuster(); + DoubleAdjuster s = new DoubleAdjuster(); + DoubleAdjuster c = new DoubleAdjuster(); + ToggleSwitch autoExposure = new ToggleSwitch(); + + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + w.setName("White point"); + w.setTooltip("Smallest value that is mapped to 1"); + w.setRange(0, 20); + w.clampMin(); + w.set(this.w); + w.onValueChange(value -> { + this.w = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + b.setName("Black point"); + b.setTooltip("Largest value that is mapped to 0"); + b.setRange(0, 2); + b.clampMin(); + b.set(this.b); + b.onValueChange(value -> { + this.b = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + t.setName("Toe strength"); + t.setRange(0, 1); + t.set(this.t); + t.onValueChange(value -> { + this.t = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + s.setName("Shoulder strength"); + s.setRange(0, 1); + s.set(this.s); + s.onValueChange(value -> { + this.s = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + c.setName("Crossover point"); + c.setRange(0, 10); + c.set(this.c); + c.onValueChange(value -> { + this.c = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + autoExposure.setText("Auto exposure"); + autoExposure.setSelected(this.autoExposure); + autoExposure.selectedProperty().addListener(((observable, oldValue, newValue) -> { + this.autoExposure = newValue; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + })); + + return new VBox(6, gamma, w, b, t, s, c, autoExposure); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GammaCorrectionFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GammaCorrectionFilter.java index 6719c5e08a..0b5a776abe 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GammaCorrectionFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GammaCorrectionFilter.java @@ -1,13 +1,23 @@ package se.llbit.chunky.renderer.postprocessing; +import javafx.scene.layout.VBox; import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; public class GammaCorrectionFilter extends SimplePixelPostProcessingFilter { + private double gamma = Scene.DEFAULT_GAMMA; + @Override public void processPixel(double[] pixel) { for(int i = 0; i < 3; ++i) { - pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); + pixel[i] = FastMath.pow(pixel[i], 1 / gamma); } } @@ -20,4 +30,48 @@ public String getName() { public String getId() { return "GAMMA"; } + + @Override + public String getDescription() { + return "Performs gamma correction only."; + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").doubleValue(Scene.DEFAULT_GAMMA); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma); + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GuyACESFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GuyACESFilmicFilter.java new file mode 100644 index 0000000000..d4f408b629 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/GuyACESFilmicFilter.java @@ -0,0 +1,45 @@ +package se.llbit.chunky.renderer.postprocessing; + +import se.llbit.json.JsonObject; + +/** + * Implementation of Romain Guy's ACES filmic tonemapping curve + * link + */ +public class GuyACESFilmicFilter extends SimplePixelPostProcessingFilter { + + @Override + public void processPixel(double[] pixel) { + for (int i = 0; i < 3; i++) { + pixel[i] = pixel[i] / (pixel[i] + 0.155) * 1.019; + } + } + + @Override + public void fromJson(JsonObject json) { + } + + @Override + public void filterSettingsToJson(JsonObject json) { + } + + @Override + public void reset() { + } + + @Override + public String getName() { + return "Guy ACES Filmic"; + } + + @Override + public String getId() { + return "GUY_ACES_FILMIC"; + } + + @Override + public String getDescription() { + return "Romain Guy's ACES filmic tonemapping curve.\n" + + "https://www.shadertoy.com/view/llXyWr"; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableFilmicFilter.java new file mode 100644 index 0000000000..240e9297cf --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableFilmicFilter.java @@ -0,0 +1,364 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of John Hable's filmic tonemapping curve (i.e. Uncharted 2) + * blog post + * GDC talk + */ +public class HableFilmicFilter extends SimplePixelPostProcessingFilter { + public enum Preset { + /** + * Parameters from John Hable's blog post + */ + FILMIC_WORLDS, + + /** + * Parameters from John Hable's GDC talk + */ + GDC + } + + private float hA; + private float hB; + private float hC; + private float hD; + private float hE; + private float hF; + private float hW; + private float whiteScale; + private float gamma; + + public HableFilmicFilter() { + reset(); + } + + private void recalculateWhiteScale() { + whiteScale = 1.0f / (((hW * (hA * hW + hC * hB) + hD * hE) / (hW * (hA * hW + hB) + hD * hF)) - hE / hF); + } + + public float getShoulderStrength() { + return hA; + } + + public void setShoulderStrength(float hA) { + this.hA = hA; + recalculateWhiteScale(); + } + + public float getLinearStrength() { + return hB; + } + + public void setLinearStrength(float hB) { + this.hB = hB; + recalculateWhiteScale(); + } + + public float getLinearAngle() { + return hC; + } + + public void setLinearAngle(float hC) { + this.hC = hC; + recalculateWhiteScale(); + } + + public float getToeStrength() { + return hD; + } + + public void setToeStrength(float hD) { + this.hD = hD; + recalculateWhiteScale(); + } + + public float getToeNumerator() { + return hE; + } + + public void setToeNumerator(float hE) { + this.hE = hE; + recalculateWhiteScale(); + } + + public float getToeDenominator() { + return hF; + } + + public void setToeDenominator(float hF) { + this.hF = hF; + recalculateWhiteScale(); + } + + public float getLinearWhitePointValue() { + return hW; + } + + public void setLinearWhitePointValue(float hW) { + this.hW = hW; + recalculateWhiteScale(); + } + + public float getGamma() { + return gamma; + } + + public void setGamma(float gamma) { + this.gamma = gamma; + } + + public void reset() { + applyPreset(Preset.FILMIC_WORLDS); + } + + public void applyPreset(Preset preset) { + switch (preset) { + case FILMIC_WORLDS: + hA = 0.15f; + hB = 0.50f; + hC = 0.10f; + hD = 0.20f; + hE = 0.02f; + hF = 0.30f; + hW = 11.2f; + gamma = Scene.DEFAULT_GAMMA; + break; + case GDC: + hA = 0.22f; + hB = 0.30f; + hC = 0.10f; + hD = 0.20f; + hE = 0.01f; + hF = 0.30f; + hW = 11.2f; + gamma = Scene.DEFAULT_GAMMA; + break; + } + recalculateWhiteScale(); + } + + @Override + public void processPixel(double[] pixel) { + for (int i = 0; i < 3; ++i) { + pixel[i] *= 2; // exposure bias + pixel[i] = ((pixel[i] * (hA * pixel[i] + hC * hB) + hD * hE) / (pixel[i] * (hA * pixel[i] + hB) + hD * hF)) - hE / hF; + pixel[i] *= whiteScale; + pixel[i] = FastMath.pow(pixel[i], 1 / gamma); + } + } + + @Override + public String getName() { + return "Hable Filmic"; + } + + @Override + public String getId() { + return "TONEMAP3"; + } + + @Override + public String getDescription() { + return "John Hable's filmic tonemapping curve, with presets from his blog post and from his " + + "GDC talk.\n" + + "- Blog post: http://filmicworlds.com/blog/filmic-tonemapping-operators/\n" + + "- GDC talk: https://www.gdcvault.com/play/1012351/Uncharted-2-HDR"; + } + + @Override + public void fromJson(JsonObject json) { + reset(); + hA = json.get("shoulderStrength").floatValue(hA); + hB = json.get("linearStrength").floatValue(hB); + hC = json.get("linearAngle").floatValue(hC); + hD = json.get("toeStrength").floatValue(hD); + hE = json.get("toeNumerator").floatValue(hE); + hF = json.get("toeDenominator").floatValue(hF); + hW = json.get("linearWhitePointValue").floatValue(hW); + gamma = json.get("gamma").floatValue(gamma); + recalculateWhiteScale(); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("shoulderStrength", hA); + json.add("linearStrength", hB); + json.add("linearAngle", hC); + json.add("toeStrength", hD); + json.add("toeNumerator", hE); + json.add("toeDenominator", hF); + json.add("linearWhitePointValue", hW); + json.add("gamma", gamma); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + RenderControlsFxController controller = parent.getController(); + + MenuButton presetChooser = new MenuButton(); + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster shoulderStrength = new DoubleAdjuster(); + DoubleAdjuster linearStrength = new DoubleAdjuster(); + DoubleAdjuster linearAngle = new DoubleAdjuster(); + DoubleAdjuster toeStrength = new DoubleAdjuster(); + DoubleAdjuster toeNumerator = new DoubleAdjuster(); + DoubleAdjuster toeDenominator = new DoubleAdjuster(); + DoubleAdjuster linearWhitePointValue = new DoubleAdjuster(); + + presetChooser.setText("Load preset"); + for (HableFilmicFilter.Preset preset : HableFilmicFilter.Preset.values()) { + MenuItem menuItem = new MenuItem(preset.toString()); + menuItem.setOnAction(e -> { + applyPreset(preset); + gamma.set(getGamma()); + shoulderStrength.set(getShoulderStrength()); + linearStrength.set(getLinearStrength()); + linearAngle.set(getLinearAngle()); + toeStrength.set(getToeStrength()); + toeNumerator.set(getToeNumerator()); + toeDenominator.set(getToeDenominator()); + linearWhitePointValue.set(getLinearWhitePointValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + presetChooser.getItems().add(menuItem); + } + + gamma.setName("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(getGamma()); + gamma.onValueChange(value -> { + setGamma(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + shoulderStrength.setName("Shoulder strength"); + shoulderStrength.setRange(0, 10); + shoulderStrength.set(getShoulderStrength()); + shoulderStrength.onValueChange(value -> { + setShoulderStrength(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + linearStrength.setName("Linear strength"); + linearStrength.setRange(0, 1); + linearStrength.set(getLinearStrength()); + linearStrength.onValueChange(value -> { + setLinearStrength(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + linearAngle.setName("Linear angle"); + linearAngle.setRange(0, 1); + linearAngle.set(getLinearAngle()); + linearAngle.onValueChange(value -> { + setLinearAngle(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + toeStrength.setName("Toe strength"); + toeStrength.setRange(0, 1); + toeStrength.set(getToeStrength()); + toeStrength.onValueChange(value -> { + setToeStrength(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + toeNumerator.setName("Toe numerator"); + toeNumerator.setRange(0, 1); + toeNumerator.set(getToeNumerator()); + toeNumerator.onValueChange(value -> { + setToeNumerator(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + toeDenominator.setName("Toe denominator"); + toeDenominator.setRange(0, 1); + toeDenominator.set(getToeDenominator()); + toeDenominator.onValueChange(value -> { + setToeDenominator(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + linearWhitePointValue.setName("Linear white point value"); + linearWhitePointValue.setRange(0, 20); + linearWhitePointValue.set(getLinearWhitePointValue()); + linearWhitePointValue.onValueChange(value -> { + setLinearWhitePointValue(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox( + 6, + presetChooser, + gamma, + shoulderStrength, + linearStrength, + linearAngle, + toeStrength, + toeNumerator, + toeDenominator, + linearWhitePointValue + ); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java deleted file mode 100644 index 09a51f0dc8..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java +++ /dev/null @@ -1,178 +0,0 @@ -package se.llbit.chunky.renderer.postprocessing; - -import org.apache.commons.math3.util.FastMath; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.json.JsonObject; -import se.llbit.util.Configurable; - -/** - * Implementation of Hable (i.e. Uncharted 2) tone mapping - * - * @link http://filmicworlds.com/blog/filmic-tonemapping-operators/ - * @link https://www.gdcvault.com/play/1012351/Uncharted-2-HDR - */ -public class HableToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { - public enum Preset { - /** - * Parameters from John Hable's blog post - */ - FILMIC_WORLDS, - - /** - * Parameters from John Hable's GDC talk - */ - GDC - } - - private float hA; - private float hB; - private float hC; - private float hD; - private float hE; - private float hF; - private float hW; - private float whiteScale; - - public HableToneMappingFilter() { - reset(); - } - - private void recalculateWhiteScale() { - whiteScale = 1.0f / (((hW * (hA * hW + hC * hB) + hD * hE) / (hW * (hA * hW + hB) + hD * hF)) - hE / hF); - } - - public float getShoulderStrength() { - return hA; - } - - public void setShoulderStrength(float hA) { - this.hA = hA; - recalculateWhiteScale(); - } - - public float getLinearStrength() { - return hB; - } - - public void setLinearStrength(float hB) { - this.hB = hB; - recalculateWhiteScale(); - } - - public float getLinearAngle() { - return hC; - } - - public void setLinearAngle(float hC) { - this.hC = hC; - recalculateWhiteScale(); - } - - public float getToeStrength() { - return hD; - } - - public void setToeStrength(float hD) { - this.hD = hD; - recalculateWhiteScale(); - } - - public float getToeNumerator() { - return hE; - } - - public void setToeNumerator(float hE) { - this.hE = hE; - recalculateWhiteScale(); - } - - public float getToeDenominator() { - return hF; - } - - public void setToeDenominator(float hF) { - this.hF = hF; - recalculateWhiteScale(); - } - - public float getLinearWhitePointValue() { - return hW; - } - - public void setLinearWhitePointValue(float hW) { - this.hW = hW; - recalculateWhiteScale(); - } - - public void reset() { - applyPreset(Preset.FILMIC_WORLDS); - } - - public void applyPreset(Preset preset) { - switch (preset) { - case FILMIC_WORLDS: - hA = 0.15f; - hB = 0.50f; - hC = 0.10f; - hD = 0.20f; - hE = 0.02f; - hF = 0.30f; - hW = 11.2f; - break; - case GDC: - hA = 0.22f; - hB = 0.30f; - hC = 0.10f; - hD = 0.20f; - hE = 0.01f; - hF = 0.30f; - hW = 11.2f; - break; - } - recalculateWhiteScale(); - } - - @Override - public void processPixel(double[] pixel) { - for (int i = 0; i < 3; ++i) { - pixel[i] *= 2; // exposure bias - pixel[i] = ((pixel[i] * (hA * pixel[i] + hC * hB) + hD * hE) / (pixel[i] * (hA * pixel[i] + hB) + hD * hF)) - hE / hF; - pixel[i] *= whiteScale; - pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); - } - } - - @Override - public String getName() { - return "Hable tone mapping"; - } - - @Override - public String getId() { - return "TONEMAP3"; - } - - @Override - public void loadConfiguration(JsonObject json) { - reset(); - hA = json.get("shoulderStrength").floatValue(hA); - hB = json.get("linearStrength").floatValue(hB); - hC = json.get("linearAngle").floatValue(hC); - hD = json.get("toeStrength").floatValue(hD); - hE = json.get("toeNumerator").floatValue(hE); - hF = json.get("toeDenominator").floatValue(hF); - hW = json.get("linearWhitePointValue").floatValue(hW); - recalculateWhiteScale(); - } - - @Override - public void storeConfiguration(JsonObject json) { - json.add("shoulderStrength", hA); - json.add("linearStrength", hB); - json.add("linearAngle", hC); - json.add("toeStrength", hD); - json.add("toeNumerator", hE); - json.add("toeDenominator", hF); - json.add("linearWhitePointValue", hW); - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableUpdatedFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableUpdatedFilmicFilter.java new file mode 100644 index 0000000000..5090725c73 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableUpdatedFilmicFilter.java @@ -0,0 +1,296 @@ +package se.llbit.chunky.renderer.postprocessing; + +import it.unimi.dsi.fastutil.floats.FloatFloatImmutablePair; +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of John Hable's filmic tonemapping curve updated with improved controls. + * link + */ +public class HableUpdatedFilmicFilter extends SimplePixelPostProcessingFilter { + private float gamma = Scene.DEFAULT_GAMMA; + private float tStr = 0.5f; + private float tLen = 0.5f; + private float sStr = 2f; + private float sLen = 0.5f; + private float sAngle = 1f; + + private FloatFloatImmutablePair asSlopeIntercept(float x0, float x1, float y0, float y1) { + float m; + float b; + float dy = y1 - y0; + float dx = x1 - x0; + if (dx == 0f) { + m = 1f; + } else { + m = dy / dx; + } + b = y0 - x0 * m; + return new FloatFloatImmutablePair(m, b); + } + + private float evalDerivativeLinearGamma(float m, float b, float g, float x) { + return g * m * (float) FastMath.pow(m * x + b, g - 1f); + } + + private FloatFloatImmutablePair solveAB(float x0, float y0, float m) { + float B = (m * x0) / y0; + float lnA = (float) FastMath.log(y0) - B * (float) FastMath.log(x0); + return new FloatFloatImmutablePair(lnA, B); + } + + private float evalCurveSegment(float x, float offsetX, float offsetY, + float scaleX, float scaleY, float lnA, float B) { + float x0 = (x - offsetX) * scaleX; + float y0 = 0f; + if (x0 > 0f) { + y0 = (float) FastMath.exp(lnA + B * FastMath.log(x0)); + } + return y0 * scaleY + offsetY; + } + + @Override + public void processPixel(double[] pixel) { + float tLen_ = (float) FastMath.pow(tLen, 2.2f); + float x0 = 0.5f * tLen_; + float y0 = (1.f - tStr) * x0; + float remainingY = 1.f - y0; + float initialW = x0 + remainingY; + float y1Offset = (1.f - sLen) * remainingY; + float x1 = x0 + y1Offset; + float y1 = y0 + y1Offset; + float extraW = (float) FastMath.pow(2.f, sStr) - 1.f; + float W = initialW + extraW; + float overshootX = (2.f * W) * sAngle * sStr; + float overshootY = 0.5f * sAngle * sStr; + float invGamma = 1.f / gamma; + + float curveWinv = 1.f / W; + x0 /= W; + x1 /= W; + overshootX /= W; + + FloatFloatImmutablePair tmp = asSlopeIntercept(x0, x1, y0, y1); + float m = tmp.firstFloat(); + float b = tmp.secondFloat(); + float g = invGamma; + + float midOffsetX = -(b / m); + float midOffsetY = 0.f; + float midScaleX = 1.f; + float midScaleY = 1.f; + float midLnA = g * (float) FastMath.log(m); + float midB = g; + + float toeM = evalDerivativeLinearGamma(m, b, g, x0); + float shoulderM = evalDerivativeLinearGamma(m, b, g, x1); + + y0 = (float) FastMath.max(1e-5f, FastMath.pow(y0, invGamma)); + y1 = (float) FastMath.max(1e-5f, FastMath.pow(y1, invGamma)); + overshootY = (float) FastMath.pow(1f + overshootY, invGamma) - 1f; + + tmp = solveAB(x0, y0, toeM); + + float toeOffsetX = 0.f; + float toeOffsetY = 0.f; + float toeScaleX = 1.f; + float toeScaleY = 1.f; + float toeLnA = tmp.firstFloat(); + float toeB = tmp.secondFloat(); + + float shoulderX0 = (1.f + overshootX) - x1; + float shoulderY0 = (1.f + overshootY) - y1; + tmp = solveAB(shoulderX0, shoulderY0, shoulderM); + + float shoulderOffsetX = 1.f + overshootX; + float shoulderOffsetY = 1.f + overshootY; + float shoulderScaleX = -1.f; + float shoulderScaleY = -1.f; + float shoulderLnA = tmp.firstFloat(); + float shoulderB = tmp.secondFloat(); + + float scale = evalCurveSegment(1f, shoulderOffsetX, shoulderOffsetY, shoulderScaleX, + shoulderScaleY, shoulderLnA, shoulderB); + float invScale = 1f / scale; + toeOffsetY *= invScale; + toeScaleY *= invScale; + midOffsetY *= invScale; + midScaleY *= invScale; + shoulderOffsetY *= invScale; + shoulderScaleY *= invScale; + + for (int i = 0; i < 3; i++) { + float normX = (float) pixel[i] * curveWinv; + float res; + if (normX < x0) { + res = evalCurveSegment(normX, + toeOffsetX, toeOffsetY, + toeScaleX, toeScaleY, + toeLnA, toeB); + } else if (normX < x1) { + res = evalCurveSegment(normX, + midOffsetX, midOffsetY, + midScaleX, midScaleY, + midLnA, midB); + } else { + res = evalCurveSegment(normX, + shoulderOffsetX, shoulderOffsetY, + shoulderScaleX, shoulderScaleY, + shoulderLnA, shoulderB); + } + pixel[i] = res; + } + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").floatValue(gamma); + tStr = json.get("tStr").floatValue(tStr); + tLen = json.get("tLen").floatValue(tLen); + sStr = json.get("sStr").floatValue(sStr); + sLen = json.get("sLen").floatValue(sLen); + sAngle = json.get("sAngle").floatValue(sAngle); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + json.add("tStr", tStr); + json.add("tLen", tLen); + json.add("sStr", sStr); + json.add("sLen", sLen); + json.add("sAngle", sAngle); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + tStr = 0.5f; + tLen = 0.5f; + sStr = 2f; + sLen = 0.5f; + sAngle = 1f; + } + + @Override + public String getName() { + return "Hable (Updated) Filmic"; + } + + @Override + public String getId() { + return "HABLE_UPDATED_FILMIC"; + } + + @Override + public String getDescription() { + return "John Hable's improved filmic tonemapping curve, based on the original," + + " but with improved controllability.\n" + + "http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster tStr = new DoubleAdjuster(); + DoubleAdjuster tLen = new DoubleAdjuster(); + DoubleAdjuster sStr = new DoubleAdjuster(); + DoubleAdjuster sLen = new DoubleAdjuster(); + DoubleAdjuster sAngle = new DoubleAdjuster(); + + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + tStr.setName("Toe strength"); + tStr.setRange(0, 1); + tStr.set(this.tStr); + tStr.onValueChange(value -> { + this.tStr = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + tLen.setName("Toe length"); + tLen.setRange(0, 1); + tLen.set(this.tLen); + tLen.onValueChange(value -> { + this.tLen = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + sStr.setName("Shoulder strength"); + sStr.setRange(0, 10); + sStr.set(this.sStr); + sStr.onValueChange(value -> { + this.sStr = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + sLen.setName("Shoulder length"); + sLen.setRange(1e-5, 1 - 1e-5); + sLen.setMaximumFractionDigits(5); + sLen.set(this.sLen); + sLen.onValueChange(value -> { + this.sLen = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + sAngle.setName("Shoulder angle"); + sAngle.setRange(0, 1); + sAngle.set(this.sAngle); + sAngle.onValueChange(value -> { + this.sAngle = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma, tStr, tLen, sStr, sLen, sAngle); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HejlBurgessDawsonFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HejlBurgessDawsonFilmicFilter.java new file mode 100644 index 0000000000..525bcf73f1 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HejlBurgessDawsonFilmicFilter.java @@ -0,0 +1,46 @@ +package se.llbit.chunky.renderer.postprocessing; + +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; + +/** + * Implementation of Jim Hejl and Richard Burgess-Dawson's filmic tonemapping curve + * link + */ +public class HejlBurgessDawsonFilmicFilter extends SimplePixelPostProcessingFilter { + @Override + public void processPixel(double[] pixel) { + for(int i = 0; i < 3; ++i) { + pixel[i] = QuickMath.max(0, pixel[i] - 0.004); + pixel[i] = (pixel[i] * (6.2 * pixel[i] + .5)) / (pixel[i] * (6.2 * pixel[i] + 1.7) + 0.06); + } + } + + @Override + public String getName() { + return "Hejl Burgess-Dawson Filmic"; + } + + @Override + public String getId() { + return "TONEMAP1"; + } + + @Override + public String getDescription() { + return "Jim Hejl and Richard Burgess-Dawson's filmic tonemapping curve.\n" + + "http://filmicworlds.com/blog/filmic-tonemapping-operators/"; + } + + @Override + public void fromJson(JsonObject json) { + } + + @Override + public void filterSettingsToJson(JsonObject json) { + } + + @Override + public void reset() { + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HillACESFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HillACESFilmicFilter.java new file mode 100644 index 0000000000..2d8ff20f58 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HillACESFilmicFilter.java @@ -0,0 +1,102 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Stephen Hill's ACES filmic tonemapping curve. + * link + */ +public class HillACESFilmicFilter extends SimplePixelPostProcessingFilter { + private float gamma = Scene.DEFAULT_GAMMA; + + @Override + public void processPixel(double[] pixel) { + double a = 0.59719 * pixel[0] + 0.35458 * pixel[1] + 0.04823 * pixel[2]; + double b = 0.07600 * pixel[0] + 0.90834 * pixel[1] + 0.01566 * pixel[2]; + double c = 0.02840 * pixel[0] + 0.13383 * pixel[1] + 0.83777 * pixel[2]; + pixel[0] = a; + pixel[1] = b; + pixel[2] = c; + + for (int i = 0; i < 3; i++) { + a = pixel[i] * (pixel[i] + 0.0245786) - 0.000090537; + b = pixel[i] * (0.983729 * pixel[i] + 0.4329510) + 0.238081; + pixel[i] = a / b; + } + + a = 1.60475 * pixel[0] - 0.53108 * pixel[1] - 0.07367 * pixel[2]; + b = -0.10208 * pixel[0] + 1.10813 * pixel[1] - 0.00605 * pixel[2]; + c = -0.00327 * pixel[0] - 0.07276 * pixel[1] + 1.07602 * pixel[2]; + pixel[0] = a; + pixel[1] = b; + pixel[2] = c; + + for (int i = 0; i < 3; i++) { + pixel[i] = FastMath.pow(pixel[i], 1f / gamma); + } + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").floatValue(gamma); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + } + + @Override + public String getName() { + return "Hill ACES Filmic"; + } + + @Override + public String getId() { + return "HILL_ACES_FILMIC"; + } + + @Override + public String getDescription() { + return "Stephen Hill's ACES filmic tonemapping curve.\n" + + "https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/LottesFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/LottesFilmicFilter.java new file mode 100644 index 0000000000..697fdcdf8f --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/LottesFilmicFilter.java @@ -0,0 +1,187 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Timothy Lottes' filmic tonemapping curve. + * link + */ +public class LottesFilmicFilter extends SimplePixelPostProcessingFilter { + + private float gamma = Scene.DEFAULT_GAMMA; + private float contrast = 1.6f; + private float shoulder = 0.977f; + private float hdrMax = 8f; + private float midIn = 0.18f; + private float midOut = 0.267f; + + @Override + public void processPixel(double[] pixel) { + // Code adapted from Tizian Zeltner's implementation of the tonemapping curve. + // https://github.com/tizian/tonemapper/blob/fe100b9052e91d034927779d22a5afed9bedc1e3/src/operators/LottesFilmicOperator.cpp#L64 + + float a = contrast; + float d = shoulder; + float b = ((float) -FastMath.pow(midIn, a) + (float) FastMath.pow(hdrMax, a) * midOut) / + (((float) FastMath.pow(hdrMax, a * d) - (float) FastMath.pow(midIn, a * d)) * + midOut); + float c = ((float) FastMath.pow(hdrMax, a * d) * (float) FastMath.pow(midIn, a) - + (float) FastMath.pow(hdrMax, a) * (float) FastMath.pow(midIn, a * d) * midOut) / + (((float) FastMath.pow(hdrMax, a * d) - (float) FastMath.pow(midIn, a * d)) * + midOut); + for (int i = 0; i < 3; i++) { + pixel[i] = FastMath.pow(pixel[i], a) / (FastMath.pow(pixel[i], a * d) * b + c); + pixel[i] = FastMath.pow(pixel[i], 1f / gamma); + } + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").floatValue(gamma); + contrast = json.get("contrast").floatValue(contrast); + shoulder = json.get("shoulder").floatValue(shoulder); + hdrMax = json.get("hdrMax").floatValue(hdrMax); + midIn = json.get("midIn").floatValue(midIn); + midOut = json.get("midOut").floatValue(midOut); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + json.add("contrast", contrast); + json.add("shoulder", shoulder); + json.add("hdrMax", hdrMax); + json.add("midIn", midIn); + json.add("midOut", midOut); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + contrast = 1.6f; + shoulder = 0.977f; + hdrMax = 8f; + midIn = 0.18f; + midOut = 0.267f; + } + + @Override + public String getName() { + return "Lottes Filmic"; + } + + @Override + public String getId() { + return "LOTTES_FILMIC"; + } + + @Override + public String getDescription() { + return "Timothy Lottes' filmic tonemapping curve.\n" + + "https://gdcvault.com/play/1023512/Advanced-Graphics-Techniques-Tutorial-Day"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster contrast = new DoubleAdjuster(); + DoubleAdjuster shoulder = new DoubleAdjuster(); + DoubleAdjuster hdrMax = new DoubleAdjuster(); + DoubleAdjuster midIn = new DoubleAdjuster(); + DoubleAdjuster midOut = new DoubleAdjuster(); + + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + contrast.setName("Contrast"); + contrast.setRange(1, 2); + contrast.set(this.contrast); + contrast.onValueChange(value -> { + this.contrast = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + shoulder.setName("Shoulder"); + shoulder.setRange(0.01, 2); + shoulder.set(this.shoulder); + shoulder.onValueChange(value -> { + this.shoulder = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + hdrMax.setName("Maximum HDR value"); + hdrMax.setRange(1, 10); + hdrMax.set(this.hdrMax); + hdrMax.onValueChange(value -> { + this.hdrMax = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + midIn.setName("Input mid-level"); + midIn.setRange(0, 1); + midIn.set(this.midIn); + midIn.onValueChange(value -> { + this.midIn = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + midOut.setName("Output mid-level"); + midOut.setRange(0, 1); + midOut.set(this.midOut); + midOut.onValueChange(value -> { + this.midOut = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma, contrast, shoulder, hdrMax, midIn, midOut); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NarkowiczACESFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NarkowiczACESFilmicFilter.java new file mode 100644 index 0000000000..02b65f5c88 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NarkowiczACESFilmicFilter.java @@ -0,0 +1,88 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Krzysztof Narkowicz's ACES filmic tonemapping curve + * link + */ +public class NarkowiczACESFilmicFilter extends SimplePixelPostProcessingFilter { + private static final float aces_a = 2.51f; + private static final float aces_b = 0.03f; + private static final float aces_c = 2.43f; + private static final float aces_d = 0.59f; + private static final float aces_e = 0.14f; + private double gamma = Scene.DEFAULT_GAMMA; + + @Override + public void processPixel(double[] pixel) { + for(int i = 0; i < 3; ++i) { + pixel[i] = QuickMath.max(QuickMath.min((pixel[i] * (aces_a * pixel[i] + aces_b)) / (pixel[i] * (aces_c * pixel[i] + aces_d) + aces_e), 1), 0); + pixel[i] = FastMath.pow(pixel[i], 1 / gamma); + } + } + + @Override + public String getName() { + return "Narkowicz ACES Filmic"; + } + + @Override + public String getId() { + return "TONEMAP2"; + } + + @Override + public String getDescription() { + return "Krzysztof Narkowicz's ACES filmic tonemapping curve.\n" + + "https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/"; + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").doubleValue(gamma); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + } + + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NoneFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NoneFilter.java deleted file mode 100644 index d53da56ee1..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/NoneFilter.java +++ /dev/null @@ -1,17 +0,0 @@ -package se.llbit.chunky.renderer.postprocessing; - -public class NoneFilter extends SimplePixelPostProcessingFilter { - @Override - public void processPixel(double[] pixel) { - } - - @Override - public String getName() { - return "None"; - } - - @Override - public String getId() { - return "NONE"; - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PixelPostProcessingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PixelPostProcessingFilter.java index 217bdfde7d..c7961ba9df 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PixelPostProcessingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PixelPostProcessingFilter.java @@ -6,7 +6,7 @@ * Post-processing filter that supports processing one pixel at a time. */ @PluginApi -public interface PixelPostProcessingFilter extends PostProcessingFilter { +public abstract class PixelPostProcessingFilter extends PostProcessingFilter { /** * Post process a single pixel * @param width The width of the image @@ -14,8 +14,15 @@ public interface PixelPostProcessingFilter extends PostProcessingFilter { * @param input The input linear image as double array * @param x The x position of the pixel to process * @param y The y position of the pixel to process - * @param exposure The exposure value * @param output The output buffer for the processed pixel */ - void processPixel(int width, int height, double[] input, int x, int y, double exposure, double[] output); + public abstract void processPixel(int width, int height, double[] input, int x, int y, + double[] output); + + /** + * Post-process a single pixel after exposure has been applied + * @param pixel Input/Output - the rgb component of the pixel with already applied exposure. + * Will also be clamped afterwards. + */ + public abstract void processPixel(double[] pixel); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java index 40afc3ddde..e67fa13cf2 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java @@ -1,38 +1,48 @@ package se.llbit.chunky.renderer.postprocessing; import se.llbit.chunky.plugin.PluginApi; -import se.llbit.chunky.resources.BitmapImage; +import se.llbit.json.JsonObject; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; import se.llbit.util.Registerable; -import se.llbit.util.TaskTracker; /** * A post-processing filter. *

* These filters are used to convert the HDR sample buffer into an SDR image that can be displayed. - * Exposure is also applied by the filter. + * Exposure is applied before filters are applied. *

* Filters that support processing a single pixel at a time should implement {@link * PixelPostProcessingFilter} instead. */ @PluginApi -public interface PostProcessingFilter extends Registerable { +public abstract class PostProcessingFilter implements Registerable, Configurable, HasControls { /** * Post process the entire frame * @param width The width of the image * @param height The height of the image - * @param input The input linear image as double array, exposure has not been applied - * @param output The output image - * @param exposure The exposure value - * @param task Task + * @param input The input linear image as double array, exposure has been applied */ - void processFrame(int width, int height, double[] input, BitmapImage output, double exposure, TaskTracker.Task task); + public abstract void processFrame(int width, int height, double[] input); /** * Get description of the post processing filter * @return The description of the post processing filter */ @Override - default String getDescription() { - return null; + public abstract String getDescription(); + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("id", getId()); + filterSettingsToJson(json); + return json; } + + /** + * Write filter-specific settings to the JSON + * @param json The JsonObject to write settings to + */ + public abstract void filterSettingsToJson(JsonObject json); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java index 7aadb64d07..802ab9254f 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java @@ -4,41 +4,70 @@ import java.util.*; -public abstract class PostProcessingFilters { +public class PostProcessingFilters { + /** + * Don't let anyone instantiate this class. + */ + private PostProcessingFilters() {} - public static final PixelPostProcessingFilter NONE = new NoneFilter(); - - private static final Map filters = new HashMap<>(); + private static final Map> filterClassesById = new HashMap<>(); // using tree map for the added benefit of sorting by name - private static final Map filtersByName = new TreeMap<>(); + private static final Map, String> filterNamesByClass = new HashMap<>(); + private static final List> filterClasses = new ArrayList<>(0); + private static final Map> filterClassesByName = new TreeMap<>(); + private static final Map, PostProcessingFilter> filtersByClass = new HashMap<>(0); static { - addPostProcessingFilter(NONE); - addPostProcessingFilter(new GammaCorrectionFilter()); - addPostProcessingFilter(new Tonemap1Filter()); - addPostProcessingFilter(new ACESFilmicFilter()); - addPostProcessingFilter(new HableToneMappingFilter()); - addPostProcessingFilter(new UE4ToneMappingFilter()); + addPostProcessingFilter(GammaCorrectionFilter.class); + addPostProcessingFilter(HejlBurgessDawsonFilmicFilter.class); + addPostProcessingFilter(AldridgeFilmicFilter.class); + addPostProcessingFilter(HableFilmicFilter.class); + addPostProcessingFilter(HableUpdatedFilmicFilter.class); + addPostProcessingFilter(LottesFilmicFilter.class); + addPostProcessingFilter(DayFilmicFilter.class); + addPostProcessingFilter(UchimuraFilmicFilter.class); + addPostProcessingFilter(HillACESFilmicFilter.class); + addPostProcessingFilter(NarkowiczACESFilmicFilter.class); + addPostProcessingFilter(GuyACESFilmicFilter.class); + addPostProcessingFilter(UE4FilmicFilter.class); + addPostProcessingFilter(AgXFilter.class); + addPostProcessingFilter(VignetteFilter.class); } - public static Optional getPostProcessingFilterFromId(String id) { - return Optional.ofNullable(filters.get(id)); + public static Optional> getPostProcessingFilterFromId(String id) { + return Optional.ofNullable(filterClassesById.get(id)); } // TODO Create a ChoiceBox that can use different string as ID and as visual representation // so this isn't needed @Deprecated - public static Optional getPostProcessingFilterFromName(String name) { - return Optional.ofNullable(filtersByName.get(name)); + public static Optional> getPostProcessingFilterFromName(String name) { + return Optional.ofNullable(filterClassesByName.get(name)); + } + + public static String getFilterName(Class filterClass) { + return filterNamesByClass.get(filterClass); + } + + public static Collection> getFilterClasses() { + return filterClasses; } - public static Collection getFilters() { - return filtersByName.values(); + public static PostProcessingFilter getSampleFilterFromClass(Class filterClass) { + return filtersByClass.get(filterClass); } @PluginApi - public static void addPostProcessingFilter(PostProcessingFilter filter) { - filters.put(filter.getId(), filter); - filtersByName.put(filter.getName(), filter); + public static void addPostProcessingFilter(Class filterClass) { + try { + PostProcessingFilter filter = filterClass.newInstance(); + filterClasses.add(filterClass); + filterClassesById.put(filter.getId(), filterClass); + filterNamesByClass.put(filterClass, filter.getName()); + filterClassesByName.put(filter.getName(), filterClass); + filtersByClass.put(filterClass, filter); + } catch (InstantiationException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PreviewFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PreviewFilter.java index 2c796e295c..9a16eb8cfc 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PreviewFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PreviewFilter.java @@ -1,6 +1,7 @@ package se.llbit.chunky.renderer.postprocessing; import org.apache.commons.math3.util.FastMath; +import se.llbit.json.JsonObject; public class PreviewFilter extends SimplePixelPostProcessingFilter { public static final PreviewFilter INSTANCE = new PreviewFilter(); @@ -14,11 +15,28 @@ public void processPixel(double[] pixel) { @Override public String getName() { - return null; + return "Preview filter"; } @Override public String getId() { - return null; + return "PREVIEW"; + } + + @Override + public String getDescription() { + return "Tonemapping curve used by the PreviewRenderer."; + } + + @Override + public void fromJson(JsonObject json) { + } + + @Override + public void filterSettingsToJson(JsonObject json) { + } + + @Override + public void reset() { } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/SimplePixelPostProcessingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/SimplePixelPostProcessingFilter.java index 98cd9bb047..1adb628419 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/SimplePixelPostProcessingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/SimplePixelPostProcessingFilter.java @@ -18,72 +18,44 @@ package se.llbit.chunky.renderer.postprocessing; import se.llbit.chunky.main.Chunky; -import se.llbit.chunky.resources.BitmapImage; -import se.llbit.math.ColorUtil; -import se.llbit.util.TaskTracker; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; /** - * Base class for post processing filter that process each pixel independently + * Base class for postprocessing filter that process each pixel independently */ -public abstract class SimplePixelPostProcessingFilter implements PixelPostProcessingFilter { - /** - * Post-process a single channel of a single pixel - * @param pixel Input/Output - the rgb component of the pixel with already applied exposure. - * Will also be clamped afterwards. - */ - public abstract void processPixel(double[] pixel); - +public abstract class SimplePixelPostProcessingFilter extends PixelPostProcessingFilter { @Override - public void processFrame( - int width, int height, - double[] input, BitmapImage output, - double exposure, TaskTracker.Task task - ) { - task.update(height, 0); - AtomicInteger done = new AtomicInteger(0); + public void processFrame(int width, int height, double[] sampleBuffer) { Chunky.getCommonThreads() - .submit(() -> - // do rows in parallel - IntStream.range(0, height).parallel() - .forEach(y -> { - double[] pixelBuffer = new double[3]; - - int rowOffset = y * width; - // columns will be processed sequential - // TODO: SIMD support once Vector API is finalized - for (int x = 0; x < width; x++) { - int pixelOffset = (rowOffset + x) * 3; - for(int i = 0; i < 3; ++i) { - pixelBuffer[i] = input[pixelOffset + i] * exposure; - } - processPixel(pixelBuffer); - for(int i = 0; i < 3; ++i) { - // TODO: extract clamping into own interface - pixelBuffer[i] = Math.min(1.0, pixelBuffer[i]); - } - output.setPixel(x, y, ColorUtil.getRGB(pixelBuffer)); - } + .submit(() -> + // do rows in parallel + IntStream.range(0, height).parallel() + .forEach(y -> { + double[] pixelBuffer = new double[3]; - task.update(height, done.incrementAndGet()); - }) - ).join(); + int rowOffset = y * width; + // columns will be processed sequential + // TODO: SIMD support once Vector API is finalized + for (int x = 0; x < width; x++) { + int pixelOffset = (rowOffset + x) * 3; + System.arraycopy(sampleBuffer, pixelOffset, pixelBuffer, 0, 3); + processPixel(pixelBuffer); + System.arraycopy(pixelBuffer, 0, sampleBuffer, pixelOffset, 3); + } + }) + ).join(); } @Override - public void processPixel( - int width, int height, - double[] input, - int x, int y, - double exposure, - double[] output + public final void processPixel( + int width, int height, + double[] input, + int x, int y, + double[] output ) { int index = (y * width + x) * 3; - for(int i = 0; i < 3; ++i) { - output[i] = input[index + i] * exposure; - } + System.arraycopy(input, index, output, 0, 3); processPixel(output); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/Tonemap1Filter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/Tonemap1Filter.java deleted file mode 100644 index 01b4600dd7..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/Tonemap1Filter.java +++ /dev/null @@ -1,27 +0,0 @@ -package se.llbit.chunky.renderer.postprocessing; - -import se.llbit.math.QuickMath; - -/** - * Implementation of the tone mapping operator from Jim Hejl and Richard Burgess-Dawson - * @link http://filmicworlds.com/blog/filmic-tonemapping-operators/ - */ -public class Tonemap1Filter extends SimplePixelPostProcessingFilter { - @Override - public void processPixel(double[] pixel) { - for(int i = 0; i < 3; ++i) { - pixel[i] = QuickMath.max(0, pixel[i] - 0.004); - pixel[i] = (pixel[i] * (6.2 * pixel[i] + .5)) / (pixel[i] * (6.2 * pixel[i] + 1.7) + 0.06); - } - } - - @Override - public String getName() { - return "Tonemap operator 1"; - } - - @Override - public String getId() { - return "TONEMAP1"; - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4FilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4FilmicFilter.java new file mode 100644 index 0000000000..46bd2d79de --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4FilmicFilter.java @@ -0,0 +1,340 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of the Unreal Engine 4 Filmic Tone Mapper. + * + * @link + * https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/ColorGrading/ + * @link https://www.desmos.com/calculator/h8rbdpawxj?lang=de + */ +public class UE4FilmicFilter extends SimplePixelPostProcessingFilter { + + public enum Preset { + /** + * ACES curve parameters + **/ + ACES, + /** + * UE4 legacy tone mapping style + **/ + LEGACY_UE4 + } + + private float saturation; + private float slope; // ga + private float toe; // t0 + private float shoulder; // s0 + private float blackClip; // t1 + private float whiteClip; // s1 + + private float ta; + private float sa; + + private float gamma; + + public UE4FilmicFilter() { + reset(); + } + + private void recalculateConstants() { + ta = (1f - toe - 0.18f) / slope - 0.733f; + sa = (shoulder - 0.18f) / slope - 0.733f; + } + + public float getSaturation() { + return saturation; + } + + public void setSaturation(float saturation) { + this.saturation = saturation; + } + + public float getSlope() { + return slope; + } + + public void setSlope(float slope) { + this.slope = slope; + this.recalculateConstants(); + } + + public float getToe() { + return toe; + } + + public void setToe(float toe) { + this.toe = toe; + recalculateConstants(); + } + + public float getShoulder() { + return shoulder; + } + + public void setShoulder(float shoulder) { + this.shoulder = shoulder; + recalculateConstants(); + } + + public float getBlackClip() { + return blackClip; + } + + public void setBlackClip(float blackClip) { + this.blackClip = blackClip; + } + + public float getWhiteClip() { + return whiteClip; + } + + public void setWhiteClip(float whiteClip) { + this.whiteClip = whiteClip; + } + + public float getGamma() { + return gamma; + } + + public void setGamma(float gamma) { + this.gamma = gamma; + } + + public void applyPreset(Preset preset) { + switch (preset) { + case ACES: + saturation = 1f; + slope = 0.88f; + toe = 0.55f; + shoulder = 0.26f; + blackClip = 0.0f; + whiteClip = 0.04f; + gamma = Scene.DEFAULT_GAMMA; + break; + case LEGACY_UE4: + saturation = 1f; + slope = 0.98f; + toe = 0.3f; + shoulder = 0.22f; + blackClip = 0.0f; + whiteClip = 0.025f; + gamma = Scene.DEFAULT_GAMMA; + break; + } + recalculateConstants(); + } + + public void reset() { + applyPreset(Preset.ACES); + } + + private float processComponent(float c) { + float logc = (float) Math.log10(c); + + if (logc >= ta && logc <= sa) { + return (float) (saturation * (slope * (logc + 0.733) + 0.18)); + } + if (logc > sa) { + return (float) (saturation * (1 + whiteClip - (2 * (1 + whiteClip - shoulder)) / (1 + + Math.exp(((2 * slope) / (1 + whiteClip - shoulder)) * (logc - sa))))); + } + // if (logc < ta) { + return (float) (saturation * ((2 * (1 + blackClip - toe)) / (1 + Math.exp( + -((2 * slope) / (1 + blackClip - toe)) * (logc - ta))) - blackClip)); + // } + } + + @Override + public void processPixel(double[] pixel) { + for (int i = 0; i < 3; ++i) { + pixel[i] = QuickMath.max(QuickMath.min(processComponent((float) pixel[i] * 1.25f), 1), 0); + pixel[i] = FastMath.pow(pixel[i], 1 / gamma); + } + } + + @Override + public String getName() { + return "Unreal Engine 4 Filmic"; + } + + @Override + public String getId() { + return "UE4_FILMIC"; + } + + @Override + public String getDescription() { + return "Unreal Engine 4 Filmic Tone Mapper with two presets.\n" + + "https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/ColorGrading/"; + } + + @Override + public void fromJson(JsonObject json) { + reset(); + saturation = json.get("saturation").floatValue(saturation); + slope = json.get("slope").floatValue(slope); + toe = json.get("toe").floatValue(toe); + shoulder = json.get("shoulder").floatValue(shoulder); + blackClip = json.get("blackClip").floatValue(blackClip); + whiteClip = json.get("whiteClip").floatValue(whiteClip); + gamma = json.get("gamma").floatValue(gamma); + recalculateConstants(); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("saturation", saturation); + json.add("slope", slope); + json.add("toe", toe); + json.add("shoulder", shoulder); + json.add("blackClip", blackClip); + json.add("whiteClip", whiteClip); + json.add("gamma", gamma); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + MenuButton presetChooser = new MenuButton(); + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster saturation = new DoubleAdjuster(); + DoubleAdjuster slope = new DoubleAdjuster(); + DoubleAdjuster toe = new DoubleAdjuster(); + DoubleAdjuster shoulder = new DoubleAdjuster(); + DoubleAdjuster blackClip = new DoubleAdjuster(); + DoubleAdjuster whiteClip = new DoubleAdjuster(); + + presetChooser.setText("Load preset"); + for (UE4FilmicFilter.Preset preset : UE4FilmicFilter.Preset.values()) { + MenuItem menuItem = new MenuItem(preset.toString()); + menuItem.setOnAction(e -> { + applyPreset(preset); + gamma.set(getGamma()); + saturation.set(getSaturation()); + slope.set(getSlope()); + toe.set(getToe()); + shoulder.set(getShoulder()); + blackClip.set(getBlackClip()); + whiteClip.set(getWhiteClip()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + presetChooser.getItems().add(menuItem); + } + + gamma.setName("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(getGamma()); + gamma.onValueChange(value -> { + setGamma(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + saturation.setName("Saturation"); + saturation.setRange(0, 2); + saturation.set(getSaturation()); + saturation.onValueChange(value -> { + setSaturation(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + slope.setName("Slope"); + slope.setRange(0, 1); + slope.set(getSlope()); + slope.onValueChange(value -> { + setSlope(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + toe.setName("Toe"); + toe.setRange(0, 1); + toe.set(getToe()); + toe.onValueChange(value -> { + setToe(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + shoulder.setName("Shoulder"); + shoulder.setRange(0, 1); + shoulder.set(getShoulder()); + shoulder.onValueChange(value -> { + setShoulder(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + blackClip.setName("Black clip"); + blackClip.setRange(0, 1); + blackClip.set(getBlackClip()); + blackClip.onValueChange(value -> { + setBlackClip(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + whiteClip.setName("White clip"); + whiteClip.setRange(0, 1); + whiteClip.set(getWhiteClip()); + whiteClip.onValueChange(value -> { + setWhiteClip(value.floatValue()); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, presetChooser, gamma, saturation, slope, toe, shoulder, blackClip, + whiteClip); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java deleted file mode 100644 index 921a631604..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java +++ /dev/null @@ -1,176 +0,0 @@ -package se.llbit.chunky.renderer.postprocessing; - -import org.apache.commons.math3.util.FastMath; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.json.JsonObject; -import se.llbit.math.QuickMath; -import se.llbit.util.Configurable; - -/** - * Implementation of the Unreal Engine 4 Filmic Tone Mapper. - * - * @link https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/ColorGrading/ - * @link https://www.desmos.com/calculator/h8rbdpawxj?lang=de - */ -public class UE4ToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { - public enum Preset { - /** - * ACES curve parameters - **/ - ACES, - /** - * UE4 legacy tone mapping style - **/ - LEGACY_UE4 - } - - private float saturation; - private float slope; // ga - private float toe; // t0 - private float shoulder; // s0 - private float blackClip; // t1 - private float whiteClip; // s1 - - private float ta; - private float sa; - - public UE4ToneMappingFilter() { - reset(); - } - - private void recalculateConstants() { - ta = (1f - toe - 0.18f) / slope - 0.733f; - sa = (shoulder - 0.18f) / slope - 0.733f; - } - - public float getSaturation() { - return saturation; - } - - public void setSaturation(float saturation) { - this.saturation = saturation; - } - - public float getSlope() { - return slope; - } - - public void setSlope(float slope) { - this.slope = slope; - this.recalculateConstants(); - } - - public float getToe() { - return toe; - } - - public void setToe(float toe) { - this.toe = toe; - recalculateConstants(); - } - - public float getShoulder() { - return shoulder; - } - - public void setShoulder(float shoulder) { - this.shoulder = shoulder; - recalculateConstants(); - } - - public float getBlackClip() { - return blackClip; - } - - public void setBlackClip(float blackClip) { - this.blackClip = blackClip; - } - - public float getWhiteClip() { - return whiteClip; - } - - public void setWhiteClip(float whiteClip) { - this.whiteClip = whiteClip; - } - - public void applyPreset(Preset preset) { - switch (preset) { - case ACES: - saturation = 1f; - slope = 0.88f; - toe = 0.55f; - shoulder = 0.26f; - blackClip = 0.0f; - whiteClip = 0.04f; - break; - case LEGACY_UE4: - saturation = 1f; - slope = 0.98f; - toe = 0.3f; - shoulder = 0.22f; - blackClip = 0.0f; - whiteClip = 0.025f; - break; - } - recalculateConstants(); - } - - public void reset() { - applyPreset(Preset.ACES); - } - - private float processComponent(float c) { - float logc = (float) Math.log10(c); - - if (logc >= ta && logc <= sa) { - return (float) (saturation * (slope * (logc + 0.733) + 0.18)); - } - if (logc > sa) { - return (float) (saturation * (1 + whiteClip - (2 * (1 + whiteClip - shoulder)) / (1 + Math.exp(((2 * slope) / (1 + whiteClip - shoulder)) * (logc - sa))))); - } - // if (logc < ta) { - return (float) (saturation * ((2 * (1 + blackClip - toe)) / (1 + Math.exp(-((2 * slope) / (1 + blackClip - toe)) * (logc - ta))) - blackClip)); - // } - } - - @Override - public void processPixel(double[] pixel) { - for (int i = 0; i < 3; ++i) { - pixel[i] = QuickMath.max(QuickMath.min(processComponent((float) pixel[i] * 1.25f), 1), 0); - pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); - } - } - - @Override - public String getName() { - return "Unreal Engine 4 Filmic tone mapping"; - } - - @Override - public String getId() { - return "UE4_FILMIC"; - } - - @Override - public void loadConfiguration(JsonObject json) { - reset(); - saturation = json.get("saturation").floatValue(saturation); - slope = json.get("slope").floatValue(slope); - toe = json.get("toe").floatValue(toe); - shoulder = json.get("shoulder").floatValue(shoulder); - blackClip = json.get("blackClip").floatValue(blackClip); - whiteClip = json.get("whiteClip").floatValue(whiteClip); - recalculateConstants(); - } - - @Override - public void storeConfiguration(JsonObject json) { - json.add("saturation", saturation); - json.add("slope", slope); - json.add("toe", toe); - json.add("shoulder", shoulder); - json.add("blackClip", blackClip); - json.add("whiteClip", whiteClip); - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UchimuraFilmicFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UchimuraFilmicFilter.java new file mode 100644 index 0000000000..0d26521e09 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UchimuraFilmicFilter.java @@ -0,0 +1,218 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +/** + * Implementation of Hajime Uchimura's filmic tonemapping curve. + * link + */ +public class UchimuraFilmicFilter extends SimplePixelPostProcessingFilter { + + private float gamma = Scene.DEFAULT_GAMMA; + private float P = 1f; + private float a = 1f; + private float m = 0.22f; + private float l = 0.4f; + private float c = 1.33f; + private float b = 0f; + + private static double smoothstep(double edge0, double edge1, double x) { + float t = (float) QuickMath.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0f - 2.0f * t); + } + + @Override + public void processPixel(double[] pixel) { + // Code adapted from Tizian Zeltner's implementation of the tonemapping curve. + // https://github.com/tizian/tonemapper/blob/fe100b9052e91d034927779d22a5afed9bedc1e3/src/operators/UchimuraFilmicOperator.cpp#L78 + + float l0 = ((P - m) * l) / a; + float S0 = m + l0; + float S1 = m + a * l0; + float C2 = (a * P) / (P - S1); + float CP = -C2 / P; + + for (int i = 0; i < 3; i++) { + double w0 = 1.0 - smoothstep(0.0, m, pixel[i]); + double w2 = smoothstep(m + l0, m + l0, pixel[i]); + double w1 = 1.0 - w0 - w2; + + double T = m * FastMath.pow(pixel[i] / m, c) + b; + double L = m + a * (pixel[i] - m); + double S = P - (P - S1) * FastMath.exp(CP * (pixel[i] - S0)); + + pixel[i] = T * w0 + L * w1 + S * w2; + pixel[i] = FastMath.pow(pixel[i], 1d / gamma); + } + } + + @Override + public void fromJson(JsonObject json) { + gamma = json.get("gamma").floatValue(gamma); + P = json.get("P").floatValue(P); + a = json.get("a").floatValue(a); + m = json.get("m").floatValue(m); + l = json.get("l").floatValue(l); + c = json.get("c").floatValue(c); + b = json.get("b").floatValue(b); + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("gamma", gamma); + json.add("P", P); + json.add("a", a); + json.add("m", m); + json.add("l", l); + json.add("c", c); + json.add("b", b); + } + + @Override + public void reset() { + gamma = Scene.DEFAULT_GAMMA; + P = 1f; + a = 1f; + m = 0.22f; + l = 0.4f; + c = 1.33f; + b = 0f; + } + + @Override + public String getName() { + return "Uchimura Filmic"; + } + + @Override + public String getId() { + return "UCHIMURA_FILMIC"; + } + + @Override + public String getDescription() { + return "Hajime Uchimura's filmic tonemapping curve.\n" + + "https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp"; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster gamma = new DoubleAdjuster(); + DoubleAdjuster P = new DoubleAdjuster(); + DoubleAdjuster a = new DoubleAdjuster(); + DoubleAdjuster m = new DoubleAdjuster(); + DoubleAdjuster l = new DoubleAdjuster(); + DoubleAdjuster c = new DoubleAdjuster(); + DoubleAdjuster b = new DoubleAdjuster(); + + gamma.setName("Gamma"); + gamma.setTooltip("Gamma correction value"); + gamma.setRange(0.001, 5); + gamma.clampMin(); + gamma.set(this.gamma); + gamma.onValueChange(value -> { + this.gamma = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + P.setName("Maximum brightness"); + P.setRange(1, 100); + P.set(this.P); + P.onValueChange(value -> { + this.P = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + a.setName("Contrast"); + a.setRange(0, 5); + a.clampMin(); + a.set(this.a); + a.onValueChange(value -> { + this.a = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + m.setName("Linear section start"); + m.setRange(0, 1); + m.set(this.m); + m.onValueChange(value -> { + this.m = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + l.setName("Linear section length"); + l.setRange(0.01, 0.99); + l.clampBoth(); + l.set(this.l); + l.onValueChange(value -> { + this.l = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + c.setName("Black tightness shape"); + c.setRange(1, 3); + c.set(this.c); + c.onValueChange(value -> { + this.c = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + b.setName("Black tightness offset"); + b.setRange(0, 1); + b.set(this.b); + b.onValueChange(value -> { + this.b = value.floatValue(); + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, gamma, P, a, m, l, c, b); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/VignetteFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/VignetteFilter.java new file mode 100644 index 0000000000..69761006d5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/VignetteFilter.java @@ -0,0 +1,183 @@ +package se.llbit.chunky.renderer.postprocessing; + +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.math.Vector2; +import se.llbit.math.Vector3; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +public class VignetteFilter extends PostProcessingFilter { + private double vignetteFalloff = 1; + private double vignetteStrength = 1; + private double vignetteDesaturation = 0; + private final Vector2 center = new Vector2(0.5, 0.5); + private double aspectRatio = 1; + + @Override + public void processFrame(int width, int height, double[] input) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixelIndex = (y * width + x) * 3; + Vector2 uv = new Vector2((double) x / width, (double) y / height); + Vector2 fromCenter = new Vector2((center.x - uv.x) * aspectRatio, center.y - uv.y); + double fromCenterLength = FastMath.sqrt(fromCenter.lengthSquared()); + double vignette = FastMath.pow(fromCenterLength, vignetteFalloff) * vignetteStrength; + double lengthColor = new Vector3(input[pixelIndex], input[pixelIndex + 1], input[pixelIndex + 2]).length(); + for (int i = 0; i < 3; i++) { + input[pixelIndex + i] *= QuickMath.clamp(1 - vignette, 0, 1); + input[pixelIndex + i] = input[pixelIndex + i] + vignette * vignetteDesaturation * (lengthColor - input[pixelIndex + i]); + } + } + } + } + + @Override + public String getName() { + return "Vignette"; + } + + @Override + public String getDescription() { + return ""; + } + + @Override + public String getId() { + return "VIGNETTE"; + } + + @Override + public void filterSettingsToJson(JsonObject json) { + json.add("vignetteFalloff", vignetteFalloff); + json.add("vignetteStrength", vignetteStrength); + json.add("vignetteDesaturation", vignetteDesaturation); + json.add("center", center.toJson()); + json.add("aspectRatio", aspectRatio); + } + + @Override + public void fromJson(JsonObject json) { + vignetteFalloff = json.get("vignetteFalloff").doubleValue(1); + vignetteStrength = json.get("vignetteStrength").doubleValue(1); + vignetteDesaturation = json.get("vignetteDesaturation").doubleValue(0); + center.fromJson(json.get("center").asObject()); + aspectRatio = json.get("aspectRatio").doubleValue(1); + } + + @Override + public void reset() { + vignetteFalloff = 1; + vignetteStrength = 1; + vignetteDesaturation = 0; + center.set(0.5, 0.5); + aspectRatio = 1; + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + RenderControlsFxController controller = parent.getController(); + + DoubleAdjuster vignetteFalloffAdjuster = new DoubleAdjuster(); + DoubleAdjuster vignetteStrengthAdjuster = new DoubleAdjuster(); + DoubleAdjuster vignetteDesaturationAdjuster = new DoubleAdjuster(); + DoubleAdjuster centerXAdjuster = new DoubleAdjuster(); + DoubleAdjuster centerYAdjuster = new DoubleAdjuster(); + DoubleAdjuster aspectRatioAdjuster = new DoubleAdjuster(); + + vignetteFalloffAdjuster.setName("Vignette falloff"); + vignetteFalloffAdjuster.setRange(0, 5); + vignetteFalloffAdjuster.clampMin(); + vignetteFalloffAdjuster.set(vignetteFalloff); + vignetteFalloffAdjuster.onValueChange(value -> { + vignetteFalloff = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + vignetteStrengthAdjuster.setName("Vignette strength"); + vignetteStrengthAdjuster.setRange(0, 5); + vignetteStrengthAdjuster.clampMin(); + vignetteStrengthAdjuster.set(vignetteStrength); + vignetteStrengthAdjuster.onValueChange(value -> { + vignetteStrength = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + vignetteDesaturationAdjuster.setName("Vignette desaturation"); + vignetteDesaturationAdjuster.setRange(0, 1); + vignetteDesaturationAdjuster.clampBoth(); + vignetteDesaturationAdjuster.set(vignetteDesaturation); + vignetteDesaturationAdjuster.onValueChange(value -> { + vignetteDesaturation = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + centerXAdjuster.setName("Center X"); + centerXAdjuster.setRange(0, 1); + centerXAdjuster.clampBoth(); + centerXAdjuster.set(center.x); + centerXAdjuster.onValueChange(value -> { + center.x = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + centerYAdjuster.setName("Center Y"); + centerYAdjuster.setRange(0, 1); + centerYAdjuster.clampBoth(); + centerYAdjuster.set(center.y); + centerYAdjuster.onValueChange(value -> { + center.y = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + aspectRatioAdjuster.setName("Aspect ratio"); + aspectRatioAdjuster.setRange(0.001, 5); + aspectRatioAdjuster.clampMin(); + aspectRatioAdjuster.set(aspectRatio); + aspectRatioAdjuster.onValueChange(value -> { + aspectRatio = value; + if (scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); + }); + + return new VBox(6, vignetteFalloffAdjuster, vignetteStrengthAdjuster, vignetteDesaturationAdjuster, centerXAdjuster, centerYAdjuster, aspectRatioAdjuster); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/projection/ApertureProjector.java b/chunky/src/java/se/llbit/chunky/renderer/projection/ApertureProjector.java index 43894fefa7..c7d2a166a7 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/projection/ApertureProjector.java +++ b/chunky/src/java/se/llbit/chunky/renderer/projection/ApertureProjector.java @@ -23,7 +23,7 @@ import se.llbit.chunky.renderer.ApertureShape; import se.llbit.chunky.resources.BitmapImage; import se.llbit.log.Log; -import se.llbit.math.Ray; +import se.llbit.math.Constants; import se.llbit.math.Vector2; import se.llbit.math.Vector3; import se.llbit.resources.ImageLoader; @@ -44,8 +44,8 @@ public class ApertureProjector implements Projector { public ApertureProjector(Projector wrapped, double apertureSize, double subjectDistance) { this.wrapped = wrapped; - this.aperture = Math.max(apertureSize, Ray.EPSILON); - this.subjectDistance = Math.max(subjectDistance, Ray.EPSILON); + this.aperture = Math.max(apertureSize, Constants.EPSILON); + this.subjectDistance = Math.max(subjectDistance, Constants.EPSILON); this.apertureMask = null; } diff --git a/chunky/src/java/se/llbit/chunky/renderer/projection/ForwardDisplacementProjector.java b/chunky/src/java/se/llbit/chunky/renderer/projection/ForwardDisplacementProjector.java index 9c7114af7d..9dc5a2082b 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/projection/ForwardDisplacementProjector.java +++ b/chunky/src/java/se/llbit/chunky/renderer/projection/ForwardDisplacementProjector.java @@ -41,7 +41,7 @@ public ForwardDisplacementProjector(Projector wrapped, double displacement) { d.normalize(); d.scale(displacementValue); - o.scaleAdd(displacementSign, d); + o.add(d.x * displacementSign, d.y * displacementSign, d.z * displacementSign); } @Override public void apply(double x, double y, Vector3 o, Vector3 d) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/projection/ParallelProjector.java b/chunky/src/java/se/llbit/chunky/renderer/projection/ParallelProjector.java index 95f0200da8..a29c4a4347 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/projection/ParallelProjector.java +++ b/chunky/src/java/se/llbit/chunky/renderer/projection/ParallelProjector.java @@ -19,6 +19,7 @@ import java.util.Random; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.Constants; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -63,15 +64,15 @@ public static void fixRay(Ray ray, Scene scene) { Vector3 d = ray.d; double t = 0; // simplified intersection test with the 6 planes that form the bounding box of the octree - if(Math.abs(d.x) > Ray.EPSILON) { + if(Math.abs(d.x) > Constants.EPSILON) { t = Math.min(t, -o.x / d.x); t = Math.min(t, (limit - o.x) / d.x); } - if(Math.abs(d.y) > Ray.EPSILON) { + if(Math.abs(d.y) > Constants.EPSILON) { t = Math.min(t, -o.y / d.y); t = Math.min(t, (limit - o.y) / d.y); } - if(Math.abs(d.z) > Ray.EPSILON) { + if(Math.abs(d.z) > Constants.EPSILON) { t = Math.min(t, -o.z / d.z); t = Math.min(t, (limit - o.z) / d.z); } @@ -79,6 +80,6 @@ public static void fixRay(Ray ray, Scene scene) { // In theory, we only would need to set it to the closest intersection point behind // but this doesn't matter because the Octree.enterOctree function // will do the same amount of math for the same result no matter what the exact point is - ray.o.scaleAdd(t, d); + ray.o.add(d.x * t, d.y * t, d.z * t); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/projection/ProjectionMode.java b/chunky/src/java/se/llbit/chunky/renderer/projection/ProjectionMode.java index fdec309482..3e80568794 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/projection/ProjectionMode.java +++ b/chunky/src/java/se/llbit/chunky/renderer/projection/ProjectionMode.java @@ -17,10 +17,12 @@ */ package se.llbit.chunky.renderer.projection; +import se.llbit.util.Registerable; + /** * Available projection modes. */ -public enum ProjectionMode { +public enum ProjectionMode implements Registerable { PINHOLE("Standard"), PARALLEL("Parallel"), FISHEYE("Fisheye"), @@ -48,4 +50,19 @@ public static ProjectionMode get(String name) { return PINHOLE; } } + + @Override + public String getName() { + return niceName; + } + + @Override + public String getDescription() { + return ""; + } + + @Override + public String getId() { + return this.name(); + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/projection/ShiftProjector.java b/chunky/src/java/se/llbit/chunky/renderer/projection/ShiftProjector.java index 9cfc841786..620d8059ee 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/projection/ShiftProjector.java +++ b/chunky/src/java/se/llbit/chunky/renderer/projection/ShiftProjector.java @@ -1,6 +1,7 @@ package se.llbit.chunky.renderer.projection; import java.util.Random; + import se.llbit.math.Vector3; public class ShiftProjector implements Projector { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/AlphaBuffer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/AlphaBuffer.java index 0562b8aab1..b118a85ad7 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/AlphaBuffer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/AlphaBuffer.java @@ -84,7 +84,6 @@ void computeAlpha(Scene scene, Type type, TaskTracker taskTracker) { Chunky.getCommonThreads().submit(() -> { IntStream.range(0, width).parallel().forEach(x -> { WorkerState state = new WorkerState(); - state.ray = new Ray(); for (int y = 0; y < height; y++) { computeAlpha(scene, x, y, cropX, cropY, width, height, fullWidth, fullHeight, state); @@ -126,7 +125,8 @@ public void computeAlpha(Scene scene, int x, int y, int cropX, int cropY, int wi if (scene.camera.getProjectionMode() == ProjectionMode.PARALLEL) { ParallelProjector.fixRay(state.ray, scene); } - occlusion += PreviewRayTracer.skyOcclusion(scene, state); + ray.setCurrentMedium(scene.getWorldMaterial(ray)); + occlusion += scene.skyOcclusion(state); } occlusion /= 4.0; diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java index e2c1a19b5f..b61551f44b 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java @@ -38,6 +38,7 @@ import se.llbit.chunky.world.Chunk; import se.llbit.json.JsonObject; import se.llbit.log.Log; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Matrix3; import se.llbit.math.QuickMath; import se.llbit.math.Ray; @@ -47,7 +48,7 @@ import se.llbit.util.annotation.Nullable; import java.util.Random; -import java.util.function.Function; +import java.util.function.BiPredicate; /** * Camera model for 3D rendering. @@ -274,8 +275,8 @@ private void initProjector() { /** * Set the camera position. */ - public void setPosition(Vector3 v) { - pos.set(v); + public void setPosition(Vector3 p) { + pos.set(p); onViewChange(); positionListener.run(); } @@ -517,9 +518,6 @@ synchronized void updateTransform() { * @param y normalized image coordinate [-0.5, 0.5] */ public void calcViewRay(Ray ray, Random random, double x, double y) { - // Reset the ray properties - current material etc. - ray.setDefault(); - projector.apply(x, y, random, ray.o, ray.d); ray.d.normalize(); @@ -539,9 +537,6 @@ public void calcViewRay(Ray ray, Random random, double x, double y) { * @param y normalized image coordinate [-0.5, 0.5] */ public void calcViewRay(Ray ray, double x, double y) { - // Reset the ray properties - current material etc. - ray.setDefault(); - projector.apply(x, y, ray.o, ray.d); ray.d.normalize(); @@ -643,7 +638,7 @@ public double getMaxFoV() { orientation.add("yaw", yaw); camera.add("orientation", orientation); - camera.add("projectionMode", projectionMode.name()); + camera.add("projectionMode", projectionMode.getId()); camera.add("fov", fov); if (dof == Double.POSITIVE_INFINITY) { camera.add("dof", "Infinity"); @@ -679,7 +674,7 @@ public void importFromJson(JsonObject json) { fov = json.get("fov").doubleValue(fov); subjectDistance = json.get("focalOffset").doubleValue(subjectDistance); projectionMode = ProjectionMode.get( - json.get("projectionMode").stringValue(projectionMode.name())); + json.get("projectionMode").stringValue(projectionMode.getId())); if (json.get("infDof").boolValue(false)) { // The infDof setting is deprecated. dof = Double.POSITIVE_INFINITY; @@ -712,16 +707,17 @@ public void moveToPlayer(Entity player) { onViewChange(); } - public void autoFocus(Function traceInScene) { + public void autoFocus(BiPredicate traceInScene) { Ray ray = new Ray(); - if (!traceInScene.apply(ray)) { + IntersectionRecord intersectionRecord = new IntersectionRecord(); + if (!traceInScene.test(ray, intersectionRecord)) { setDof(Double.POSITIVE_INFINITY); } else { if(projectionMode == ProjectionMode.PARALLEL) { - ray.distance -= worldDiagonalSize; + intersectionRecord.distance -= worldDiagonalSize; } - setSubjectDistance(ray.distance); - setDof(ray.distance * ray.distance); + setSubjectDistance(intersectionRecord.distance); + setDof(intersectionRecord.distance * intersectionRecord.distance); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/CanvasConfig.java b/chunky/src/java/se/llbit/chunky/renderer/scene/CanvasConfig.java index b1abab3091..56fe942bcf 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/CanvasConfig.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/CanvasConfig.java @@ -177,7 +177,8 @@ public int getPixelCount() { } @Override - public void storeConfiguration(JsonObject json) { + public JsonObject toJson() { + JsonObject json = new JsonObject(); json.add("width", width); json.add("height", height); @@ -187,10 +188,12 @@ public void storeConfiguration(JsonObject json) { json.add("cropX", cropX); json.add("cropY", cropY); + + return json; } @Override - public void loadConfiguration(JsonObject json) { + public void fromJson(JsonObject json) { width = json.get("width").intValue(width); height = json.get("height").intValue(height); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/CloudLayer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/CloudLayer.java new file mode 100644 index 0000000000..d74ce90d91 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/CloudLayer.java @@ -0,0 +1,412 @@ +package se.llbit.chunky.renderer.scene; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import se.llbit.chunky.block.minecraft.Air; +import se.llbit.chunky.resources.SolidColorTexture; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.dialogs.EditMaterialDialog; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.world.Clouds; +import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.material.TextureMaterial; +import se.llbit.json.JsonObject; +import se.llbit.math.Intersectable; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; +import se.llbit.math.Vector4; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; +import se.llbit.util.Pair; + +import java.util.Random; + +public class CloudLayer implements Intersectable, Configurable, HasControls { + /** + * Default cloud y-position + */ + private static final int DEFAULT_CLOUD_HEIGHT = 128; + private static final int DEFAULT_CLOUD_SIZE = 12; + private static final int DEFAULT_CLOUD_THICKNESS = 4; + + private final SimpleDoubleProperty scaleX = new SimpleDoubleProperty(DEFAULT_CLOUD_SIZE); + private final SimpleDoubleProperty scaleY = new SimpleDoubleProperty(DEFAULT_CLOUD_THICKNESS); + private final SimpleDoubleProperty scaleZ = new SimpleDoubleProperty(DEFAULT_CLOUD_SIZE); + private final SimpleDoubleProperty offsetX = new SimpleDoubleProperty(0); + private final SimpleDoubleProperty offsetY = new SimpleDoubleProperty(DEFAULT_CLOUD_HEIGHT); + private final SimpleDoubleProperty offsetZ = new SimpleDoubleProperty(0); + + private final Material material = new TextureMaterial(new SolidColorTexture(new Vector4(1, 1, 1, 1))); + + public SimpleDoubleProperty scaleXProperty() { + return scaleX; + } + + public SimpleDoubleProperty scaleYProperty() { + return scaleY; + } + + public SimpleDoubleProperty scaleZProperty() { + return scaleZ; + } + + public SimpleDoubleProperty offsetXProperty() { + return offsetX; + } + + public SimpleDoubleProperty offsetYProperty() { + return offsetY; + } + + public SimpleDoubleProperty offsetZProperty() { + return offsetZ; + } + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, + Random random) { + Pair intersection = cloudIntersection(ray, intersectionRecord, scene); + boolean hit = intersection.thing1; + boolean inCloud = intersection.thing2; + if (hit) { + if (inCloud) { + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + intersectionRecord.material = Air.INSTANCE; + intersectionRecord.color.set(1, 1, 1, 0); + } else { + this.material.getColor(intersectionRecord); + intersectionRecord.material = material; + } + return true; + } + return false; + } + + /** + * Test for a cloud intersection. If the ray intersects a cloud, + * the distance to the intersection is stored in ray.t. + * @param ray Ray with which to test for cloud intersection. + * @return {@link se.llbit.util.Pair} of Booleans. + * pair.thing1: true if the ray intersected a cloud. + * pair.thing2: true if the ray origin is inside a cloud. + */ + private Pair cloudIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + double ox = ray.o.x + scene.origin.x; + double oy = ray.o.y + scene.origin.y; + double oz = ray.o.z + scene.origin.z; + double offsetX = this.offsetX.doubleValue(); + double offsetY = this.offsetY.doubleValue(); + double offsetZ = this.offsetZ.doubleValue(); + double invSizeX = 1 / this.scaleX.doubleValue(); + double invSizeZ = 1 / this.scaleZ.doubleValue(); + double cloudTop = offsetY + this.scaleY.doubleValue(); + int target = 1; + double t_offset = 0; + if (oy < offsetY || oy > cloudTop) { + if (ray.d.y > 0) { + t_offset = (offsetY - oy) / ray.d.y; + } else { + t_offset = (cloudTop - oy) / ray.d.y; + } + if (t_offset < 0) { + return new Pair<>(false, false); + } + // Ray is entering cloud. + if (inCloud((ray.d.x * t_offset + ox) * invSizeX + offsetX, + (ray.d.z * t_offset + oz) * invSizeZ + offsetZ)) { + intersectionRecord.setNormal(0, -Math.signum(ray.d.y), 0); + intersectionRecord.distance = t_offset; + return new Pair<>(true, false); + } + } else if (inCloud(ox * invSizeX + offsetX, oz * invSizeZ + offsetZ)) { + target = 0; + } + double tExit; + if (ray.d.y > 0) { + tExit = (cloudTop - oy) / ray.d.y - t_offset; + } else { + tExit = (offsetY - oy) / ray.d.y - t_offset; + } + if (intersectionRecord.distance < tExit) { + tExit = intersectionRecord.distance; + } + double x0 = (ox + ray.d.x * t_offset) * invSizeX + offsetX; + double z0 = (oz + ray.d.z * t_offset) * invSizeZ + offsetZ; + double xp = x0; + double zp = z0; + int ix = (int) Math.floor(xp); + int iz = (int) Math.floor(zp); + int xmod = (int) Math.signum(ray.d.x), zmod = (int) Math.signum(ray.d.z); + int xo = (1 + xmod) / 2, zo = (1 + zmod) / 2; + double dx = Math.abs(ray.d.x) * invSizeX; + double dz = Math.abs(ray.d.z) * invSizeZ; + double t = 0; + int i = 0; + int nx = 0, nz = 0; + if (dx > dz) { + double m = dz / dx; + double xrem = xmod * (ix + xo - xp); + double zlimit = xrem * m; + while (t < tExit) { + double zrem = zmod * (iz + zo - zp); + if (zrem < zlimit) { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i / dx + zrem / dz; + nx = 0; + nz = -zmod; + break; + } + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i + xrem) / dx; + nx = -xmod; + nz = 0; + break; + } + } else { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i + xrem) / dx; + nx = -xmod; + nz = 0; + break; + } + if (zrem <= m) { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i / dx + zrem / dz; + nx = 0; + nz = -zmod; + break; + } + } + } + t = i / dx; + i += 1; + zp = z0 + zmod * i * m; + } + } else { + double m = dx / dz; + double zrem = zmod * (iz + zo - zp); + double xlimit = zrem * m; + while (t < tExit) { + double xrem = xmod * (ix + xo - xp); + if (xrem < xlimit) { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i / dz + xrem / dx; + nx = -xmod; + nz = 0; + break; + } + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i + zrem) / dz; + nx = 0; + nz = -zmod; + break; + } + } else { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i + zrem) / dz; + nx = 0; + nz = -zmod; + break; + } + if (xrem <= m) { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i / dz + xrem / dx; + nx = -xmod; + nz = 0; + break; + } + } + } + t = i / dz; + i += 1; + xp = x0 + xmod * i * m; + } + } + int ny = 0; + if (target == 1) { + if (t > tExit) { + return new Pair<>(false, false); + } + if (nx == 0 && ny == 0 && nz == 0) { + // fix ray.n being set to zero (issue #643) + return new Pair<>(false, false); + } + intersectionRecord.setNormal(nx, ny, nz); + intersectionRecord.distance = t + t_offset; + return new Pair<>(true, false); + } else { + if (t > tExit) { + nx = 0; + ny = (int) Math.signum(ray.d.y); + nz = 0; + t = tExit; + } else { + nx = -nx; + nz = -nz; + } + if (nx == 0 && ny == 0 && nz == 0) { + // fix ray.n being set to zero (issue #643) + return new Pair<>(false, false); + } + intersectionRecord.setNormal(nx, ny, nz); + intersectionRecord.distance = t; + return new Pair<>(true, true); + } + } + + private static boolean inCloud(double x, double z) { + return Clouds.getCloud((int) Math.floor(x), (int) Math.floor(z)) == 1; + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("scaleX", scaleX.doubleValue()); + json.add("scaleY", scaleY.doubleValue()); + json.add("scaleZ", scaleZ.doubleValue()); + json.add("offsetX", offsetX.doubleValue()); + json.add("offsetY", offsetY.doubleValue()); + json.add("offsetZ", offsetZ.doubleValue()); + json.add("materialProperties", material.saveMaterialProperties()); + return json; + } + + public void fromJson(JsonObject json) { + scaleX.set(json.get("scaleX").doubleValue(DEFAULT_CLOUD_SIZE)); + scaleY.set(json.get("scaleY").doubleValue(DEFAULT_CLOUD_THICKNESS)); + scaleZ.set(json.get("scaleZ").doubleValue(DEFAULT_CLOUD_SIZE)); + offsetX.set(json.get("offsetX").doubleValue(0)); + offsetY.set(json.get("offsetY").doubleValue(DEFAULT_CLOUD_HEIGHT)); + offsetZ.set(json.get("offsetZ").doubleValue(0)); + this.material.loadMaterialProperties(json.get("materialProperties").asObject()); + } + + @Override + public void reset() { + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + GridPane settings = new GridPane(); + + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(100d / 3); + + settings.getColumnConstraints().addAll(columnConstraints, columnConstraints); + settings.setHgap(10); + settings.setVgap(10); + + DoubleTextField scaleX = new DoubleTextField(); + DoubleTextField scaleY = new DoubleTextField(); + DoubleTextField scaleZ = new DoubleTextField(); + + scaleX.setTooltip(new Tooltip("Scale of the X-dimension of the clouds, measured in blocks per pixel of clouds.png texture")); + scaleY.setTooltip(new Tooltip("Scale of the Y-dimension of the clouds, measured in blocks per pixel of clouds.png texture")); + scaleZ.setTooltip(new Tooltip("Scale of the Z-dimension of the clouds, measured in blocks per pixel of clouds.png texture")); + + scaleX.valueProperty().setValue(this.scaleX.doubleValue()); + scaleY.valueProperty().setValue(this.scaleY.doubleValue()); + scaleZ.valueProperty().setValue(this.scaleZ.doubleValue()); + + scaleX.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scaleX.set(newValue.doubleValue()); + scene.refresh(); + }); + scaleY.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scaleY.set(newValue.doubleValue()); + scene.refresh(); + }); + scaleZ.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scaleZ.set(newValue.doubleValue()); + scene.refresh(); + }); + + + DoubleTextField offsetX = new DoubleTextField(); + DoubleTextField offsetY = new DoubleTextField(); + DoubleTextField offsetZ = new DoubleTextField(); + + offsetX.valueProperty().setValue(this.offsetX.doubleValue()); + offsetY.valueProperty().setValue(this.offsetY.doubleValue()); + offsetZ.valueProperty().setValue(this.offsetZ.doubleValue()); + + offsetX.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offsetX.set(newValue.doubleValue()); + scene.refresh(); + }); + offsetY.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offsetY.set(newValue.doubleValue()); + scene.refresh(); + }); + offsetZ.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offsetZ.set(newValue.doubleValue()); + scene.refresh(); + }); + + TextFieldLabelWrapper x1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper x2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z2Text = new TextFieldLabelWrapper(); + + x1Text.setTextField(scaleX); + y1Text.setTextField(scaleY); + z1Text.setTextField(scaleZ); + x2Text.setTextField(offsetX); + y2Text.setTextField(offsetY); + z2Text.setTextField(offsetZ); + + x1Text.setLabelText("x:"); + y1Text.setLabelText("y:"); + z1Text.setLabelText("z:"); + x2Text.setLabelText("x:"); + y2Text.setLabelText("y:"); + z2Text.setLabelText("z:"); + + Button editMaterialButton = new Button("Edit material"); + editMaterialButton.setOnAction(e -> { + EditMaterialDialog dialog = new EditMaterialDialog(this.material, scene); + dialog.showAndWait(); + dialog = null; + }); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + GridPane scaleAndOffsetPane = new GridPane(); + scaleAndOffsetPane.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + scaleAndOffsetPane.setVgap(6); + scaleAndOffsetPane.setHgap(6); + + scaleAndOffsetPane.addRow(0, new Label("Scale"), x1Text, y1Text, z1Text); + scaleAndOffsetPane.addRow(1, new Label("Offset"), x2Text, y2Text, z2Text); + + return new VBox( + 6, + scaleAndOffsetPane, + editMaterialButton + ); + } +} \ No newline at end of file diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/EmitterMappingType.java b/chunky/src/java/se/llbit/chunky/renderer/scene/EmitterMappingType.java new file mode 100644 index 0000000000..1471ad059f --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/EmitterMappingType.java @@ -0,0 +1,34 @@ +package se.llbit.chunky.renderer.scene; + +import se.llbit.util.Registerable; + +public enum EmitterMappingType implements Registerable { + NONE("None", "Fallback to default option - should only be used for materials (not global default)"), + BRIGHTEST_CHANNEL("Brightest Channel", "Emitted light (R', G', B') = (R*M^P, G*M^P, B*M^P) where M = max(R, G, B) and P is the specified power. Emitted light will always match pixel color."), + INDEPENDENT_CHANNELS("Independent Channels", "Emitted light (R', G', B') = (R^P, G^P, B^P) where P is the specified power. Saturation of emitted light increases with P - possibly less realistic in some situations."); + + private final String displayName; + private final String description; + EmitterMappingType(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + @Override + public String getName() { + return this.displayName; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String getId() { + return this.name(); + } + + @Override public String toString() { + return displayName; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/EmitterSamplingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/scene/EmitterSamplingStrategy.java similarity index 87% rename from chunky/src/java/se/llbit/chunky/renderer/EmitterSamplingStrategy.java rename to chunky/src/java/se/llbit/chunky/renderer/scene/EmitterSamplingStrategy.java index 58b47fa533..cc05deb4e1 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/EmitterSamplingStrategy.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/EmitterSamplingStrategy.java @@ -1,4 +1,4 @@ -package se.llbit.chunky.renderer; +package se.llbit.chunky.renderer.scene; import se.llbit.util.Registerable; @@ -21,6 +21,11 @@ public String getName() { return name; } + @Override + public String toString() { + return name; + } + @Override public String getDescription() { return description; diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/FogMode.java b/chunky/src/java/se/llbit/chunky/renderer/scene/FogMode.java deleted file mode 100644 index 3b78c9f334..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/FogMode.java +++ /dev/null @@ -1,13 +0,0 @@ -package se.llbit.chunky.renderer.scene; - -public enum FogMode { - NONE, UNIFORM, LAYERED; - - public static FogMode get(String name) { - try { - return valueOf(name); - } catch (IllegalArgumentException e) { - return NONE; - } - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/OctreeFinalizer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/OctreeFinalizer.java index a418d59427..2d795e44f4 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/OctreeFinalizer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/OctreeFinalizer.java @@ -19,6 +19,7 @@ import se.llbit.chunky.block.minecraft.Lava; import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.chunk.BlockPalette; +import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.Material; import se.llbit.math.Octree; @@ -42,7 +43,7 @@ public class OctreeFinalizer { * @param origin Origin of the octree * @param cp Position of the chunk to finalize */ - public static void finalizeChunk(Octree worldTree, Octree waterTree, BlockPalette palette, + public static void finalizeChunk(Octree worldTree, BlockPalette palette, Set loadedChunks, Vector3i origin, ChunkPosition cp, int yMin, int yMax) { for (int cy = yMin; cy < yMax; ++cy) { for (int cz = 0; cz < 16; ++cz) { @@ -52,7 +53,7 @@ public static void finalizeChunk(Octree worldTree, Octree waterTree, BlockPalett // process blocks that are at the edge of the chunk, the other should have be taken care of during the loading if (cy == yMin || cy == yMax - 1 || cz == 0 || cz == 15 || cx == 0 || cx == 15) { hideBlocks(worldTree, palette, x, cy, z, yMin, yMax, origin); - processBlock(worldTree, waterTree, palette, loadedChunks, x, cy, z, origin); + processBlock(worldTree, palette, loadedChunks, x, cy, z, origin); } } } @@ -77,48 +78,46 @@ private static void hideBlocks(Octree worldTree, BlockPalette palette, int x, } } - private static void processBlock(Octree worldTree, Octree waterTree, BlockPalette palette, + private static void processBlock(Octree worldTree, BlockPalette palette, Set loadedChunks, int x, int cy, int z, Vector3i origin) { int y = cy - origin.y; Material mat = worldTree.getMaterial(x, y, z, palette); - Material wmat = waterTree.getMaterial(x, y, z, palette); - if (wmat instanceof Water) { - Material above = waterTree.getMaterial(x, y + 1, z, palette); - Material aboveBlock = worldTree.getMaterial(x, y + 1, z, palette); - int level0 = 8 - ((Water) wmat).level; - if (!above.isWaterFilled() && !aboveBlock.solid) { + if (mat instanceof Water) { + Material above = worldTree.getMaterial(x, y + 1, z, palette); + int level0 = 8 - ((Water) mat).level; + if (!above.isWaterFilled() && !above.solid) { int corner0 = level0; int corner1 = level0; int corner2 = level0; int corner3 = level0; - int level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x - 1, y, z, level0); + int level = waterLevelAt(worldTree, palette, loadedChunks, x - 1, y, z, level0); corner3 += level; corner0 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x - 1, y, z + 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x - 1, y, z + 1, level0); corner0 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x, y, z + 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x, y, z + 1, level0); corner0 += level; corner1 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x + 1, y, z + 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x + 1, y, z + 1, level0); corner1 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x + 1, y, z, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x + 1, y, z, level0); corner1 += level; corner2 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x + 1, y, z - 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x + 1, y, z - 1, level0); corner2 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x, y, z - 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x, y, z - 1, level0); corner2 += level; corner3 += level; - level = waterLevelAt(worldTree, waterTree, palette, loadedChunks, x - 1, y, z - 1, level0); + level = waterLevelAt(worldTree, palette, loadedChunks, x - 1, y, z - 1, level0); corner3 += level; corner0 = Math.min(7, 8 - (corner0 / 4)); @@ -126,12 +125,12 @@ private static void processBlock(Octree worldTree, Octree waterTree, BlockPalett corner2 = Math.min(7, 8 - (corner2 / 4)); corner3 = Math.min(7, 8 - (corner3 / 4)); - waterTree.set(palette.getWaterId(((Water) wmat).level, (corner0 << Water.CORNER_0) - | (corner1 << Water.CORNER_1) - | (corner2 << Water.CORNER_2) - | (corner3 << Water.CORNER_3)), x, y, z); + worldTree.set(palette.getWaterId(((Water) mat).level, (corner0 << WaterModel.CORNER_0) + | (corner1 << WaterModel.CORNER_1) + | (corner2 << WaterModel.CORNER_2) + | (corner3 << WaterModel.CORNER_3)), x, y, z); } else if (above.isWaterFilled()) { - waterTree.set(palette.getWaterId(0, 1 << Water.FULL_BLOCK), x, y, z); + worldTree.set(palette.getWaterId(0, 1 << Water.FULL_BLOCK_DATA), x, y, z); } } else if (mat instanceof Lava) { Material above = worldTree.getMaterial(x, y + 1, z, palette); @@ -178,25 +177,25 @@ private static void processBlock(Octree worldTree, Octree waterTree, BlockPalett corner3 = Math.min(7, 8 - (corner3 / 4)); worldTree.set(palette.getLavaId( lava.level, - (corner0 << Water.CORNER_0) - | (corner1 << Water.CORNER_1) - | (corner2 << Water.CORNER_2) - | (corner3 << Water.CORNER_3) + (corner0 << WaterModel.CORNER_0) + | (corner1 << WaterModel.CORNER_1) + | (corner2 << WaterModel.CORNER_2) + | (corner3 << WaterModel.CORNER_3) ), x, y, z); } } } - private static int waterLevelAt(Octree worldTree, Octree waterTree, BlockPalette palette, + private static int waterLevelAt(Octree worldTree, BlockPalette palette, Set loadedChunks, int x, int cy, int z, int baseLevel) { // If the position isn't in a loaded chunk, return the baseLevel to make the edge-of-world water flat if (!loadedChunks.contains(new ChunkPosition(x >> 4, z >> 4))) { return baseLevel; } - Material corner = waterTree.getMaterial(x, cy, z, palette); + Material corner = worldTree.getMaterial(x, cy, z, palette); if (corner instanceof Water) { - Material above = waterTree.getMaterial(x, cy + 1, z, palette); + Material above = worldTree.getMaterial(x, cy + 1, z, palette); boolean isFullBlock = above.isWaterFilled(); return isFullBlock ? 8 : 8 - ((Water) corner).level; } else if (corner.waterlogged) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java index b876e46f08..f42e7a1cb6 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -17,21 +17,22 @@ */ package se.llbit.chunky.renderer.scene; +import java.util.List; import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.block.Void; import se.llbit.chunky.block.minecraft.Air; -import se.llbit.chunky.block.minecraft.Water; -import se.llbit.chunky.renderer.EmitterSamplingStrategy; import se.llbit.chunky.renderer.WorkerState; +import se.llbit.chunky.renderer.scene.fog.FogMode; import se.llbit.chunky.world.Material; import se.llbit.math.*; -import java.util.List; import java.util.Random; +import se.llbit.math.Grid.EmitterPosition; /** * Static methods for path tracing. * - * @author Jesper Öqvist + * @author Jesper Öqvist , Peregrine05, and Chunky Contributors */ public class PathTracer implements RayTracer { @@ -40,482 +41,296 @@ public class PathTracer implements RayTracer { */ @Override public void trace(Scene scene, WorkerState state) { Ray ray = state.ray; - if (scene.isInWater(ray)) { - ray.setCurrentMaterial(Water.INSTANCE); - } else { - ray.setCurrentMaterial(Air.INSTANCE); - } - pathTrace(scene, ray, state, true); + ray.setSpecular(true); + + ray.setCurrentMedium(scene.getWorldMaterial(ray)); + pathTrace(scene, state); } /** * Path trace the ray in this scene. - * - * @param firstReflection {@code true} if the ray has not yet hit the first - * diffuse or specular reflection */ - public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, - boolean firstReflection) { + private static void pathTrace(Scene scene, WorkerState state) { + final Random random = state.random; + final Ray ray = state.ray; + + final Vector4 cumulativeColor = state.color; + final Vector3 throughput = state.throughput; + final IntersectionRecord intersectionRecord = state.intersectionRecord; + final Vector3 emittance = state.emittance; + final Vector4 sampleColor = state.sampleColor; + + final Vector3 ox = new Vector3(ray.o); + final Vector3 od = new Vector3(ray.d); + + for (int i = scene.rayDepth; i > 0; i--) { + intersectionRecord.reset(); + emittance.set(0, 0, 0); + sampleColor.set(0, 0, 0, 0); + + ox.set(ray.o); + od.set(ray.d); + + if (scene.intersect(ray, intersectionRecord, random)) { + if (intersectionRecord.color.w < Constants.EPSILON) { + intersectionRecord.color.set(1, 1, 1, 0); + } - boolean hit = false; - Random random = state.random; - Vector3 ox = new Vector3(ray.o); - Vector3 od = new Vector3(ray.d); - double airDistance = 0; - - while (true) { - - if (!PreviewRayTracer.nextIntersection(scene, ray)) { - if (ray.getPrevMaterial().isWater()) { - ray.color.set(0, 0, 0, 1); - hit = true; - } else if (ray.depth == 0) { - // Direct sky hit. - if (!scene.transparentSky()) { - scene.sky.getSkyColorInterpolated(ray); - addSkyFog(scene, ray, state, ox, od); - hit = true; - } - } else if (ray.specular) { - // Indirect sky hit - specular color. - scene.sky.getSkyColor(ray, true); - addSkyFog(scene, ray, state, ox, od); - hit = true; + ray.o.scaleAdd(intersectionRecord.distance, ray.d); + + ray.getCurrentMedium().absorption(throughput, intersectionRecord.distance); + + int prevFlags = ray.flags; // saving this for emitter sampling + + ray.clearReflectionFlags(); + if (intersectionRecord.isVolumeIntersect()) { + emittance.set(intersectionRecord.material.volumeEmittance); + intersectionRecord.material.volumeScatter(ray, random); } else { - // Indirect sky hit - diffuse color. - scene.sky.getSkyColorDiffuseSun(ray, scene.getSunSamplingStrategy().isDiffuseSun()); - // Skip sky fog - likely not noticeable in diffuse reflection. - hit = true; + if (intersectionRecord.material.scatter(ray, intersectionRecord, scene, emittance, random)) { + ray.setCurrentMedium(intersectionRecord.material); + } } - break; - } - Material currentMat = ray.getCurrentMaterial(); - Material prevMat = ray.getPrevMaterial(); + // Sun sampling - if (!(scene.getCurrentWaterShader() instanceof StillWaterShader) && ray.getNormal().y != 0 && - ((currentMat.isWater() && prevMat == Air.INSTANCE) - || (currentMat == Air.INSTANCE && prevMat.isWater()))) { - scene.getCurrentWaterShader().doWaterShading(ray, scene.getAnimationTime()); - if (currentMat == Air.INSTANCE) { - ray.invertNormal(); + if (scene.sunSamplingStrategy.doSunSampling() && (ray.isDiffuse() || state.intersectionRecord.isVolumeIntersect())) { + doSunSampling(scene, state, i); } - } - float pSpecular = currentMat.specular; + // -------- - double pDiffuse = scene.fancierTranslucency ? 1 - Math.pow(1 - ray.color.w, Math.max(ray.color.x, Math.max(ray.color.y, ray.color.z))) : ray.color.w; - double pAbsorb = scene.fancierTranslucency ? 1 - (1 - ray.color.w)/(1 - pDiffuse + Ray.EPSILON) : ray.color.w; + // This is a simplistic fog model which gives greater artistic freedom but + // less realism. The user can select fog color and density; in a more + // realistic model color would depend on viewing angle and sun color/position. + if (intersectionRecord.distance > 0 && scene.fog.isFogEnabled() && (ray.getCurrentMedium() == Air.INSTANCE || ray.getCurrentMedium() == Void.INSTANCE)) { + sampleFog(scene, state, ox, od, i); + } - float n1 = prevMat.ior; - float n2 = currentMat.ior; + // -------- + // Emitter sampling - if (prevMat == Air.INSTANCE || prevMat.isWater()) { - airDistance = ray.distance; - } - - if (ray.color.w + pSpecular < Ray.EPSILON && n1 == n2) { - // Transmission without refraction. - // This can happen when the ray passes through a transparent - // material into another. It can also happen for example - // when passing through a transparent part of an otherwise solid - // object. - // TODO: material color may change here. - continue; - } - if(ray.depth + 1 >= scene.rayDepth) { - break; - } - ray.depth += 1; - Vector4 cumulativeColor = new Vector4(0, 0, 0, 0); - Ray next = new Ray(); - float pMetal = currentMat.metalness; - // Reusing first rays - a simplified form of "branched path tracing" (what Blender used to call it before they implemented something fancier) - // The initial rays cast into the scene are very similar between each sample, since they are almost entirely a function of the pixel coordinates - // Because of that, casting those initial rays on every sample is redundant and can be skipped - // If the ray depth is high, this doesn't help much (just a few percent), but in some outdoor/low depth scenes, this can improve performance by >40% - // The main caveat is that antialiasing is achieved by varying the starting rays at the subpixel level (see PathTracingRenderer.java) - // Therefore, it's still necessary to have a decent amount (20 is ok, 50 is better) of distinct starting rays for each pixel - // scene.branchCount is the number of times we use the same first ray before casting a new one - int count = firstReflection ? scene.getCurrentBranchCount() : 1; - for (int i = 0; i < count; i++) { - boolean doMetal = pMetal > Ray.EPSILON && random.nextFloat() < pMetal; - if (doMetal || (pSpecular > Ray.EPSILON && random.nextFloat() < pSpecular)) { - hit |= doSpecularReflection(ray, next, cumulativeColor, doMetal, random, state, scene); - } else if(random.nextFloat() < pDiffuse) { - hit |= doDiffuseReflection(ray, next, currentMat, cumulativeColor, random, state, scene); - } else if (n1 != n2) { - hit |= doRefraction(ray, next, currentMat, prevMat, cumulativeColor, n1, n2, pAbsorb, random, state, scene); - } else { - hit |= doTransmission(ray, next, cumulativeColor, pAbsorb, state, scene); + if (scene.emitterSamplingStrategy != EmitterSamplingStrategy.NONE + && scene.getEmitterGrid() != null + && (ray.isDiffuse() + || intersectionRecord.isVolumeIntersect())) { + doEmitterSampling(scene, state, prevFlags); } - } - ray.color.set(cumulativeColor); - ray.color.scale(1d/count); - if (hit && prevMat.isWater()) { - // Render water fog effect. - if(scene.waterVisibility == 0) { - ray.color.scale(0.); - } else { - double a = ray.distance / scene.waterVisibility; - double attenuation = Math.exp(-a); - ray.color.scale(attenuation); - } - } + // Light emitted by object should not be affected by the tinting of reflected light. + // Thus, emittance is added to cumulativeColor before the object's color is applied to + // the throughput. - break; - } - if (!hit) { - ray.color.set(0, 0, 0, 1); - if (firstReflection) { - airDistance = ray.distance; - } - } + cumulativeColor.x += emittance.x * scene.emitterIntensity * throughput.x; + cumulativeColor.y += emittance.y * scene.emitterIntensity * throughput.y; + cumulativeColor.z += emittance.z * scene.emitterIntensity * throughput.z; - // This is a simplistic fog model which gives greater artistic freedom but - // less realism. The user can select fog color and density; in a more - // realistic model color would depend on viewing angle and sun color/position. - if (airDistance > 0 && scene.fog.fogEnabled()) { - - // Pick point between ray origin and intersected object. - // The chosen point is used to test if the sun is lighting the - // fog between the camera and the first diffuse ray target. - // The sun contribution will be proportional to the amount of - // sunlit fog areas in the ray path, thus giving an approximation - // of the sun inscatter leading to effects like god rays. - // The way the sun contribution point is chosen is not - // entirely correct because the original ray may have - // travelled through glass or other materials between air gaps. - // However, the results are probably close enough to not be distracting, - // so this seems like a reasonable approximation. - Ray atmos = new Ray(); - double offset = scene.fog.sampleGroundScatterOffset(ray, ox, random); - atmos.o.scaleAdd(offset, od, ox); - scene.sun.getRandomSunDirection(atmos, random); - atmos.setCurrentMaterial(Air.INSTANCE); + throughput.x *= intersectionRecord.color.x; + throughput.y *= intersectionRecord.color.y; + throughput.z *= intersectionRecord.color.z; - // Check sun visibility at random point to determine inscatter brightness. - getDirectLightAttenuation(scene, atmos, state); - scene.fog.addGroundFog(ray, ox, airDistance, state.attenuation, offset); - } + cumulativeColor.x += sampleColor.x * throughput.x; + cumulativeColor.y += sampleColor.y * throughput.y; + cumulativeColor.z += sampleColor.z * throughput.z; - return hit; - } + } else { + scene.sky.intersect(ray, intersectionRecord); - private static boolean doSpecularReflection(Ray ray, Ray next, Vector4 cumulativeColor, boolean doMetal, Random random, WorkerState state, Scene scene) { - boolean hit = false; - next.specularReflection(ray, random); - if (pathTrace(scene, next, state, false)) { + addSkyFog(scene, state, ox, od, i); - if (doMetal) { - // use the albedo color as specular color - cumulativeColor.x += ray.color.x * next.color.x; - cumulativeColor.y += ray.color.y * next.color.y; - cumulativeColor.z += ray.color.z * next.color.z; - } else { - cumulativeColor.x += next.color.x; - cumulativeColor.y += next.color.y; - cumulativeColor.z += next.color.z; + cumulativeColor.x += throughput.x * intersectionRecord.color.x; + cumulativeColor.y += throughput.y * intersectionRecord.color.y; + cumulativeColor.z += throughput.z * intersectionRecord.color.z; + break; } - hit = true; } - return hit; } - private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMat, Vector4 cumulativeColor, Random random, WorkerState state, Scene scene) { - boolean hit = false; - Vector3 emittance = new Vector3(); - Vector4 indirectEmitterColor = new Vector4(0, 0, 0, 0); - - if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 1) && currentMat.emittance > Ray.EPSILON) { - - // Quadratic emittance mapping, so a pixel that's 50% darker will emit only 25% as much light - // This is arbitrary but gives pretty good results in most cases. - emittance = new Vector3(ray.color.x * ray.color.x, ray.color.y * ray.color.y, ray.color.z * ray.color.z); - emittance.scale(currentMat.emittance * scene.emitterIntensity); - - hit = true; - } else if (scene.emittersEnabled && scene.emitterSamplingStrategy != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() != null) { - // Sample emitter - switch (scene.emitterSamplingStrategy) { - case ONE: - case ONE_BLOCK: { - Grid.EmitterPosition pos = scene.getEmitterGrid().sampleEmitterPosition((int) ray.o.x, (int) ray.o.y, (int) ray.o.z, random); - if (pos != null) { - indirectEmitterColor.scaleAdd(Math.PI, sampleEmitter(scene, ray, pos, random)); - } - break; - } - case ALL: { - List positions = scene.getEmitterGrid().getEmitterPositions((int) ray.o.x, (int) ray.o.y, (int) ray.o.z); - double sampleScaler = Math.PI / positions.size(); - for (Grid.EmitterPosition pos : positions) { - indirectEmitterColor.scaleAdd(sampleScaler, sampleEmitter(scene, ray, pos, random)); - } - break; - } - } - } - - if (scene.getSunSamplingStrategy().doSunSampling()) { - next.set(ray); - scene.sun.getRandomSunDirection(next, random); - - double directLightR = 0; - double directLightG = 0; - double directLightB = 0; - - boolean frontLight = next.d.dot(ray.getNormal()) > 0; - - if (frontLight || (currentMat.subSurfaceScattering - && random.nextFloat() < Scene.fSubSurface)) { - - if (!frontLight) { - next.o.scaleAdd(-Ray.OFFSET, ray.getNormal()); - } + private static void doSunSampling(Scene scene, WorkerState state, int rayDepth) { + Ray ray = state.ray; + IntersectionRecord intersectionRecord = state.intersectionRecord; - next.setCurrentMaterial(next.getPrevMaterial(), next.getPrevData()); + state.sampleRay.set(ray); + scene.sun.getRandomSunDirection(state.sampleRay.d, state.random); - getDirectLightAttenuation(scene, next, state); + scene.sky.getSkyColor(state.sampleRay, state.sampleRecord, true); + state.sampleColor.set(state.sampleRecord.color); - Vector4 attenuation = state.attenuation; - if (attenuation.w > 0) { - double mult = QuickMath.abs(next.d.dot(ray.getNormal())) * (scene.getSunSamplingStrategy().isSunLuminosity() ? scene.sun().getLuminosityPdf() : 1); - directLightR = attenuation.x * attenuation.w * mult; - directLightG = attenuation.y * attenuation.w * mult; - directLightB = attenuation.z * attenuation.w * mult; - hit = true; - } - } - - next.diffuseReflection(ray, random, scene); - hit = pathTrace(scene, next, state, false) || hit; - if (hit) { - Vector3 sunEmittance = scene.sun().getEmittance(); - cumulativeColor.x += emittance.x + ray.color.x * (directLightR * sunEmittance.x + next.color.x + indirectEmitterColor.x); - cumulativeColor.y += emittance.y + ray.color.y * (directLightG * sunEmittance.y + next.color.y + indirectEmitterColor.y); - cumulativeColor.z += emittance.z + ray.color.z * (directLightB * sunEmittance.z + next.color.z + indirectEmitterColor.z); - } else if (indirectEmitterColor.x > Ray.EPSILON || indirectEmitterColor.y > Ray.EPSILON || indirectEmitterColor.z > Ray.EPSILON) { - hit = true; - cumulativeColor.x += ray.color.x * indirectEmitterColor.x; - cumulativeColor.y += ray.color.y * indirectEmitterColor.y; - cumulativeColor.z += ray.color.z * indirectEmitterColor.z; - } + transmittance(scene, state, rayDepth); + double scaleFactor; + if (intersectionRecord.isVolumeIntersect()) { + scaleFactor = Material.phaseHG(ray.d.rScale(-1).dot(state.sampleRay.d), intersectionRecord.material.volumeAnisotropy); } else { - // If diffuse sun sampling is performed, then ray.color will be altered, but it should be the same on each iteration of ray branching - Vector4 rayColor = new Vector4(ray.color); - next.diffuseReflection(ray, random, scene); - - hit = pathTrace(scene, next, state, false) || hit; - if (hit) { - cumulativeColor.x += emittance.x + ray.color.x * (next.color.x + indirectEmitterColor.x); - cumulativeColor.y += emittance.y + ray.color.y * (next.color.y + indirectEmitterColor.y); - cumulativeColor.z += emittance.z + ray.color.z * (next.color.z + indirectEmitterColor.z); - } else if (indirectEmitterColor.x > Ray.EPSILON || indirectEmitterColor.y > Ray.EPSILON || indirectEmitterColor.z > Ray.EPSILON) { - hit = true; - cumulativeColor.x += ray.color.x * indirectEmitterColor.x; - cumulativeColor.y += ray.color.y * indirectEmitterColor.y; - cumulativeColor.z += ray.color.z * indirectEmitterColor.z; - } - ray.color.set(rayColor); + scaleFactor = QuickMath.abs(state.sampleRay.d.dot(intersectionRecord.shadeN)); } - return hit; + scaleFactor *= scene.sun.radius * scene.sun.radius; + state.sampleColor.scale(scaleFactor); + state.sampleColor.x *= state.attenuation.x; + state.sampleColor.y *= state.attenuation.y; + state.sampleColor.z *= state.attenuation.z; } - private static boolean doRefraction(Ray ray, Ray next, Material currentMat, Material prevMat, Vector4 cumulativeColor, float n1, float n2, double pAbsorb, Random random, WorkerState state, Scene scene) { - boolean hit = false; - // TODO: make this decision dependent on the material properties: - boolean doRefraction = currentMat.refractive || prevMat.refractive; - - float n1n2 = n1 / n2; - double cosTheta = -ray.getNormal().dot(ray.d); - double radicand = 1 - n1n2 * n1n2 * (1 - cosTheta * cosTheta); - if (doRefraction && radicand < Ray.EPSILON) { - // Total internal reflection. - next.specularReflection(ray, random); - if (pathTrace(scene, next, state, false)) { - - cumulativeColor.x += next.color.x; - cumulativeColor.y += next.color.y; - cumulativeColor.z += next.color.z; - hit = true; - } - } else { - next.set(ray); - - // Calculate angle-dependent reflectance using - // Fresnel equation approximation: - // R(cosineAngle) = R0 + (1 - R0) * (1 - cos(cosineAngle))^5 - float a = (n1n2 - 1); - float b = (n1n2 + 1); - double R0 = a * a / (b * b); - double c = 1 - cosTheta; - double Rtheta = R0 + (1 - R0) * c * c * c * c * c; - - if (random.nextFloat() < Rtheta) { - next.specularReflection(ray, random); - if (pathTrace(scene, next, state, false)) { - - cumulativeColor.x += next.color.x; - cumulativeColor.y += next.color.y; - cumulativeColor.z += next.color.z; - hit = true; + private static void transmittance(Scene scene, WorkerState state, int rayDepth) { + state.sampleRecord.reset(); + state.attenuation.set(0); + switch (scene.sunSamplingStrategy) { + case SAMPLE_ONLY: + case MIX: + if (!scene.intersect(state.sampleRay, state.sampleRecord, state.random)) { + state.attenuation.set(1); } - } else { - if (doRefraction) { - - double t2 = FastMath.sqrt(radicand); - Vector3 n = ray.getNormal(); - if (cosTheta > 0) { - next.d.x = n1n2 * ray.d.x + (n1n2 * cosTheta - t2) * n.x; - next.d.y = n1n2 * ray.d.y + (n1n2 * cosTheta - t2) * n.y; - next.d.z = n1n2 * ray.d.z + (n1n2 * cosTheta - t2) * n.z; - } else { - next.d.x = n1n2 * ray.d.x - (-n1n2 * cosTheta - t2) * n.x; - next.d.y = n1n2 * ray.d.y - (-n1n2 * cosTheta - t2) * n.y; - next.d.z = n1n2 * ray.d.z - (-n1n2 * cosTheta - t2) * n.z; + break; + case SAMPLE_THROUGH_OPACITY: + state.attenuation.set(1); + for (int i = 0; i < rayDepth; i++) { + if (!scene.intersect(state.sampleRay, state.sampleRecord, state.random)) { + break; } - - next.d.normalize(); - - // See Ray.specularReflection for information on why this is needed - // This is the same thing but for refraction instead of reflection - // so this time we want the signs of the dot product to be the same - if (QuickMath.signum(next.getGeometryNormal().dot(next.d)) != QuickMath.signum(next.getGeometryNormal().dot(ray.d))) { - double factor = QuickMath.signum(next.getGeometryNormal().dot(ray.d)) * -Ray.EPSILON - next.d.dot(next.getGeometryNormal()); - next.d.scaleAdd(factor, next.getGeometryNormal()); - next.d.normalize(); + double mult = 1 - state.sampleRecord.color.w * state.sampleRecord.material.alpha; + if (mult < Constants.EPSILON) { + state.attenuation.set(0); + break; } + state.attenuation.scale(mult); + state.attenuation.x *= 1 - state.sampleRecord.material.transmissionMetalness * (1 - state.sampleRecord.color.x); + state.attenuation.y *= 1 - state.sampleRecord.material.transmissionMetalness * (1 - state.sampleRecord.color.y); + state.attenuation.z *= 1 - state.sampleRecord.material.transmissionMetalness * (1 - state.sampleRecord.color.z); - next.o.scaleAdd(Ray.OFFSET, next.d); - } + state.attenuation.x *= state.sampleRecord.material.transmissionSpecularColor.x; + state.attenuation.y *= state.sampleRecord.material.transmissionSpecularColor.y; + state.attenuation.z *= state.sampleRecord.material.transmissionSpecularColor.z; - if (pathTrace(scene, next, state, false)) { - // Calculate the color and emittance of the refracted ray - translucentRayColor(scene, ray, next, cumulativeColor, pAbsorb); - hit = true; + state.sampleRay.getCurrentMedium().absorption(state.attenuation.toVec3(), state.sampleRecord.distance); + + state.sampleRay.o.scaleAdd(state.sampleRecord.distance, state.sampleRay.d); + if (!state.sampleRecord.isNoMediumChange()) { + state.sampleRay.setCurrentMedium(state.sampleRecord.material); + } + state.sampleRay.o.scaleAdd(-Constants.OFFSET, state.sampleRecord.n); + state.sampleRecord.reset(); } - } + break; } - return hit; } - private static boolean doTransmission(Ray ray, Ray next, Vector4 cumulativeColor, double pAbsorb, WorkerState state, Scene scene) { - boolean hit = false; - next.set(ray); - next.o.scaleAdd(Ray.OFFSET, next.d); + private static void sampleFog(Scene scene, WorkerState state, Vector3 ox, Vector3 od, int rayDepth) { + // Pick point between ray origin and intersected object. + // The chosen point is used to test if the sun is lighting the + // fog between the camera and the first diffuse ray target. + // The sun contribution will be proportional to the amount of + // sunlit fog areas in the ray path, thus giving an approximation + // of the sun inscatter leading to effects like god rays. + // The way the sun contribution point is chosen is not + // entirely correct because the original ray may have + // travelled through glass or other materials between air gaps. + // However, the results are probably close enough to not be distracting, + // so this seems like a reasonable approximation. + Ray ray = state.ray; + IntersectionRecord intersectionRecord = state.intersectionRecord; + Random random = state.random; + Vector3 emittance = state.emittance; + Vector4 cumulativeColor = state.color; + Vector3 throughput = state.throughput; + + Ray atmos = state.sampleRay; + double offset = scene.fog.sampleGroundScatterOffset(ray, intersectionRecord.distance, ox, od, random); + atmos.o.scaleAdd(offset, od, ox); + atmos.setCurrentMedium(scene.getWorldMaterial(atmos)); + scene.sun.getRandomSunDirection(atmos.d, random); + + // Check sun visibility at random point to determine inscatter brightness. + transmittance(scene, state, rayDepth); + Vector4 fogColor = new Vector4(0); + scene.fog.addGroundFog(ray, fogColor, intersectionRecord.color, emittance, ox, od, intersectionRecord.distance, state.attenuation, offset); + + cumulativeColor.x += fogColor.x * throughput.x; + cumulativeColor.y += fogColor.y * throughput.y; + cumulativeColor.z += fogColor.z * throughput.z; + } - if (pathTrace(scene, next, state, false)) { - // Calculate the color and emittance of the refracted ray - translucentRayColor(scene, ray, next, cumulativeColor, pAbsorb); - hit = true; + private static void addSkyFog(Scene scene, WorkerState state, Vector3 ox, Vector3 od, int rayDepth) { + if (scene.fog.getFogMode() == FogMode.UNIFORM) { + scene.fog.addSkyFog(state.ray, state.intersectionRecord, null); + } else if (scene.fog.getFogMode() == FogMode.LAYERED) { + Ray atmos = state.sampleRay; + double offset = scene.fog.sampleSkyScatterOffset(scene, state.ray, state.random); + atmos.o.scaleAdd(offset, od, ox); + atmos.setCurrentMedium(scene.getWorldMaterial(atmos)); + scene.sun.getRandomSunDirection(atmos.d, state.random); + transmittance(scene, state, rayDepth); + scene.fog.addSkyFog(state.ray, state.intersectionRecord, state.attenuation); } - return hit; } - private static void translucentRayColor(Scene scene, Ray ray, Ray next, Vector4 cumulativeColor, double absorption) { - Vector3 rgbTrans; - if(scene.fancierTranslucency) { - // Color-based transmission value - double colorTrans = (ray.color.x + ray.color.y + ray.color.z) / 3; - // Total amount of light we want to transmit (overall transparency of texture) - double shouldTrans = 1 - absorption; - // Amount of each color to transmit - default to overall transparency if RGB values add to 0 (e.g. regular glass) - rgbTrans = new Vector3(shouldTrans, shouldTrans, shouldTrans); - if (colorTrans > 0) { - // Amount to transmit of each color is scaled so the total transmitted amount matches the texture's transparency - rgbTrans.set(ray.color.toVec3()); - rgbTrans.scale(shouldTrans / colorTrans); - } - double transmissivityCap = scene.transmissivityCap; - // Determine the color with the highest transmissivity - double maxTrans = Math.max(rgbTrans.x, Math.max(rgbTrans.y, rgbTrans.z)); - if (maxTrans > transmissivityCap) { - if (maxTrans == rgbTrans.x) { - // Give excess transmission from red to green and blue - double gTransNew = reassignTransmissivity(rgbTrans.x, rgbTrans.y, rgbTrans.z, shouldTrans, transmissivityCap); - rgbTrans.z = reassignTransmissivity(rgbTrans.x, rgbTrans.z, rgbTrans.y, shouldTrans, transmissivityCap); - rgbTrans.y = gTransNew; - rgbTrans.x = transmissivityCap; - } else if (maxTrans == rgbTrans.y) { - // Give excess transmission from green to red and blue - double rTransNew = reassignTransmissivity(rgbTrans.y, rgbTrans.x, rgbTrans.z, shouldTrans, transmissivityCap); - rgbTrans.z = reassignTransmissivity(rgbTrans.y, rgbTrans.z, rgbTrans.x, shouldTrans, transmissivityCap); - rgbTrans.x = rTransNew; - rgbTrans.y = transmissivityCap; - } else if (maxTrans == rgbTrans.z) { - // Give excess transmission from blue to green and red - double gTransNew = reassignTransmissivity(rgbTrans.z, rgbTrans.y, rgbTrans.x, shouldTrans, transmissivityCap); - rgbTrans.x = reassignTransmissivity(rgbTrans.z, rgbTrans.x, rgbTrans.y, shouldTrans, transmissivityCap); - rgbTrans.y = gTransNew; - rgbTrans.z = transmissivityCap; + private static void doEmitterSampling(Scene scene, WorkerState state, int prevFlags) { + Ray ray = state.ray; + IntersectionRecord intersectionRecord = state.intersectionRecord; + Vector3 emittance = state.emittance; + Vector4 sampleColor = state.sampleColor; + Random random = state.random; + + switch (scene.emitterSamplingStrategy) { + case ONE: + case ONE_BLOCK: { + Grid.EmitterPosition pos = scene.getEmitterGrid().sampleEmitterPosition((int) ray.o.x, (int) ray.o.y, (int) ray.o.z, random); + if (pos != null) { + sampleColor.scaleAdd(FastMath.PI, sampleEmitter(scene, ray, intersectionRecord, pos, random)); + + if (scene.isPreventNormalEmitterWithSampling() && (prevFlags & Ray.INDIRECT) != 0) { + emittance.set(0); + } } + break; } - // Don't need to check for energy gain if transmissivity cap is 1 - if (transmissivityCap > 1) { - double currentEnergy = rgbTrans.x * next.color.x + rgbTrans.y * next.color.y + rgbTrans.z * next.color.z; - double nextEnergy = next.color.x + next.color.y + next.color.z; - double energyRatio = nextEnergy / currentEnergy; - // Normalize if there is net energy gain across all channels (more likely for higher transmissivityCap combined with high-saturation light source) - if (energyRatio < 1) { - rgbTrans.scale(energyRatio); + case ALL: { + List positions = scene.getEmitterGrid() + .getEmitterPositions((int) ray.o.x, (int) ray.o.y, (int) ray.o.z); + double sampleScaler = FastMath.PI / positions.size(); + for (Grid.EmitterPosition pos : positions) { + sampleColor.scaleAdd(sampleScaler, sampleEmitter(scene, ray, intersectionRecord, pos, random)); + + if (scene.isPreventNormalEmitterWithSampling() && (prevFlags & Ray.INDIRECT) != 0) { + emittance.set(0); + } } + break; } - } else { - // Old method (see https://github.com/chunky-dev/chunky/pull/1513) - rgbTrans = new Vector3(1 - absorption, 1 - absorption, 1 - absorption); - rgbTrans.scaleAdd(absorption, ray.color.toVec3()); } - // Scale color based on next ray - Vector4 outputColor = new Vector4(0, 0, 0, 0); - outputColor.multiplyEntrywise(new Vector4(rgbTrans, 1), next.color); - cumulativeColor.add(outputColor); } - private static double reassignTransmissivity(double from, double to, double other, double trans, double cap) { - // Formula here derived algebraically from this system: - // (cap - to_new)/(cap - other_new) = (from - to)/(from - other), (cap + to_new + other_new)/3 = trans - return (cap*(other - 2*to + from) + (3*trans)*(to - from))/(other + to - 2*from); - } - - private static void addSkyFog(Scene scene, Ray ray, WorkerState state, Vector3 ox, Vector3 od) { - if (scene.fog.mode == FogMode.UNIFORM) { - scene.fog.addSkyFog(ray, null); - } else if (scene.fog.mode == FogMode.LAYERED) { - Ray atmos = new Ray(); - double offset = scene.fog.sampleSkyScatterOffset(scene, ray, state.random); - atmos.o.scaleAdd(offset, od, ox); - scene.sun.getRandomSunDirection(atmos, state.random); - atmos.setCurrentMaterial(Air.INSTANCE); - getDirectLightAttenuation(scene, atmos, state); - scene.fog.addSkyFog(ray, state.attenuation); - } - } - - private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition pos, int face, Vector4 result, double scaler, Random random) { + private static void sampleEmitterFace(Scene scene, Ray ray, IntersectionRecord intersectionRecord, Grid.EmitterPosition pos, int face, Vector4 result, double scaler, Random random) { Ray emitterRay = new Ray(ray); pos.sampleFace(face, emitterRay.d, random); emitterRay.d.sub(emitterRay.o); - if (emitterRay.d.dot(ray.getNormal()) > 0) { + if (emitterRay.d.dot(intersectionRecord.n) > 0) { double distance = emitterRay.d.length(); emitterRay.d.scale(1 / distance); - emitterRay.o.scaleAdd(Ray.OFFSET, emitterRay.d); - emitterRay.distance += Ray.OFFSET; - PreviewRayTracer.nextIntersection(scene, emitterRay); - if (Math.abs(emitterRay.distance - distance) < Ray.OFFSET) { - double e = Math.abs(emitterRay.d.dot(emitterRay.getNormal())); - e /= Math.max(distance * distance, 1); + emitterRay.o.scaleAdd(Constants.OFFSET, emitterRay.d); + IntersectionRecord emitterIntersection = new IntersectionRecord(); + scene.intersect(emitterRay, emitterIntersection, random); + if (FastMath.abs(emitterIntersection.distance + Constants.OFFSET - distance) < Constants.OFFSET) { + double e; + if (intersectionRecord.isVolumeIntersect()) { + e = Material.phaseHG(ray.d.rScale(-1).dot(emitterRay.d), intersectionRecord.material.volumeAnisotropy); + } else { + e = FastMath.abs(emitterRay.d.dot(emitterIntersection.n)); + } + e /= FastMath.max(distance * distance, 1); e *= pos.block.surfaceArea(face); - e *= emitterRay.getCurrentMaterial().emittance; + e *= emitterIntersection.material.emittance; e *= scene.emitterIntensity; e *= scaler; - result.scaleAdd(e, emitterRay.color); + Vector3 emittance = new Vector3(); + Material.tintColor(emitterIntersection.color, 1, emitterIntersection.material.emittanceColor); + emitterIntersection.material.doEmitterMapping(emittance, emitterIntersection.color, scene); + result.x += emittance.x * e; + result.y += emittance.y * e; + result.z += emittance.z * e; } } } @@ -529,59 +344,24 @@ private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition * @param random RNG * @return The contribution of the emitter */ - private static Vector4 sampleEmitter(Scene scene, Ray ray, Grid.EmitterPosition pos, Random random) { + private static Vector4 sampleEmitter(Scene scene, Ray ray, IntersectionRecord intersectionRecord, Grid.EmitterPosition pos, Random random) { Vector4 result = new Vector4(); result.set(0, 0, 0, 1); switch (scene.getEmitterSamplingStrategy()) { - default: - case ONE: - sampleEmitterFace(scene, ray, pos, random.nextInt(pos.block.faceCount()), result, 1, random); - break; case ONE_BLOCK: case ALL: double scaler = 1.0 / pos.block.faceCount(); for (int i = 0; i < pos.block.faceCount(); i++) { - sampleEmitterFace(scene, ray, pos, i, result, scaler, random); + sampleEmitterFace(scene, ray, intersectionRecord, pos, i, result, scaler, random); } break; + case ONE: + default: + sampleEmitterFace(scene, ray, intersectionRecord, pos, random.nextInt(pos.block.faceCount()), result, 1, random); + break; } return result; } - - /** - * Calculate direct lighting attenuation. - */ - public static void getDirectLightAttenuation(Scene scene, Ray ray, WorkerState state) { - - Vector4 attenuation = state.attenuation; - attenuation.x = 1; - attenuation.y = 1; - attenuation.z = 1; - attenuation.w = 1; - while (attenuation.w > 0) { - ray.o.scaleAdd(Ray.OFFSET, ray.d); - if (!PreviewRayTracer.nextIntersection(scene, ray)) { - break; - } - double mult = 1 - ray.color.w; - attenuation.x *= ray.color.x * ray.color.w + mult; - attenuation.y *= ray.color.y * ray.color.w + mult; - attenuation.z *= ray.color.z * ray.color.w + mult; - attenuation.w *= mult; - if (ray.getPrevMaterial().isWater()) { - if(scene.waterVisibility == 0) { - attenuation.w = 0; - } else { - double a = ray.distance / scene.waterVisibility; - attenuation.w *= Math.exp(-a); - } - } - if (scene.getSunSamplingStrategy().isStrictDirectLight() && ray.getPrevMaterial().ior != ray.getCurrentMaterial().ior) { - attenuation.w = 0; - } - } - } - } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java index b6ef946f26..a4efa0b87c 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java @@ -17,10 +17,10 @@ */ package se.llbit.chunky.renderer.scene; -import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.MinecraftBlock; -import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.renderer.WorkerState; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.math.Vector4; @@ -35,108 +35,43 @@ public class PreviewRayTracer implements RayTracer { */ @Override public void trace(Scene scene, WorkerState state) { Ray ray = state.ray; - if (scene.isInWater(ray)) { - ray.setCurrentMaterial(Water.INSTANCE); - } else { - ray.setCurrentMaterial(Air.INSTANCE); - } - while (true) { - if (!nextIntersection(scene, ray)) { - if (mapIntersection(scene, ray)) { + ray.setSpecular(true); + ray.setIndirect(true); + ray.setCurrentMedium(scene.getWorldMaterial(ray)); + + IntersectionRecord intersectionRecord = state.intersectionRecord; + Vector3 throughput = state.throughput; + + for (int i = scene.rayDepth; i > 0; i--) { + intersectionRecord.reset(); + if (scene.intersect(ray, intersectionRecord, state.random)) { + if (intersectionRecord.isVolumeIntersect()) { break; } - break; - } else if (ray.getCurrentMaterial() != Air.INSTANCE && ray.color.w > 0) { + ray.o.scaleAdd(intersectionRecord.distance, ray.d); + ray.getCurrentMedium().absorption(throughput, intersectionRecord.distance); + ray.clearReflectionFlags(); + if (intersectionRecord.material.scatter(ray, intersectionRecord, scene, state.emittance, state.random)) { + ray.setCurrentMedium(intersectionRecord.material); + } + if (ray.isDiffuse()) { + scene.sun.flatShading(intersectionRecord); + break; + } else { + throughput.x *= intersectionRecord.color.x; + throughput.y *= intersectionRecord.color.y; + throughput.z *= intersectionRecord.color.z; + } + } else if (mapIntersection(scene, ray, intersectionRecord)) { break; } else { - ray.o.scaleAdd(Ray.OFFSET, ray.d); - } - } - - if (ray.getCurrentMaterial() == Air.INSTANCE) { - scene.sky.getApparentSkyColor(ray, true); - } else { - scene.sun.flatShading(ray); - } - } - - /** - * Calculate sky occlusion. - * @return occlusion value (1 = occluded, 0 = transparent) - */ - public static double skyOcclusion(Scene scene, WorkerState state) { - Ray ray = state.ray; - double occlusion = 1.0; - while (true) { - if (!nextIntersection(scene, ray)) { + scene.sky.intersect(ray, intersectionRecord); break; - } else { - occlusion *= (1 - ray.color.w); - if (occlusion == 0) { - return 1; // occlusion can't become > 0 anymore - } - ray.o.scaleAdd(Ray.OFFSET, ray.d); } } - return 1 - occlusion; - } - - /** - * Find next ray intersection. - * @return true if intersected, false if no intersection has been found - */ - public static boolean nextIntersection(Scene scene, Ray ray) { - ray.setPrevMaterial(ray.getCurrentMaterial(), ray.getCurrentData()); - ray.t = Double.POSITIVE_INFINITY; - boolean hit = false; - if (scene.sky().cloudsEnabled()) { - hit = scene.sky().cloudIntersection(scene, ray); - } - if (scene.isWaterPlaneEnabled()) { - hit = waterPlaneIntersection(scene, ray) || hit; - } - if (scene.intersect(ray)) { - // Octree tracer handles updating distance. - return true; - } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - scene.updateOpacity(ray); - return true; - } else { - ray.setCurrentMaterial(Air.INSTANCE); - return false; - } - } - - private static boolean waterPlaneIntersection(Scene scene, Ray ray) { - double t = (scene.getEffectiveWaterPlaneHeight() - ray.o.y - scene.origin.y) / ray.d.y; - if (scene.getWaterPlaneChunkClip()) { - Vector3 pos = new Vector3(ray.o); - pos.scaleAdd(t, ray.d); - if (scene.isChunkLoaded((int)Math.floor(pos.x), (int)Math.floor(pos.y), (int)Math.floor(pos.z))) - return false; - } - if (ray.d.y < 0) { - if (t > 0 && t < ray.t) { - ray.t = t; - Water.INSTANCE.getColor(ray); - ray.setNormal(0, 1, 0); - ray.setCurrentMaterial(scene.getPalette().water); - return true; - } - } - if (ray.d.y > 0) { - if (t > 0 && t < ray.t) { - ray.t = t; - Water.INSTANCE.getColor(ray); - ray.setNormal(0, -1, 0); - ray.setCurrentMaterial(Air.INSTANCE); - return true; - } - } - return false; + state.color.x = throughput.x * intersectionRecord.color.x; + state.color.y = throughput.y * intersectionRecord.color.y; + state.color.z = throughput.z * intersectionRecord.color.z; } // Chunk pattern config @@ -157,17 +92,17 @@ private static boolean waterPlaneIntersection(Scene scene, Ray ray) { * Changes colors for chunks inside the octree and submerged scenes. * Use only in preview mode - the ray should hit the sky in a real render. */ - private static boolean mapIntersection(Scene scene, Ray ray) { + private static boolean mapIntersection(Scene scene, Ray ray, IntersectionRecord intersectionRecord) { if (ray.d.y < 0) { // ray going below horizon double t = (scene.yMin - ray.o.y - scene.origin.y) / ray.d.y; - if (t > 0 && t < ray.t) { - Vector3 vec = new Vector3(); - vec.scaleAdd(t + Ray.OFFSET, ray.d, ray.o); + if (t > 0 && t < intersectionRecord.distance) { + Vector3 point = new Vector3(ray.o); + point.scaleAdd(t + Constants.OFFSET, ray.d); // must be submerged if water plane is enabled otherwise ray already had collided with water boolean isSubmerged = scene.isWaterPlaneEnabled(); - boolean insideOctree = scene.isInsideOctree(vec); - ray.t = t; - ray.o.set(vec); + boolean insideOctree = scene.isInsideOctree(point); + intersectionRecord.distance = t; + ray.o.set(point); double xm = ((ray.o.x) % 16.0 + 8.0) % 16.0; double zm = ((ray.o.z) % 16.0 + 8.0) % 16.0; if ( @@ -175,23 +110,23 @@ private static boolean mapIntersection(Scene scene, Ray ray) { (zm < chunkPatternLinePosition || zm > chunkPatternLinePosition + chunkPatternLineWidth) ) { // chunk fill if (isSubmerged) { - ray.color.set(chunkPatternFillColorSubmerged); + intersectionRecord.color.set(chunkPatternFillColorSubmerged); } else { - ray.color.set(chunkPatternFillColor); + intersectionRecord.color.set(chunkPatternFillColor); } } else { // chunk border if (isSubmerged) { - ray.color.set(chunkPatternLineColorSubmerged); + intersectionRecord.color.set(chunkPatternLineColorSubmerged); } else { - ray.color.set(chunkPatternLineColor); + intersectionRecord.color.set(chunkPatternLineColor); } } if(insideOctree) { - ray.color.scale(chunkPatternInsideOctreeColorFactor); + intersectionRecord.color.scale(chunkPatternInsideOctreeColorFactor); } // handle like a solid horizontal plane - ray.setCurrentMaterial(MinecraftBlock.STONE); - ray.setNormal(0, 1, 0); + intersectionRecord.material = MinecraftBlock.STONE; + intersectionRecord.setNormal(0, 1, 0); return true; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 061c76da3b..6a33fb2a26 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -19,8 +19,14 @@ import it.unimi.dsi.fastutil.io.FastBufferedInputStream; import it.unimi.dsi.fastutil.io.FastBufferedOutputStream; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import org.apache.commons.math3.util.FastMath; import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; import se.llbit.chunky.PersistentSettings; +import se.llbit.chunky.block.Void; import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.Block; import se.llbit.chunky.block.minecraft.Lava; @@ -31,6 +37,8 @@ import se.llbit.chunky.chunk.EmptyChunkData; import se.llbit.chunky.chunk.biome.BiomeData; import se.llbit.chunky.entity.*; +import se.llbit.chunky.main.Chunky; +import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.*; import se.llbit.chunky.renderer.export.PictureExportFormat; @@ -43,6 +51,16 @@ import se.llbit.chunky.renderer.renderdump.RenderDump; import se.llbit.chunky.renderer.scene.biome.BiomeBlendingUtility; import se.llbit.chunky.renderer.scene.biome.BiomeStructure; +import se.llbit.chunky.renderer.scene.fog.Fog; +import se.llbit.chunky.renderer.scene.fog.FogMode; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolume; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolumeShape; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolumeStore; +import se.llbit.chunky.renderer.scene.watershading.LegacyWaterShader; +import se.llbit.chunky.renderer.scene.watershading.SimplexWaterShader; +import se.llbit.chunky.renderer.scene.watershading.StillWaterShader; +import se.llbit.chunky.renderer.scene.watershading.WaterShader; +import se.llbit.chunky.renderer.scene.watershading.WaterShadingStrategy; import se.llbit.chunky.renderer.scene.biome.ChunkBiomeBlendingHelper; import se.llbit.chunky.renderer.scene.sky.Sky; import se.llbit.chunky.renderer.scene.sky.Sun; @@ -53,13 +71,13 @@ import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; +import se.llbit.chunky.world.material.WaterPlaneMaterial; import se.llbit.chunky.world.region.MCRegion; import se.llbit.json.*; import se.llbit.log.Log; import se.llbit.math.*; import se.llbit.math.structures.Position2IntStructure; -import se.llbit.nbt.CompoundTag; -import se.llbit.nbt.Tag; +import se.llbit.nbt.*; import se.llbit.util.*; import se.llbit.util.annotation.NotNull; import se.llbit.util.io.PositionalInputStream; @@ -86,32 +104,28 @@ *

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 class Scene implements Configurable, 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 = 10; - - protected static final double fSubSurface = 0.3; + public static final int SDF_VERSION = 11; /** * Minimum exposure. */ - public static final double MIN_EXPOSURE = 0.001; + public static final double MIN_EXPOSURE = -10; /** * Maximum exposure. */ - public static final double MAX_EXPOSURE = 1000.0; + public static final double MAX_EXPOSURE = 10; /** * Default gamma for the gamma correction post process. */ public static final float DEFAULT_GAMMA = 2.2f; - public static final boolean DEFAULT_EMITTERS_ENABLED = false; - /** * Default emitter intensity. */ @@ -120,7 +134,7 @@ public class Scene implements JsonSerializable, Refreshable { /** * Minimum emitter intensity. */ - public static final double MIN_EMITTER_INTENSITY = 0.01; + public static final double MIN_EMITTER_INTENSITY = 0; /** * Maximum emitter intensity. @@ -128,24 +142,30 @@ public class Scene implements JsonSerializable, Refreshable { public static final double MAX_EMITTER_INTENSITY = 1000; /** - * Default transmissivity cap. + * Default method for emitter mapping. + */ + public static final EmitterMappingType DEFAULT_EMITTER_MAPPING_TYPE = + EmitterMappingType.BRIGHTEST_CHANNEL; + + /** + * Default exponent for emitter mapping. */ - public static final double DEFAULT_TRANSMISSIVITY_CAP = 1; + public static final double DEFAULT_EMITTER_MAPPING_EXPONENT = 1.5; /** - * Minimum transmissivity cap. + * Minimum emitter mapping exponent. */ - public static final double MIN_TRANSMISSIVITY_CAP = 1; + public static final double MIN_EMITTER_MAPPING_EXPONENT = 0; /** - * Maximum transmissivity cap. + * Maximum emitter mapping exponent. */ - public static final double MAX_TRANSMISSIVITY_CAP = 3; + public static final double MAX_EMITTER_MAPPING_EXPONENT = 5; /** * Default exposure. */ - public static final double DEFAULT_EXPOSURE = 1.0; + public static final double DEFAULT_EXPOSURE = 0.0; /** * Default fog density. @@ -155,23 +175,19 @@ public class Scene implements JsonSerializable, Refreshable { /** * Default post processing filter. */ - public static final PostProcessingFilter DEFAULT_POSTPROCESSING_FILTER = PostProcessingFilters - .getPostProcessingFilterFromId("GAMMA").orElse(PostProcessingFilters.NONE); + public static final String DEFAULT_POSTPROCESSING_FILTER_ID = "GAMMA"; private static boolean invalidWarn = false; 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()); public int sdfVersion = -1; public String name = "default_" + new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); public CanvasConfig canvasConfig = new CanvasConfig(); - public PostProcessingFilter postProcessingFilter = DEFAULT_POSTPROCESSING_FILTER; + public ArrayList postprocessingFilters = new ArrayList<>(); private PictureExportFormat pictureExportFormat = PictureExportFormats.PNG; public long renderTime; /** @@ -186,7 +202,9 @@ public class Scene implements JsonSerializable, Refreshable { /** * Branch count for the scene. */ - protected int branchCount = PersistentSettings.getBranchCountDefault(); + // TODO: Make branched path tracing work on the new path tracer. + // protected int branchCount = PersistentSettings.getBranchCountDefault(); + protected int branchCount = 1; /** * Recursive ray depth limit (not including Russian Roulette). */ @@ -196,26 +214,19 @@ public class Scene implements JsonSerializable, Refreshable { 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 double emitterMappingExponent = DEFAULT_EMITTER_MAPPING_EXPONENT; + protected EmitterMappingType emitterMappingType = DEFAULT_EMITTER_MAPPING_TYPE; protected EmitterSamplingStrategy emitterSamplingStrategy = EmitterSamplingStrategy.NONE; - protected boolean fancierTranslucency = true; - protected double transmissivityCap = DEFAULT_TRANSMISSIVITY_CAP; - protected SunSamplingStrategy sunSamplingStrategy = SunSamplingStrategy.FAST; + protected SunSamplingStrategy sunSamplingStrategy = SunSamplingStrategy.SAMPLE_THROUGH_OPACITY; - /** - * Water opacity modifier. - */ - protected double waterOpacity = PersistentSettings.getWaterOpacity(); - protected double waterVisibility = PersistentSettings.getWaterVisibility(); protected WaterShadingStrategy waterShadingStrategy = WaterShadingStrategy.valueOf(PersistentSettings.getWaterShadingStrategy()); private final StillWaterShader stillWaterShader = new StillWaterShader(); private final LegacyWaterShader legacyWaterShader = new LegacyWaterShader(); private final SimplexWaterShader simplexWaterShader = new SimplexWaterShader(); private WaterShader currentWaterShader = getWaterShader(waterShadingStrategy); - protected boolean useCustomWaterColor = PersistentSettings.getUseCustomWaterColor(); protected boolean waterPlaneEnabled = false; protected double waterPlaneHeight = World.SEA_LEVEL; @@ -255,9 +266,10 @@ public class Scene implements JsonSerializable, Refreshable { private BlockPalette palette; private Octree worldOctree; - private Octree waterOctree; - private SceneEntities entities = new SceneEntities(); + private final SceneEntities entities = new SceneEntities(); + private final FogVolumeStore fogVolumeStore = new FogVolumeStore(); + private final ArrayList cloudLayers = new ArrayList<>(0); /** Material properties for this scene. */ public Map materials = new HashMap<>(); @@ -341,7 +353,7 @@ public class Scene implements JsonSerializable, Refreshable { * *

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(). + * fromJson(), or fromJson(), or by calling initBuffers(). */ public Scene() { sppTarget = PersistentSettings.getSppTargetDefault(); @@ -349,8 +361,14 @@ public Scene() { palette = new BlockPalette(); worldOctree = new Octree(octreeImplementation, 1); - waterOctree = new Octree(octreeImplementation, 1); emitterGrid = null; + try { + postprocessingFilters.add(PostProcessingFilters.getPostProcessingFilterFromId(DEFAULT_POSTPROCESSING_FILTER_ID).get() + .newInstance()); + } catch (InstantiationException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } /** @@ -410,7 +428,6 @@ public synchronized void copyState(Scene other, boolean copyChunks) { // When the other scene is changed it must create a new octree. palette = other.palette; worldOctree = other.worldOctree; - waterOctree = other.waterOctree; entities.copyState(other.entities); @@ -434,20 +451,18 @@ public synchronized void copyState(Scene other, boolean copyChunks) { waterShadingStrategy = other.waterShadingStrategy; currentWaterShader = other.currentWaterShader.clone(); - waterOpacity = other.waterOpacity; - waterVisibility = other.waterVisibility; - useCustomWaterColor = other.useCustomWaterColor; - waterColor.set(other.waterColor); fog.set(other.fog); + fogVolumeStore.copyState(other.fogVolumeStore); + cloudLayers.clear(); + cloudLayers.addAll(other.cloudLayers); biomeColors = other.biomeColors; biomeBlendingRadius = other.biomeBlendingRadius; sunSamplingStrategy = other.sunSamplingStrategy; - emittersEnabled = other.emittersEnabled; emitterIntensity = other.emitterIntensity; + emitterMappingExponent = other.emitterMappingExponent; + emitterMappingType = other.emitterMappingType; emitterSamplingStrategy = other.emitterSamplingStrategy; preventNormalEmitterWithSampling = other.preventNormalEmitterWithSampling; - fancierTranslucency = other.fancierTranslucency; - transmissivityCap = other.transmissivityCap; transparentSky = other.transparentSky; yClipMin = other.yClipMin; yClipMax = other.yClipMax; @@ -602,16 +617,6 @@ public double getExposure() { return exposure; } - /** - * Set emitters enable flag. - */ - public synchronized void setEmittersEnabled(boolean value) { - if (value != emittersEnabled) { - emittersEnabled = value; - refresh(); - } - } - /** * Set sun sampling strategy. */ @@ -633,11 +638,11 @@ public SunSamplingStrategy getSunSamplingStrategy() { * @return true if emitters are enabled */ public boolean getEmittersEnabled() { - return emittersEnabled; + return emitterIntensity > Constants.EPSILON; } /** - * @return The BlockPallete for the scene + * @return The BlockPalette for the scene */ public BlockPalette getPalette() { return palette; } @@ -664,7 +669,7 @@ public void rayTrace(RayTracer rayTracer, WorkerState state) { * @param ray ray to test against scene * @return true if an intersection was found */ - public boolean intersect(Ray ray) { + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord, Random random) { boolean hit = false; if (Double.isNaN(ray.d.x) || Double.isNaN(ray.d.y) || Double.isNaN(ray.d.z) || @@ -679,84 +684,153 @@ public boolean intersect(Ray ray) { ray.d.set(0, 1, 0); } - if (entities.intersect(ray)) { - hit = true; + if (worldOctree.closestIntersection(ray, intersectionRecord, this)) { + if (intersectionRecord.distance > Constants.EPSILON) { + hit = true; + } else { + intersectionRecord.reset(); + } } - if (worldIntersection(ray)) { + + IntersectionRecord intersectionTest = new IntersectionRecord(); + if (entities.closestIntersection(ray, intersectionTest, this) && intersectionTest.distance < intersectionRecord.distance - Constants.EPSILON) { hit = true; + if (intersectionTest.material == Air.INSTANCE) { + Vector3 o = ray.o.rScaleAdd(intersectionTest.distance, ray.d); + o.scaleAdd(-Constants.OFFSET, intersectionTest.n); + intersectionTest.material = getWorldMaterial(new Ray(o, ray.d)); + } + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; } - if (hit) { - ray.distance += ray.t; - ray.o.scaleAdd(ray.t, ray.d); - updateOpacity(ray); - return true; - } - return false; - } - /** - * Test whether the ray intersects any voxel before exiting the Octree. - * - * @param ray the ray - * @return {@code true} if the ray intersects a voxel - */ - private boolean worldIntersection(Ray ray) { - Ray start = new Ray(ray); - start.setCurrentMaterial(ray.getPrevMaterial(), ray.getPrevData()); - boolean hit = false; - Ray r = new Ray(start); - r.setCurrentMaterial(start.getPrevMaterial(), start.getPrevData()); - if (worldOctree.enterBlock(this, r, palette) && r.distance < ray.t) { - ray.t = r.distance; - ray.setNormal(r.getNormal()); - ray.color.set(r.color); - ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData()); - ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData()); + intersectionTest.reset(); + if (waterPlaneEnabled && waterPlaneIntersection(ray, intersectionTest) && intersectionTest.distance < intersectionRecord.distance - Constants.EPSILON) { hit = true; + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; } - if (start.getCurrentMaterial().isWater()) { - r = new Ray(start); - r.setCurrentMaterial(start.getPrevMaterial(), start.getPrevData()); - if(waterOctree.exitWater(this, r, palette) && r.distance < ray.t - Ray.EPSILON) { - ray.t = r.distance; - ray.setNormal(r.getNormal()); - ray.color.set(r.color); - ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData()); - ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData()); + + if (random != null) { + intersectionTest.reset(); + if (ray.getCurrentMedium().volumeIntersect(intersectionTest, random) && intersectionTest.distance < intersectionRecord.distance - Constants.EPSILON) { hit = true; - } else if(ray.getPrevMaterial() == Air.INSTANCE) { - ray.setPrevMaterial(Water.INSTANCE, 1 << Water.FULL_BLOCK); + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; } - } else { - r = new Ray(start); - r.setCurrentMaterial(start.getPrevMaterial(), start.getPrevData()); - if (waterOctree.enterBlock(this, r, palette) && r.distance < ray.t) { - ray.t = r.distance; - ray.setNormal(r.getNormal()); - ray.color.set(r.color); - ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData()); - ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData()); + + intersectionTest.reset(); + if (fogVolumeStore.closestIntersection(ray, intersectionTest, this, random) && intersectionTest.distance < intersectionRecord.distance) { hit = true; + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; } + + for (CloudLayer cloudLayer : cloudLayers) { + intersectionTest.reset(); + if (cloudLayer.closestIntersection(ray, intersectionTest, this, random) && intersectionTest.distance < intersectionRecord.distance) { + hit = true; + if (intersectionTest.material == Air.INSTANCE) { + Vector3 o = ray.o.rScaleAdd(intersectionTest.distance, ray.d); + o.scaleAdd(-Constants.OFFSET, intersectionTest.n); + intersectionTest.material = getWorldMaterial(new Ray(o, ray.d)); + } + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; + } + } + } + + if (hit && intersectionRecord.material == Air.INSTANCE && isUnderWaterPlane(ray.o.rScaleAdd(intersectionRecord.distance + Constants.OFFSET, ray.d))) { + intersectionRecord.material = WaterPlaneMaterial.INSTANCE; } + return hit; } - public void updateOpacity(Ray ray) { - if (ray.getCurrentMaterial().isWater() || (ray.getCurrentMaterial() == Air.INSTANCE - && ray.getPrevMaterial().isWater())) { - if (useCustomWaterColor) { - ray.color.x = waterColor.x; - ray.color.y = waterColor.y; - ray.color.z = waterColor.z; + private boolean waterPlaneIntersection(Ray ray, IntersectionRecord intersectionRecord) { + double t = (getEffectiveWaterPlaneHeight() - ray.o.y - origin.y) / ray.d.y; + if (getWaterPlaneChunkClip()) { + Vector3 pos = ray.o.rScaleAdd(t, ray.d); + if (isChunkLoaded((int)Math.floor(pos.x), (int)Math.floor(pos.y), (int)Math.floor(pos.z))) { + return false; + } + } + if (t > 0) { + intersectionRecord.distance = t; + // Create a new ray at the intersection position to get the normal. + Ray testRay = new Ray(ray); + testRay.o.scaleAdd(intersectionRecord.distance, testRay.d); + Vector3 shadeNormal = currentWaterShader.doWaterShading(testRay, intersectionRecord, animationTime); + intersectionRecord.shadeN.set(shadeNormal); + if (ray.d.y < 0) { + WaterPlaneMaterial.INSTANCE.getColor(intersectionRecord); + intersectionRecord.n.set(0, 1, 0); + intersectionRecord.material = WaterPlaneMaterial.INSTANCE; + } else if (ray.d.y > 0) { + Air.INSTANCE.getColor(intersectionRecord); + intersectionRecord.n.set(0, -1, 0); + intersectionRecord.shadeN.scale(-1); + intersectionRecord.material = Air.INSTANCE; } else { - float[] waterColor = ray.getBiomeWaterColor(this); - ray.color.x *= waterColor[0]; - ray.color.y *= waterColor[1]; - ray.color.z *= waterColor[2]; + return false; } - ray.color.w = waterOpacity; + return true; } + return false; + } + + public boolean isUnderWaterPlane(Vector3 pos) { + if (waterPlaneEnabled && pos.y + origin.y < getEffectiveWaterPlaneHeight()) { + if (waterPlaneChunkClip) { + return !isChunkLoaded((int) Math.floor(pos.x), (int) Math.floor(pos.y), + (int) Math.floor(pos.z)); + } + return true; + } + return false; + } + + public Block waterPlaneMaterial(Vector3 pos) { + return isUnderWaterPlane(pos) ? WaterPlaneMaterial.INSTANCE : Air.INSTANCE; + } + + /** + * Calculate sky occlusion. + * @return occlusion value (1 = occluded, 0 = transparent) + */ + public double skyOcclusion(WorkerState state) { + Ray ray = state.ray; + IntersectionRecord intersectionRecord = state.intersectionRecord; + double occlusion = 1.0; + while (occlusion > Constants.EPSILON) { + intersectionRecord.reset(); + if (!intersect(ray, intersectionRecord, state.random)) { + break; + } else { + occlusion *= (1 - intersectionRecord.color.w * intersectionRecord.material.alpha); + ray.o.scaleAdd((intersectionRecord.distance + Constants.OFFSET), ray.d); + if (!intersectionRecord.isNoMediumChange()) { + ray.setCurrentMedium(intersectionRecord.material); + } + } + } + return 1 - occlusion; } /** @@ -813,9 +887,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Map= yMax) - continue; - 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 yCubeMin = Math.floorDiv(yMin, 16); + int yCubeMax = (yMax+15) / 16; + for(int yCube = yCubeMin; yCube < yCubeMax; ++yCube) { + // Reset the cubes + Arrays.fill(cubeWorldBlocks, 1); + for(int cy = 0; cy < 16; ++cy) { //Uses chunk min and max, rather than global - minor optimisation for pre1.13 worlds + int y = yCube * 16 + cy; + if (y < yMin || y >= yMax) + continue; + 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 cubeIndex = (cz * 16 + cy) * 16 + cx; + int cubeIndex = (cz * 16 + cy) * 16 + cx; - // Change the type of hidden blocks to ANY_TYPE - boolean onEdge = y <= yMin || y >= yMax - 1 || chunkData.isBlockOnEdge(cx, y, cz); - boolean isHidden = !onEdge + // Change the type of hidden blocks to ANY_TYPE + boolean onEdge = y <= yMin || y >= yMax - 1 || chunkData.isBlockOnEdge(cx, y, cz); + boolean isHidden = !onEdge && palette.get(chunkData.getBlockAt(cx + 1, y, cz)).opaque && palette.get(chunkData.getBlockAt(cx - 1, y, cz)).opaque && palette.get(chunkData.getBlockAt(cx, y + 1, cz)).opaque @@ -990,179 +1061,174 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Map 1e-4) { - // X and Z are Chunky position but Y is world position - emitterGrid.addEmitter(new Grid.EmitterPosition(x, y - origin.y, z, block)); + } else if (y + 1 < yMax && block instanceof Lava) { + if (palette.get(chunkData.getBlockAt(cx, y + 1, cz)) instanceof Lava) { + octNode = palette.getLavaId(0, 1 << Water.FULL_BLOCK_DATA); + } else if (!onEdge) { + // Compute lava level for blocks not on edge + Lava lava = (Lava) block; + int level0 = 8 - lava.level; + int corner0 = level0; + int corner1 = level0; + int corner2 = level0; + int corner3 = level0; + + int level = Chunk.lavaLevelAt(chunkData, palette, cx - 1, y, cz, level0); + corner3 += level; + corner0 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx - 1, y, cz + 1, level0); + corner0 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx, y, cz + 1, level0); + corner0 += level; + corner1 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx + 1, y, cz + 1, level0); + corner1 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx + 1, y, cz, level0); + corner1 += level; + corner2 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx + 1, y, cz - 1, level0); + corner2 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx, y, cz - 1, level0); + corner2 += level; + corner3 += level; + + level = Chunk.lavaLevelAt(chunkData, palette, cx - 1, y, cz - 1, level0); + corner3 += level; + + corner0 = Math.min(7, 8 - (corner0 / 4)); + corner1 = Math.min(7, 8 - (corner1 / 4)); + corner2 = Math.min(7, 8 - (corner2 / 4)); + corner3 = Math.min(7, 8 - (corner3 / 4)); + octNode = palette.getLavaId( + lava.level, + (corner0 << WaterModel.CORNER_0) + | (corner1 << WaterModel.CORNER_1) + | (corner2 << WaterModel.CORNER_2) + | (corner3 << WaterModel.CORNER_3) + ); } } + cubeWorldBlocks[cubeIndex] = octNode; + + if (emitterGrid != null && block.emittance > 1e-4) { + // X and Z are Chunky position but Y is world position + emitterGrid.addEmitter(new Grid.EmitterPosition(x, y - origin.y, z, block)); + } } } } - worldOctree.setCube(4, cubeWorldBlocks, cp.x * 16 - origin.x, yCube * 16 - origin.y, cp.z * 16 - origin.z); - waterOctree.setCube(4, cubeWaterBlocks, cp.x * 16 - origin.x, yCube * 16 - origin.y, cp.z * 16 - origin.z); } + worldOctree.setCube(4, cubeWorldBlocks, cp.x*16 - origin.x, yCube*16 - origin.y, cp.z*16 - origin.z); + } // Block entities are also called "tile entities". These are extra bits of metadata // about certain blocks or entities. @@ -1230,7 +1296,6 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Map= Math.max(ycenter-128, yMin); --y) { Material block = worldOctree.getMaterial(xcenter - origin.x, y - origin.y, zcenter - origin.z, palette); - if (!(block instanceof Air)) { + if (block != Void.INSTANCE && block != Air.INSTANCE) { return new Vector3(xcenter, y + 5, zcenter); } } @@ -1699,21 +1763,27 @@ synchronized public void clearResetFlags() { * * @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(Water.INSTANCE); - } else { - ray.setCurrentMaterial(Air.INSTANCE); - } + public boolean traceTarget(Ray ray, IntersectionRecord intersectionRecord) { 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() != Air.INSTANCE) { + ray.setCurrentMedium(getWorldMaterial(ray)); + while (true) { + IntersectionRecord intersectionTest = new IntersectionRecord(); + if (!intersect(ray, intersectionTest, null)) { + break; + } else if (!intersectionTest.material.isSameMaterial(ray.getCurrentMedium()) && intersectionTest.color.w > Constants.EPSILON) { + ray.o.scaleAdd((intersectionTest.distance), ray.d); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.setNormal(intersectionTest.n); + intersectionRecord.distance = intersectionTest.distance; return true; + } else { + ray.o.scaleAdd((intersectionTest.distance + Constants.OFFSET), ray.d); + if (!intersectionTest.material.isSameMaterial(ray.getCurrentMedium())) { + ray.setCurrentMedium(intersectionTest.material); + } } } return false; @@ -1733,7 +1803,8 @@ public void autoFocus() { */ public Vector3 getTargetPosition() { Ray ray = new Ray(); - if (!traceTarget(ray)) { + IntersectionRecord intersectionRecord = new IntersectionRecord(); + if (!traceTarget(ray, intersectionRecord)) { return null; } else { Vector3 target = new Vector3(ray.o); @@ -1742,6 +1813,23 @@ public Vector3 getTargetPosition() { } } + public Material getTargetMaterial(double x, double y) { + Ray ray = new Ray(); + + camera.calcViewRay(ray, x, y); + ray.o.x -= origin.x; + ray.o.y -= origin.y; + ray.o.z -= origin.z; + ray.setCurrentMedium(getWorldMaterial(ray)); + + IntersectionRecord intersectionRecord = new IntersectionRecord(); + if (intersect(ray, intersectionRecord, null)) { + return intersectionRecord.material; + } else { + return Air.INSTANCE; + } + } + /** * @return World origin in the Octree */ @@ -1754,7 +1842,7 @@ public Vector3i getOrigin() { */ public void setName(String newName) { newName = AsynchronousSceneManager.sanitizedSceneName(newName); - if (newName.length() > 0) { + if (!newName.isEmpty()) { name = newName; } } @@ -1762,19 +1850,26 @@ public void setName(String newName) { /** * @return The current postprocessing filter */ - public PostProcessingFilter getPostProcessingFilter() { - return postProcessingFilter; + public List getPostprocessingFilters() { + return new ArrayList<>(postprocessingFilters); } /** - * Change the postprocessing filter + * Add a postprocessing filter * * @param p The new postprocessing filter */ - public synchronized void setPostprocess(PostProcessingFilter p) { - postProcessingFilter = p; - if (postProcessingFilter instanceof Configurable) { - ((Configurable) postProcessingFilter).reset(); + public synchronized void addPostprocessingFilter(PostProcessingFilter p) { + postprocessingFilters.add(p); + if (mode == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + refresh(); + } + } + + public synchronized void removePostprocessingFilter(int index) { + if (index < postprocessingFilters.size()) { + postprocessingFilters.remove(index); } if (mode == RenderMode.PREVIEW) { // Don't interrupt the render if we are currently rendering. @@ -1797,6 +1892,36 @@ public void setEmitterIntensity(double value) { refresh(); } + /** + * @return The current emitter mapping exponent + */ + public double getEmitterMappingExponent() { + return emitterMappingExponent; + } + + /** + * Set the emitter mapping exponent. + */ + public void setEmitterMappingExponent(double value) { + emitterMappingExponent = value; + refresh(); + } + + /** + * @return The current emitter mapping type. + */ + public EmitterMappingType getEmitterMappingType() { + return emitterMappingType; + } + + /** + * Set the emitter mapping type. + */ + public void setEmitterMappingType(EmitterMappingType value) { + emitterMappingType = value; + refresh(); + } + /** * Set the transparent sky option. */ @@ -1852,7 +1977,7 @@ public double getWaterPlaneHeight() { */ public double getEffectiveWaterPlaneHeight() { if(waterPlaneOffsetEnabled) { - return waterPlaneHeight - Water.TOP_BLOCK_GAP; + return waterPlaneHeight - WaterModel.TOP_BLOCK_GAP; } else { return waterPlaneHeight; } @@ -1923,7 +2048,7 @@ public boolean shouldSaveDumps() { */ public synchronized void copyTransients(Scene other) { name = other.name; - postProcessingFilter = other.postProcessingFilter; + postprocessingFilters = other.postprocessingFilters; exposure = other.exposure; dumpFrequency = other.dumpFrequency; saveSnapshots = other.saveSnapshots; @@ -2120,16 +2245,52 @@ public synchronized void writeFrame(OutputStream out, PictureExportFormat mode, * but in some cases a separate post-processing pass is needed. */ public void postProcessFrame(TaskTracker.Task task) { - PostProcessingFilter filter = postProcessingFilter; - if(mode == RenderMode.PREVIEW) { - filter = PreviewFilter.INSTANCE; - } - filter.processFrame( - canvasConfig.getWidth(), canvasConfig.getHeight(), - samples, backBuffer, - exposure, - task - ); + List filters; + if (mode == RenderMode.PREVIEW) { + filters = Collections.singletonList(PreviewFilter.INSTANCE); + } else { + filters = getPostprocessingFilters(); + } + + int width = canvasConfig.getWidth(); + int height = canvasConfig.getHeight(); + + double[] intermediate = new double[samples.length]; + for (int i = 0; i < intermediate.length; i++) { + intermediate[i] = samples[i] * FastMath.pow(2, exposure); + } + + task.update(height, 0); + AtomicInteger done = new AtomicInteger(0); + + for (PostProcessingFilter filter : filters) { + filter.processFrame(width, height, intermediate); + } + + Chunky.getCommonThreads() + .submit(() -> + // do rows in parallel + IntStream.range(0, height).parallel() + .forEach(y -> { + double[] pixelBuffer = new double[3]; + + int rowOffset = y * width; + // columns will be processed sequential + // TODO: SIMD support once Vector API is finalized + for (int x = 0; x < width; x++) { + int pixelOffset = (rowOffset + x) * 3; + System.arraycopy(intermediate, pixelOffset, pixelBuffer, 0, 3); + // TODO: extract clamping into own interface + Arrays.setAll(pixelBuffer, k -> Math.min(1, pixelBuffer[k])); + backBuffer.setPixel(x, y, ColorUtil.getRGB(pixelBuffer)); + } + + task.update(height, done.incrementAndGet()); + }) + ).join(); + + task.update(height, done.incrementAndGet()); + finalized = true; } @@ -2168,7 +2329,7 @@ private synchronized void saveOctree(SceneIOProvider ioContext, TaskTracker task boolean saved = false; try (DataOutputStream out = new DataOutputStream(new FastBufferedOutputStream(new GZIPOutputStream(ioContext.getSceneFileOutputStream(fileName))))) { - OctreeFileFormat.store(out, worldOctree, waterOctree, palette, grassTexture, foliageTexture, dryFoliageTexture, waterTexture); + OctreeFileFormat.store(out, worldOctree, palette, grassTexture, foliageTexture, dryFoliageTexture, waterTexture); saved = true; task.update(2); @@ -2238,7 +2399,6 @@ private synchronized boolean loadOctree(SceneIOProvider ioContext, TaskTracker t worldOctree = data.worldTree; worldOctree.setTimestamp(fileTimestamp); - waterOctree = data.waterTree; grassTexture = data.grassColors; foliageTexture = data.foliageColors; dryFoliageTexture = data.dryFoliageColors; @@ -2327,10 +2487,11 @@ public synchronized String sceneStatus() { } 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(block.name); + IntersectionRecord intersectionRecord = new IntersectionRecord(); + if (traceTarget(ray, intersectionRecord) && intersectionRecord.material instanceof Block) { + Block block = (Block) intersectionRecord.material; + buf.append(String.format("Target distance: %.2f m\n", intersectionRecord.distance)); + buf.append(String.format("Target material: %s\n", block.name)); String description = block.description(); if (!description.isEmpty()) { buf.append(" (").append(description).append(")"); @@ -2338,9 +2499,11 @@ public synchronized String sceneStatus() { buf.append("\n"); } Vector3 pos = camera.getPosition(); - buf.append(String.format("pos: (%.1f, %.1f, %.1f)\n", pos.x, pos.y, pos.z)); + buf.append(String.format("Camera position: (%.1f, %.1f, %.1f)\n", pos.x, pos.y, pos.z)); + ray.o.set(pos.rSub(origin)); + buf.append(String.format("Ray material: %s\n", getWorldMaterial(ray).name)); - buf.append("facing: "); + buf.append("Camera facing: "); double yaw = camera.getYaw(); yaw = (yaw + Math.PI*2) % (Math.PI*2); int index = (int)Math.floor((yaw + Math.PI/8) / (Math.PI/4)) % 8; @@ -2521,6 +2684,35 @@ public boolean shouldSaveSnapshots() { return saveSnapshots; } + public Material getWorldMaterial(Ray ray) { + int x = (int) QuickMath.floor(ray.o.x); + int y = (int) QuickMath.floor(ray.o.y); + int z = (int) QuickMath.floor(ray.o.z); + Material material = worldOctree.getMaterial(x, y, z, palette); + if (material instanceof Block && !material.hidden) { + if (material == Air.INSTANCE || material == Void.INSTANCE) { + if (isUnderWaterPlane(ray.o)) { + return WaterPlaneMaterial.INSTANCE; + } else { + return material; + } + } else if (((Block) material).isInside(ray)) { + return material; + } else if (material.waterlogged) { + if (worldOctree.getMaterial(x, y + 1, z, palette).isWaterFilled()) { + return palette.water; + } else if (ray.o.y - QuickMath.floor(ray.o.y) < 1 - WaterModel.TOP_BLOCK_GAP) { + return palette.water; + } + } + } + if (isUnderWaterPlane(ray.o)) { + return WaterPlaneMaterial.INSTANCE; + } else { + return Air.INSTANCE; + } + } + public boolean isInWater(Ray ray) { if (isWaterPlaneEnabled() && ray.o.y + origin.y < getEffectiveWaterPlaneHeight()) { if (getWaterPlaneChunkClip()) { @@ -2531,50 +2723,19 @@ public boolean isInWater(Ray ray) { return true; } } - if (waterOctree.isInside(ray.o)) { + 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); - Material block = waterOctree.getMaterial(x, y, z, palette); + Material block = worldOctree.getMaterial(x, y, z, palette); return block.isWater() - && ((ray.o.y - y) < 0.875 || ((Water) block).isFullBlock()); + && ((Water) block).isInside(ray); } return false; } - 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 boolean isInsideOctree(Vector3 point) { + return worldOctree.isInside(point); } public void setFogColor(Vector3 color) { @@ -2582,33 +2743,21 @@ public void setFogColor(Vector3 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); - canvasConfig.storeConfiguration(json); + json.add("canvasConfig", canvasConfig.toJson()); json.add("yClipMin", yClipMin); json.add("yClipMax", yClipMax); json.add("yMin", yMin); json.add("yMax", yMax); json.add("exposure", exposure); - json.add("postprocess", postProcessingFilter.getId()); - if (postProcessingFilter instanceof Configurable) { - JsonObject postprocessJson = new JsonObject(); - ((Configurable) postProcessingFilter).storeConfiguration(postprocessJson); - json.add("postprocessSettings", postprocessJson); + JsonArray postprocessingFilters = new JsonArray(); + for (PostProcessingFilter filter : this.postprocessingFilters) { + postprocessingFilters.add(filter.toJson()); } + json.add("postprocessingFilters", postprocessingFilters); json.add("outputMode", pictureExportFormat.getName()); json.add("renderTime", renderTime); json.add("spp", spp); @@ -2618,20 +2767,19 @@ public void setUseCustomWaterColor(boolean value) { json.add("pathTrace", mode != RenderMode.PREVIEW); json.add("dumpFrequency", dumpFrequency); json.add("saveSnapshots", saveSnapshots); - json.add("emittersEnabled", emittersEnabled); json.add("emitterIntensity", emitterIntensity); - json.add("fancierTranslucency", fancierTranslucency); - json.add("transmissivityCap", transmissivityCap); + json.add("emitterMappingExponent", emitterMappingExponent); + json.add("emitterMappingType", emitterMappingType.getId()); json.add("sunSamplingStrategy", sunSamplingStrategy.getId()); json.add("waterShadingStrategy", waterShadingStrategy.getId()); - json.add("waterOpacity", waterOpacity); - json.add("waterVisibility", waterVisibility); - json.add("useCustomWaterColor", useCustomWaterColor); - if (useCustomWaterColor) { - json.add("waterColor", JsonUtil.rgbToJson(waterColor)); - } - currentWaterShader.save(json); + json.add("currentWaterShader", currentWaterShader.toJson()); json.add("fog", fog.toJson()); + json.add("fogVolumeStore", fogVolumeStore.toJson()); + JsonArray cloudLayersArray = new JsonArray(); + for (CloudLayer cloudLayer : cloudLayers) { + cloudLayersArray.add(cloudLayer.toJson()); + } + json.add("cloudLayers", cloudLayersArray); json.add("biomeColorsEnabled", biomeColors); json.add("biomeBlendingRadius", biomeBlendingRadius); json.add("transparentSky", transparentSky); @@ -2695,6 +2843,7 @@ private JsonObject mapToJson(Map map) { /** * Reset the scene settings and import from a JSON object. */ + @Override public synchronized void fromJson(JsonObject json) { boolean finalizeBufferPrev = finalizeBuffer; // Remember the finalize setting. Scene scene = new Scene(); @@ -2708,6 +2857,11 @@ public synchronized void fromJson(JsonObject json) { name = json.get("name").stringValue("default"); } + @Override + public void reset() { + + } + public EntityLoadingPreferences getEntityLoadingPreferences() { return entities.getEntityLoadingPreferences(); } @@ -2731,7 +2885,7 @@ public void addActor(Entity entity) { public void removeEntity(Entity entity) { entities.removeEntity(entity); - rebuildActorBvh(); + rebuildBvh(); } public void addPlayer(PlayerEntity player) { @@ -2739,12 +2893,59 @@ public void addPlayer(PlayerEntity player) { rebuildActorBvh(); } + public void clearEntities() { + entities.clear(); + rebuildBvh(); + } + + public List getCloudLayers() { + return this.cloudLayers; + } + + public void addCloudLayer() { + this.cloudLayers.add(new CloudLayer()); + refresh(); + } + + public void removeCloudLayer(int index) { + this.cloudLayers.remove(index); + refresh(); + } + + public List getFogVolumes() { + return this.fogVolumeStore.listFogVolumes(); + } + + public void addFogVolume(FogVolumeShape shape) { + if (fogVolumeStore.addVolume(shape)) { + buildFogVolumeBVH(); + } else { + refresh(); + } + } + + public void removeFogVolume(int index) { + if (fogVolumeStore.removeVolume(index)) { + buildFogVolumeBVH(); + } else { + refresh(); + } + } + + public void buildFogVolumeBVH() { + fogVolumeStore.finalizeLoading(); + fogVolumeStore.buildBvh(TaskTracker.Task.NONE, origin); + refresh(); + } + /** * Clears the scene, preparing to load fresh chunks. */ public void clear() { cameraPresets = new JsonObject(); entities.clear(); + fogVolumeStore.clear(); + materials.clear(); } /** Create a backup of a scene file. */ @@ -2842,7 +3043,7 @@ public synchronized void importFromJson(JsonObject json) { int oldWidth = canvasConfig.getWidth(); int oldHeight = canvasConfig.getHeight(); - canvasConfig.loadConfiguration(json); + canvasConfig.fromJson(json.get("canvasConfig").asObject()); if(oldWidth != canvasConfig.getWidth() || oldHeight != canvasConfig.getHeight() || samples == null) { initBuffers(); } @@ -2853,17 +3054,29 @@ public synchronized void importFromJson(JsonObject json) { yMax = json.get("yMax").asInt(Math.min(yClipMax, yMax)); exposure = json.get("exposure").doubleValue(exposure); - postProcessingFilter = PostProcessingFilters - .getPostProcessingFilterFromId(json.get("postprocess").stringValue(postProcessingFilter.getId())) - .orElseGet(() -> { - if (json.get("postprocess").stringValue(null) != null) { - Log.warn("The post processing filter " + json + - " is unknown. Maybe you're missing a plugin that was used to create this scene?"); - } - return DEFAULT_POSTPROCESSING_FILTER; - }); - if (postProcessingFilter instanceof Configurable) { - ((Configurable) postProcessingFilter).loadConfiguration(json.get("postprocessSettings").asObject()); + JsonArray postprocessingFilters = json.get("postprocessingFilters").array(); + this.postprocessingFilters.clear(); + for (JsonValue filter : postprocessingFilters) { + JsonObject filterJson = filter.object(); + PostProcessingFilter postprocessingFilter; + try { + postprocessingFilter = + PostProcessingFilters.getPostProcessingFilterFromId(filterJson.get("id"). + stringValue(DEFAULT_POSTPROCESSING_FILTER_ID)). + orElseGet(() -> { + String filterId = filterJson.get("id").stringValue(null); + if (filterId != null) { + Log.warn("The post processing filter " + filterId + + " is unknown. Maybe you're missing a plugin that was used to " + + "create this scene?"); + } + return PostProcessingFilters.getPostProcessingFilterFromId(DEFAULT_POSTPROCESSING_FILTER_ID).get(); + }).newInstance(); + } catch (InstantiationException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + postprocessingFilter.fromJson(filterJson); + this.postprocessingFilters.add(postprocessingFilter); } pictureExportFormat = PictureExportFormats .getFormat(json.get("outputMode").stringValue(pictureExportFormat.getName())) @@ -2881,61 +3094,16 @@ public synchronized void importFromJson(JsonObject json) { } dumpFrequency = json.get("dumpFrequency").intValue(dumpFrequency); saveSnapshots = json.get("saveSnapshots").boolValue(saveSnapshots); - emittersEnabled = json.get("emittersEnabled").boolValue(emittersEnabled); emitterIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity); - fancierTranslucency = json.get("fancierTranslucency").boolValue(fancierTranslucency); - transmissivityCap = json.get("transmissivityCap").doubleValue(transmissivityCap); - - if (json.get("sunSamplingStrategy").isUnknown()) { - boolean sunSampling = json.get("sunEnabled").boolValue(false); - boolean drawSun = json.get("sun").asObject().get("drawTexture").boolValue(false); - if (drawSun) { - if (sunSampling) { - sunSamplingStrategy = SunSamplingStrategy.FAST; - } else { - sunSamplingStrategy = SunSamplingStrategy.NON_LUMINOUS; - } - } else { - sunSamplingStrategy = SunSamplingStrategy.FAST; - } - } else { - sunSamplingStrategy = SunSamplingStrategy.valueOf(json.get("sunSamplingStrategy").asString(SunSamplingStrategy.FAST.getId())); - } + emitterMappingExponent = json.get("emitterMappingExponent").doubleValue(emitterMappingExponent); + emitterMappingType = EmitterMappingType.valueOf(json.get("emitterMappingType").asString(DEFAULT_EMITTER_MAPPING_TYPE.getId())); - if (json.get("sunEnabled").boolValue(false)) { - sunSamplingStrategy = SunSamplingStrategy.FAST; - } else { - sunSamplingStrategy = SunSamplingStrategy.valueOf(json.get("sunSamplingStrategy").asString(SunSamplingStrategy.FAST.getId())); - } - - waterShadingStrategy = WaterShadingStrategy.valueOf(json.get("waterShadingStrategy").asString(WaterShadingStrategy.SIMPLEX.getId())); - if (!json.get("waterShader").isUnknown()) { - String waterShader = json.get("waterShader").stringValue("SIMPLEX"); - if(waterShader.equals("LEGACY")) - waterShadingStrategy = WaterShadingStrategy.TILED_NORMALMAP; - else if(waterShader.equals("SIMPLEX")) - waterShadingStrategy = WaterShadingStrategy.SIMPLEX; - else { - Log.infof("Unknown water shader %s, using SIMPLEX", waterShader); - waterShadingStrategy = WaterShadingStrategy.SIMPLEX; - } - } else { - waterShadingStrategy = WaterShadingStrategy.TILED_NORMALMAP; - } - if (!json.get("stillWater").isUnknown()) { - if (json.get("stillWater").boolValue(false)) { - waterShadingStrategy = WaterShadingStrategy.STILL; - } - } + sunSamplingStrategy = SunSamplingStrategy.get(json.get("sunSamplingStrategy").asString(sunSamplingStrategy.getId())); + + waterShadingStrategy = WaterShadingStrategy.valueOf(json.get("waterShadingStrategy").asString(waterShadingStrategy.getId())); setCurrentWaterShader(waterShadingStrategy); - currentWaterShader.load(json); + currentWaterShader.fromJson(json.get("currentWaterShader").asObject()); - waterOpacity = json.get("waterOpacity").doubleValue(waterOpacity); - waterVisibility = json.get("waterVisibility").doubleValue(waterVisibility); - useCustomWaterColor = json.get("useCustomWaterColor").boolValue(useCustomWaterColor); - if (useCustomWaterColor) { - JsonUtil.rgbFromJson(json.get("waterColor"), waterColor); - } biomeColors = json.get("biomeColorsEnabled").boolValue(biomeColors); biomeBlendingRadius = json.get("biomeBlendingRadius").intValue(biomeBlendingRadius); transparentSky = json.get("transparentSky").boolValue(transparentSky); @@ -2990,6 +3158,18 @@ else if(waterShader.equals("SIMPLEX")) this.cameraPresets = cameraPresets; } + fogVolumeStore.fromJson(json.get("fogVolumeStore").asObject()); + + cloudLayers.clear(); + if (json.get("cloudLayers").isArray()) { + JsonArray cloudLayersArray = json.get("cloudLayers").array(); + for (JsonValue element : cloudLayersArray) { + CloudLayer cloudLayer = new CloudLayer(); + cloudLayer.fromJson(element.asObject()); + cloudLayers.add(cloudLayer); + } + } + // Current SPP and render time are read after loading // other settings which can reset the render status. spp = json.get("spp").intValue(spp); @@ -3010,7 +3190,7 @@ else if(waterShader.equals("SIMPLEX")) octreeImplementation = json.get("octreeImplementation").asString(PersistentSettings.getOctreeImplementation()); - emitterSamplingStrategy = EmitterSamplingStrategy.valueOf(json.get("emitterSamplingStrategy").asString("NONE")); + emitterSamplingStrategy = EmitterSamplingStrategy.valueOf(json.get("emitterSamplingStrategy").asString(emitterSamplingStrategy.getId())); preventNormalEmitterWithSampling = json.get("preventNormalEmitterWithSampling").asBoolean(PersistentSettings.getPreventNormalEmitterWithSampling()); animationTime = json.get("animationTime").doubleValue(animationTime); @@ -3104,29 +3284,29 @@ public RenderMode getMode() { } public void setFogDensity(double newValue) { - if (newValue != fog.uniformDensity) { - fog.uniformDensity = newValue; + if (newValue != fog.getUniformDensity()) { + fog.setUniformDensity(newValue); refresh(); } } public void setSkyFogDensity(double newValue) { - if (newValue != fog.skyFogDensity) { - fog.skyFogDensity = newValue; + if (newValue != fog.getSkyFogDensity()) { + fog.setSkyFogDensity(newValue); refresh(); } } public void setFastFog(boolean value) { - if (fog.fastFog != value) { - fog.fastFog = value; + if (fog.isFastFog() != value) { + fog.setFastFog(value); refresh(); } } public void setFogMode(FogMode mode) { - if (fog.mode != mode) { - fog.mode = mode; + if (fog.getFogMode() != mode) { + fog.setFogMode(mode); refresh(); } } @@ -3161,9 +3341,7 @@ public void setResetReason(ResetReason resetReason) { } } - public void importMaterials() { - ExtraMaterials.loadDefaultMaterialProperties(); - MaterialStore.collections.forEach((name, coll) -> importMaterial(materials, name, coll)); + private void importPaletteMaterials() { MaterialStore.blockIds.forEach((name) -> { JsonValue properties = materials.get(name); if (properties != null) { @@ -3172,6 +3350,12 @@ public void importMaterials() { }); } }); + } + + public void importMaterials() { + ExtraMaterials.loadDefaultMaterialProperties(); + MaterialStore.collections.forEach((name, coll) -> importMaterial(materials, name, coll)); + importPaletteMaterials(); ExtraMaterials.idMap.forEach((name, material) -> { JsonValue properties = materials.get(name); if (properties != null) { @@ -3200,6 +3384,92 @@ public void setEmittance(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the emittance color property for the given material. + */ + public void setEmittanceColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("emittanceColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the emittance property for the given material. + */ + public void setEmitterMappingOffset(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("emitterMappingOffset", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the useReferenceColors property for the given material. + */ + public void setUseReferenceColors(String materialName, boolean value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("useReferenceColors", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the emittance property for the given material. + */ + public void setEmitterMappingTypeOverride(String materialName, EmitterMappingType value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("emitterMappingType", Json.of(value.getId())); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + public void setEmitterMappingReferenceColors(String materialName, List values) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + JsonArray referenceColors = new JsonArray(values.size()); + values.forEach(value -> { + JsonObject referenceColorObject = new JsonObject(); + referenceColorObject.add("red", value.x); + referenceColorObject.add("green", value.y); + referenceColorObject.add("blue", value.z); + referenceColorObject.add("range", value.w); + referenceColors.add(referenceColorObject); + }); + material.set("emitterMappingReferenceColors", referenceColors); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the alpha property for the given material. + */ + public void setAlpha(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("alpha", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the subsurface scattering probability property for the given material. + */ + public void setSubsurfaceScattering(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("subsurfaceScattering", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the diffuse color property for the given material. + */ + public void setDiffuseColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("diffuseColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + /** * Modifies the specular coefficient property for the given material. */ @@ -3230,6 +3500,16 @@ public void setPerceptualSmoothness(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the transmission roughness property for the given material. + */ + public void setPerceptualTransmissionSmoothness(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("transmissionRoughness", Json.of(Math.pow(1 - value, 2))); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + /** * Modifies the metalness property for the given material. */ @@ -3240,6 +3520,116 @@ public void setMetalness(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the transmission metalness property for the given material. + */ + public void setTransmissionMetalness(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("transmissionMetalness", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the specular color property for the given material. + */ + public void setSpecularColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("specularColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the transmission specular color property for the given material. + */ + public void setTransmissionSpecularColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("transmissionSpecularColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the volume density property for the given material. + */ + public void setVolumeDensity(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("volumeDensity", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the volume anisotropy property for the given material. + */ + public void setVolumeAnisotropy(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("volumeAnisotropy", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the volume anisotropy property for the given material. + */ + public void setVolumeEmittance(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("volumeEmittance", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the volume color property for the given material. + */ + public void setVolumeColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("volumeColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the absorption property for the given material. + */ + public void setAbsorption(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("absorption", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the absorption color property for the given material. + */ + public void setAbsorptionColor(String materialName, Vector3 value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("absorptionColor", ColorUtil.rgbToJson(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the opaque property for the given material. + */ + public void setOpaque(String materialName, boolean value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("opaque", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the hidden property for the given material. + */ + public void setHidden(String materialName, boolean value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("hidden", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + public int getYClipMin() { return yClipMin; } @@ -3256,6 +3646,14 @@ public void setYClipMax(int yClipMax) { this.yClipMax = yClipMax; } + public int getYMin() { + return this.yMin; + } + + public int getYMax() { + return this.yMax; + } + public Grid getEmitterGrid() { return emitterGrid; } @@ -3290,9 +3688,10 @@ public Octree getWorldOctree() { return worldOctree; } + @Deprecated @PluginApi public Octree getWaterOctree() { - return waterOctree; + return getWorldOctree(); } public EmitterSamplingStrategy getEmitterSamplingStrategy() { @@ -3420,21 +3819,4 @@ public boolean getHideUnknownBlocks() { public void setHideUnknownBlocks(boolean hideUnknownBlocks) { this.hideUnknownBlocks = hideUnknownBlocks; } - public boolean getFancierTranslucency() { - return fancierTranslucency; - } - - public void setFancierTranslucency(boolean value) { - fancierTranslucency = value; - refresh(); - } - - public double getTransmissivityCap() { - return transmissivityCap; - } - - public void setTransmissivityCap(double value) { - transmissivityCap = value; - refresh(); - } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java index 92e8c51254..755e065a02 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java @@ -9,10 +9,7 @@ import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.log.Log; -import se.llbit.math.Octree; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; -import se.llbit.math.Vector3i; +import se.llbit.math.*; import se.llbit.math.bvh.BVH; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.ListTag; @@ -24,20 +21,13 @@ import se.llbit.util.mojangapi.MojangApi; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; /** * Encapsulates entity handling for a scene. */ -public class SceneEntities { +public class SceneEntities implements Intersectable { private EntityLoadingPreferences entityLoadingPreferences = new EntityLoadingPreferences(); @@ -83,15 +73,19 @@ public void copyState(SceneEntities other) { bvhImplementation = other.bvhImplementation; } - public boolean intersect(Ray ray) { - boolean hit = false; + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + boolean hit = bvh.closestIntersection(ray, intersectionRecord, scene); - if (bvh.closestIntersection(ray)) { - hit = true; - } if (renderActors) { - if (actorBvh.closestIntersection(ray)) { + IntersectionRecord intersectionTest = new IntersectionRecord(); + if (actorBvh.closestIntersection(ray, intersectionTest, scene) && intersectionTest.distance < intersectionRecord.distance - Constants.EPSILON) { hit = true; + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.setNormal(intersectionTest); + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.material = intersectionTest.material; + intersectionRecord.flags = intersectionTest.flags; } } @@ -194,7 +188,7 @@ public void addActor(Entity entity) { if (actor.getClass().equals(entity.getClass())) { Vector3 distance = new Vector3(actor.position); distance.sub(entity.position); - return distance.lengthSquared() < Ray.EPSILON; + return distance.lengthSquared() < Constants.EPSILON; } return false; })) { @@ -258,12 +252,12 @@ public void loadDataFromOctree( public void buildBvh(TaskTracker.Task task, Vector3i origin) { Vector3 worldOffset = new Vector3(-origin.x, -origin.y, -origin.z); - bvh = BVH.Factory.create(bvhImplementation, entities, worldOffset, task); + bvh = BVH.Factory.create(bvhImplementation, Collections.unmodifiableList(entities), worldOffset, task); } public void buildActorBvh(TaskTracker.Task task, Vector3i origin) { Vector3 worldOffset = new Vector3(-origin.x, -origin.y, -origin.z); - actorBvh = BVH.Factory.create(bvhImplementation, actors, worldOffset, task); + actorBvh = BVH.Factory.create(bvhImplementation, Collections.unmodifiableList(actors), worldOffset, task); } public void finalizeLoading() { @@ -321,7 +315,7 @@ public void importJsonData(JsonObject json) { // 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()); + Entity entity = Entity.loadFromJson(element.object()); if (entity != null) { if (entity instanceof PlayerEntity) { actors.add(entity); @@ -331,7 +325,7 @@ public void importJsonData(JsonObject json) { } } for (JsonValue element : json.get("actors").array()) { - Entity entity = Entity.fromJson(element.object()); + Entity entity = Entity.loadFromJson(element.object()); actors.add(entity); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexNoiseConfig.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexNoiseConfig.java new file mode 100644 index 0000000000..a5cefc76ad --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexNoiseConfig.java @@ -0,0 +1,336 @@ +package se.llbit.chunky.renderer.scene; + +import javafx.scene.control.Label; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.SimplexNoise; +import se.llbit.math.Vector3; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; + +/** + * Wraps a {@link se.llbit.math.SimplexNoise} instance and provides controls for the noise. + */ +public class SimplexNoiseConfig implements Configurable, HasControls { + private long seed; + private int iterations = 5; + private float gain = 0.5f; + private float lacunarity = 2; + private float amplitude = 1; + private float frequency = 0.01f; + private final Vector3 scale = new Vector3(1); + private final Vector3 offset = new Vector3(); + private SimplexNoise simplexNoise; + + public SimplexNoiseConfig() { + this(0); + } + + public SimplexNoiseConfig(long seed) { + setSeed(seed); + } + + public void setSeed(long seed) { + this.seed = seed; + this.simplexNoise = new SimplexNoise(seed); + } + + public long getSeed() { + return this.seed; + } + + public void setIterations(int iterations) { + this.iterations = iterations; + } + + public int getIterations() { + return this.iterations; + } + + public void setAmplitude(float amplitude) { + this.amplitude = amplitude; + } + + public float getAmplitude() { + return this.amplitude; + } + + public void setFrequency(float frequency) { + this.frequency = frequency; + } + + public float getFrequency() { + return this.frequency; + } + + public void setGain(float gain) { + this.gain = gain; + } + + public float getGain() { + return this.gain; + } + + public void setLacunarity(float lacunarity) { + this.lacunarity = lacunarity; + } + + public float getLacunarity() { + return this.lacunarity; + } + + public void setScaleX(double value) { + this.scale.x = value; + } + + public void setScaleY(double value) { + this.scale.y = value; + } + + public void setScaleZ(double value) { + this.scale.z = value; + } + + public double getScaleX() { + return this.scale.x; + } + + public double getScaleY() { + return this.scale.y; + } + + public double getScaleZ() { + return this.scale.z; + } + + public void setOffsetX(double value) { + this.offset.x = value; + } + + public void setOffsetY(double value) { + this.offset.y = value; + } + + public void setOffsetZ(double value) { + this.offset.z = value; + } + + public double getOffsetX() { + return this.offset.x; + } + + public double getOffsetY() { + return this.offset.y; + } + + public double getOffsetZ() { + return this.offset.z; + } + + public float calculate(float x, float y, float z) { + float value = 0; + float amplitude = this.amplitude; + float frequency = this.frequency; + for (int i = 0; i < this.iterations; i++) { + value += this.simplexNoise.calculate( + (x + (float) this.offset.x) / (float) this.scale.x * frequency, + (y + (float) this.offset.y) / (float) this.scale.y * frequency, + (z + (float) this.offset.z) / (float) this.scale.z * frequency + ) * amplitude; + + frequency *= lacunarity; + amplitude *= gain; + } + return value; + } + + public SimplexNoise getSimplexNoise() { + return this.simplexNoise; + } + + @Override + public void fromJson(JsonObject json) { + seed = json.get("seed").longValue(seed); + iterations = json.get("iterations").intValue(iterations); + amplitude = json.get("amplitude").floatValue(amplitude); + frequency = json.get("frequency").floatValue(frequency); + gain = json.get("gain").floatValue(gain); + lacunarity = json.get("lacunarity").floatValue(lacunarity); + scale.fromJson(json.get("scale").asObject()); + offset.fromJson(json.get("offset").asObject()); + setSeed(seed); + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("seed", seed); + json.add("iterations", iterations); + json.add("amplitude", amplitude); + json.add("frequency", frequency); + json.add("gain", gain); + json.add("lacunarity", lacunarity); + json.add("scale", scale.toJson()); + json.add("offset", offset.toJson()); + return json; + } + + @Override + public void reset() { + + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + IntegerAdjuster iterationsAdjuster = new IntegerAdjuster(); + DoubleAdjuster amplitudeAdjuster = new DoubleAdjuster(); + DoubleAdjuster frequencyAdjuster = new DoubleAdjuster(); + DoubleAdjuster gainAdjuster = new DoubleAdjuster(); + DoubleAdjuster lacunarityAdjuster = new DoubleAdjuster(); + DoubleTextField xScale = new DoubleTextField(); + DoubleTextField yScale = new DoubleTextField(); + DoubleTextField zScale = new DoubleTextField(); + DoubleTextField xOffset = new DoubleTextField(); + DoubleTextField yOffset = new DoubleTextField(); + DoubleTextField zOffset = new DoubleTextField(); + + TextFieldLabelWrapper x1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper x2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z2Text = new TextFieldLabelWrapper(); + + x1Text.setTextField(xScale); + y1Text.setTextField(yScale); + z1Text.setTextField(zScale); + x2Text.setTextField(xOffset); + y2Text.setTextField(yOffset); + z2Text.setTextField(zOffset); + + x1Text.setLabelText("x:"); + y1Text.setLabelText("y:"); + z1Text.setLabelText("z:"); + x2Text.setLabelText("x:"); + y2Text.setLabelText("y:"); + z2Text.setLabelText("z:"); + + iterationsAdjuster.setName("Iterations"); + iterationsAdjuster.setRange(1, 25); + iterationsAdjuster.clampMin(); + iterationsAdjuster.set(this.iterations); + iterationsAdjuster.onValueChange(value -> { + this.iterations = value; + scene.refresh(); + }); + + amplitudeAdjuster.setName("Amplitude"); + amplitudeAdjuster.setRange(0.001, 2); + amplitudeAdjuster.clampMin(); + amplitudeAdjuster.set(this.amplitude); + amplitudeAdjuster.onValueChange(value -> { + this.amplitude = value.floatValue(); + scene.refresh(); + }); + + frequencyAdjuster.setName("Frequency"); + frequencyAdjuster.setRange(0.001, 1); + frequencyAdjuster.clampMin(); + frequencyAdjuster.set(this.frequency); + frequencyAdjuster.onValueChange(value -> { + this.frequency = value.floatValue(); + scene.refresh(); + }); + + gainAdjuster.setName("Gain"); + gainAdjuster.setRange(0.001, 10); + gainAdjuster.set(this.gain); + gainAdjuster.onValueChange(value -> { + this.gain = value.floatValue(); + scene.refresh(); + }); + + lacunarityAdjuster.setName("Lacunarity"); + lacunarityAdjuster.setRange(0.001, 10); + lacunarityAdjuster.set(this.lacunarity); + lacunarityAdjuster.onValueChange(value -> { + this.lacunarity = value.floatValue(); + scene.refresh(); + }); + + xScale.valueProperty().setValue(this.scale.x); + xScale.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scale.x = newValue.doubleValue(); + scene.refresh(); + }); + + yScale.valueProperty().setValue(this.scale.y); + yScale.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scale.y = newValue.doubleValue(); + scene.refresh(); + }); + + zScale.valueProperty().setValue(this.scale.z); + zScale.valueProperty().addListener((observable, oldValue, newValue) -> { + this.scale.z = newValue.doubleValue(); + scene.refresh(); + }); + + xOffset.valueProperty().setValue(this.offset.x); + xOffset.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offset.x = newValue.doubleValue(); + scene.refresh(); + }); + + yOffset.valueProperty().setValue(this.offset.y); + yOffset.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offset.y = newValue.doubleValue(); + scene.refresh(); + }); + + zOffset.valueProperty().setValue(this.offset.z); + zOffset.valueProperty().addListener((observable, oldValue, newValue) -> { + this.offset.z = newValue.doubleValue(); + scene.refresh(); + }); + + GridPane pane1 = new GridPane(); + pane1.setVgap(6); + pane1.setHgap(6); + + pane1.addRow(0, iterationsAdjuster, amplitudeAdjuster); + pane1.addRow(1, frequencyAdjuster, gainAdjuster); + pane1.addRow(2, lacunarityAdjuster); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + GridPane pane2 = new GridPane(); + pane2.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + pane2.setVgap(6); + pane2.setHgap(6); + + pane2.addRow(0, new Label("Scale"), x1Text, y1Text, z1Text); + pane2.addRow(1, new Label("Offset"), x2Text, y2Text, z2Text); + + return new VBox( + 6, + pane1, + pane2 + ); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java deleted file mode 100644 index 682a231900..0000000000 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java +++ /dev/null @@ -1,101 +0,0 @@ -/* Copyright (c) 2012-2021 Chunky contributors - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ -package se.llbit.chunky.renderer.scene; - -import se.llbit.json.JsonObject; -import se.llbit.math.Ray; -import se.llbit.math.SimplexNoise; -import se.llbit.math.Vector3; - -public class SimplexWaterShader implements WaterShader { - /* - Water shading is implemented using fractal noise based on simplex noise - (superimposed layers of simplex noise with increasing frequency and decreasing amplitude) - This 3D noise function gives the height of the water for a given x, z and t, - what we are really interested in are the partial derivatives of that function - with respect to x and z as they give us the slope along x and z, - and the normal is simply the cross product of the slope along x and the slope along z. - */ - - public int iterations = 4; /// Number of iteration of the fractal noise - public double baseFrequency = 0.4; /// frequency of the first iteration, doubles each iteration - public double baseAmplitude = 0.025; /// amplitude of the first iteration, halves each iteration - public double animationSpeed = 1; /// animation speed - private final SimplexNoise noise = new SimplexNoise(); - - - @Override - public void doWaterShading(Ray ray, double animationTime) { - double frequency = baseFrequency; - double amplitude = baseAmplitude; - - double ddx = 0; - double ddz = 0; - - for(int i = 0; i < iterations; ++i) { - noise.calculate((float)(ray.o.x * frequency), (float)(ray.o.z * frequency), (float)(animationTime * animationSpeed)); - double ddxNext = ddx - amplitude * noise.ddx; - double ddzNext = ddz - amplitude * noise.ddy; - if (Double.isNaN(ddxNext + ddzNext)) { - break; - } - ddx = ddxNext; - ddz = ddzNext; - - frequency *= 2; - amplitude *= 0.5; - } - Vector3 xslope = new Vector3(1, ddx, 0); - Vector3 zslope = new Vector3(0, ddz, 1); - Vector3 normal = new Vector3(); - normal.cross(zslope, xslope); - normal.normalize(); - ray.setShadingNormal(normal.x, normal.y, normal.z); - } - - @Override - public WaterShader clone() { - SimplexWaterShader shader = new SimplexWaterShader(); - shader.iterations = iterations; - shader.baseFrequency = baseFrequency; - shader.baseAmplitude = baseAmplitude; - shader.animationSpeed = animationSpeed; - return shader; - } - - @Override - public void save(JsonObject json) { - JsonObject params = new JsonObject(); - params.add("iterations", iterations); - params.add("frequency", baseFrequency); - params.add("amplitude", baseAmplitude); - params.add("animationSpeed", animationSpeed); - json.add("simplexWaterShader", params); - } - - @Override - public void load(JsonObject json) { - JsonObject params = json.get("simplexWaterShader").asObject(); - if(params == null) - return; - - iterations = params.get("iterations").intValue(4); - baseFrequency = params.get("frequency").doubleValue(0.4); - baseAmplitude = params.get("amplitude").doubleValue(0.025); - animationSpeed = params.get("animationSpeed").doubleValue(1); - } -} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SunSamplingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SunSamplingStrategy.java new file mode 100644 index 0000000000..679011c3a7 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SunSamplingStrategy.java @@ -0,0 +1,79 @@ +/* Copyright (c) 2022 Chunky Contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.scene; + +import se.llbit.util.Registerable; + +public enum SunSamplingStrategy implements Registerable { +// OFF("Off", "Sun is not sampled with next event estimation.", false, true, false, true), +// NON_LUMINOUS("Non-Luminous", "Sun is drawn on the skybox but it does not contribute to the lighting of the scene.", false, false, false, false), +// FAST("Fast", "Fast sun sampling algorithm. Lower noise but does not correctly model some visual effects.", true, false, false, false), +// HIGH_QUALITY("High Quality", "High quality sun sampling. More noise but correctly models visual effects such as caustics.", true, true, true, true); + + SAMPLE_THROUGH_OPACITY("Sample through opacity", "Sample the sun and sky through translucent textures", true, false), + SAMPLE_ONLY("Sample only", "Sample the sun on diffuse reflections.", true, false), + MIX("Mix", "Sample the sun on diffuse reflections, and diffusely intersect on specular interactions.", true, true), + OFF("Diffuse", "Diffusely intersect on all interactions.", false, true); + + private final String displayName; + private final String description; + + private final boolean sunSampling; + private final boolean diffuseSun; + + SunSamplingStrategy(String displayName, String description, boolean sunSampling, boolean diffuseSun) { + this.displayName = displayName; + this.description = description; + + this.sunSampling = sunSampling; + this.diffuseSun = diffuseSun; + } + + @Override + public String toString() { + return getName(); + } + + @Override + public String getName() { + return this.displayName; + } + + @Override public String getDescription() { + return this.description; + } + + @Override public String getId() { + return this.name(); + } + + public boolean doSunSampling() { + return sunSampling; + } + + public boolean isDiffuseSun() { + return diffuseSun; + } + + public static SunSamplingStrategy get(String name) { + try { + return valueOf(name); + } catch (IllegalArgumentException e) { + return SAMPLE_THROUGH_OPACITY; + } + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/Fog.java similarity index 69% rename from chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java rename to chunky/src/java/se/llbit/chunky/renderer/scene/fog/Fog.java index 87c69ad06b..1aa8ffae07 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/Fog.java @@ -1,13 +1,16 @@ -package se.llbit.chunky.renderer.scene; +package se.llbit.chunky.renderer.scene.fog; import java.util.ArrayList; import java.util.Random; import java.util.stream.Collectors; import se.llbit.chunky.PersistentSettings; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -17,22 +20,21 @@ public final class Fog implements JsonSerializable { private final Scene scene; - protected FogMode mode = FogMode.NONE; - protected boolean fastFog = true; - protected double uniformDensity = Scene.DEFAULT_FOG_DENSITY; - protected double skyFogDensity = 1; - protected ArrayList layers = new ArrayList<>(0); - protected Vector3 fogColor = new Vector3(PersistentSettings.getFogColorRed(), PersistentSettings.getFogColorGreen(), PersistentSettings.getFogColorBlue()); + private FogMode mode = FogMode.NONE; + private boolean fastFog = true; + private double uniformDensity = Scene.DEFAULT_FOG_DENSITY; + private double skyFogDensity = 1; + private ArrayList layers = new ArrayList<>(0); + private Vector3 fogColor = new Vector3(PersistentSettings.getFogColorRed(), PersistentSettings.getFogColorGreen(), PersistentSettings.getFogColorBlue()); private static final double EXTINCTION_FACTOR = 0.04; public static final double FOG_LIMIT = 30000; - public static final Vector4 SKY_SCATTER = new Vector4(1, 1, 1, 1); public Fog(Scene scene) { this.scene = scene; } - public boolean fogEnabled() { + public boolean isFogEnabled() { return mode != FogMode.NONE; } @@ -44,18 +46,34 @@ public double getUniformDensity() { return uniformDensity; } + public void setUniformDensity(double value) { + this.uniformDensity = value; + } + public double getSkyFogDensity() { return skyFogDensity; } + public void setSkyFogDensity(double value) { + this.skyFogDensity = value; + } + public FogMode getFogMode() { return mode; } - public boolean fastFog() { + public void setFogMode(FogMode mode) { + this.mode = mode; + } + + public boolean isFastFog() { return fastFog; } + public void setFastFog(boolean value) { + this.fastFog = value; + } + public ArrayList getFogLayers() { return layers; } @@ -98,7 +116,7 @@ private static double clampDy(double dy) { return dy; } - public void addSkyFog(Ray ray, Vector4 scatterLight) { + public void addSkyFog(Ray ray, IntersectionRecord intersectionRecord, Vector4 scatterLight) { if (mode == FogMode.UNIFORM) { if (uniformDensity > 0.0) { double fog; @@ -109,41 +127,43 @@ public void addSkyFog(Ray ray, Vector4 scatterLight) { fog = 1; } fog *= skyFogDensity; - ray.color.x = (1 - fog) * ray.color.x + fog * fogColor.x; - ray.color.y = (1 - fog) * ray.color.y + fog * fogColor.y; - ray.color.z = (1 - fog) * ray.color.z + fog * fogColor.z; + intersectionRecord.color.x = (1 - fog) * intersectionRecord.color.x + fog * fogColor.x; + intersectionRecord.color.y = (1 - fog) * intersectionRecord.color.y + fog * fogColor.y; + intersectionRecord.color.z = (1 - fog) * intersectionRecord.color.z + fog * fogColor.z; } } else if (mode == FogMode.LAYERED) { double dy = ray.d.y; double y1 = ray.o.y; double y2 = y1 + dy * FOG_LIMIT; - addLayeredFog(ray.color, dy, y1, y2, scatterLight); + addLayeredFog(intersectionRecord.color, intersectionRecord.color, null, dy, y1, y2, scatterLight); } } - public void addGroundFog(Ray ray, Vector3 ox, double airDistance, Vector4 scatterLight, double scatterOffset) { - Vector4 color = ray.color; + public void addGroundFog(Ray ray, Vector4 fogColor1, Vector4 color, Vector3 emittance, Vector3 ox, Vector3 od, double airDistance, Vector4 scatterLight, double scatterOffset) { if (mode == FogMode.UNIFORM) { double fogDensity = uniformDensity * EXTINCTION_FACTOR; double extinction = Math.exp(-airDistance * fogDensity); color.scale(extinction); - if (scatterLight.w > Ray.EPSILON) { + if (emittance != null) { + emittance.scale(extinction); + } + if (scatterLight.w > Constants.EPSILON) { double inscatter = scatterLight.w; if (fastFog) { inscatter *= (1 - extinction); } else { inscatter *= airDistance * fogDensity * Math.exp(-scatterOffset * fogDensity); } - color.x += scatterLight.x * fogColor.x * inscatter; - color.y += scatterLight.y * fogColor.y * inscatter; - color.z += scatterLight.z * fogColor.z * inscatter; + fogColor1.x += scatterLight.x * fogColor.x * inscatter; + fogColor1.y += scatterLight.y * fogColor.y * inscatter; + fogColor1.z += scatterLight.z * fogColor.z * inscatter; } } else if (mode == FogMode.LAYERED) { - addLayeredFog(color, ray.d.y, ox.y, ray.o.y, scatterLight); + addLayeredFog(fogColor1, color, emittance, od.y, ox.y, ray.o.y, scatterLight); } } - public void addLayeredFog(Vector4 color, double dy, double y1, double y2, Vector4 scatterLight) { + public void addLayeredFog(Vector4 fogColor1, Vector4 color, Vector3 emittance, double dy, double y1, double y2, Vector4 scatterLight) { double total = 0; for (FogLayer layer : layers) { // Logistic distribution CDF. It is the integral of the PDF, which is a nice bell shaped sigmoid function @@ -152,20 +172,22 @@ public void addLayeredFog(Vector4 color, double dy, double y1, double y2, Vector } double extinction = Math.exp(total / clampDy(dy)); color.scale(extinction); - if (scatterLight.w > Ray.EPSILON) { + if (emittance != null) { + emittance.scale(extinction); + } + if (scatterLight.w > Constants.EPSILON) { double inscatter = (1 - extinction) * scatterLight.w; - color.x += inscatter * scatterLight.x * fogColor.x; - color.y += inscatter * scatterLight.y * fogColor.y; - color.z += inscatter * scatterLight.z * fogColor.z; + fogColor1.x += inscatter * scatterLight.x * fogColor.x; + fogColor1.y += inscatter * scatterLight.y * fogColor.y; + fogColor1.z += inscatter * scatterLight.z * fogColor.z; } } - public double sampleGroundScatterOffset(Ray ray, Vector3 ox, Random random) { - double airDistance = ray.distance; + public double sampleGroundScatterOffset(Ray ray, double airDistance, Vector3 ox, Vector3 od, Random random) { if (mode == FogMode.UNIFORM) { - return QuickMath.clamp(airDistance * random.nextFloat(), Ray.EPSILON, airDistance - Ray.EPSILON); + return QuickMath.clamp(airDistance * random.nextFloat(), Constants.EPSILON, airDistance - Constants.EPSILON); } else if (mode == FogMode.LAYERED) { - double dy = ray.d.y; + double dy = od.y; double y1 = ox.y; double y2 = ray.o.y; return sampleLayeredScatterOffset(random, y1, y2, dy); @@ -178,7 +200,7 @@ public double sampleSkyScatterOffset(Scene scene, Ray ray, Random random) { if (mode == FogMode.LAYERED) { double dy = ray.d.y; double y1 = ray.o.y; - double y2 = dy > 0 ? scene.yMax : scene.yMin; + double y2 = dy > 0 ? scene.getYMax() : scene.getYMin(); return sampleLayeredScatterOffset(random, y1, y2, dy); } else { throw new IllegalStateException("Tried to sample sky fog scatter offset even though fog is not layered."); @@ -186,8 +208,8 @@ public double sampleSkyScatterOffset(Scene scene, Ray ray, Random random) { } private double sampleLayeredScatterOffset(Random random, double y1, double y2, double dy) { - if (layers.size() == 0) { - return Ray.EPSILON; + if (layers.isEmpty()) { + return Constants.EPSILON; } // This works only for one fog layer yet. FogLayer layer = layers.get(0); @@ -220,7 +242,7 @@ private double sampleLayeredScatterOffset(Random random, double y1, double y2, d } public void importFromJson(JsonObject json, Scene scene) { - mode = FogMode.get(json.get("mode").stringValue(mode.name())); + mode = FogMode.valueOf(json.get("mode").stringValue(mode.getId())); uniformDensity = json.get("uniformDensity").doubleValue(uniformDensity); skyFogDensity = json.get("skyFogDensity").doubleValue(skyFogDensity); layers = json.get("layers").array().elements.stream().map(JsonValue::object).map(o -> new FogLayer( diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/FogLayer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogLayer.java similarity index 80% rename from chunky/src/java/se/llbit/chunky/renderer/scene/FogLayer.java rename to chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogLayer.java index 54ceaf5527..1dee9c17d5 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/FogLayer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogLayer.java @@ -1,4 +1,6 @@ -package se.llbit.chunky.renderer.scene; +package se.llbit.chunky.renderer.scene.fog; + +import se.llbit.chunky.renderer.scene.Scene; public final class FogLayer { public static final double DEFAULT_Y = 62; @@ -7,11 +9,11 @@ public final class FogLayer { public double yMin, y, yWithOrigin, breadth, breadthInv, density; public FogLayer(double y, double breadth, double density, Scene scene) { - this(y, breadth, density, scene.yMin); + this(y, breadth, density, scene.getYMin()); } public FogLayer(Scene scene) { - this(DEFAULT_Y, DEFAULT_BREADTH, DEFAULT_DENSITY, scene.yMin); + this(DEFAULT_Y, DEFAULT_BREADTH, DEFAULT_DENSITY, scene.getYMin()); } private FogLayer(double y, double breadth, double density, double yMin) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogMode.java b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogMode.java new file mode 100644 index 0000000000..b5be054c86 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/fog/FogMode.java @@ -0,0 +1,38 @@ +package se.llbit.chunky.renderer.scene.fog; + +import se.llbit.util.Registerable; + +public enum FogMode implements Registerable { + NONE("None", "No fog is present."), + UNIFORM("Uniform", "Fog is distributed uniformly throughout the scene."), + LAYERED("Layered", "Fog is distributed throughout the scene in layers."); + + private final String displayName; + private final String description; + + FogMode(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + @Override + public String getName() { + return this.displayName; + } + + @Override + public String toString() { + return this.displayName; + } + + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String getId() { + return this.name(); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/NishitaSky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/NishitaSky.java index bc61ee0215..87e42bcaea 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/NishitaSky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/NishitaSky.java @@ -16,10 +16,23 @@ */ package se.llbit.chunky.renderer.scene.sky; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import org.apache.commons.math3.util.FastMath; -import se.llbit.math.QuickMath; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.*; import static java.lang.Math.PI; @@ -28,26 +41,56 @@ */ public class NishitaSky implements SimulatedSky { // Atmospheric constants - private static final double EARTH_RADIUS = 6360e3; - private static final double ATM_THICKNESS = 100e3; + private static final double DEFAULT_EARTH_RADIUS = 6360e3; + private static final double DEFAULT_ATM_THICKNESS = 60e3; - private static final double RAYLEIGH_SCALE = 8e3; - private static final double MIE_SCALE = 1.2e3; + private static final int DEFAULT_HORIZON_OFFSET = 0; + private static final int DEFAULT_ALTITUDE = 1; - private static final Vector3 BETA_R = new Vector3(3.8e-6, 13.5e-6, 33.1e-6); - private static final Vector3 BETA_M = new Vector3(21e-6, 21e-6, 21e-6); + private double earthRadius = DEFAULT_EARTH_RADIUS; + private double atmosphereThickness = DEFAULT_ATM_THICKNESS; - private static final int SAMPLES = 16; - private static final int SAMPLES_LIGHT = 8; + private static final double DEFAULT_RAYLEIGH_SCALE = 7.994e3; + private static final double DEFAULT_MIE_SCALE = 1.2e3; + private static final double DEFAULT_OZONE_SCALE = 10000; + + private double rayleighScale = DEFAULT_RAYLEIGH_SCALE; + private double mieScale = DEFAULT_MIE_SCALE; + private double ozoneScale = DEFAULT_OZONE_SCALE; + + private static final int DEFAULT_ANISOTROPY = 1; + private static final double DEFAULT_OZONE_DENSITY = 0.00000022512612292678; + + private static final Vector3 DEFAULT_BETA_R = new Vector3(5.8e-6, 13.0e-6, 22.4e-6); + private static final Vector3 DEFAULT_BETA_M = new Vector3(21e-6); + private static final Vector3 DEFAULT_BETA_O = new Vector3(3.808e-6); + + private final Vector3 betaR = new Vector3(DEFAULT_BETA_R); + private final Vector3 betaM = new Vector3(DEFAULT_BETA_M); + private final Vector3 betaO = new Vector3(DEFAULT_BETA_O); + + private static final int DEFAULT_VIEW_SAMPLES = 16; + private static final int DEFAULT_LIGHT_SAMPLES = 8; + + private int viewSamples = DEFAULT_VIEW_SAMPLES; + private int lightSamples = DEFAULT_LIGHT_SAMPLES; + + private static final double MIN_ANISOTROPY = 0.1; + private static final double MAX_ANISOTROPY = 1.315; // Sun position vector. Final to prevent unnecessary reallocation private final Vector3 sunPosition = new Vector3(0, 1, 0); private double sunIntensity = 1; - private double horizonOffset = 0; + private double horizonOffset = DEFAULT_HORIZON_OFFSET; // Sun position in spherical form for faster update checking private double theta; private double phi; + private double altitude = DEFAULT_ALTITUDE; + private double ozoneDensity = DEFAULT_OZONE_DENSITY; + private double anisotropy = DEFAULT_ANISOTROPY; + + private boolean useToneMap = true; /** * Create a new sky renderer. @@ -56,15 +99,14 @@ public NishitaSky() { } @Override - public boolean updateSun(Sun sun, double horizonOffset) { - if (sunIntensity != sun.getIntensity() || theta != sun.getAzimuth() || phi != sun.getAltitude() || this.horizonOffset != horizonOffset) { + public boolean updateSun(Sun sun) { + if (sunIntensity != sun.getIntensity() || theta != sun.getAzimuth() || phi != sun.getAltitude()) { theta = sun.getAzimuth(); phi = sun.getAltitude(); double r = QuickMath.abs(FastMath.cos(phi)); sunPosition.set(FastMath.cos(theta) * r, FastMath.sin(phi), FastMath.sin(theta) * r); - sunIntensity = sun.getIntensity(); + sunIntensity = sun.getIntensity() * sun.radius * sun.radius; - this.horizonOffset = horizonOffset; sunPosition.y += horizonOffset; sunPosition.normalize(); @@ -86,82 +128,96 @@ public String getDescription() { @Override public Vector3 calcIncidentLight(Ray ray) { // Render from just above the surface of "earth" - Vector3 origin = new Vector3(0, ray.o.y + EARTH_RADIUS + 1, 0); + Vector3 origin = new Vector3(0, ray.o.y + earthRadius + altitude, 0); Vector3 direction = ray.d; direction.y += horizonOffset; direction.normalize(); // Calculate the distance from the origin to the edge of the atmosphere - double distance = sphereIntersect(origin, direction, EARTH_RADIUS + ATM_THICKNESS); + double distance = sphereIntersect(origin, direction, earthRadius + atmosphereThickness); if (distance == -1) { // No intersection, black return new Vector3(0, 0, 0); } // Ray march segment length - double segmentLength = distance / SAMPLES; + double segmentLength = distance / viewSamples; double currentDist = 0; double optDepthR = 0; double optDepthM = 0; + double optDepthO = 0; double mu = direction.dot(sunPosition); - double phaseR = (3 / (16 * PI)) * (1 + mu*mu); - double g = 0.76; - double phaseM = 3 / (8 * PI) * ((1 - g*g) * (1 + mu*mu)) / ((2 + g*g) * FastMath.pow(1 + g*g - 2*g*mu, 1.5)); + double mu2 = mu * mu; + double phaseR = (3 / (16 * PI)) * (1 + mu2); + double g = 0.76 * QuickMath.clamp(anisotropy, MIN_ANISOTROPY, MAX_ANISOTROPY); + double g2 = g * g; + double phaseM = 3 / (8 * PI) * ((1 - g2) * (1 + mu * mu)) / ((2 + g2) * FastMath.pow(1 + g2 - 2 * g * mu, 1.5)); Vector3 sumR = new Vector3(0, 0, 0); Vector3 sumM = new Vector3(0, 0, 0); + Vector3 sumO = new Vector3(0, 0, 0); + + double oD = 0; + if (ozoneDensity > Constants.EPSILON) { + oD = (ozoneDensity * 1000 / -15000); + oD = QuickMath.clamp(oD, -2000, 3000); + } // Primary sample values Vector3 samplePosition = new Vector3(); - double height, hr, hm; + double height, hr, hm, ho; // Sun sampling values Vector3 sunSamplePosition = new Vector3(); - double sunLength, sunSegment, sunCurrent, optDepthSunR, optDepthSunM, sunHeight; + double sunLength, sunSegment, sunCurrent, optDepthSunR, optDepthSunM, optDepthSunO, sunHeight; Vector3 tau = new Vector3(); Vector3 attenuation = new Vector3(); // Primary ray march out towards space - for (int i = 0; i < SAMPLES; i++) { + for (int i = 0; i < viewSamples; i++) { samplePosition.set( - origin.x + (currentDist + segmentLength/2) * direction.x, - origin.y + (currentDist + segmentLength/2) * direction.y, - origin.z + (currentDist + segmentLength/2) * direction.z + origin.x + (currentDist + segmentLength / 2) * direction.x, + origin.y + (currentDist + segmentLength / 2) * direction.y, + origin.z + (currentDist + segmentLength / 2) * direction.z ); - height = samplePosition.length() - EARTH_RADIUS; + height = samplePosition.length() - earthRadius; - hr = FastMath.exp(-height / RAYLEIGH_SCALE) * segmentLength; - hm = FastMath.exp(-height / MIE_SCALE) * segmentLength; + hr = FastMath.exp(-height / rayleighScale) * segmentLength; + hm = FastMath.exp(-height / mieScale) * segmentLength; + ho = FastMath.exp(-height / ozoneScale) * segmentLength; optDepthR += hr; optDepthM += hm; + optDepthO += ho; // Calculate the distance from the current point to the atmosphere in the direction of the sun - sunLength = sphereIntersect(samplePosition, sunPosition, EARTH_RADIUS + ATM_THICKNESS); - sunSegment = sunLength / SAMPLES_LIGHT; + sunLength = sphereIntersect(samplePosition, sunPosition, earthRadius + atmosphereThickness); + sunSegment = sunLength / lightSamples; sunCurrent = 0; optDepthSunR = 0; optDepthSunM = 0; + optDepthSunO = 0; // Ray march towards the sun boolean flag = false; - for (int j = 0; j < SAMPLES_LIGHT; j++) { + for (int j = 0; j < lightSamples; j++) { sunSamplePosition.set( - samplePosition.x + (sunCurrent + sunSegment/2) * sunPosition.x, - samplePosition.y + (sunCurrent + sunSegment/2) * sunPosition.y, - samplePosition.z + (sunCurrent + sunSegment/2) * sunPosition.z + samplePosition.x + (sunCurrent + sunSegment / 2) * sunPosition.x, + samplePosition.y + (sunCurrent + sunSegment / 2) * sunPosition.y, + samplePosition.z + (sunCurrent + sunSegment / 2) * sunPosition.z ); - sunHeight = sunSamplePosition.length() - EARTH_RADIUS; + sunHeight = sunSamplePosition.length() - earthRadius; if (sunHeight < 0) { flag = true; break; } - optDepthSunR += FastMath.exp(-sunHeight / RAYLEIGH_SCALE) * sunSegment; - optDepthSunM += FastMath.exp(-sunHeight / MIE_SCALE) * sunSegment; + optDepthSunR += FastMath.exp(-sunHeight / rayleighScale) * sunSegment; + optDepthSunM += FastMath.exp(-sunHeight / mieScale) * sunSegment; + optDepthSunO += FastMath.exp(-sunHeight / ozoneScale) * sunSegment; sunCurrent += sunSegment; } @@ -169,27 +225,33 @@ public Vector3 calcIncidentLight(Ray ray) { // Only execute if we successfully march out of the atmosphere if (!flag) { tau.set( - BETA_R.x * (optDepthR + optDepthSunR) + BETA_M.x * 1.1 * (optDepthM + optDepthSunM), - BETA_R.y * (optDepthR + optDepthSunR) + BETA_M.y * 1.1 * (optDepthM + optDepthSunM), - BETA_R.z * (optDepthR + optDepthSunR) + BETA_M.z * 1.1 * (optDepthM + optDepthSunM) + betaR.x * (optDepthR + optDepthSunR) + betaM.x * 1.1 * (optDepthM + optDepthSunM) + betaO.x * (optDepthO + optDepthSunO), + betaR.y * (optDepthR + optDepthSunR) + betaM.y * 1.1 * (optDepthM + optDepthSunM) + betaO.y * (optDepthO + optDepthSunO), + betaR.z * (optDepthR + optDepthSunR) + betaM.z * 1.1 * (optDepthM + optDepthSunM) + betaO.z * (optDepthO + optDepthSunO) ); attenuation.set( - FastMath.exp(-1 * tau.x), - FastMath.exp(-1 * tau.y), - FastMath.exp(-1 * tau.z) + FastMath.exp(-tau.x), + FastMath.exp(-tau.y), + FastMath.exp(-tau.z) ); sumR.add( - attenuation.x * hr, - attenuation.y * hr, - attenuation.z * hr + attenuation.x * hr, + attenuation.y * hr, + attenuation.z * hr ); sumM.add( - attenuation.x * hm, - attenuation.y * hm, - attenuation.z * hm + attenuation.x * hm, + attenuation.y * hm, + attenuation.z * hm + ); + + sumO.add( + attenuation.x * ho, + attenuation.y * ho, + attenuation.z * ho ); } @@ -197,26 +259,33 @@ public Vector3 calcIncidentLight(Ray ray) { } Vector3 color = new Vector3( - (sumR.x* BETA_R.x*phaseR + sumM.x* BETA_M.x*phaseM) * sunIntensity * 5, - (sumR.y* BETA_R.y*phaseR + sumM.y* BETA_M.y*phaseM) * sunIntensity * 5, - (sumR.z* BETA_R.z*phaseR + sumM.z* BETA_M.z*phaseM) * sunIntensity * 5 + ((sumR.x * betaR.x * phaseR) + (sumM.x * betaM.x * phaseM) + (sumO.x * betaO.x * oD)), + ((sumR.y * betaR.y * phaseR) + (sumM.y * betaM.y * phaseM) + (sumO.y * betaO.y * oD)), + ((sumR.z * betaR.z * phaseR) + (sumM.z * betaM.z * phaseM) + (sumO.z * betaO.z * oD)) ); + double scale = sunIntensity * 10; + color.scale(scale); + // Tone-mapping function for more realistic colors - color.set( - color.x < 1.413 ? FastMath.pow(color.x * 0.38317, 1.0/2.2) : 1.0 - FastMath.exp(-color.x), - color.y < 1.413 ? FastMath.pow(color.y * 0.38317, 1.0/2.2) : 1.0 - FastMath.exp(-color.y), - color.z < 1.413 ? FastMath.pow(color.z * 0.38317, 1.0/2.2) : 1.0 - FastMath.exp(-color.z) - ); + if (useToneMap) { + color.set( + color.x < 1.413 && color.x >= 0 ? FastMath.pow(color.x * 0.38317, 1.0 / Scene.DEFAULT_GAMMA) : 1.0 - FastMath.exp(-color.x), + color.y < 1.413 && color.y >= 0 ? FastMath.pow(color.y * 0.38317, 1.0 / Scene.DEFAULT_GAMMA) : 1.0 - FastMath.exp(-color.y), + color.z < 1.413 && color.z >= 0 ? FastMath.pow(color.z * 0.38317, 1.0 / Scene.DEFAULT_GAMMA) : 1.0 - FastMath.exp(-color.z) + ); + } return color; } - /** Calculate the distance from origin to the edge of a sphere centered at (0, 0, 0) in direction.*/ + /** + * Calculate the distance from origin to the edge of a sphere centered at (0, 0, 0) in direction. + */ private double sphereIntersect(Vector3 origin, Vector3 direction, double sphere_radius) { double a = direction.lengthSquared(); double b = 2 * direction.dot(origin); - double c = origin.lengthSquared() - sphere_radius*sphere_radius; + double c = origin.lengthSquared() - sphere_radius * sphere_radius; if (b == 0) { if (a == 0) { @@ -227,12 +296,367 @@ private double sphereIntersect(Vector3 origin, Vector3 direction, double sphere_ return FastMath.sqrt(-c / a); } - double disc = b*b - 4*a*c; + double disc = b * b - 4 * a * c; if (disc < 0) { // No intersection return -1; } - return (-b + FastMath.sqrt(disc)) / (2*a); + return (-b + FastMath.sqrt(disc)) / (2 * a); + } + + @Override + public void fromJson(JsonObject json) { + betaR.fromJson(json.get("betaR").asObject()); + rayleighScale = json.get("rayleighScale").doubleValue(DEFAULT_RAYLEIGH_SCALE); + betaM.fromJson(json.get("betaM").asObject()); + mieScale = json.get("mieScale").doubleValue(DEFAULT_MIE_SCALE); + anisotropy = json.get("anisotropy").doubleValue(DEFAULT_ANISOTROPY); + betaO.fromJson(json.get("betaO").asObject()); + ozoneScale = json.get("ozoneScale").doubleValue(DEFAULT_OZONE_SCALE); + ozoneDensity = json.get("ozoneDensity").doubleValue(DEFAULT_OZONE_DENSITY); + altitude = json.get("altitude").doubleValue(DEFAULT_ALTITUDE); + horizonOffset = json.get("horizonOffset").doubleValue(DEFAULT_HORIZON_OFFSET); + earthRadius = json.get("earthRadius").doubleValue(DEFAULT_EARTH_RADIUS); + atmosphereThickness = json.get("atmosphereThickness").doubleValue(DEFAULT_ATM_THICKNESS); + viewSamples = json.get("viewSamples").intValue(DEFAULT_VIEW_SAMPLES); + lightSamples = json.get("lightSamples").intValue(DEFAULT_LIGHT_SAMPLES); + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("betaR", betaR.toJson()); + json.add("rayleighScale", rayleighScale); + json.add("betaM", betaM.toJson()); + json.add("mieScale", mieScale); + json.add("anisotropy", anisotropy); + json.add("betaO", betaO.toJson()); + json.add("ozoneScale", ozoneScale); + json.add("ozoneDensity", ozoneDensity); + json.add("altitude", altitude); + json.add("horizonOffset", horizonOffset); + json.add("earthRadius", earthRadius); + json.add("atmosphereThickness", atmosphereThickness); + json.add("viewSamples", viewSamples); + json.add("lightSamples", lightSamples); + return json; + } + + @Override + public void reset() { + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + VBox controls = new VBox(6); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + // -------- + + DoubleTextField betaRX = new DoubleTextField(); + DoubleTextField betaRY = new DoubleTextField(); + DoubleTextField betaRZ = new DoubleTextField(); + + betaRX.setMaximumFractionDigits(12); + betaRY.setMaximumFractionDigits(12); + betaRZ.setMaximumFractionDigits(12); + + betaRX.valueProperty().setValue(betaR.x); + betaRY.valueProperty().setValue(betaR.y); + betaRZ.valueProperty().setValue(betaR.z); + + betaRX.valueProperty().addListener((observable, oldValue, newValue) -> { + betaR.x = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaRY.valueProperty().addListener((observable, oldValue, newValue) -> { + betaR.y = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaRZ.valueProperty().addListener((observable, oldValue, newValue) -> { + betaR.z = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + DoubleAdjuster rayleighScaleAdjuster = new DoubleAdjuster(); + rayleighScaleAdjuster.setName("Scale height"); + rayleighScaleAdjuster.setRange(1.0, atmosphereThickness); + rayleighScaleAdjuster.clampBoth(); + rayleighScaleAdjuster.set(rayleighScale); + rayleighScaleAdjuster.onValueChange(value -> { + rayleighScale = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + Button rayleighDefaultsButton = new Button("Defaults"); + rayleighDefaultsButton.setOnAction(e -> { + betaR.set(DEFAULT_BETA_R); + betaRX.valueProperty().setValue(betaR.x); + betaRY.valueProperty().setValue(betaR.y); + betaRZ.valueProperty().setValue(betaR.z); + + rayleighScale = DEFAULT_RAYLEIGH_SCALE; + rayleighScaleAdjuster.valueProperty().setValue(rayleighScale); + + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + GridPane rayleighPane = new GridPane(); + rayleighPane.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + rayleighPane.setHgap(6); + rayleighPane.addRow(0, new Label("Coefficient"), betaRX, betaRY, betaRZ); + controls.getChildren().addAll(new HBox(6, new Label("Rayleigh scattering"), rayleighDefaultsButton), rayleighPane, rayleighScaleAdjuster, new Separator()); + + // -------- + + DoubleTextField betaMX = new DoubleTextField(); + DoubleTextField betaMY = new DoubleTextField(); + DoubleTextField betaMZ = new DoubleTextField(); + + betaMX.setMaximumFractionDigits(12); + betaMY.setMaximumFractionDigits(12); + betaMZ.setMaximumFractionDigits(12); + + betaMX.valueProperty().setValue(betaM.x); + betaMY.valueProperty().setValue(betaM.y); + betaMZ.valueProperty().setValue(betaM.z); + + betaMX.valueProperty().addListener((observable, oldValue, newValue) -> { + betaM.x = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaMY.valueProperty().addListener((observable, oldValue, newValue) -> { + betaM.y = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaMZ.valueProperty().addListener((observable, oldValue, newValue) -> { + betaM.z = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + DoubleAdjuster mieScaleAdjuster = new DoubleAdjuster(); + mieScaleAdjuster.setName("Scale height"); + mieScaleAdjuster.setRange(1.0, atmosphereThickness); + mieScaleAdjuster.clampBoth(); + mieScaleAdjuster.set(mieScale); + mieScaleAdjuster.onValueChange(value -> { + mieScale = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + DoubleAdjuster anisotropyAdjuster = new DoubleAdjuster(); + anisotropyAdjuster.setName("Anisotropy"); + anisotropyAdjuster.setRange(MIN_ANISOTROPY, MAX_ANISOTROPY); + anisotropyAdjuster.clampBoth(); + anisotropyAdjuster.set(this.anisotropy); + anisotropyAdjuster.onValueChange(value -> { + this.anisotropy = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + Button mieDefaultsButton = new Button("Defaults"); + mieDefaultsButton.setOnAction(e -> { + betaM.set(DEFAULT_BETA_M); + betaMX.valueProperty().setValue(betaM.x); + betaMY.valueProperty().setValue(betaM.y); + betaMZ.valueProperty().setValue(betaM.z); + + anisotropy = 1; + anisotropyAdjuster.valueProperty().setValue(anisotropy); + mieScale = DEFAULT_MIE_SCALE; + mieScaleAdjuster.valueProperty().setValue(mieScale); + + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + GridPane miePane = new GridPane(); + miePane.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + miePane.setHgap(6); + miePane.addRow(0, new Label("Coefficient"), betaMX, betaMY, betaMZ); + controls.getChildren().addAll(new HBox(6, new Label("Mie scattering"), mieDefaultsButton), miePane, anisotropyAdjuster, mieScaleAdjuster, new Separator()); + + // -------- + + DoubleTextField betaOX = new DoubleTextField(); + DoubleTextField betaOY = new DoubleTextField(); + DoubleTextField betaOZ = new DoubleTextField(); + + betaOX.setMaximumFractionDigits(12); + betaOY.setMaximumFractionDigits(12); + betaOZ.setMaximumFractionDigits(12); + + betaOX.valueProperty().setValue(betaO.x); + betaOY.valueProperty().setValue(betaO.y); + betaOZ.valueProperty().setValue(betaO.z); + + betaOX.valueProperty().addListener((observable, oldValue, newValue) -> { + betaO.x = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaOY.valueProperty().addListener((observable, oldValue, newValue) -> { + betaO.y = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + betaOZ.valueProperty().addListener((observable, oldValue, newValue) -> { + betaO.z = newValue.doubleValue(); + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + DoubleAdjuster ozoneScaleAdjuster = new DoubleAdjuster(); + ozoneScaleAdjuster.setName("Scale height"); + ozoneScaleAdjuster.setRange(1.0, atmosphereThickness); + ozoneScaleAdjuster.clampBoth(); + ozoneScaleAdjuster.set(ozoneScale); + ozoneScaleAdjuster.onValueChange(value -> { + ozoneScale = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + DoubleAdjuster ozoneDensityAdjuster = new DoubleAdjuster(); + ozoneDensityAdjuster.setName("Ozone density"); + ozoneDensityAdjuster.setTooltip("Density of atmosphere ozone."); + ozoneDensityAdjuster.setRange(0, 2); + ozoneDensityAdjuster.setMaximumFractionDigits(20); + ozoneDensityAdjuster.clampBoth(); + ozoneDensityAdjuster.set(this.ozoneDensity); + ozoneDensityAdjuster.onValueChange(value -> { + this.ozoneDensity = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + Button ozoneDefaultsButton = new Button("Defaults"); + ozoneDefaultsButton.setOnAction(e -> { + betaO.set(DEFAULT_BETA_O); + betaOX.valueProperty().setValue(betaO.x); + betaOY.valueProperty().setValue(betaO.y); + betaOZ.valueProperty().setValue(betaO.z); + + ozoneDensity = DEFAULT_OZONE_DENSITY; + ozoneDensityAdjuster.valueProperty().setValue(ozoneDensity); + ozoneScale = DEFAULT_OZONE_SCALE; + ozoneScaleAdjuster.valueProperty().setValue(ozoneScale); + + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + + GridPane ozonePane = new GridPane(); + ozonePane.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + ozonePane.setHgap(6); + ozonePane.addRow(0, new Label("Coefficient"), betaOX, betaOY, betaOZ); + controls.getChildren().addAll(new HBox(6, new Label("Ozone"), ozoneDefaultsButton), ozonePane, ozoneDensityAdjuster, ozoneScaleAdjuster, new Separator()); + + // -------- + + controls.getChildren().add(new Label("Other")); + + DoubleAdjuster altitudeAdjuster = new DoubleAdjuster(); + altitudeAdjuster.setName("Altitude"); + altitudeAdjuster.setTooltip("Altitude of the simulated camera above the surface of the earth, in meters."); + altitudeAdjuster.setRange(0.001, 10000); + altitudeAdjuster.clampMin(); + altitudeAdjuster.set(this.altitude); + altitudeAdjuster.onValueChange(value -> { + this.altitude = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(altitudeAdjuster); + + DoubleAdjuster horizonOffsetAdjuster = new DoubleAdjuster(); + horizonOffsetAdjuster.setName("Horizon offset"); + horizonOffsetAdjuster.setRange(0, 1); + horizonOffsetAdjuster.clampBoth(); + horizonOffsetAdjuster.set(this.horizonOffset); + horizonOffsetAdjuster.onValueChange(value -> { + this.horizonOffset = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(horizonOffsetAdjuster); + + DoubleAdjuster earthRadiusAdjuster = new DoubleAdjuster(); + earthRadiusAdjuster.setName("Earth radius"); + earthRadiusAdjuster.setRange(1, 10000); + earthRadiusAdjuster.clampBoth(); + earthRadiusAdjuster.set(this.earthRadius / 1000); + earthRadiusAdjuster.onValueChange(value -> { + this.earthRadius = value * 1000; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(earthRadiusAdjuster); + + DoubleAdjuster atmosphereThicknessAdjuster = new DoubleAdjuster(); + atmosphereThicknessAdjuster.setName("Atmosphere thickness"); + atmosphereThicknessAdjuster.setRange(1, 10000); + atmosphereThicknessAdjuster.clampBoth(); + atmosphereThicknessAdjuster.set(this.atmosphereThickness / 1000); + atmosphereThicknessAdjuster.onValueChange(value -> { + this.atmosphereThickness = value * 1000; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(atmosphereThicknessAdjuster); + + IntegerAdjuster viewSamplesAdjuster = new IntegerAdjuster(); + viewSamplesAdjuster.setName("View samples"); + viewSamplesAdjuster.setRange(1, 64); + viewSamplesAdjuster.clampBoth(); + viewSamplesAdjuster.set(this.viewSamples); + viewSamplesAdjuster.onValueChange(value -> { + this.viewSamples = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(viewSamplesAdjuster); + + IntegerAdjuster lightSamplesAdjuster = new IntegerAdjuster(); + lightSamplesAdjuster.setName("Light samples"); + lightSamplesAdjuster.setRange(1, 64); + lightSamplesAdjuster.clampBoth(); + lightSamplesAdjuster.set(this.lightSamples); + lightSamplesAdjuster.onValueChange(value -> { + this.lightSamples = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(lightSamplesAdjuster); + + ToggleSwitch useToneMap = new ToggleSwitch("Use tone map"); + useToneMap.setSelected(this.useToneMap); + useToneMap.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useToneMap = newValue; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(useToneMap); + + return controls; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/PreethamSky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/PreethamSky.java index f5dcb8cac5..36a8dec58e 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/PreethamSky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/PreethamSky.java @@ -14,29 +14,33 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ + package se.llbit.chunky.renderer.scene.sky; +import javafx.scene.layout.VBox; import org.apache.commons.math3.util.FastMath; -import se.llbit.math.QuickMath; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.*; import static java.lang.Math.PI; public class PreethamSky implements SimulatedSky { - private static final double xZenithChroma[][] = + private static final double[][] xZenithChroma = {{0.00166, -0.00375, 0.00209, 0}, {-0.02903, 0.06377, -0.03203, 0.00394}, {0.11693, -0.21196, 0.06052, 0.25886},}; - private static final double yZenithChroma[][] = + private static final double[][] yZenithChroma = {{0.00275, -0.00610, 0.00317, 0}, {-0.04214, 0.08970, -0.04153, 0.00516}, {0.15346, -0.26756, 0.06670, 0.26688},}; - private static final double mdx[][] = + private static final double[][] mdx = {{-0.0193, -0.2592}, {-0.0665, 0.0008}, {-0.0004, 0.2125}, {-0.0641, -0.8989}, {-0.0033, 0.0452}}; - private static final double mdy[][] = + private static final double[][] mdy = {{-0.0167, -0.2608}, {-0.0950, 0.0092}, {-0.0079, 0.2102}, {-0.0441, -1.6537}, {-0.0109, 0.0529}}; - private static final double mdY[][] = + private static final double[][] mdY = {{0.1787, -1.4630}, {-0.3554, 0.4275}, {-0.0227, 5.3251}, {0.1206, -2.5771}, {-0.0670, 0.3703}}; @@ -93,22 +97,20 @@ public PreethamSky() { } @Override - public boolean updateSun(Sun sun, double horizonOffset) { + public boolean updateSun(Sun sun) { // Clamp sky to be above horizon and follow sun properly double alt = QuickMath.clamp(sun.getAltitude(), 0, PI); if (alt > PI/2) { alt = PI - alt; } - if (theta != sun.getAzimuth() || phi != alt || this.horizonOffset != horizonOffset) { + if (theta != sun.getAzimuth() || phi != alt) { theta = sun.getAzimuth(); phi = alt; double r = QuickMath.abs(FastMath.cos(phi)); sw.set(FastMath.cos(theta) * r, FastMath.sin(phi), FastMath.sin(theta) * r); updateSkylightValues(alt); - this.horizonOffset = horizonOffset; - return true; } return false; @@ -136,7 +138,7 @@ public Vector3 calcIncidentLight(Ray ray) { double x = zenith_x * perezF(cosTheta, gamma, cos2Gamma, A.x, B.x, C.x, D.x, E.x) * f0_x; double y = zenith_y * perezF(cosTheta, gamma, cos2Gamma, A.y, B.y, C.y, D.y, E.y) * f0_y; double z = zenith_Y * perezF(cosTheta, gamma, cos2Gamma, A.z, B.z, C.z, D.z, E.z) * f0_Y; - if (y <= Ray.EPSILON) { + if (y <= Constants.EPSILON) { return new Vector3(0, 0, 0); } else { double f = (z / y); @@ -184,4 +186,44 @@ private static double perezF(double cosTheta, double gamma, double cos2Gamma, do double C, double D, double E) { return (1 + A * FastMath.exp(B / cosTheta)) * (1 + C * FastMath.exp(D * gamma) + E * cos2Gamma); } + + @Override + public void fromJson(JsonObject json) { + horizonOffset = json.get("horizonOffset").doubleValue(horizonOffset); + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("horizonOffset", horizonOffset); + return json; + } + + @Override + public void reset() { + + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + VBox controls = new VBox(); + + DoubleAdjuster horizonOffsetAdjuster = new DoubleAdjuster(); + horizonOffsetAdjuster.setName("Horizon offset"); + horizonOffsetAdjuster.setRange(0, 1); + horizonOffsetAdjuster.clampBoth(); + horizonOffsetAdjuster.set(this.horizonOffset); + horizonOffsetAdjuster.onValueChange(value -> { + this.horizonOffset = value; + scene.sky().updateSimulatedSky(scene.sun()); + scene.refresh(); + }); + controls.getChildren().add(horizonOffsetAdjuster); + + controls.setSpacing(6); + + return controls; + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SimulatedSky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SimulatedSky.java index dde49752ed..371677678e 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SimulatedSky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SimulatedSky.java @@ -14,19 +14,22 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ + package se.llbit.chunky.renderer.scene.sky; import se.llbit.math.Ray; import se.llbit.math.Vector3; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; /** * Interface for simulated skies. */ -public interface SimulatedSky { +public interface SimulatedSky extends Configurable, HasControls { /** * Update the sun if necessary. Returns true if the sun was updated (and cache needs to be purged). */ - boolean updateSun(Sun sun, double horizonOffset); + boolean updateSun(Sun sun); /** * Calculate the sky color for a given ray. diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sky.java index 8a7cb15b11..8eb474d754 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sky.java @@ -24,9 +24,7 @@ import se.llbit.chunky.resources.HDRTexture; import se.llbit.chunky.resources.PFMTexture; import se.llbit.chunky.resources.Texture; -import se.llbit.chunky.world.Clouds; import se.llbit.chunky.world.SkymapTexture; -import se.llbit.chunky.world.material.CloudMaterial; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; @@ -37,10 +35,8 @@ import se.llbit.util.JsonSerializable; import se.llbit.util.JsonUtil; import se.llbit.util.annotation.NotNull; -import se.llbit.util.annotation.Nullable; import java.io.File; -import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; @@ -51,19 +47,10 @@ */ public class Sky implements JsonSerializable { - //private static final double CLOUD_OPACITY = 0.4; - /** * Default sky light intensity */ - public static final double DEFAULT_INTENSITY = 1; - - /** - * Default cloud y-position - */ - protected static final int DEFAULT_CLOUD_HEIGHT = 128; - - protected static final int DEFAULT_CLOUD_SIZE = 12; + public static final double DEFAULT_EMITTANCE = 1; /** * Minimum sky light intensity @@ -75,16 +62,6 @@ public class Sky implements JsonSerializable { */ public static final double MAX_INTENSITY = 50; - /** - * Minimum apparent sky light intensity - */ - public static final double MIN_APPARENT_INTENSITY = 0.0; - - /** - * Maximum apparent sky light intensity - */ - public static final double MAX_APPARENT_INTENSITY = 50; - public static final int SKYBOX_UP = 0; public static final int SKYBOX_DOWN = 1; public static final int SKYBOX_FRONT = 2; @@ -118,7 +95,7 @@ public enum SkyMode { /** Skybox. */ SKYBOX("Skybox"); - private String name; + private final String name; SkyMode(String name) { this.name = name; @@ -153,23 +130,20 @@ public static SkyMode get(String name) { private double yaw = 0, pitch = 0, roll = 0; private boolean mirrored = true; - private boolean cloudsEnabled = false; - private double cloudSize = DEFAULT_CLOUD_SIZE; - private final Vector3 cloudOffset = new Vector3(0, DEFAULT_CLOUD_HEIGHT, 0); - private double skyExposure = DEFAULT_INTENSITY; - private double skyLightModifier = DEFAULT_INTENSITY; - private double apparentSkyLightModifier = DEFAULT_INTENSITY; + private double skyEmittance = DEFAULT_EMITTANCE; /** Color gradient used for the GRADIENT sky mode. */ private List gradient = new LinkedList<>(); /** Color used for the SOLID_COLOR sky mode. */ - private Vector3 color = new Vector3(0, 0, 0); + private final Vector3 color = new Vector3(0, 0, 0); /** Current sky rendering mode. */ private SkyMode mode = SkyMode.DEFAULT; + private boolean textureInterpolation = true; + /** Simulated skies. */ public final static List skies = new ArrayList<>(); @@ -180,7 +154,6 @@ public static SkyMode get(String name) { /** Simulated sky mode. */ private SimulatedSky simulatedSkyMode = skies.get(0); - double horizonOffset = 0; private final SkyCache skyCache; @@ -226,9 +199,6 @@ public void loadSkymap(SceneIOProvider ioContext, String fileName) { * Set the sky equal to other sky. */ public void set(Sky other) { - cloudsEnabled = other.cloudsEnabled; - cloudOffset.set(other.cloudOffset); - cloudSize = other.cloudSize; skymapFileName = other.skymapFileName; skymap = other.skymap; yaw = other.yaw; @@ -236,13 +206,10 @@ public void set(Sky other) { roll = other.roll; rotation.set(other.rotation); mirrored = other.mirrored; - skyExposure = other.skyExposure; - skyLightModifier = other.skyLightModifier; - apparentSkyLightModifier = other.apparentSkyLightModifier; + skyEmittance = other.skyEmittance; gradient = new ArrayList<>(other.gradient); color.set(other.color); mode = other.mode; - horizonOffset = other.horizonOffset; for (int i = 0; i < 6; ++i) { skybox[i] = other.skybox[i]; skyboxFileName[i] = other.skyboxFileName[i]; @@ -251,18 +218,19 @@ public void set(Sky other) { simulatedSkyMode = other.simulatedSkyMode; skyCache.set(other.skyCache); skyCache.setSimulatedSkyMode(other.simulatedSkyMode); - if (simulatedSkyMode.updateSun(scene.sun(), horizonOffset)) { + if (simulatedSkyMode.updateSun(scene.sun())) { skyCache.precalculateSky(); } + textureInterpolation = other.textureInterpolation; } /** * Calculate sky color for the ray, based on sky mode. */ - public void getSkyDiffuseColorInner(Ray ray) { + private void getSkyColorInner(Ray ray, IntersectionRecord intersectionRecord) { switch (mode) { case SOLID_COLOR: { - ray.color.set(color.x, color.y, color.z, 1); + intersectionRecord.color.set(color.x, color.y, color.z, 1); break; } case GRADIENT: { @@ -282,13 +250,13 @@ public void getSkyDiffuseColorInner(Ray ray) { xx = 0.5 * (Math.sin(Math.PI * xx - Constants.HALF_PI) + 1); double a = 1 - xx; double b = xx; - ray.color.set(a * c0.x + b * c1.x, a * c0.y + b * c1.y, a * c0.z + b * c1.z, 1); + intersectionRecord.color.set(a * c0.x + b * c1.x, a * c0.y + b * c1.y, a * c0.z + b * c1.z, 1); } break; } case SIMULATED: { Vector3 color = skyCache.calcIncidentLight(ray); - ray.color.set(color.x, color.y, color.z, 1); + intersectionRecord.color.set(color.x, color.y, color.z, 1); break; } case SKYMAP_EQUIRECTANGULAR: { @@ -302,12 +270,12 @@ public void getSkyDiffuseColorInner(Ray ray) { theta = (theta % 1 + 1) % 1; } double phi = Math.abs(Math.asin(y)) / Constants.HALF_PI; - skymap.getColor(theta, phi, ray.color); + skymap.getColor(theta, phi, intersectionRecord.color); } else { double theta = FastMath.atan2(z, x) / Constants.TAU; theta = (theta % 1 + 1) % 1; double phi = (Math.asin(y) + Constants.HALF_PI) / Math.PI; - skymap.getColor(theta, phi, ray.color); + getSkymapColor(skymap, theta, phi, intersectionRecord.color); } break; } @@ -316,10 +284,10 @@ public void getSkyDiffuseColorInner(Ray ray) { double y = rotation.transformY(ray.d); double z = rotation.transformZ(ray.d); double len = Math.sqrt(x * x + y * y); - double theta = (len < Ray.EPSILON) ? 0 : Math.acos(-z) / (Constants.TAU * len); + double theta = (len < Constants.EPSILON) ? 0 : Math.acos(-z) / (Constants.TAU * len); double u = theta * x + .5; double v = .5 + theta * y; - skymap.getColor(u, v, ray.color); + getSkymapColor(skymap, u, v, intersectionRecord.color); break; } case SKYBOX: { @@ -331,164 +299,58 @@ public void getSkyDiffuseColorInner(Ray ray) { double zabs = QuickMath.abs(z); if (y > xabs && y > zabs) { double alpha = 1 / yabs; - skybox[SKYBOX_UP].getColor((1 + x * alpha) / 2.0, (1 + z * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_UP], (1 + x * alpha) / 2.0, (1 + z * alpha) / 2.0, intersectionRecord.color); } else if (-z > xabs && -z > yabs) { double alpha = 1 / zabs; - skybox[SKYBOX_FRONT].getColor((1 + x * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_FRONT], (1 + x * alpha) / 2.0, (1 + y * alpha) / 2.0, intersectionRecord.color); } else if (z > xabs && z > yabs) { double alpha = 1 / zabs; - skybox[SKYBOX_BACK].getColor((1 - x * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_BACK], (1 - x * alpha) / 2.0, (1 + y * alpha) / 2.0, intersectionRecord.color); } else if (-x > zabs && -x > yabs) { double alpha = 1 / xabs; - skybox[SKYBOX_LEFT].getColor((1 - z * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_LEFT], (1 - z * alpha) / 2.0, (1 + y * alpha) / 2.0, intersectionRecord.color); } else if (x > zabs && x > yabs) { double alpha = 1 / xabs; - skybox[SKYBOX_RIGHT].getColor((1 + z * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_RIGHT], (1 + z * alpha) / 2.0, (1 + y * alpha) / 2.0, intersectionRecord.color); } else if (-y > xabs && -y > zabs) { double alpha = 1 / yabs; - skybox[SKYBOX_DOWN].getColor((1 + x * alpha) / 2.0, (1 - z * alpha) / 2.0, ray.color); + getSkymapColor(skybox[SKYBOX_DOWN], (1 + x * alpha) / 2.0, (1 - z * alpha) / 2.0, intersectionRecord.color); } break; } } } - /** - * Panoramic skymap color. - */ - public void getSkyColor(Ray ray, boolean drawSun) { - getSkyDiffuseColorInner(ray); - ray.color.scale(skyExposure); - ray.color.scale(skyLightModifier); - if (drawSun) addSunColor(ray); - ray.color.w = 1; - } - - public void getApparentSkyColor(Ray ray, boolean drawSun) { - getSkyDiffuseColorInner(ray); - ray.color.scale(skyExposure); - ray.color.scale(apparentSkyLightModifier); - if (drawSun) addSunColor(ray); - ray.color.w = 1; - } - - /** - * Bilinear interpolated panoramic skymap color. - */ - public void getSkyColorInterpolated(Ray ray) { - switch (mode) { - case SKYMAP_EQUIRECTANGULAR: { - double x = rotation.transformX(ray.d); - double y = rotation.transformY(ray.d); - double z = rotation.transformZ(ray.d); - if (mirrored) { - double theta = FastMath.atan2(z, x) / Constants.TAU; - theta = (theta % 1 + 1) % 1; - double phi = Math.abs(Math.asin(y)) / Constants.HALF_PI; - skymap.getColorInterpolated(theta, phi, ray.color); - } else { - double theta = FastMath.atan2(z, x) / Constants.TAU; - if (theta > 1 || theta < 0) { - theta = (theta % 1 + 1) % 1; - } - double phi = (Math.asin(y) + Constants.HALF_PI) / Math.PI; - skymap.getColorInterpolated(theta, phi, ray.color); - } - break; - } - case SKYMAP_ANGULAR: { - double x = rotation.transformX(ray.d); - double y = rotation.transformY(ray.d); - double z = rotation.transformZ(ray.d); - double len = Math.sqrt(x * x + y * y); - double theta = (len < Ray.EPSILON) ? 0 : Math.acos(-z) / (Constants.TAU * len); - double u = theta * x + .5; - double v = .5 + theta * y; - skymap.getColorInterpolated(u, v, ray.color); - break; - } - case SKYBOX: { - double x = rotation.transformX(ray.d); - double y = rotation.transformY(ray.d); - double z = rotation.transformZ(ray.d); - double xabs = QuickMath.abs(x); - double yabs = QuickMath.abs(y); - double zabs = QuickMath.abs(z); - if (y > xabs && y > zabs) { - double alpha = 1 / yabs; - skybox[SKYBOX_UP] - .getColorInterpolated((1 + x * alpha) / 2.0, (1 + z * alpha) / 2.0, ray.color); - } else if (-z > xabs && -z > yabs) { - double alpha = 1 / zabs; - skybox[SKYBOX_FRONT] - .getColorInterpolated((1 + x * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); - } else if (z > xabs && z > yabs) { - double alpha = 1 / zabs; - skybox[SKYBOX_BACK] - .getColorInterpolated((1 - x * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); - } else if (-x > zabs && -x > yabs) { - double alpha = 1 / xabs; - skybox[SKYBOX_LEFT] - .getColorInterpolated((1 - z * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); - } else if (x > zabs && x > yabs) { - double alpha = 1 / xabs; - skybox[SKYBOX_RIGHT] - .getColorInterpolated((1 + z * alpha) / 2.0, (1 + y * alpha) / 2.0, ray.color); - } else if (-y > xabs && -y > zabs) { - double alpha = 1 / yabs; - skybox[SKYBOX_DOWN] - .getColorInterpolated((1 + x * alpha) / 2.0, (1 - z * alpha) / 2.0, ray.color); - } - break; - } - default: { - getSkyDiffuseColorInner(ray); - } + private void getSkymapColor(Texture texture, double u, double v, Vector4 color) { + if (textureInterpolation) { + texture.getColorInterpolated(u, v, color); + } else { + texture.getColor(u, v, color); } - ray.color.scale(skyExposure); - ray.color.scale(apparentSkyLightModifier); - addSunColor(ray); - ray.color.w = 1; } /** - * Add sun color contribution. This does not alpha blend the sun color - * because the Minecraft sun texture has no alpha channel. + * Panoramic skymap color. */ - private void addSunColor(Ray ray) { - double r = ray.color.x; - double g = ray.color.y; - double b = ray.color.z; - if (scene.sun().intersect(ray)) { - - // Blend sun color with current color. - ray.color.x = ray.color.x + r; - ray.color.y = ray.color.y + g; - ray.color.z = ray.color.z + b; + public void getSkyColor(Ray ray, IntersectionRecord intersectionRecord, boolean addSun) { + getSkyColorInner(ray, intersectionRecord); + intersectionRecord.color.scale(skyEmittance); + if (addSun) { + intersectionRecord.color.add(scene.sun().getSunIntersectionColor(ray)); } + intersectionRecord.color.w = 1; } - public void getSkyColorDiffuseSun(Ray ray, boolean diffuseSun) { - getSkyDiffuseColorInner(ray); - ray.color.scale(skyExposure); - ray.color.scale(skyLightModifier); - if (diffuseSun) addSunColorDiffuseSun(ray); - ray.color.w = 1; - } - - public void addSunColorDiffuseSun(Ray ray) { - double r = ray.color.x; - double g = ray.color.y; - double b = ray.color.z; - - if (scene.sun().intersectDiffuse(ray)) { - double mult = scene.sun().getLuminosity(); - - // Blend sun color with current color. - ray.color.x = ray.color.x * mult + r; - ray.color.y = ray.color.y * mult + g; - ray.color.z = ray.color.z * mult + b; + public void intersect(Ray ray, IntersectionRecord intersectionRecord) { + if ((ray.isIndirect()) && scene.sun().intersect(ray, intersectionRecord)) { + if ((ray.isDiffuse()) && scene.getSunSamplingStrategy().doSunSampling()) { + return; + } + if ((ray.isSpecular()) && !scene.getSunSamplingStrategy().isDiffuseSun()) { + return; + } } + getSkyColor(ray, intersectionRecord, true); } /** @@ -588,7 +450,7 @@ public SkyMode getSkyMode() { */ public void setSimulatedSkyMode(int mode) { this.simulatedSkyMode = skies.get(mode); - this.simulatedSkyMode.updateSun(scene.sun(), horizonOffset); + this.simulatedSkyMode.updateSun(scene.sun()); skyCache.setSimulatedSkyMode(this.simulatedSkyMode); scene.refresh(); } @@ -604,9 +466,8 @@ public SimulatedSky getSimulatedSky() { * Update the current simulated sky */ public void updateSimulatedSky(Sun sun) { - if (simulatedSkyMode.updateSun(sun, horizonOffset)) { - skyCache.precalculateSky(); - } + simulatedSkyMode.updateSun(sun); + skyCache.precalculateSky(); } /** @@ -622,19 +483,14 @@ public void setSkyCacheResolution(int resolution) { sky.add("skyPitch", pitch); sky.add("skyRoll", roll); sky.add("skyMirrored", mirrored); - sky.add("skyExposure", skyExposure); - sky.add("skyLight", skyLightModifier); - sky.add("apparentSkyLight", apparentSkyLightModifier); + sky.add("skyEmittance", skyEmittance); sky.add("mode", mode.name()); - sky.add("horizonOffset", horizonOffset); - sky.add("cloudsEnabled", cloudsEnabled); - sky.add("cloudSize", cloudSize); - sky.add("cloudOffset", cloudOffset.toJson()); // Always save gradient. sky.add("gradient", gradientJson(gradient)); sky.add("color", JsonUtil.rgbToJson(color)); + sky.add("textureInterpolation", textureInterpolation); switch (mode) { case SKYMAP_EQUIRECTANGULAR: @@ -658,6 +514,7 @@ public void setSkyCacheResolution(int resolution) { } case SIMULATED: { sky.add("simulatedSky", simulatedSkyMode.getName()); + sky.add("simulatedSkySettings", simulatedSkyMode.toJson()); sky.add("skyCacheResolution", skyCache.getSkyResolution()); break; } @@ -674,9 +531,7 @@ public void importFromJson(JsonObject json) { roll = json.get("skyRoll").doubleValue(roll); updateTransform(); mirrored = json.get("skyMirrored").boolValue(mirrored); - skyExposure = json.get("skyExposure").doubleValue(skyExposure); - skyLightModifier = json.get("skyLight").doubleValue(skyLightModifier); - apparentSkyLightModifier = json.get("apparentSkyLight").doubleValue(apparentSkyLightModifier); + skyEmittance = json.get("skyEmittance").doubleValue(skyEmittance); if (json.get("mode").stringValue(mode.name()).equals("BLACK")) { mode = SkyMode.SOLID_COLOR; color.x = 0; @@ -689,12 +544,6 @@ public void importFromJson(JsonObject json) { } else { mode = SkyMode.get(json.get("mode").stringValue(mode.name())); } - horizonOffset = json.get("horizonOffset").doubleValue(horizonOffset); - cloudsEnabled = json.get("cloudsEnabled").boolValue(cloudsEnabled); - cloudSize = json.get("cloudSize").doubleValue(cloudSize); - if (json.get("cloudOffset").isObject()) { - cloudOffset.fromJson(json.get("cloudOffset").object()); - } if (json.get("gradient").isArray()) { List theGradient = gradientFromJson(json.get("gradient").array()); @@ -723,58 +572,34 @@ public void importFromJson(JsonObject json) { break; } case SIMULATED: { - skyCache.setSkyResolution(json.get("skyCacheResolution").asInt(skyCache.getSkyResolution())); - String simSkyName = json.get("simulatedSky").asString(simulatedSkyMode.getName()); Optional match = skies.stream().filter(skyMode -> skyMode.getName().equals(simSkyName)).findAny(); simulatedSkyMode = match.orElseGet(() -> simulatedSkyMode); - simulatedSkyMode.updateSun(scene.sun(), horizonOffset); + simulatedSkyMode.fromJson(json.get("simulatedSkySettings").asObject()); + simulatedSkyMode.updateSun(scene.sun()); skyCache.setSimulatedSkyMode(simulatedSkyMode); - skyCache.precalculateSky(); + skyCache.setSkyResolution(json.get("skyCacheResolution").asInt(skyCache.getSkyResolution())); scene.refresh(); break; } default: break; } + textureInterpolation = json.get("textureInterpolation").boolValue(true); } private void updateTransform() { rotation.rotate(-pitch, -yaw, -roll); } - public void setSkyExposure(double newValue) { - skyExposure = newValue; - scene.refresh(); - } - - /** - * Set the sky light modifier. - */ - public void setSkyLight(double newValue) { - skyLightModifier = newValue; - scene.refresh(); - } - - public void setApparentSkyLight(double newValue) { - apparentSkyLightModifier = newValue; + public void setSkyEmittance(double newValue) { + skyEmittance = newValue; scene.refresh(); } - public double getSkyExposure() { - return skyExposure; - } - - /** - * @return Current sky light modifier - */ - public double getSkyLight() { - return skyLightModifier; - } - - public double getApparentSkyLight() { - return apparentSkyLightModifier; + public double getSkyEmittance() { + return skyEmittance; } public void setGradient(List newGradient) { @@ -889,281 +714,6 @@ private Texture loadSkyTexture(SceneIOProvider ioContext, String fileName, Textu } } - public void setHorizonOffset(double newValue) { - newValue = Math.min(1, Math.max(0, newValue)); - if (newValue != horizonOffset) { - horizonOffset = newValue; - scene.refresh(); - } - } - - public double getHorizonOffset() { - return horizonOffset; - } - - public void setCloudSize(double newValue) { - if (newValue != cloudSize) { - cloudSize = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } - } - - public double cloudSize() { - return cloudSize; - } - - public void setCloudXOffset(double newValue) { - if (newValue != cloudOffset.x) { - cloudOffset.x = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } - } - - /** - * Change the cloud height - */ - public void setCloudYOffset(double newValue) { - if (newValue != cloudOffset.y) { - cloudOffset.y = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } - } - - public void setCloudZOffset(double newValue) { - if (newValue != cloudOffset.z) { - cloudOffset.z = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } - } - - public double cloudXOffset() { - return cloudOffset.x; - } - - /** - * @return The current cloud height - */ - public double cloudYOffset() { - return cloudOffset.y; - } - - public double cloudZOffset() { - return cloudOffset.z; - } - - - /** - * Enable/disable clouds rendering. - */ - public void setCloudsEnabled(boolean newValue) { - if (newValue != cloudsEnabled) { - cloudsEnabled = newValue; - scene.refresh(); - } - } - - /** - * @return true if cloud rendering is enabled - */ - public boolean cloudsEnabled() { - return cloudsEnabled; - } - - public boolean cloudIntersection(Scene scene, Ray ray) { - double ox = ray.o.x + scene.getOrigin().x; - double oy = ray.o.y + scene.getOrigin().y; - double oz = ray.o.z + scene.getOrigin().z; - double offsetX = cloudOffset.x; - double offsetY = cloudOffset.y; - double offsetZ = cloudOffset.z; - double inv_size = 1 / cloudSize; - double cloudTop = offsetY + 5; - int target = 1; - double t_offset = 0; - if (oy < offsetY || oy > cloudTop) { - if (ray.d.y > 0) { - t_offset = (offsetY - oy) / ray.d.y; - } else { - t_offset = (cloudTop - oy) / ray.d.y; - } - if (t_offset < 0) { - return false; - } - // Ray is entering cloud. - if (inCloud((ray.d.x * t_offset + ox) * inv_size + offsetX, - (ray.d.z * t_offset + oz) * inv_size + offsetZ)) { - ray.setNormal(0, -Math.signum(ray.d.y), 0); - enterCloud(ray, t_offset); - return true; - } - } else if (inCloud(ox * inv_size + offsetX, oz * inv_size + offsetZ)) { - target = 0; - } - double tExit; - if (ray.d.y > 0) { - tExit = (cloudTop - oy) / ray.d.y - t_offset; - } else { - tExit = (offsetY - oy) / ray.d.y - t_offset; - } - if (ray.t < tExit) { - tExit = ray.t; - } - double x0 = (ox + ray.d.x * t_offset) * inv_size + offsetX; - double z0 = (oz + ray.d.z * t_offset) * inv_size + offsetZ; - double xp = x0; - double zp = z0; - int ix = (int) Math.floor(xp); - int iz = (int) Math.floor(zp); - int xmod = (int) Math.signum(ray.d.x), zmod = (int) Math.signum(ray.d.z); - int xo = (1 + xmod) / 2, zo = (1 + zmod) / 2; - double dx = Math.abs(ray.d.x) * inv_size; - double dz = Math.abs(ray.d.z) * inv_size; - double t = 0; - int i = 0; - int nx = 0, nz = 0; - if (dx > dz) { - double m = dz / dx; - double xrem = xmod * (ix + xo - xp); - double zlimit = xrem * m; - while (t < tExit) { - double zrem = zmod * (iz + zo - zp); - if (zrem < zlimit) { - iz += zmod; - if (Clouds.getCloud(ix, iz) == target) { - t = i / dx + zrem / dz; - nx = 0; - nz = -zmod; - break; - } - ix += xmod; - if (Clouds.getCloud(ix, iz) == target) { - t = (i + xrem) / dx; - nx = -xmod; - nz = 0; - break; - } - } else { - ix += xmod; - if (Clouds.getCloud(ix, iz) == target) { - t = (i + xrem) / dx; - nx = -xmod; - nz = 0; - break; - } - if (zrem <= m) { - iz += zmod; - if (Clouds.getCloud(ix, iz) == target) { - t = i / dx + zrem / dz; - nx = 0; - nz = -zmod; - break; - } - } - } - t = i / dx; - i += 1; - zp = z0 + zmod * i * m; - } - } else { - double m = dx / dz; - double zrem = zmod * (iz + zo - zp); - double xlimit = zrem * m; - while (t < tExit) { - double xrem = xmod * (ix + xo - xp); - if (xrem < xlimit) { - ix += xmod; - if (Clouds.getCloud(ix, iz) == target) { - t = i / dz + xrem / dx; - nx = -xmod; - nz = 0; - break; - } - iz += zmod; - if (Clouds.getCloud(ix, iz) == target) { - t = (i + zrem) / dz; - nx = 0; - nz = -zmod; - break; - } - } else { - iz += zmod; - if (Clouds.getCloud(ix, iz) == target) { - t = (i + zrem) / dz; - nx = 0; - nz = -zmod; - break; - } - if (xrem <= m) { - ix += xmod; - if (Clouds.getCloud(ix, iz) == target) { - t = i / dz + xrem / dx; - nx = -xmod; - nz = 0; - break; - } - } - } - t = i / dz; - i += 1; - xp = x0 + xmod * i * m; - } - } - int ny = 0; - if (target == 1) { - if (t > tExit) { - return false; - } - if (nx == 0 && ny == 0 && nz == 0) { - // fix ray.n being set to zero (issue #643) - return false; - } - ray.setNormal(nx, ny, nz); - enterCloud(ray, t + t_offset); - return true; - } else { - if (t > tExit) { - nx = 0; - ny = (int) Math.signum(ray.d.y); - nz = 0; - t = tExit; - } else { - nx = -nx; - nz = -nz; - } - if (nx == 0 && ny == 0 && nz == 0) { - // fix ray.n being set to zero (issue #643) - return false; - } - ray.setNormal(nx, ny, nz); - exitCloud(ray, t); - } - return true; - } - - private static void enterCloud(Ray ray, double t) { - ray.t = t; - ray.color.set(CloudMaterial.color); - ray.setCurrentMaterial(CloudMaterial.INSTANCE); - } - - private static void exitCloud(Ray ray, double t) { - ray.t = t; - ray.color.set(CloudMaterial.color); - ray.setCurrentMaterial(Air.INSTANCE); - } - - private static boolean inCloud(double x, double z) { - return Clouds.getCloud((int) Math.floor(x), (int) Math.floor(z)) == 1; - } - public void setColor(Vector3 color) { this.color.set(color); scene.refresh(); @@ -1172,4 +722,12 @@ public void setColor(Vector3 color) { public Vector3 getColor() { return color; } + + public boolean getTextureInterpolation() { + return this.textureInterpolation; + } + + public void setTextureInterpolation(boolean textureInterpolation) { + this.textureInterpolation = textureInterpolation; + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SkyCache.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SkyCache.java index a70d5d42cd..b0daacb4de 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SkyCache.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/SkyCache.java @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ + package se.llbit.chunky.renderer.scene.sky; import static java.lang.Math.PI; @@ -23,7 +24,6 @@ import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.main.Chunky; import se.llbit.log.Log; -import se.llbit.math.ColorUtil; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -134,9 +134,7 @@ public Vector3 calcIncidentLight(Ray ray) { theta = ((theta % 1) + 1) % 1; double phi = (FastMath.asin(QuickMath.clamp(ray.d.y, -1, 1)) + PI / 2) / PI; - Vector3 color = getColorInterpolated(theta, phi); - ColorUtil.RGBfromHSL(color, color.x, color.y, color.z); - return color; + return getColorInterpolated(theta, phi); } // Linear interpolation between 2 points in 1 dimension @@ -175,8 +173,6 @@ private Vector3 getSkyColorAt(int x, int y) { double r = FastMath.cos(phi); ray.d.set(FastMath.cos(theta) * r, FastMath.sin(phi), FastMath.sin(theta) * r); - Vector3 color = simSky.calcIncidentLight(ray); - ColorUtil.RGBtoHSL(color, color.x, color.y, color.z); - return color; + return simSky.calcIncidentLight(ray); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sun.java b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sun.java index a91617db74..4271e907f3 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sun.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/sky/Sun.java @@ -25,11 +25,8 @@ import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.json.JsonObject; -import se.llbit.math.QuickMath; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.*; import se.llbit.util.JsonSerializable; -import se.llbit.util.JsonUtil; /** * Sun model for ray tracing. @@ -41,120 +38,29 @@ public class Sun implements JsonSerializable { /** * Default sun intensity */ - public static final double DEFAULT_INTENSITY = 1.25; + public static final double DEFAULT_INTENSITY = 2_500; /** * Maximum sun intensity */ - public static final double MAX_INTENSITY = 50; + public static final double MAX_INTENSITY = 1_000_000_000; /** * Minimum sun intensity */ - public static final double MIN_INTENSITY = 0.1; - - /** - * Minimum apparent sun brightness - */ - public static final double MIN_APPARENT_BRIGHTNESS = 0.01; - - /** - * Maximum apparent sun brightness - */ - public static final double MAX_APPARENT_BRIGHTNESS = 50; - - /** - * Default probability for importance sun sampling - */ - public static final double DEFAULT_IMPORTANCE_SAMPLE_CHANCE = 0.1; - - /** - * Minimum probability for importance sun sampling - */ - public static final double MIN_IMPORTANCE_SAMPLE_CHANCE = 0.001; - - /** - * Maximum probability for importance sun sampling - */ - public static final double MAX_IMPORTANCE_SAMPLE_CHANCE = 0.9; - - /** - * Default radius (relative to sun) for importance sun sampling - */ - public static final double DEFAULT_IMPORTANCE_SAMPLE_RADIUS = 1.2; - - /** - * Minimum radius (relative to sun) for importance sun sampling - */ - public static final double MIN_IMPORTANCE_SAMPLE_RADIUS = 0.1; - - /** - * Maximum radius (relative to sun) for importance sun sampling - */ - public static final double MAX_IMPORTANCE_SAMPLE_RADIUS = 5; - - private static final double xZenithChroma[][] = - {{0.00166, -0.00375, 0.00209, 0}, {-0.02903, 0.06377, -0.03203, 0.00394}, - {0.11693, -0.21196, 0.06052, 0.25886},}; - private static final double yZenithChroma[][] = - {{0.00275, -0.00610, 0.00317, 0}, {-0.04214, 0.08970, -0.04153, 0.00516}, - {0.15346, -0.26756, 0.06670, 0.26688},}; - private static final double mdx[][] = - {{-0.0193, -0.2592}, {-0.0665, 0.0008}, {-0.0004, 0.2125}, {-0.0641, -0.8989}, - {-0.0033, 0.0452}}; - private static final double mdy[][] = - {{-0.0167, -0.2608}, {-0.0950, 0.0092}, {-0.0079, 0.2102}, {-0.0441, -1.6537}, - {-0.0109, 0.0529}}; - private static final double mdY[][] = - {{0.1787, -1.4630}, {-0.3554, 0.4275}, {-0.0227, 5.3251}, {0.1206, -2.5771}, - {-0.0670, 0.3703}}; - - private static double turb = 2.5; - private static double turb2 = turb * turb; - private static Vector3 A = new Vector3(); - private static Vector3 B = new Vector3(); - private static Vector3 C = new Vector3(); - private static Vector3 D = new Vector3(); - private static Vector3 E = new Vector3(); + public static final double MIN_INTENSITY = 0.001; /** * Sun texture */ public static Texture texture = new Texture(); - static { - A.x = mdx[0][0] * turb + mdx[0][1]; - B.x = mdx[1][0] * turb + mdx[1][1]; - C.x = mdx[2][0] * turb + mdx[2][1]; - D.x = mdx[3][0] * turb + mdx[3][1]; - E.x = mdx[4][0] * turb + mdx[4][1]; - - A.y = mdy[0][0] * turb + mdy[0][1]; - B.y = mdy[1][0] * turb + mdy[1][1]; - C.y = mdy[2][0] * turb + mdy[2][1]; - D.y = mdy[3][0] * turb + mdy[3][1]; - E.y = mdy[4][0] * turb + mdy[4][1]; - - A.z = mdY[0][0] * turb + mdY[0][1]; - B.z = mdY[1][0] * turb + mdY[1][1]; - C.z = mdY[2][0] * turb + mdY[2][1]; - D.z = mdY[3][0] * turb + mdY[3][1]; - E.z = mdY[4][0] * turb + mdY[4][1]; - } - - private double zenith_Y; - private double zenith_x; - private double zenith_y; - private double f0_Y; - private double f0_x; - private double f0_y; - private final Refreshable scene; /** * Sun radius */ - public double radius = .03; + public double radius = 0.041887902; public double radiusCos = FastMath.cos(radius); public double radiusSin = FastMath.sin(radius); @@ -162,15 +68,7 @@ public class Sun implements JsonSerializable { private double intensity = DEFAULT_INTENSITY; - private double luminosity = 100; - private double luminosityPdf = 1.0 / luminosity; - - private double apparentBrightness = DEFAULT_INTENSITY; - private Vector3 apparentTextureBrightness = new Vector3(1, 1, 1); - private boolean enableTextureModification = false; - - private double importanceSampleChance = DEFAULT_IMPORTANCE_SAMPLE_CHANCE; - private double importanceSampleRadius = DEFAULT_IMPORTANCE_SAMPLE_RADIUS; + private boolean useFlatTexture = true; private double azimuth = Math.PI / 2.5; private double altitude = Math.PI / 3; @@ -184,36 +82,11 @@ public class Sun implements JsonSerializable { */ private final Vector3 sw = new Vector3(); - private final Vector3 emittance = new Vector3(1, 1, 1); - - private static final double pE = FastMath.pow(DEFAULT_INTENSITY, Scene.DEFAULT_GAMMA); - - protected static final Vector3 previewEmittance = new Vector3(pE, pE, pE); - // final to ensure that we don't do a lot of redundant re-allocation private final Vector3 color = new Vector3(1, 1, 1); - private final Vector3 apparentColor = new Vector3(1, 1, 1); - private boolean drawTexture = true; - private double chroma(double turb, double turb2, double sunTheta, double[][] matrix) { - - double t1 = sunTheta; - double t2 = t1 * t1; - double t3 = t1 * t2; - - return turb2 * (matrix[0][0] * t3 + matrix[0][1] * t2 + matrix[0][2] * t1 + matrix[0][3]) + - turb * (matrix[1][0] * t3 + matrix[1][1] * t2 + matrix[1][2] * t1 + matrix[1][3]) + - (matrix[2][0] * t3 + matrix[2][1] * t2 + matrix[2][2] * t1 + matrix[2][3]); - } - - private static double perezF(double cosTheta, double gamma, double cos2Gamma, double A, double B, - double C, double D, double E) { - - return (1 + A * FastMath.exp(B / cosTheta)) * (1 + C * FastMath.exp(D * gamma) + E * cos2Gamma); - } - /** * Create new sun model. */ @@ -229,16 +102,10 @@ public void set(Sun other) { azimuth = other.azimuth; altitude = other.altitude; color.set(other.color); - apparentColor.set(other.apparentColor); drawTexture = other.drawTexture; + useFlatTexture = other.useFlatTexture; intensity = other.intensity; - luminosity = other.luminosity; - apparentBrightness = other.apparentBrightness; radius = other.radius; - enableTextureModification = other.enableTextureModification; - luminosityPdf = other.luminosityPdf; - importanceSampleRadius = other.importanceSampleRadius; - importanceSampleChance = other.importanceSampleChance; initSun(); } @@ -262,22 +129,10 @@ private void initSun() { sv.normalize(); su.cross(sv, sw); - emittance.set(color); - emittance.scale(FastMath.pow(intensity, Scene.DEFAULT_GAMMA)); - - if (enableTextureModification) { - apparentTextureBrightness.set(apparentColor); - } else { - apparentTextureBrightness.set(1, 1, 1); - } - apparentTextureBrightness.scale(FastMath.pow(apparentBrightness, Scene.DEFAULT_GAMMA)); - Sky sky = ((Scene) scene).sky(); if (sky.getSkyMode() == Sky.SkyMode.SIMULATED) { sky.updateSimulatedSky(this); } - - updateSkylightValues(); } /** @@ -317,65 +172,41 @@ public double getAzimuth() { * * @return true if the ray intersects the sun model */ - public boolean intersect(Ray ray) { - if (!drawTexture || ray.d.dot(sw) < .5) { - return false; - } - - double width = radius * 4; - double width2 = width * 2; - double a; - a = Math.PI / 2 - FastMath.acos(ray.d.dot(su)) + width; - if (a >= 0 && a < width2) { - double b = Math.PI / 2 - FastMath.acos(ray.d.dot(sv)) + width; - if (b >= 0 && b < width2) { - texture.getColor(a / width2, b / width2, ray.color); - ray.color.x *= apparentTextureBrightness.x * 10; - ray.color.y *= apparentTextureBrightness.y * 10; - ray.color.z *= apparentTextureBrightness.z * 10; - return true; - } - } - - return false; - } - - /** - * Used with SSS: OFF, SSS: IMPORTANCE, and SSS: HIGH_QUALITY. - */ - public boolean intersectDiffuse(Ray ray) { - if (ray.d.dot(sw) < .5) { - return false; - } - - double width = radius * 4; - double width2 = width * 2; - double a; - a = Math.PI / 2 - FastMath.acos(ray.d.dot(su)) + width; - if (a >= 0 && a < width2) { - double b = Math.PI / 2 - FastMath.acos(ray.d.dot(sv)) + width; - if (b >= 0 && b < width2) { - texture.getColor(a / width2, b / width2, ray.color); - ray.color.x *= color.x * 10; - ray.color.y *= color.y * 10; - ray.color.z *= color.z * 10; - return true; + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord) { + return (ray.d.dot(sw) > radiusCos); + } + + public Vector4 getSunIntersectionColor(Ray ray) { + final Vector4 sunColor = new Vector4(1, 1, 1, 1); + if (intersect(ray, null) && drawTexture) { + double width = radius * Constants.SQRT_2; + double half_width = width / 2; + double a = Math.PI / 2 - FastMath.acos(ray.d.dot(su)) + half_width; + if (a >= 0 && a < width) { + double b = Math.PI / 2 - FastMath.acos(ray.d.dot(sv)) + half_width; + if (b >= 0 && b < width) { + if (!useFlatTexture) { + texture.getColor(a / width, b / width, sunColor); + } + sunColor.x *= color.x * intensity; + sunColor.y *= color.y * intensity; + sunColor.z *= color.z * intensity; + return sunColor; + } } } - - return false; + sunColor.set(0, 0, 0, 1); + return sunColor; } /** * Calculate flat shading for ray. */ - public void flatShading(Ray ray) { - Vector3 n = ray.getNormal(); + public void flatShading(IntersectionRecord intersectionRecord) { + Vector3 n = intersectionRecord.shadeN; double shading = n.x * sw.x + n.y * sw.y + n.z * sw.z; shading = QuickMath.max(AMBIENT, shading); - ray.color.x *= previewEmittance.x * shading; - ray.color.y *= previewEmittance.y * shading; - ray.color.z *= previewEmittance.z * shading; + intersectionRecord.color.scale(shading); } public void setColor(Vector3 newColor) { @@ -384,26 +215,6 @@ public void setColor(Vector3 newColor) { scene.refresh(); } - public void setApparentColor(Vector3 newColor) { - this.apparentColor.set(newColor); - initSun(); - scene.refresh(); - } - - private void updateSkylightValues() { - double sunTheta = Math.PI / 2 - altitude; - double cosTheta = FastMath.cos(sunTheta); - double cos2Theta = cosTheta * cosTheta; - double chi = (4.0 / 9.0 - turb / 120.0) * (Math.PI - 2 * sunTheta); - zenith_Y = (4.0453 * turb - 4.9710) * Math.tan(chi) - 0.2155 * turb + 2.4192; - zenith_Y = (zenith_Y < 0) ? -zenith_Y : zenith_Y; - zenith_x = chroma(turb, turb2, sunTheta, xZenithChroma); - zenith_y = chroma(turb, turb2, sunTheta, yZenithChroma); - f0_x = 1 / perezF(1, sunTheta, cos2Theta, A.x, B.x, C.x, D.x, E.x); - f0_y = 1 / perezF(1, sunTheta, cos2Theta, A.y, B.y, C.y, D.y, E.y); - f0_Y = 1 / perezF(1, sunTheta, cos2Theta, A.z, B.z, C.z, D.z, E.z); - } - /** * Set the sun intensity */ @@ -420,40 +231,6 @@ public double getIntensity() { return intensity; } - public void setLuminosity(double value) { - luminosity = value; - luminosityPdf = 1 / value; - scene.refresh(); - } - - public double getLuminosity() { - return luminosity; - } - - public double getLuminosityPdf() { - return luminosityPdf; - } - - public void setApparentBrightness(double value) { - apparentBrightness = value; - initSun(); - scene.refresh(); - } - - public double getApparentBrightness() { - return apparentBrightness; - } - - public void setEnableTextureModification(boolean value) { - enableTextureModification = value; - initSun(); - scene.refresh(); - } - - public boolean getEnableTextureModification() { - return enableTextureModification; - } - /** * @param value Sun radius in radians. */ @@ -473,7 +250,7 @@ public double getSunRadius() { /** * Point ray in random direction within sun solid angle */ - public void getRandomSunDirection(Ray reflected, Random random) { + public void getRandomSunDirection(Vector3 d, Random random) { double x1 = random.nextDouble(); double x2 = random.nextDouble(); double cos_a = 1 - x1 + x1 * radiusCos; @@ -488,9 +265,9 @@ public void getRandomSunDirection(Ray reflected, Random random) { v.scale(FastMath.sin(phi) * sin_a); w.scale(cos_a); - reflected.d.add(u, v); - reflected.d.add(w); - reflected.d.normalize(); + d.add(u, v); + d.add(w); + d.normalize(); } @Override public JsonObject toJson() { @@ -498,17 +275,10 @@ public void getRandomSunDirection(Ray reflected, Random random) { sun.add("altitude", altitude); sun.add("azimuth", azimuth); sun.add("intensity", intensity); - sun.add("luminosity", luminosity); - sun.add("apparentBrightness", apparentBrightness); sun.add("radius", radius); - sun.add("modifySunTexture", enableTextureModification); - sun.add("color", JsonUtil.rgbToJson(color)); - sun.add("apparentColor", JsonUtil.rgbToJson(apparentColor)); - JsonObject importanceSamplingObj = new JsonObject(); - importanceSamplingObj.add("chance", importanceSampleChance); - importanceSamplingObj.add("radius", importanceSampleRadius); - sun.add("importanceSampling", importanceSamplingObj); + sun.add("color", ColorUtil.rgbToJson(color)); sun.add("drawTexture", drawTexture); + sun.add("useFlatTexture", useFlatTexture); return sun; } @@ -516,27 +286,10 @@ public void importFromJson(JsonObject json) { azimuth = json.get("azimuth").doubleValue(azimuth); altitude = json.get("altitude").doubleValue(altitude); intensity = json.get("intensity").doubleValue(intensity); - setLuminosity(json.get("luminosity").doubleValue(luminosity)); - apparentBrightness = json.get("apparentBrightness").doubleValue(apparentBrightness); radius = json.get("radius").doubleValue(radius); - enableTextureModification = json.get("modifySunTexture").boolValue(enableTextureModification); - - if (!json.get("color").isUnknown()) { - JsonUtil.rgbFromJson(json.get("color"), color); - } - - if (!json.get("apparentColor").isUnknown()) { - JsonUtil.rgbFromJson(json.get("apparentColor"), apparentColor); - } - - if(json.get("importanceSampling").isObject()) { - JsonObject importanceSamplingObj = json.get("importanceSampling").object(); - importanceSampleChance = importanceSamplingObj.get("chance").doubleValue(DEFAULT_IMPORTANCE_SAMPLE_CHANCE); - importanceSampleRadius = importanceSamplingObj.get("radius").doubleValue(DEFAULT_IMPORTANCE_SAMPLE_RADIUS); - } - + color.set(ColorUtil.jsonToRGB(json.get("color").asObject())); drawTexture = json.get("drawTexture").boolValue(drawTexture); - + useFlatTexture = json.get("useFlatTexture").boolValue(useFlatTexture); initSun(); } @@ -547,14 +300,6 @@ public Vector3 getColor() { return color; } - public Vector3 getEmittance() { - return emittance; - } - - public Vector3 getApparentColor() { - return apparentColor; - } - public void setDrawTexture(boolean value) { if (value != drawTexture) { drawTexture = value; @@ -562,21 +307,18 @@ public void setDrawTexture(boolean value) { } } - public boolean drawTexture() { + public boolean getDrawTexture() { return drawTexture; } - public double getImportanceSampleChance() { return importanceSampleChance; } - - public void setImportanceSampleChance(double d) { - importanceSampleChance = d; - scene.refresh(); + public void setUseFlatTexture(boolean value) { + if (value != useFlatTexture) { + useFlatTexture = value; + scene.refresh(); + } } - public double getImportanceSampleRadius() { return importanceSampleRadius; } - - public void setImportanceSampleRadius(double d) { - importanceSampleRadius = d; - scene.refresh(); + public boolean getUseFlatTexture() { + return useFlatTexture; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ContinuousFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ContinuousFogVolume.java new file mode 100644 index 0000000000..c78e2e9fe5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ContinuousFogVolume.java @@ -0,0 +1,11 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +/** + * A {@link se.llbit.chunky.renderer.scene.volumetricfog.FogVolume} that has no bounds. + */ +public abstract class ContinuousFogVolume extends FogVolume { + @Override + public boolean isDiscrete() { + return false; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/CuboidFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/CuboidFogVolume.java new file mode 100644 index 0000000000..29bce286f8 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/CuboidFogVolume.java @@ -0,0 +1,385 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import it.unimi.dsi.fastutil.doubles.DoubleDoubleImmutablePair; +import javafx.beans.value.ChangeListener; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.world.Material; +import se.llbit.json.JsonObject; +import se.llbit.math.*; +import se.llbit.math.primitive.Primitive; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Random; + +public class CuboidFogVolume extends DiscreteFogVolume { + private static final double DEFAULT_XMIN = -10; + private static final double DEFAULT_XMAX = 10; + private static final double DEFAULT_YMIN = 100; + private static final double DEFAULT_YMAX = 120; + private static final double DEFAULT_ZMIN = -10; + private static final double DEFAULT_ZMAX = 10; + + private double xmin = DEFAULT_XMIN; + private double xmax = DEFAULT_XMAX; + private double ymin = DEFAULT_YMIN; + private double ymax = DEFAULT_YMAX; + private double zmin = DEFAULT_ZMIN; + private double zmax = DEFAULT_ZMAX; + + private final AABB bounds = new AABB( + this.xmin, + this.xmax, + this.ymin, + this.ymax, + this.zmin, + this.zmax + ); + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double distance; + double distanceLimit; + + DoubleDoubleImmutablePair intersectionDistance = this.bounds.intersectionDistance(ray); + if (Double.isNaN(intersectionDistance.leftDouble())) { + return false; + } + + double t0 = intersectionDistance.leftDouble(); + double t1 = intersectionDistance.rightDouble(); + + if (t1 < 0) { + return false; + } else if (t0 < 0) { + distance = 0; + distanceLimit = t1; + } else if (t0 > intersectionRecord.distance) { + return false; + } else { + distance = t0; + distanceLimit = t1; + } + + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + for (int i = 0; i < this.noiseConfig.marchSteps; i++) { + double dist = Material.fogDistance(this.material.volumeDensity, random); + if (dist + distance > intersectionRecord.distance || dist + distance > distanceLimit) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + if (noiseConfig.useNoise) { + o.scaleAdd(dist, ray.d); + float noiseValue = noiseConfig.calculate((float) o.x, (float) o.y, (float) o.z); + if (noiseConfig.cutoff) { + if (noiseValue < noiseConfig.lowerThreshold || noiseValue > noiseConfig.upperThreshold) { + distance += dist; + continue; + } + } else { + if (random.nextDouble(noiseConfig.lowerThreshold, noiseConfig.upperThreshold + Constants.EPSILON) > noiseValue) { + distance += dist; + continue; + } + } + } + intersectionRecord.distance = dist + distance; + intersectionRecord.material = this.material; + intersectionRecord.color.set(material.volumeColor.x, material.volumeColor.y, material.volumeColor.z, 1); + intersectionRecord.setVolumeIntersect(true); + return true; + } + return false; + } + + @Override + public AABB bounds() { + return this.bounds; + } + + @Override + public Collection primitives(Vector3 offset) { + Collection primitives = new LinkedList<>(); + this.bounds.xmin = this.xmin + offset.x; + this.bounds.xmax = this.xmax + offset.x; + this.bounds.ymin = this.ymin + offset.y; + this.bounds.ymax = this.ymax + offset.y; + this.bounds.zmin = this.zmin + offset.z; + this.bounds.zmax = this.zmax + offset.z; + primitives.add(this); + return primitives; + } + + @Override + public FogVolumeShape getShape() { + return FogVolumeShape.CUBOID; + } + + @Override + public JsonObject saveVolumeSpecificConfiguration() { + JsonObject json = new JsonObject(); + json.add("xmin", xmin); + json.add("xmax", xmax); + json.add("ymin", ymin); + json.add("ymax", ymax); + json.add("zmin", zmin); + json.add("zmax", zmax); + return json; + } + + @Override + public void loadVolumeSpecificConfiguration(JsonObject json) { + xmin = json.get("xmin").doubleValue(xmin); + xmax = json.get("xmax").doubleValue(xmax); + ymin = json.get("ymin").doubleValue(ymin); + ymax = json.get("ymax").doubleValue(ymax); + zmin = json.get("zmin").doubleValue(zmin); + zmax = json.get("zmax").doubleValue(zmax); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + DoubleTextField x1 = new DoubleTextField(); + DoubleTextField y1 = new DoubleTextField(); + DoubleTextField z1 = new DoubleTextField(); + DoubleTextField x2 = new DoubleTextField(); + DoubleTextField y2 = new DoubleTextField(); + DoubleTextField z2 = new DoubleTextField(); + + x1.setTooltip(new Tooltip("X-coordinate (east/west) of first corner")); + y1.setTooltip(new Tooltip("Y-coordinate (up/down) of first corner")); + z1.setTooltip(new Tooltip("Z-coordinate (south/north) of first corner")); + x2.setTooltip(new Tooltip("X-coordinate (east/west) of second corner")); + y2.setTooltip(new Tooltip("Y-coordinate (up/down) of second corner")); + z2.setTooltip(new Tooltip("Z-coordinate (south/north) of second corner")); + + x1.valueProperty().setValue(this.bounds.xmin); + y1.valueProperty().setValue(this.bounds.ymin); + z1.valueProperty().setValue(this.bounds.zmin); + x2.valueProperty().setValue(this.bounds.xmax); + y2.valueProperty().setValue(this.bounds.ymax); + z2.valueProperty().setValue(this.bounds.zmax); + + ChangeListener x1Listener = (observable, oldValue, newValue) -> { + this.xmin = Math.min(newValue.doubleValue(), x2.valueProperty().doubleValue()); + this.xmax = Math.max(newValue.doubleValue(), x2.valueProperty().doubleValue()); + scene.buildFogVolumeBVH(); + }; + ChangeListener y1Listener = (observable, oldValue, newValue) -> { + this.ymin = Math.min(newValue.doubleValue(), y2.valueProperty().doubleValue()); + this.ymax = Math.max(newValue.doubleValue(), y2.valueProperty().doubleValue()); + scene.buildFogVolumeBVH(); + }; + ChangeListener z1Listener = (observable, oldValue, newValue) -> { + this.zmin = Math.min(newValue.doubleValue(), z2.valueProperty().doubleValue()); + this.zmax = Math.max(newValue.doubleValue(), z2.valueProperty().doubleValue()); + scene.buildFogVolumeBVH(); + }; + ChangeListener x2Listener = (observable, oldValue, newValue) -> { + this.xmin = Math.min(x1.valueProperty().doubleValue(), newValue.doubleValue()); + this.xmax = Math.max(x1.valueProperty().doubleValue(), newValue.doubleValue()); + scene.buildFogVolumeBVH(); + }; + ChangeListener y2Listener = (observable, oldValue, newValue) -> { + this.ymin = Math.min(y1.valueProperty().doubleValue(), newValue.doubleValue()); + this.ymax = Math.max(y1.valueProperty().doubleValue(), newValue.doubleValue()); + scene.buildFogVolumeBVH(); + }; + ChangeListener z2Listener = (observable, oldValue, newValue) -> { + this.zmin = Math.min(z1.valueProperty().doubleValue(), newValue.doubleValue()); + this.zmax = Math.max(z1.valueProperty().doubleValue(), newValue.doubleValue()); + scene.buildFogVolumeBVH(); + }; + + x1.valueProperty().addListener(x1Listener); + y1.valueProperty().addListener(y1Listener); + z1.valueProperty().addListener(z1Listener); + + x2.valueProperty().addListener(x2Listener); + y2.valueProperty().addListener(y2Listener); + z2.valueProperty().addListener(z2Listener); + + TextFieldLabelWrapper x1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z1Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper x2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper y2Text = new TextFieldLabelWrapper(); + TextFieldLabelWrapper z2Text = new TextFieldLabelWrapper(); + + x1Text.setTextField(x1); + y1Text.setTextField(y1); + z1Text.setTextField(z1); + x2Text.setTextField(x2); + y2Text.setTextField(y2); + z2Text.setTextField(z2); + + x1Text.setLabelText("x:"); + y1Text.setLabelText("y:"); + z1Text.setLabelText("z:"); + x2Text.setLabelText("x:"); + y2Text.setLabelText("y:"); + z2Text.setLabelText("z:"); + + Button pos1ToCamera = new Button(); + pos1ToCamera.setText("To camera"); + pos1ToCamera.setOnAction(event -> { + Vector3 cameraPosition = scene.camera().getPosition(); + this.xmin = Math.min(cameraPosition.x, x2.valueProperty().doubleValue()); + this.xmax = Math.max(cameraPosition.y, x2.valueProperty().doubleValue()); + + this.ymin = Math.min(cameraPosition.y, y2.valueProperty().doubleValue()); + this.ymax = Math.max(cameraPosition.y, y2.valueProperty().doubleValue()); + + this.zmin = Math.min(cameraPosition.z, z2.valueProperty().doubleValue()); + this.zmax = Math.max(cameraPosition.z, z2.valueProperty().doubleValue()); + + x1.valueProperty().removeListener(x1Listener); + y1.valueProperty().removeListener(y1Listener); + z1.valueProperty().removeListener(z1Listener); + + x1.valueProperty().setValue(cameraPosition.x); + y1.valueProperty().setValue(cameraPosition.y); + z1.valueProperty().setValue(cameraPosition.z); + + x1.valueProperty().addListener(x1Listener); + y1.valueProperty().addListener(y1Listener); + z1.valueProperty().addListener(z1Listener); + scene.buildFogVolumeBVH(); + }); + + Button pos1ToTarget = new Button(); + pos1ToTarget.setText("To target"); + pos1ToTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + this.xmin = Math.min(targetPosition.x, x2.valueProperty().doubleValue()); + this.xmax = Math.max(targetPosition.y, x2.valueProperty().doubleValue()); + + this.ymin = Math.min(targetPosition.y, y2.valueProperty().doubleValue()); + this.ymax = Math.max(targetPosition.y, y2.valueProperty().doubleValue()); + + this.zmin = Math.min(targetPosition.z, z2.valueProperty().doubleValue()); + this.zmax = Math.max(targetPosition.z, z2.valueProperty().doubleValue()); + + x1.valueProperty().removeListener(x1Listener); + y1.valueProperty().removeListener(y1Listener); + z1.valueProperty().removeListener(z1Listener); + + x1.valueProperty().setValue(targetPosition.x); + y1.valueProperty().setValue(targetPosition.y); + z1.valueProperty().setValue(targetPosition.z); + + x1.valueProperty().addListener(x1Listener); + y1.valueProperty().addListener(y1Listener); + z1.valueProperty().addListener(z1Listener); + scene.buildFogVolumeBVH(); + } + }); + + Button pos2ToCamera = new Button(); + pos2ToCamera.setText("To camera"); + pos2ToCamera.setOnAction(event -> { + Vector3 cameraPosition = scene.camera().getPosition(); + this.xmin = Math.min(x1.valueProperty().doubleValue(), cameraPosition.x); + this.xmax = Math.max(x1.valueProperty().doubleValue(), cameraPosition.x); + + this.ymin = Math.min(y1.valueProperty().doubleValue(), cameraPosition.y); + this.ymax = Math.max(y1.valueProperty().doubleValue(), cameraPosition.y); + + this.zmin = Math.min(z1.valueProperty().doubleValue(), cameraPosition.z); + this.zmax = Math.max(z1.valueProperty().doubleValue(), cameraPosition.z); + + x2.valueProperty().removeListener(x2Listener); + y2.valueProperty().removeListener(y2Listener); + z2.valueProperty().removeListener(z2Listener); + + x2.valueProperty().setValue(cameraPosition.x); + y2.valueProperty().setValue(cameraPosition.y); + z2.valueProperty().setValue(cameraPosition.z); + + x2.valueProperty().addListener(x2Listener); + y2.valueProperty().addListener(y2Listener); + z2.valueProperty().addListener(z2Listener); + scene.buildFogVolumeBVH(); + }); + + Button pos2ToTarget = new Button(); + pos2ToTarget.setText("To target"); + pos2ToTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + this.xmin = Math.min(x1.valueProperty().doubleValue(), targetPosition.x); + this.xmax = Math.max(x1.valueProperty().doubleValue(), targetPosition.x); + + this.ymin = Math.min(y1.valueProperty().doubleValue(), targetPosition.y); + this.ymax = Math.max(y1.valueProperty().doubleValue(), targetPosition.y); + + this.zmin = Math.min(z1.valueProperty().doubleValue(), targetPosition.z); + this.zmax = Math.max(z1.valueProperty().doubleValue(), targetPosition.z); + + x2.valueProperty().removeListener(x2Listener); + y2.valueProperty().removeListener(y2Listener); + z2.valueProperty().removeListener(z2Listener); + + x2.valueProperty().setValue(targetPosition.x); + y2.valueProperty().setValue(targetPosition.y); + z2.valueProperty().setValue(targetPosition.z); + + x2.valueProperty().addListener(x2Listener); + y2.valueProperty().addListener(y2Listener); + z2.valueProperty().addListener(z2Listener); + scene.buildFogVolumeBVH(); + } + }); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + GridPane gridPane1 = new GridPane(); + gridPane1.setHgap(6); + gridPane1.getColumnConstraints().addAll( + labelConstraints, + posFieldConstraints, + posFieldConstraints, + posFieldConstraints + ); + gridPane1.addRow(0, new Label("Corner 1:"), x1Text, y1Text, z1Text); + + HBox hBox1 = new HBox(); + hBox1.setSpacing(10); + hBox1.getChildren().addAll(pos1ToCamera, pos1ToTarget); + + GridPane gridPane2 = new GridPane(); + gridPane2.setHgap(6); + gridPane2.getColumnConstraints().addAll( + labelConstraints, + posFieldConstraints, + posFieldConstraints, + posFieldConstraints + ); + gridPane2.addRow(1, new Label("Corner 2:"), x2Text, y2Text, z2Text); + + HBox hBox2 = new HBox(); + hBox2.setSpacing(10); + hBox2.getChildren().addAll(pos2ToCamera, pos2ToTarget); + + VBox noiseControls = this.noiseConfig.getControls(parent); + + return new VBox(6, gridPane1, hBox1, gridPane2, hBox2, new Separator(), noiseControls); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/DiscreteFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/DiscreteFogVolume.java new file mode 100644 index 0000000000..e1139de070 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/DiscreteFogVolume.java @@ -0,0 +1,14 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import se.llbit.chunky.renderer.HasPrimitives; +import se.llbit.math.primitive.Primitive; + +/** + * A {@link se.llbit.chunky.renderer.scene.volumetricfog.FogVolume} that has finite bounds. + */ +public abstract class DiscreteFogVolume extends FogVolume implements Primitive, HasPrimitives { + @Override + public boolean isDiscrete() { + return true; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ExponentialFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ExponentialFogVolume.java new file mode 100644 index 0000000000..151ec5a7d1 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/ExponentialFogVolume.java @@ -0,0 +1,210 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import javafx.scene.control.Separator; +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; +import se.llbit.math.Vector3i; + +public class ExponentialFogVolume extends ContinuousFogVolume { + public static final double DEFAULT_SCALE_HEIGHT = 20; + public static final double DEFAULT_Y_OFFSET = 0; + + private double scaleHeight = DEFAULT_SCALE_HEIGHT; + private double yOffset = DEFAULT_Y_OFFSET; + + private boolean useUpperBounds = false; + private boolean useLowerBounds = false; + private double upperBounds = 82; + private double lowerBounds = 42; + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double actualLowerBounds = (useLowerBounds) ? lowerBounds : Double.NEGATIVE_INFINITY; + double actualUpperBounds = (useUpperBounds) ? upperBounds : Double.POSITIVE_INFINITY; + + Vector3i origin = scene.getOrigin(); + + double tLower = (actualLowerBounds - ray.o.y - origin.y) / ray.d.y; + double tUpper = (actualUpperBounds - ray.o.y - origin.y) / ray.d.y; + + double distance; + double distanceLimit; + + if (ray.o.y < actualLowerBounds - origin.y) { + if (tLower < 0) { + return false; + } + distance = tLower; + distanceLimit = tUpper; + } else if (ray.o.y < actualUpperBounds - origin.y) { + distance = 0; + distanceLimit = (ray.d.y != 0) ? Math.max(tLower, tUpper) : Double.POSITIVE_INFINITY; + } else { + if (tUpper < 0) { + return false; + } + distance = tUpper; + distanceLimit = tLower; + } + if (distance > intersectionRecord.distance) { + return false; + } + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + for (int i = 0; i < this.noiseConfig.marchSteps; i++) { + // Amount of fog the ray should pass through before being scattered + // Sampled from an exponential distribution + double fogPenetrated = -FastMath.log(1 - random.nextDouble()); + double expHeightDiff = fogPenetrated * ray.d.y / (scaleHeight * this.material.volumeDensity); + double expYfHs = FastMath.exp(-(o.y + origin.y - yOffset) / scaleHeight) - expHeightDiff; + if (expYfHs <= 0) { + // The ray does not encounter enough fog to be scattered - no intersection. + return false; + } + double yf = -FastMath.log(expYfHs) * scaleHeight + yOffset; + double dist = (yf - (o.y + origin.y)) / ray.d.y; + if (dist + distance > intersectionRecord.distance || dist + distance > distanceLimit) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + + if (noiseConfig.useNoise) { + o.scaleAdd(dist, ray.d); + float noiseValue = noiseConfig.calculate((float) o.x, (float) o.y, (float) o.z); + if (noiseConfig.cutoff) { + if (noiseValue < noiseConfig.lowerThreshold || noiseValue > noiseConfig.upperThreshold) { + distance += dist; + continue; + } + } else { + if (random.nextDouble(noiseConfig.lowerThreshold, noiseConfig.upperThreshold + Constants.EPSILON) > noiseValue) { + distance += dist; + continue; + } + } + } + intersectionRecord.distance = dist + distance; + intersectionRecord.material = this.material; + intersectionRecord.color.set(material.volumeColor.x, material.volumeColor.y, material.volumeColor.z, 1); + intersectionRecord.setVolumeIntersect(true); + return true; + } + return false; + } + + @Override + public FogVolumeShape getShape() { + return FogVolumeShape.EXPONENTIAL; + } + + @Override + public JsonObject saveVolumeSpecificConfiguration() { + JsonObject json = new JsonObject(); + json.add("scaleHeight", scaleHeight); + json.add("yOffset", yOffset); + json.add("upperBounds", upperBounds); + json.add("lowerBounds", lowerBounds); + json.add("useUpperBounds", useUpperBounds); + json.add("useLowerBounds", useLowerBounds); + return json; + } + + @Override + public void loadVolumeSpecificConfiguration(JsonObject json) { + scaleHeight = json.get("scaleHeight").doubleValue(scaleHeight); + yOffset = json.get("yOffset").doubleValue(yOffset); + upperBounds = json.get("upperBounds").doubleValue(upperBounds); + lowerBounds = json.get("lowerBounds").doubleValue(lowerBounds); + useUpperBounds = json.get("useUpperBounds").boolValue(useUpperBounds); + useLowerBounds = json.get("useLowerBounds").boolValue(useLowerBounds); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + DoubleAdjuster scaleHeight = new DoubleAdjuster(); + scaleHeight.setName("Height scale"); + scaleHeight.setTooltip("Scales the vertical distribution of the fog"); + scaleHeight.setRange(1, 50); + scaleHeight.set(this.scaleHeight); + scaleHeight.onValueChange(value -> { + this.scaleHeight = value; + scene.refresh(); + }); + + DoubleAdjuster yOffset = new DoubleAdjuster(); + yOffset.setName("Y-offset"); + yOffset.setTooltip("Y-offset (altitude) of the distribution"); + yOffset.setRange(-100, 100); + yOffset.set(this.yOffset); + yOffset.onValueChange(value -> { + this.yOffset = value; + scene.refresh(); + }); + + ToggleSwitch useLowerBoundsSwitch = new ToggleSwitch("Use lower bound"); + useLowerBoundsSwitch.setSelected(this.useLowerBounds); + useLowerBoundsSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useLowerBounds = newValue; + scene.refresh(); + }); + + ToggleSwitch useUpperBoundsSwitch = new ToggleSwitch("Use upper bound"); + useUpperBoundsSwitch.setSelected(this.useUpperBounds); + useUpperBoundsSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useUpperBounds = newValue; + scene.refresh(); + }); + + DoubleAdjuster lowerBoundsAdjuster = new DoubleAdjuster(); + DoubleAdjuster upperBoundsAdjuster = new DoubleAdjuster(); + + lowerBoundsAdjuster.setName("Lower bound"); + lowerBoundsAdjuster.setRange(-64, 320); + lowerBoundsAdjuster.set(this.lowerBounds); + lowerBoundsAdjuster.onValueChange(value -> { + if (value < upperBoundsAdjuster.get()) { + lowerBoundsAdjuster.setInvalid(false); + upperBoundsAdjuster.setInvalid(false); + this.lowerBounds = value; + this.upperBounds = upperBoundsAdjuster.get(); + scene.refresh(); + } else { + lowerBoundsAdjuster.setInvalid(true); + upperBoundsAdjuster.setInvalid(true); + } + }); + + upperBoundsAdjuster.setName("Upper bound"); + upperBoundsAdjuster.setRange(-64, 320); + upperBoundsAdjuster.set(this.upperBounds); + upperBoundsAdjuster.onValueChange(value -> { + if (value > lowerBoundsAdjuster.get()) { + lowerBoundsAdjuster.setInvalid(false); + upperBoundsAdjuster.setInvalid(false); + this.upperBounds = value; + this.lowerBounds = lowerBoundsAdjuster.get(); + scene.refresh(); + } else { + lowerBoundsAdjuster.setInvalid(true); + upperBoundsAdjuster.setInvalid(true); + } + }); + + VBox noiseControls = this.noiseConfig.getControls(parent); + + return new VBox(6, scaleHeight, yOffset, lowerBoundsAdjuster, upperBoundsAdjuster, useLowerBoundsSwitch, useUpperBoundsSwitch, new Separator(), noiseControls); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolume.java new file mode 100644 index 0000000000..e56151ec78 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolume.java @@ -0,0 +1,184 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import javafx.scene.layout.VBox; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.SimplexNoiseConfig; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.material.TextureMaterial; +import se.llbit.json.JsonObject; +import se.llbit.math.ColorUtil; +import se.llbit.math.Intersectable; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; + +/** + * A volume of volumetric fog. + */ +public abstract class FogVolume implements HasControls, Configurable, Intersectable { + + /** + * Custom Simplex Noise Config for volumetric fog. + */ + public static class NoiseConfig extends SimplexNoiseConfig { + public boolean useNoise = false; + public int marchSteps = 10; + public double lowerThreshold = -10; + public double upperThreshold = -0.5; + public boolean cutoff = true; + + @Override + public JsonObject toJson() { + JsonObject json = super.toJson(); + json.add("useNoise", useNoise); + json.add("marchSteps", marchSteps); + json.add("lowerThreshold", lowerThreshold); + json.add("upperThreshold", upperThreshold); + json.add("cutoff", cutoff); + return json; + } + + @Override + public void fromJson(JsonObject json) { + super.fromJson(json); + useNoise = json.get("useNoise").boolValue(useNoise); + marchSteps = json.get("marchSteps").intValue(marchSteps); + lowerThreshold = json.get("lowerThreshold").doubleValue(lowerThreshold); + upperThreshold = json.get("upperThreshold").doubleValue(upperThreshold); + cutoff = json.get("cutoff").boolValue(cutoff); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + ToggleSwitch useNoiseSwitch = new ToggleSwitch("Use noise (experimental)"); + IntegerAdjuster marchStepsAdjuster = new IntegerAdjuster(); + DoubleAdjuster lowerThresholdAdjuster = new DoubleAdjuster(); + DoubleAdjuster upperThresholdAdjuster = new DoubleAdjuster(); + ToggleSwitch cutoffSwitch = new ToggleSwitch("Cutoff"); + VBox noiseControls = super.getControls(parent); + noiseControls.setDisable(!this.useNoise); + + useNoiseSwitch.setSelected(this.useNoise); + useNoiseSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useNoise = newValue; + noiseControls.setDisable(!newValue); + marchStepsAdjuster.setDisable(!newValue); + lowerThresholdAdjuster.setDisable(!newValue); + upperThresholdAdjuster.setDisable(!newValue); + cutoffSwitch.setDisable(!newValue); + scene.refresh(); + }); + + marchStepsAdjuster.setName("March steps"); + marchStepsAdjuster.setRange(1, 100); + marchStepsAdjuster.clampMin(); + marchStepsAdjuster.set(this.marchSteps); + marchStepsAdjuster.onValueChange(value -> { + this.marchSteps = value; + scene.refresh(); + }); + marchStepsAdjuster.setDisable(!useNoise); + + lowerThresholdAdjuster.setName("Lower threshold"); + lowerThresholdAdjuster.setRange(-10, 10); + lowerThresholdAdjuster.set(this.lowerThreshold); + lowerThresholdAdjuster.onValueChange(value -> { + if (value < upperThresholdAdjuster.get()) { + lowerThresholdAdjuster.setInvalid(false); + upperThresholdAdjuster.setInvalid(false); + this.lowerThreshold = value; + this.upperThreshold = upperThresholdAdjuster.get(); + scene.refresh(); + } else { + lowerThresholdAdjuster.setInvalid(true); + upperThresholdAdjuster.setInvalid(true); + } + }); + lowerThresholdAdjuster.setDisable(!this.useNoise); + + upperThresholdAdjuster.setName("Upper threshold"); + upperThresholdAdjuster.setRange(-10, 10); + upperThresholdAdjuster.set(this.upperThreshold); + upperThresholdAdjuster.onValueChange(value -> { + if (value > lowerThresholdAdjuster.get()) { + lowerThresholdAdjuster.setInvalid(false); + upperThresholdAdjuster.setInvalid(false); + this.upperThreshold = value; + this.lowerThreshold = lowerThresholdAdjuster.get(); + scene.refresh(); + } else { + lowerThresholdAdjuster.setInvalid(true); + upperThresholdAdjuster.setInvalid(true); + } + }); + upperThresholdAdjuster.setDisable(!this.useNoise); + + cutoffSwitch.setSelected(this.cutoff); + cutoffSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.cutoff = newValue; + scene.refresh(); + }); + cutoffSwitch.setDisable(!useNoise); + + return new VBox(6, useNoiseSwitch, noiseControls, marchStepsAdjuster, lowerThresholdAdjuster, upperThresholdAdjuster, cutoffSwitch); + } + } + + protected final Material material = new TextureMaterial(Texture.EMPTY_TEXTURE); + protected final NoiseConfig noiseConfig = new NoiseConfig(); + + public abstract boolean isDiscrete(); + + public abstract FogVolumeShape getShape(); + + public Material getMaterial() { + return this.material; + } + + protected abstract JsonObject saveVolumeSpecificConfiguration(); + + protected abstract void loadVolumeSpecificConfiguration(JsonObject json); + + protected JsonObject saveMaterialProperties() { + JsonObject json = new JsonObject(); + json.add("density", material.volumeDensity); + json.add("anisotropy", material.volumeAnisotropy); + json.add("emittance", material.volumeEmittance); + json.add("color", ColorUtil.rgbToJson(material.volumeColor)); + return json; + } + + protected void loadMaterialProperties(JsonObject json) { + material.volumeDensity = json.get("density").floatValue(material.volumeDensity); + material.volumeAnisotropy = json.get("anisotropy").floatValue(material.volumeAnisotropy); + material.volumeEmittance = json.get("emittance").floatValue(material.volumeEmittance); + material.volumeColor.set(ColorUtil.jsonToRGB(json.get("color").asObject(), material.volumeColor)); + } + + @Override + public void fromJson(JsonObject json) { + loadVolumeSpecificConfiguration(json.get("volumeSpecificConfiguration").asObject()); + loadMaterialProperties(json.get("materialProperties").asObject()); + noiseConfig.fromJson(json.get("noiseProperties").asObject()); + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("shape", getShape().name()); + json.add("volumeSpecificConfiguration", saveVolumeSpecificConfiguration()); + json.add("materialProperties", saveMaterialProperties()); + json.add("noiseProperties", noiseConfig.toJson()); + return json; + } + + @Override + public void reset() { + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeShape.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeShape.java new file mode 100644 index 0000000000..ca8b6c80fb --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeShape.java @@ -0,0 +1,15 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +public enum FogVolumeShape { + EXPONENTIAL("Exponential"), LAYER("Layer"), SPHERE("Sphere"), CUBOID("Cuboid"); + + private final String displayName; + + FogVolumeShape(String displayName) { + this.displayName = displayName; + } + + public String toString() { + return this.displayName; + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeStore.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeStore.java new file mode 100644 index 0000000000..72303c19a0 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/FogVolumeStore.java @@ -0,0 +1,186 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import java.util.Objects; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.json.JsonArray; +import se.llbit.json.JsonObject; +import se.llbit.json.JsonValue; +import se.llbit.math.*; +import se.llbit.math.bvh.BVH; +import se.llbit.util.Configurable; +import se.llbit.util.JsonSerializable; +import se.llbit.util.TaskTracker; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Stream; + +/** + * Wrapper for fog volumes. + */ +public class FogVolumeStore implements Intersectable, JsonSerializable, Configurable { + private static final float DEFAULT_DENSITY = 0.1f; + + private final ArrayList continuousFogVolumes = new ArrayList<>(0); + private final ArrayList discreteFogVolumes = new ArrayList<>(0); + private BVH fogVolumeBVH = BVH.EMPTY; + + public FogVolumeStore() { + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + JsonArray continuousFogVolumesArray = new JsonArray(); + continuousFogVolumes.stream() + .map(FogVolume::toJson) + .filter(Objects::nonNull) + .forEach(continuousFogVolumesArray::add); + json.add("continuousFogVolumes", continuousFogVolumesArray); + JsonArray discreteFogVolumesArray = new JsonArray(); + discreteFogVolumes.stream() + .map(FogVolume::toJson) + .filter(Objects::nonNull) + .forEach(discreteFogVolumesArray::add); + json.add("discreteFogVolumes", discreteFogVolumesArray); + return json; + } + + @Override + public void fromJson(JsonObject json) { + clear(); + JsonArray continuousFogVolumesArray = json.get("continuousFogVolumes").array(); + for (JsonValue element : continuousFogVolumesArray) { + JsonObject fogVolumeJson = element.asObject(); + FogVolumeShape shape = FogVolumeShape.valueOf(fogVolumeJson.get("shape").stringValue("")); + FogVolume fogVolume = fromShape(shape); + if (fogVolume != null) { + fogVolume.fromJson(fogVolumeJson); + addVolume(fogVolume); + } + } + JsonArray discreteFogVolumesArray = json.get("discreteFogVolumes").array(); + for (JsonValue element : discreteFogVolumesArray) { + JsonObject fogVolumeJson = element.asObject(); + FogVolumeShape shape = FogVolumeShape.valueOf(fogVolumeJson.get("shape").stringValue("")); + FogVolume fogVolume = fromShape(shape); + if (fogVolume != null) { + fogVolume.fromJson(fogVolumeJson); + addVolume(fogVolume); + } + } + } + + public void copyState(FogVolumeStore other) { + this.continuousFogVolumes.clear(); + this.continuousFogVolumes.addAll(other.continuousFogVolumes); + this.discreteFogVolumes.clear(); + this.discreteFogVolumes.addAll(other.discreteFogVolumes); + this.fogVolumeBVH = other.fogVolumeBVH; + } + + public List listFogVolumes() { + return Stream.concat(this.continuousFogVolumes.stream(), this.discreteFogVolumes.stream()).toList(); + } + + private FogVolume fromShape(FogVolumeShape shape) { + switch (shape) { + case EXPONENTIAL: { + ExponentialFogVolume fogVolume = new ExponentialFogVolume(); + fogVolume.material.volumeDensity = DEFAULT_DENSITY; + return fogVolume; + } + case LAYER: { + LayerFogVolume fogVolume = new LayerFogVolume(); + fogVolume.material.volumeDensity = DEFAULT_DENSITY; + return fogVolume; + } + case CUBOID: { + CuboidFogVolume fogVolume = new CuboidFogVolume(); + fogVolume.material.volumeDensity = DEFAULT_DENSITY; + return fogVolume; + } + case SPHERE: { + SphericalFogVolume fogVolume = new SphericalFogVolume(); + fogVolume.material.volumeDensity = DEFAULT_DENSITY; + return fogVolume; + } + default: + return null; + } + } + + /** + * Adds a fog volume to the fog volume store. + * @param shape The shape of the fog volume to add + * @return Whether to rebuild the fog volume BVH + */ + public boolean addVolume(FogVolumeShape shape) { + FogVolume fogVolume = fromShape(shape); + if (fogVolume != null) { + return addVolume(fogVolume); + } + return false; + } + + private boolean addVolume(FogVolume fogVolume) { + if (fogVolume.isDiscrete()) { + discreteFogVolumes.add((DiscreteFogVolume) fogVolume); + return true; + } else { + continuousFogVolumes.add((ContinuousFogVolume) fogVolume); + return false; + } + } + + public boolean removeVolume(int index) { + if (index < continuousFogVolumes.size()) { + continuousFogVolumes.remove(index); + return false; + } else { + discreteFogVolumes.remove(index - continuousFogVolumes.size()); + return true; + } + } + + public void clear() { + this.continuousFogVolumes.clear(); + this.discreteFogVolumes.clear(); + } + + @Override + public void reset() { + clear(); + } + + public void finalizeLoading() { + this.continuousFogVolumes.trimToSize(); + this.discreteFogVolumes.trimToSize(); + } + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + boolean hit = fogVolumeBVH.closestIntersection(ray, intersectionRecord, scene, random); + + IntersectionRecord intersectionTest = new IntersectionRecord(); + for (ContinuousFogVolume fogVolume : continuousFogVolumes) { + if (fogVolume.closestIntersection(ray, intersectionTest, scene, random) && intersectionTest.distance < intersectionRecord.distance) { + hit = true; + intersectionRecord.distance = intersectionTest.distance; + intersectionRecord.material = intersectionTest.material; + intersectionRecord.color.set(intersectionTest.color); + intersectionRecord.flags = intersectionTest.flags; + } + intersectionTest.reset(); + } + + return hit; + } + + public void buildBvh(TaskTracker.Task task, Vector3i origin) { + Vector3 worldOffset = new Vector3(-origin.x, -origin.y, -origin.z); + fogVolumeBVH = BVH.Factory.create("SAH_MA", Collections.unmodifiableList(discreteFogVolumes), worldOffset, task); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/LayerFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/LayerFogVolume.java new file mode 100644 index 0000000000..dffca43aa4 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/LayerFogVolume.java @@ -0,0 +1,210 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import javafx.scene.control.Separator; +import javafx.scene.layout.VBox; +import org.apache.commons.math3.util.FastMath; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; +import se.llbit.math.Vector3i; + +public class LayerFogVolume extends ContinuousFogVolume { + public static final double DEFAULT_Y_OFFSET = 62; + public static final double DEFAULT_BREADTH = 5; + + private double layerBreadth = DEFAULT_BREADTH; + private double yOffset = DEFAULT_Y_OFFSET; + private boolean useUpperBounds = false; + private boolean useLowerBounds = false; + private double upperBounds = 82; + private double lowerBounds = 42; + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double actualLowerBounds = (useLowerBounds) ? lowerBounds : Double.NEGATIVE_INFINITY; + double actualUpperBounds = (useUpperBounds) ? upperBounds : Double.POSITIVE_INFINITY; + + Vector3i origin = scene.getOrigin(); + + double tLower = (actualLowerBounds - ray.o.y - origin.y) / ray.d.y; + double tUpper = (actualUpperBounds - ray.o.y - origin.y) / ray.d.y; + + double distance; + double distanceLimit; + + if (ray.o.y < actualLowerBounds - origin.y) { + if (tLower < 0) { + return false; + } + distance = tLower; + distanceLimit = tUpper; + } else if (ray.o.y < actualUpperBounds - origin.y) { + distance = 0; + distanceLimit = (ray.d.y != 0) ? Math.max(tLower, tUpper) : Double.POSITIVE_INFINITY; + } else { + if (tUpper < 0) { + return false; + } + distance = tUpper; + distanceLimit = tLower; + } + if (distance > intersectionRecord.distance) { + return false; + } + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + for (int i = 0; i < this.noiseConfig.marchSteps; i++) { + // Amount of fog the ray should pass through before being scattered + // Sampled from an exponential distribution + double fogPenetrated = -FastMath.log(1 - random.nextDouble()); + double atanHeightDiff = fogPenetrated * ray.d.y / (layerBreadth * this.material.volumeDensity); + double atanYfHs = Math.atan((o.y + origin.y - yOffset) / layerBreadth) + atanHeightDiff; + if (Math.PI/2 - Math.abs(atanYfHs) <= 0) { + // The ray does not encounter enough fog to be scattered - no intersection. + return false; + } + double yf = Math.tan(atanYfHs) * layerBreadth + yOffset; + double dist = (yf - (o.y + origin.y)) / ray.d.y; + if (dist + distance > intersectionRecord.distance || dist + distance > distanceLimit) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + + if (noiseConfig.useNoise) { + o.scaleAdd(dist, ray.d); + float noiseValue = noiseConfig.calculate((float) o.x, (float) o.y, (float) o.z); + if (noiseConfig.cutoff) { + if (noiseValue < noiseConfig.lowerThreshold || noiseValue > noiseConfig.upperThreshold) { + distance += dist; + continue; + } + } else { + if (random.nextDouble(noiseConfig.lowerThreshold, noiseConfig.upperThreshold + Constants.EPSILON) > noiseValue) { + distance += dist; + continue; + } + } + } + intersectionRecord.distance = dist + distance; + intersectionRecord.material = this.material; + intersectionRecord.color.set(material.volumeColor.x, material.volumeColor.y, material.volumeColor.z, 1); + intersectionRecord.setVolumeIntersect(true); + return true; + } + return false; + } + + @Override + public FogVolumeShape getShape() { + return FogVolumeShape.LAYER; + } + + @Override + public JsonObject saveVolumeSpecificConfiguration() { + JsonObject json = new JsonObject(); + json.add("layerBreadth", layerBreadth); + json.add("yOffset", yOffset); + json.add("upperBounds", upperBounds); + json.add("lowerBounds", lowerBounds); + json.add("useUpperBounds", useUpperBounds); + json.add("useLowerBounds", useLowerBounds); + return json; + } + + @Override + public void loadVolumeSpecificConfiguration(JsonObject json) { + layerBreadth = json.get("layerBreadth").doubleValue(layerBreadth); + yOffset = json.get("yOffset").doubleValue(yOffset); + upperBounds = json.get("upperBounds").doubleValue(upperBounds); + lowerBounds = json.get("lowerBounds").doubleValue(lowerBounds); + useUpperBounds = json.get("useUpperBounds").boolValue(useUpperBounds); + useLowerBounds = json.get("useLowerBounds").boolValue(useLowerBounds); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + DoubleAdjuster layerBreadth = new DoubleAdjuster(); + layerBreadth.setName("Layer thickness"); + layerBreadth.setTooltip("Scales the vertical distribution of the fog"); + layerBreadth.setRange(0.001, 100); + layerBreadth.set(this.layerBreadth); + layerBreadth.clampMin(); + layerBreadth.onValueChange(value -> { + this.layerBreadth = value; + scene.refresh(); + }); + + DoubleAdjuster yOffset = new DoubleAdjuster(); + yOffset.setName("Layer altitude"); + yOffset.setTooltip("Y-coordinate (altitude) of the fog layer"); + yOffset.setRange(-64, 320); + yOffset.set(this.yOffset); + yOffset.onValueChange(value -> { + this.yOffset = value; + scene.refresh(); + }); + + ToggleSwitch useLowerBoundsSwitch = new ToggleSwitch("Use lower bound"); + useLowerBoundsSwitch.setSelected(this.useLowerBounds); + useLowerBoundsSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useLowerBounds = newValue; + scene.refresh(); + }); + + ToggleSwitch useUpperBoundsSwitch = new ToggleSwitch("Use upper bound"); + useUpperBoundsSwitch.setSelected(this.useUpperBounds); + useUpperBoundsSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> { + this.useUpperBounds = newValue; + scene.refresh(); + }); + + DoubleAdjuster lowerBoundsAdjuster = new DoubleAdjuster(); + DoubleAdjuster upperBoundsAdjuster = new DoubleAdjuster(); + + lowerBoundsAdjuster.setName("Lower bound"); + lowerBoundsAdjuster.setRange(-64, 320); + lowerBoundsAdjuster.set(this.lowerBounds); + lowerBoundsAdjuster.onValueChange(value -> { + if (value < upperBoundsAdjuster.get()) { + lowerBoundsAdjuster.setInvalid(false); + upperBoundsAdjuster.setInvalid(false); + this.lowerBounds = value; + this.upperBounds = upperBoundsAdjuster.get(); + scene.refresh(); + } else { + lowerBoundsAdjuster.setInvalid(true); + upperBoundsAdjuster.setInvalid(true); + } + }); + + upperBoundsAdjuster.setName("Upper bound"); + upperBoundsAdjuster.setRange(-64, 320); + upperBoundsAdjuster.set(this.upperBounds); + upperBoundsAdjuster.onValueChange(value -> { + if (value > lowerBoundsAdjuster.get()) { + lowerBoundsAdjuster.setInvalid(false); + upperBoundsAdjuster.setInvalid(false); + this.upperBounds = value; + this.lowerBounds = lowerBoundsAdjuster.get(); + scene.refresh(); + } else { + lowerBoundsAdjuster.setInvalid(true); + upperBoundsAdjuster.setInvalid(true); + } + }); + + VBox noiseControls = this.noiseConfig.getControls(parent); + + return new VBox(6, layerBreadth, yOffset, lowerBoundsAdjuster, upperBoundsAdjuster, useLowerBoundsSwitch, useUpperBoundsSwitch, new Separator(), noiseControls); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/SphericalFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/SphericalFogVolume.java new file mode 100644 index 0000000000..014d899066 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/volumetricfog/SphericalFogVolume.java @@ -0,0 +1,274 @@ +package se.llbit.chunky.renderer.scene.volumetricfog; + +import javafx.beans.value.ChangeListener; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.world.Material; +import se.llbit.json.JsonObject; +import se.llbit.math.*; +import se.llbit.math.primitive.Primitive; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Random; + +public class SphericalFogVolume extends DiscreteFogVolume { + private static final double DEFAULT_X = 0; + private static final double DEFAULT_Y = 100; + private static final double DEFAULT_Z = 0; + private static final double DEFAULT_RADIUS = 10; + + private final Vector3 position = new Vector3(DEFAULT_X, DEFAULT_Y, DEFAULT_Z); + private final Vector3 center = new Vector3(); + private double radius = DEFAULT_RADIUS; + + @Override + public AABB bounds() { + return new AABB( + center.x - radius, + center.x + radius, + center.y - radius, + center.y + radius, + center.z - radius, + center.z + radius + ); + } + + @Override + public Collection primitives(Vector3 offset) { + Collection primitives = new LinkedList<>(); + this.center.set(this.position.rAdd(offset)); + primitives.add(this); + return primitives; + } + + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double t0; + double t1; + + double radiusSquared = radius * radius; + + Vector3 l = center.rSub(ray.o); + double tca = ray.d.dot(l); + if (tca < 0.0 && l.length() > radius) { + return false; + } + + double d2 = l.dot(l) - tca * tca; + if (d2 > radiusSquared) { + return false; + } + double thc = FastMath.sqrt(radiusSquared - d2); + t0 = tca - thc; + t1 = tca + thc; + + if (t0 > t1) { + double tmp = t0; + t0 = t1; + t1 = tmp; + } + + double distance; + double distanceLimit; + + if (t1 < 0) { + return false; + } else if (t0 < 0) { + distance = 0; + distanceLimit = t1; + } else if (t0 > intersectionRecord.distance) { + return false; + } else { + distance = t0; + distanceLimit = t1; + } + + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + for (int i = 0; i < this.noiseConfig.marchSteps; i++) { + double dist = Material.fogDistance(this.material.volumeDensity, random); + if (dist + distance > intersectionRecord.distance || dist + distance > distanceLimit) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + if (noiseConfig.useNoise) { + o.scaleAdd(dist, ray.d); + float noiseValue = noiseConfig.calculate((float) o.x, (float) o.y, (float) o.z); + if (noiseConfig.cutoff) { + if (noiseValue < noiseConfig.lowerThreshold || noiseValue > noiseConfig.upperThreshold) { + distance += dist; + continue; + } + } else { + if (random.nextDouble(noiseConfig.lowerThreshold, noiseConfig.upperThreshold + Constants.EPSILON) > noiseValue) { + distance += dist; + continue; + } + } + } + intersectionRecord.distance = dist + distance; + intersectionRecord.material = this.material; + intersectionRecord.color.set(material.volumeColor.x, material.volumeColor.y, material.volumeColor.z, 1); + intersectionRecord.setVolumeIntersect(true); + return true; + } + return false; + } + + @Override + public FogVolumeShape getShape() { + return FogVolumeShape.SPHERE; + } + + @Override + public JsonObject saveVolumeSpecificConfiguration() { + JsonObject json = new JsonObject(); + json.add("position", position.toJson()); + json.add("radius", radius); + return json; + } + + @Override + public void loadVolumeSpecificConfiguration(JsonObject json) { + position.fromJson(json.get("position").asObject()); + radius = json.get("radius").doubleValue(radius); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + DoubleTextField posX = new DoubleTextField(); + DoubleTextField posY = new DoubleTextField(); + DoubleTextField posZ = new DoubleTextField(); + + posX.setTooltip(new Tooltip("Sphere x-coordinate (east/west)")); + posY.setTooltip(new Tooltip("Sphere y-coordinate (up/down)")); + posZ.setTooltip(new Tooltip("Sphere z-coordinate (south/north)")); + + posX.valueProperty().setValue(this.position.x); + posY.valueProperty().setValue(this.position.y); + posZ.valueProperty().setValue(this.position.z); + + ChangeListener posXListener = (observable, oldValue, newValue) -> { + this.position.x = newValue.doubleValue(); + scene.buildFogVolumeBVH(); + }; + ChangeListener posYListener = (observable, oldValue, newValue) -> { + this.position.y = newValue.doubleValue(); + scene.buildFogVolumeBVH(); + }; + ChangeListener posZListener = (observable, oldValue, newValue) -> { + this.position.z = newValue.doubleValue(); + scene.buildFogVolumeBVH(); + }; + + posX.valueProperty().addListener(posXListener); + posY.valueProperty().addListener(posYListener); + posZ.valueProperty().addListener(posZListener); + + TextFieldLabelWrapper xText = new TextFieldLabelWrapper(); + TextFieldLabelWrapper yText = new TextFieldLabelWrapper(); + TextFieldLabelWrapper zText = new TextFieldLabelWrapper(); + + xText.setTextField(posX); + yText.setTextField(posY); + zText.setTextField(posZ); + + xText.setLabelText("x:"); + yText.setLabelText("y:"); + zText.setLabelText("z:"); + + Button toCamera = new Button(); + toCamera.setText("To camera"); + toCamera.setOnAction(event -> { + Vector3 cameraPosition = scene.camera().getPosition(); + this.position.x = cameraPosition.x; + this.position.y = cameraPosition.y; + this.position.z = cameraPosition.z; + + posX.valueProperty().removeListener(posXListener); + posY.valueProperty().removeListener(posYListener); + posZ.valueProperty().removeListener(posZListener); + + posX.valueProperty().setValue(cameraPosition.x); + posY.valueProperty().setValue(cameraPosition.y); + posZ.valueProperty().setValue(cameraPosition.z); + + posX.valueProperty().addListener(posXListener); + posY.valueProperty().addListener(posYListener); + posZ.valueProperty().addListener(posZListener); + scene.buildFogVolumeBVH(); + }); + + Button toTarget = new Button(); + toTarget.setText("To target"); + toTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + this.position.x = targetPosition.x; + this.position.y = targetPosition.y; + this.position.z = targetPosition.z; + + posX.valueProperty().removeListener(posXListener); + posY.valueProperty().removeListener(posYListener); + posZ.valueProperty().removeListener(posZListener); + + posX.valueProperty().setValue(targetPosition.x); + posY.valueProperty().setValue(targetPosition.y); + posZ.valueProperty().setValue(targetPosition.z); + + posX.valueProperty().addListener(posXListener); + posY.valueProperty().addListener(posYListener); + posZ.valueProperty().addListener(posZListener); + scene.buildFogVolumeBVH(); + } + }); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + GridPane gridPane = new GridPane(); + gridPane.setHgap(6); + gridPane.getColumnConstraints().addAll( + labelConstraints, + posFieldConstraints, + posFieldConstraints, + posFieldConstraints + ); + gridPane.addRow(0, new Label("Center:"), xText, yText, zText); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + hBox.getChildren().addAll(toCamera, toTarget); + + DoubleAdjuster radius = new DoubleAdjuster(); + radius.setName("Radius"); + radius.setTooltip("Radius of the sphere"); + radius.setRange(0.001, 100); + radius.set(this.radius); + radius.clampMin(); + radius.onValueChange(value -> { + this.radius = value; + scene.buildFogVolumeBVH(); + }); + + VBox noiseControls = this.noiseConfig.getControls(parent); + + return new VBox(6, gridPane, hBox, radius, new Separator(), noiseControls); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/LegacyWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/LegacyWaterShader.java new file mode 100644 index 0000000000..181e845461 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/LegacyWaterShader.java @@ -0,0 +1,145 @@ +/* Copyright (c) 2012-2021 Chunky contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.scene.watershading; + +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.*; + +public class LegacyWaterShader implements WaterShader { + private static final float[] normalMap; + private static final int normalMapW; + + private final Vector2 offset = new Vector2(); + private final Vector2 scale = new Vector2(1, 1); + + static { + // precompute normal map + Texture waterHeight = new Texture("water-height"); + normalMapW = waterHeight.getWidth(); + normalMap = new float[normalMapW*normalMapW*2]; + for (int u = 0; u < normalMapW; ++u) { + for (int v = 0; v < normalMapW; ++v) { + + float hx0 = (waterHeight.getColorWrapped(u, v) & 0xFF) / 255.f; + float hx1 = (waterHeight.getColorWrapped(u + 1, v) & 0xFF) / 255.f; + float hz0 = (waterHeight.getColorWrapped(u, v) & 0xFF) / 255.f; + float hz1 = (waterHeight.getColorWrapped(u, v + 1) & 0xFF) / 255.f; + normalMap[(u*normalMapW + v) * 2] = hx1 - hx0; + normalMap[(u*normalMapW + v) * 2 + 1] = hz1 - hz0; + } + } + } + + /** + * Displace the normal using the water displacement map. + */ + @Override + public Vector3 doWaterShading(Ray ray, IntersectionRecord intersectionRecord, double animationTime) { + int w = (1 << 4); + double ox = (ray.o.x + offset.x) / scale.x; + double oz = (ray.o.z + offset.y) / scale.y; + double x = ox / w - QuickMath.floor(ox / w); + double z = oz / w - QuickMath.floor(oz / w); + int u = (int) (x * normalMapW - Constants.EPSILON); + int v = (int) ((1 - z) * normalMapW - Constants.EPSILON); + Vector3 n = new Vector3(normalMap[(u*normalMapW + v) * 2], .15f, normalMap[(u*normalMapW + v) * 2 + 1]); + w = (1 << 1); + x = ox / w - QuickMath.floor(ox / w); + z = oz / w - QuickMath.floor(oz / w); + u = (int) (x * normalMapW - Constants.EPSILON); + v = (int) ((1 - z) * normalMapW - Constants.EPSILON); + n.x += normalMap[(u*normalMapW + v) * 2] / 2; + n.z += normalMap[(u*normalMapW + v) * 2 + 1] / 2; + n.normalize(); + return n; + } + + @Override + public WaterShader clone() { + return new LegacyWaterShader(); + } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.add("offset", offset.toJson()); + json.add("scale", scale.toJson()); + return json; + } + + @Override + public void fromJson(JsonObject json) { + offset.fromJson(json.get("offset").asObject()); + scale.fromJson(json.get("scale").asObject()); + } + + @Override + public void reset() { + this.scale.set(1, 1); + this.offset.set(0, 0); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + DoubleAdjuster xScale = new DoubleAdjuster(); + xScale.setName("X scale"); + xScale.setRange(0.001, 64); + xScale.clampMin(); + xScale.set(this.scale.x); + xScale.onValueChange(value -> { + this.scale.x = value; + scene.refresh(); + }); + + DoubleAdjuster zScale = new DoubleAdjuster(); + zScale.setName("Z scale"); + zScale.setRange(0.001, 64); + zScale.clampMin(); + zScale.set(this.scale.y); + zScale.onValueChange(value -> { + this.scale.y = value; + scene.refresh(); + }); + + DoubleAdjuster xOffset = new DoubleAdjuster(); + xOffset.setName("X offset"); + xOffset.setRange(-128, 128); + xOffset.set(this.offset.x); + xOffset.onValueChange(value -> { + this.offset.x = value; + scene.refresh(); + }); + + DoubleAdjuster zOffset = new DoubleAdjuster(); + zOffset.setName("Z offset"); + zOffset.setRange(-128, 128); + zOffset.set(this.offset.y); + zOffset.onValueChange(value -> { + this.offset.y = value; + scene.refresh(); + }); + + return new VBox(6, xScale, zScale, xOffset, zOffset); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/SimplexWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/SimplexWaterShader.java new file mode 100644 index 0000000000..a8aa39c18b --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/SimplexWaterShader.java @@ -0,0 +1,205 @@ +/* Copyright (c) 2012-2021 Chunky contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.scene.watershading; + +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.json.JsonObject; +import se.llbit.math.*; + +public class SimplexWaterShader implements WaterShader { + /* + Water shading is implemented using fractal noise based on simplex noise + (superimposed layers of simplex noise with increasing frequency and decreasing amplitude) + This 3D noise function gives the height of the water for a given x, z and t, + what we are really interested in are the partial derivatives of that function + with respect to x and z as they give us the slope along x and z, + and the normal is simply the cross product of the slope along x and the slope along z. + */ + + public int iterations = 4; /// Number of iteration of the fractal noise + public double baseFrequency = 0.4; /// frequency of the first iteration, doubles each iteration + public double baseAmplitude = 0.025; /// amplitude of the first iteration, halves each iteration + public double animationSpeed = 1; /// animation speed + private final Vector2 offset = new Vector2(); + private final Vector2 scale = new Vector2(1, 1); + private final SimplexNoise noise = new SimplexNoise(); + + + @Override + public Vector3 doWaterShading(Ray ray, IntersectionRecord intersectionRecord, double animationTime) { + double frequency = baseFrequency; + double amplitude = baseAmplitude; + + double ddx = 0; + double ddz = 0; + + for(int i = 0; i < iterations; ++i) { + noise.calculate((float)(ray.o.x * frequency), (float)(ray.o.z * frequency), (float)(animationTime * animationSpeed)); + noise.calculate( + (float) ((ray.o.x + offset.x) / scale.x * frequency), + (float) ((ray.o.z + offset.y) / scale.y * frequency), + (float) (animationTime * animationSpeed) + ); + double ddxNext = ddx - amplitude * noise.ddx; + double ddzNext = ddz - amplitude * noise.ddy; + if (Double.isNaN(ddxNext + ddzNext)) { + break; + } + ddx = ddxNext; + ddz = ddzNext; + + frequency *= 2; + amplitude *= 0.5; + } + Vector3 xSlope = new Vector3(1, ddx, 0); + Vector3 zSlope = new Vector3(0, ddz, 1); + Vector3 normal = new Vector3(); + normal.cross(zSlope, xSlope); + normal.normalize(); + return normal; + } + + @Override + public WaterShader clone() { + SimplexWaterShader shader = new SimplexWaterShader(); + shader.iterations = iterations; + shader.baseFrequency = baseFrequency; + shader.baseAmplitude = baseAmplitude; + shader.animationSpeed = animationSpeed; + shader.offset.set(offset); + shader.scale.set(scale); + return shader; + } + + @Override + public JsonObject toJson() { + JsonObject params = new JsonObject(); + params.add("iterations", iterations); + params.add("frequency", baseFrequency); + params.add("amplitude", baseAmplitude); + params.add("animationSpeed", animationSpeed); + params.add("offset", offset.toJson()); + params.add("scale", scale.toJson()); + return params; + } + + @Override + public void fromJson(JsonObject json) { + iterations = json.get("iterations").intValue(4); + baseFrequency = json.get("frequency").doubleValue(0.4); + baseAmplitude = json.get("amplitude").doubleValue(0.025); + animationSpeed = json.get("animationSpeed").doubleValue(1); + offset.fromJson(json.get("offset").asObject()); + scale.fromJson(json.get("scale").asObject()); + } + + @Override + public void reset() { + iterations = 4; + baseAmplitude = 0.025; + baseFrequency = 0.4; + animationSpeed = 1; + offset.set(0, 0); + scale.set(1, 1); + } + + @Override + public VBox getControls(RenderControlsTab parent) { + Scene scene = parent.getChunkyScene(); + + IntegerAdjuster iterations = new IntegerAdjuster(); + iterations.setName("Iterations"); + iterations.setRange(1, 10); + iterations.clampMin(); + iterations.set(this.iterations); + iterations.onValueChange(value -> { + this.iterations = value; + scene.refresh(); + }); + + DoubleAdjuster frequency = new DoubleAdjuster(); + frequency.setName("Frequency"); + frequency.setRange(0, 1); + frequency.set(this.baseFrequency); + frequency.onValueChange(value -> { + this.baseFrequency = value; + scene.refresh(); + }); + + DoubleAdjuster amplitude = new DoubleAdjuster(); + amplitude.setName("Amplitude"); + amplitude.setRange(0, 1); + amplitude.set(this.baseAmplitude); + amplitude.onValueChange(value -> { + this.baseAmplitude = value; + scene.refresh(); + }); + + DoubleAdjuster animationSpeed = new DoubleAdjuster(); + animationSpeed.setName("Animation speed"); + animationSpeed.setRange(0, 10); + animationSpeed.set(this.animationSpeed); + animationSpeed.onValueChange(value -> { + this.animationSpeed = value; + scene.refresh(); + }); + + DoubleAdjuster xScale = new DoubleAdjuster(); + xScale.setName("X scale"); + xScale.setRange(0.001, 64); + xScale.clampMin(); + xScale.set(this.scale.x); + xScale.onValueChange(value -> { + this.scale.x = value; + scene.refresh(); + }); + + DoubleAdjuster zScale = new DoubleAdjuster(); + zScale.setName("Z scale"); + zScale.setRange(0.001, 64); + zScale.clampMin(); + zScale.set(this.scale.y); + zScale.onValueChange(value -> { + this.scale.y = value; + scene.refresh(); + }); + + DoubleAdjuster xOffset = new DoubleAdjuster(); + xOffset.setName("X offset"); + xOffset.setRange(-128, 128); + xOffset.set(this.offset.x); + xOffset.onValueChange(value -> { + this.offset.x = value; + scene.refresh(); + }); + + DoubleAdjuster zOffset = new DoubleAdjuster(); + zOffset.setName("Z offset"); + zOffset.setRange(-128, 128); + zOffset.set(this.offset.y); + zOffset.onValueChange(value -> { + this.offset.y = value; + scene.refresh(); + }); + + return new VBox(6, iterations, frequency, amplitude, animationSpeed, xScale, zScale, xOffset, zOffset); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/StillWaterShader.java similarity index 69% rename from chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java rename to chunky/src/java/se/llbit/chunky/renderer/scene/watershading/StillWaterShader.java index 1e3347d6ba..183a0fa5b5 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/StillWaterShader.java @@ -14,15 +14,17 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ -package se.llbit.chunky.renderer.scene; +package se.llbit.chunky.renderer.scene.watershading; -import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.json.JsonObject; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; +import se.llbit.math.Vector3; public class StillWaterShader implements WaterShader { @Override - public void doWaterShading(Ray ray, double animationTime) { + public Vector3 doWaterShading(Ray ray, IntersectionRecord intersectionRecord, double animationTime) { + return new Vector3(intersectionRecord.n); } @Override @@ -31,10 +33,15 @@ public WaterShader clone() { } @Override - public void save(JsonObject json) { + public JsonObject toJson() { + return new JsonObject(0); } @Override - public void load(JsonObject json) { + public void fromJson(JsonObject json) { + } + + @Override + public void reset() { } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/WaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShader.java similarity index 68% rename from chunky/src/java/se/llbit/chunky/renderer/scene/WaterShader.java rename to chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShader.java index c2edbb2244..ca1e3b150d 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/WaterShader.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShader.java @@ -14,15 +14,16 @@ * You should have received a copy of the GNU General Public License * along with Chunky. If not, see . */ -package se.llbit.chunky.renderer.scene; +package se.llbit.chunky.renderer.scene.watershading; -import se.llbit.json.JsonObject; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; +import se.llbit.math.Vector3; +import se.llbit.util.Configurable; +import se.llbit.util.HasControls; -public interface WaterShader { - void doWaterShading(Ray ray, double animationTime); +public interface WaterShader extends Configurable, HasControls { + Vector3 doWaterShading(Ray ray, IntersectionRecord intersectionRecord, double animationTime); WaterShader clone(); - void save(JsonObject json); - void load(JsonObject json); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShadingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShadingStrategy.java new file mode 100644 index 0000000000..06066156f6 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/watershading/WaterShadingStrategy.java @@ -0,0 +1,54 @@ +/* Copyright (c) 2023 Chunky Contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.scene.watershading; + +import se.llbit.util.Registerable; + +public enum WaterShadingStrategy implements Registerable { + SIMPLEX("Simplex", "Uses configurable noise to shade the water, which prevents tiling at great distances."), + TILED_NORMALMAP("Tiled normal map", "Uses a built-in tiled normal map to shade the water"), + STILL("Still", "Renders the water surface as flat."); + + private final String displayName; + private final String description; + + WaterShadingStrategy(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + @Override + public String getName() { + return this.displayName; + } + + @Override + public String toString() { + return this.displayName; + } + + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String getId() { + return this.name(); + } +} diff --git a/chunky/src/java/se/llbit/chunky/resources/AnimatedTexture.java b/chunky/src/java/se/llbit/chunky/resources/AnimatedTexture.java index 2c98c8d4f3..cc4501eb46 100644 --- a/chunky/src/java/se/llbit/chunky/resources/AnimatedTexture.java +++ b/chunky/src/java/se/llbit/chunky/resources/AnimatedTexture.java @@ -16,7 +16,7 @@ */ package se.llbit.chunky.resources; -import se.llbit.math.Ray; +import se.llbit.math.Constants; /** * Basic animated texture extension. @@ -47,8 +47,8 @@ public float[] getColor(double u, double v) { */ public float[] getColor(double u, double v, int frame) { int i = Math.floorMod(frame, numFrames); - return getColor((int) (u * width - Ray.EPSILON), - (int) ((1 - v) * frameHeight - Ray.EPSILON + i * frameHeight)); + return getColor((int) (u * width - Constants.EPSILON), + (int) ((1 - v) * frameHeight - Constants.EPSILON + i * frameHeight)); } @Override public void setTexture(BitmapImage newImage) { diff --git a/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java b/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java index 9c9dc0aa72..a7e781d237 100644 --- a/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java +++ b/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java @@ -72,7 +72,7 @@ public HDRTexture(File file) { MappedByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, start, byteBufLen); // Precompute exponents. - double exp[] = new double[256]; + double[] exp = new double[256]; for (int e = 0; e < 256; ++e) { exp[e] = Math.pow(2, e - 136); } diff --git a/chunky/src/java/se/llbit/chunky/resources/OctreeFileFormat.java b/chunky/src/java/se/llbit/chunky/resources/OctreeFileFormat.java index 1342836f39..5b05bab9b9 100644 --- a/chunky/src/java/se/llbit/chunky/resources/OctreeFileFormat.java +++ b/chunky/src/java/se/llbit/chunky/resources/OctreeFileFormat.java @@ -63,8 +63,6 @@ public static OctreeData load(DataInputStream in, String octreeImpl, String lega data.palette = BlockPalette.read(in); stepConsumer.accept("world octree"); data.worldTree = Octree.load(octreeImpl, version < 5 ? convertDataNodes(data.palette, in) : in); - stepConsumer.accept("water octree"); - data.waterTree = Octree.load(octreeImpl, version < 5 ? convertDataNodes(data.palette, in) : in); if(version >= 7) { stepConsumer.accept("grass tints"); @@ -177,7 +175,7 @@ private static DataInputStream convertDataNodes(BlockPalette palette, final Data * Save octrees and grass/foliage/water textures to a file. */ public static void store(DataOutputStream out, Octree octree, - Octree waterTree, BlockPalette palette, + BlockPalette palette, BiomeStructure grassColors, BiomeStructure foliageColors, BiomeStructure dryFoliageColors, @@ -186,7 +184,6 @@ public static void store(DataOutputStream out, Octree octree, out.writeInt(OCTREE_VERSION); palette.write(out); octree.store(out); - waterTree.store(out); if (grassColors != null) { out.writeUTF(grassColors.biomeFormat()); grassColors.store(out); @@ -214,7 +211,7 @@ public static void store(DataOutputStream out, Octree octree, } public static class OctreeData { - public Octree worldTree, waterTree; + public Octree worldTree; public BiomeStructure grassColors, foliageColors, dryFoliageColors, waterColors; public BlockPalette palette; public int version; diff --git a/chunky/src/java/se/llbit/chunky/resources/PFMTexture.java b/chunky/src/java/se/llbit/chunky/resources/PFMTexture.java index 99af07e129..6458ca75b2 100644 --- a/chunky/src/java/se/llbit/chunky/resources/PFMTexture.java +++ b/chunky/src/java/se/llbit/chunky/resources/PFMTexture.java @@ -61,12 +61,12 @@ public PFMTexture(File file) { scan.close(); try (RandomAccessFile f = new RandomAccessFile(file, "r")) { long len = f.length(); - long start = len - width * height * components * 4; + long start = len - (long) width * height * components * 4; buf = new float[width * height * 3]; int offset = 0; FileChannel channel = f.getChannel(); - MappedByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, start, buf.length * 4); + MappedByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, start, buf.length * 4L); if (bigEndian) { byteBuf.order(ByteOrder.BIG_ENDIAN); } else { diff --git a/chunky/src/java/se/llbit/chunky/resources/SignTexture.java b/chunky/src/java/se/llbit/chunky/resources/SignTexture.java index 7a33e4bb18..9b809ff67e 100644 --- a/chunky/src/java/se/llbit/chunky/resources/SignTexture.java +++ b/chunky/src/java/se/llbit/chunky/resources/SignTexture.java @@ -20,7 +20,7 @@ import se.llbit.chunky.resources.texturepack.FontTexture.Glyph; import se.llbit.json.JsonArray; import se.llbit.json.JsonValue; -import se.llbit.math.Ray; +import se.llbit.math.Constants; import se.llbit.util.annotation.Nullable; public class SignTexture extends Texture { @@ -151,8 +151,8 @@ public SignTexture(JsonArray[] text, Color dyeColor, boolean isGlowing, Texture @Override public float[] getColor(double u, double v) { if (textColor != null) { - int x = (int) (u * textColor.width - Ray.EPSILON); - int y = (int) ((1 - v) * textColor.height - Ray.EPSILON); + int x = (int) (u * textColor.width - Constants.EPSILON); + int y = (int) ((1 - v) * textColor.height - Constants.EPSILON); if (textMask != null && textMask.getPixel(x, y)) { Color characterColor = Color.get(textColor.getPixel(x, y)); return characterColor.linearColor; diff --git a/chunky/src/java/se/llbit/chunky/resources/SolidColorTexture.java b/chunky/src/java/se/llbit/chunky/resources/SolidColorTexture.java index 0f38969bd4..b67ab85ab3 100644 --- a/chunky/src/java/se/llbit/chunky/resources/SolidColorTexture.java +++ b/chunky/src/java/se/llbit/chunky/resources/SolidColorTexture.java @@ -20,6 +20,7 @@ import se.llbit.math.Vector4; public class SolidColorTexture extends Texture { + public static final SolidColorTexture EMPTY = new SolidColorTexture(new Vector4(1, 1, 1, 1)); private final Vector4 color; diff --git a/chunky/src/java/se/llbit/chunky/resources/Texture.java b/chunky/src/java/se/llbit/chunky/resources/Texture.java index 467f22b99d..5af05e0f88 100644 --- a/chunky/src/java/se/llbit/chunky/resources/Texture.java +++ b/chunky/src/java/se/llbit/chunky/resources/Texture.java @@ -25,8 +25,9 @@ import se.llbit.chunky.resources.texturepack.TexturePath; import se.llbit.fxutil.FxImageUtil; import se.llbit.math.ColorUtil; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.QuickMath; -import se.llbit.math.Ray; import se.llbit.math.Vector4; import se.llbit.resources.ImageLoader; import se.llbit.util.annotation.NotNull; @@ -1806,10 +1807,10 @@ public void getColor(double u, double v, Vector4 c) { /** * Get linear color values. * - * @param ray ray to store color value in. + * @param intersectionRecord IntersectionRecord to store color value in. */ - public void getColor(Ray ray) { - getColor(ray.u, ray.v, ray.color); + public void getColor(IntersectionRecord intersectionRecord) { + getColor(intersectionRecord.uv.x, intersectionRecord.uv.y, intersectionRecord.color); } /** @@ -1818,7 +1819,7 @@ public void getColor(Ray ray) { * @return color */ public float[] getColor(double u, double v) { - return getColor((int) (u * width - Ray.EPSILON), (int) ((1 - v) * height - Ray.EPSILON)); + return getColor((int) (u * width - Constants.EPSILON), (int) ((1 - v) * height - Constants.EPSILON)); } /** diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index 1654177257..ee2836dbef 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -19,22 +19,16 @@ import javafx.application.Platform; import javafx.geometry.Point2D; -import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; -import javafx.scene.control.Button; -import javafx.scene.control.Dialog; import javafx.scene.control.MenuItem; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; -import javafx.scene.layout.Border; -import javafx.scene.layout.GridPane; import javafx.stage.PopupWindow; import se.llbit.chunky.map.MapBuffer; import se.llbit.chunky.map.MapView; @@ -45,7 +39,6 @@ import se.llbit.chunky.renderer.scene.SceneManager; import se.llbit.chunky.ui.controller.ChunkyFxController; import se.llbit.chunky.ui.dialogs.SelectChunksInRadiusDialog; -import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; import se.llbit.chunky.world.*; import se.llbit.chunky.world.Dimension; import se.llbit.chunky.world.listeners.ChunkUpdateListener; @@ -57,7 +50,6 @@ import java.io.File; import java.io.IOException; import java.util.Collection; -import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -185,7 +177,7 @@ public ChunkMap(WorldMapLoader loader, ChunkyFxController controller, MenuItem exportZip = new MenuItem("Export selected chunks…"); exportZip.setOnAction(e -> controller.exportZip()); - exportZip.setDisable(chunkSelection.size() == 0); + exportZip.setDisable(chunkSelection.isEmpty()); MenuItem exportPng = new MenuItem("Save map view as…"); exportPng.setOnAction(e -> controller.exportMapView()); @@ -203,7 +195,7 @@ public ChunkMap(WorldMapLoader loader, ChunkyFxController controller, .forEach(t -> t.accept(contextMenu)); chunkSelection.addSelectionListener(() -> { - boolean noChunksSelected = chunkSelection.size() == 0; + boolean noChunksSelected = chunkSelection.isEmpty(); clearSelection.setDisable(noChunksSelected); newScene.setDisable(noChunksSelected); loadSelection.setDisable(noChunksSelected); @@ -483,10 +475,10 @@ private ChunkPosition getChunk(MouseEvent event) { Chunk hoveredChunk = mapLoader.getWorld().currentDimension().getChunk(cp); if (!hoveredChunk.isEmpty()) { tooltip.setText( - String.format("%s, %s\nBlock: [%s, %s]\n%d chunks selected", hoveredChunk.toString(), hoveredChunk.biomeAt(bx, bz), worldBlockX, worldBlockZ, chunkSelection.size())); + String.format("%s, %s\nBlock: [%s, %s]\n%d chunks selected", hoveredChunk, hoveredChunk.biomeAt(bx, bz), worldBlockX, worldBlockZ, chunkSelection.size())); } else { tooltip.setText( - String.format("%s\nBlock: [%s, %s]\n%d chunks selected", hoveredChunk.toString(), worldBlockX, worldBlockZ, chunkSelection.size())); + String.format("%s\nBlock: [%s, %s]\n%d chunks selected", hoveredChunk, worldBlockX, worldBlockZ, chunkSelection.size())); } Scene scene = mapOverlay.getScene(); if (mapOverlay.isFocused()) { diff --git a/chunky/src/java/se/llbit/chunky/ui/RenderCanvasFx.java b/chunky/src/java/se/llbit/chunky/ui/RenderCanvasFx.java index d3e07e8ee2..54bb1d019c 100644 --- a/chunky/src/java/se/llbit/chunky/ui/RenderCanvasFx.java +++ b/chunky/src/java/se/llbit/chunky/ui/RenderCanvasFx.java @@ -49,10 +49,11 @@ import javafx.stage.PopupWindow; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.renderer.*; -import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.scene.Camera; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.ui.controller.ChunkyFxController; +import se.llbit.chunky.ui.dialogs.EditMaterialDialog; +import se.llbit.chunky.world.Material; import se.llbit.math.Vector2; import java.nio.IntBuffer; @@ -237,6 +238,15 @@ public RenderCanvasFx(ChunkyFxController chunkyFxController, copyFrame.setOnAction(e -> chunkyFxController.copyCurrentFrame()); contextMenu.getItems().add(copyFrame); + MenuItem editMaterial = new MenuItem("Edit material"); + editMaterial.setOnAction(e -> { + Material material = scene.getTargetMaterial(target.x, target.y); + EditMaterialDialog dialog = new EditMaterialDialog(material, scene); + dialog.showAndWait(); + dialog = null; + }); + contextMenu.getItems().add(editMaterial); + chunkyFxController.getChunky() .getRenderContextMenuTransformers() .forEach(t -> t.accept(contextMenu)); diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java index 7fd253d564..7a3e7f1c10 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java @@ -53,6 +53,7 @@ import javafx.scene.input.KeyCombination; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; @@ -79,7 +80,6 @@ import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.Icon; import se.llbit.chunky.world.World; -import se.llbit.fx.ToolPane; import se.llbit.fxutil.Dialogs; import se.llbit.fxutil.GroupedChangeListener; import se.llbit.log.ConsoleReceiver; @@ -109,7 +109,7 @@ public class ChunkyFxController @FXML private Canvas mapOverlay; @FXML private Label mapName; @FXML private MenuItem menuExit; - @FXML private ToolPane renderControls; + @FXML private VBox renderControls; @FXML private Button changeWorldBtn; @FXML private Button reloadWorldBtn; @FXML private ToggleButton overworldBtn; @@ -942,10 +942,8 @@ private boolean promptSaveScene(String sceneName) { alert.setTitle("Overwrite existing scene"); alert.setContentText("A scene with that name already exists. This will overwrite the existing scene, are you sure you want to continue?"); - if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) { - return false; - } + return alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK; } - return true; + return true; } } diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java b/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java index d491ed2965..166514303a 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java @@ -89,6 +89,10 @@ public class CreditsController implements Initializable { @FXML private Hyperlink apacheCliLicense; @FXML + private Hyperlink controlsfx; + @FXML + private Hyperlink controlsFXLicense; + @FXML private VBox pluginBox; @FXML private ImageView logoImage; @@ -202,6 +206,11 @@ public void initialize(URL location, ResourceBundle resources) { apacheCliLicense.setBorder(Border.EMPTY); apacheCliLicense.setOnAction(e -> launchAndReset(apacheCliLicense, "http://www.apache.org/licenses/LICENSE-2.0")); + controlsfx.setBorder(Border.EMPTY); + controlsfx.setOnAction(e -> launchAndReset(controlsfx, "https://controlsfx.github.io/")); + controlsFXLicense.setBorder(Border.EMPTY); + controlsFXLicense.setOnAction(e -> launchAndReset(controlsFXLicense, "https://github.com/controlsfx/controlsfx/blob/master/license.txt")); + if (!plugins.isEmpty()) { plugins.forEach((key, item) -> pluginBox.getChildren().addAll(buildBox(item))); } else { diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java index d7b4bfa7c4..5f85f716cf 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java @@ -19,8 +19,9 @@ import javafx.application.Platform; import javafx.geometry.Point2D; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import se.llbit.chunky.renderer.RenderController; import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.scene.AsynchronousSceneManager; @@ -28,8 +29,6 @@ import se.llbit.chunky.ui.RenderCanvasFx; import se.llbit.chunky.ui.render.tabs.*; import se.llbit.chunky.ui.render.RenderControlsTab; -import se.llbit.fx.ToolPane; -import se.llbit.fx.ToolTab; import se.llbit.log.Log; import java.util.ArrayList; @@ -63,13 +62,13 @@ public RenderController getRenderController() { /** Maps JavaFX tabs to tab controllers. */ private final Tooltip tooltip; - private final ToolPane toolPane; + private final VBox toolPane; private ChunkyFxController controller; - private final Map tabMap = new IdentityHashMap<>(); + private final Map tabMap = new IdentityHashMap<>(); - public RenderControlsFxController(ChunkyFxController controller, ToolPane toolPane, + public RenderControlsFxController(ChunkyFxController controller, VBox toolPane, RenderCanvasFx canvas, RenderManager renderManager) { this.controller = controller; this.toolPane = toolPane; @@ -96,8 +95,9 @@ private void buildTabs() { try { // Create the default tabs: tabs.add(new GeneralTab()); - tabs.add(new LightingTab()); - tabs.add(new SkyTab()); + tabs.add(new EnvironmentTab()); + tabs.add(new FogTab()); + tabs.add(new EmittersTab()); tabs.add(new WaterTab()); tabs.add(new CameraTab()); tabs.add(new EntitiesTab()); @@ -119,10 +119,18 @@ private void buildTabs() { // We run this in runLater because it fails if run directly, for unknown reasons. // TODO(llbit): check why adding tabs has to happen inside runLater! for (RenderControlsTab tab : tabs) { - ToolTab toolTab = new ToolTab(tab.getTabTitle(), tab.getTabContent()); + String title = tab.getTabTitle(); + + VBox content = tab.getTabContent(); + content.setSpacing(6); + + TitledPane toolTab = new TitledPane(title, content); + toolTab.setAnimated(false); + toolTab.setExpanded(false); + tabMap.put(tab, toolTab); - toolPane.getTabs().add(toolTab); - toolTab.selectedProperty().addListener((observable, oldValue, selected) -> { + toolPane.getChildren().add(toolTab); + toolTab.expandedProperty().addListener((observable, oldValue, selected) -> { if (selected) { tab.update(scene); } @@ -135,8 +143,8 @@ private void buildTabs() { } public void refreshSettings() { - for (Map.Entry entry : tabMap.entrySet()) { - if (entry.getValue().getSelected()) { + for (Map.Entry entry : tabMap.entrySet()) { + if (entry.getValue().isExpanded()) { entry.getKey().update(scene); } } diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/SceneChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/SceneChooserController.java index dc449f1c2c..742ab7d4e2 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/SceneChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/SceneChooserController.java @@ -327,8 +327,18 @@ private void parseScene(File sceneFile) { try (JsonParser parser = new JsonParser(new FileInputStream(new File(sceneFile.getParentFile(), sceneFile.getName())))) { JsonObject scene = parser.parse().object(); - int width = scene.get("width").intValue(400); - int height = scene.get("height").intValue(400); + int width; + int height; + + if (scene.get("canvasConfig").isObject()) { + JsonObject canvasConfig = scene.get("canvasConfig").asObject(); + width = canvasConfig.get("width").intValue(400); + height = canvasConfig.get("height").intValue(400); + } else { + width = scene.get("width").intValue(400); + height = scene.get("height").intValue(400); + } + dimensions = String.format("%sx%s", width, height); chunkSize = scene.get("chunkList").array().size(); diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java index a2575d6808..f5c7467fe2 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java @@ -31,7 +31,6 @@ import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.resources.MinecraftFinder; import se.llbit.chunky.resources.ResourcePackLoader; -import se.llbit.chunky.resources.TexturePackLoader; import se.llbit.chunky.ui.TableSortConfigSerializer; import se.llbit.chunky.world.World; import se.llbit.fxutil.Dialogs; diff --git a/chunky/src/java/se/llbit/chunky/ui/data/MaterialReferenceColorData.java b/chunky/src/java/se/llbit/chunky/ui/data/MaterialReferenceColorData.java new file mode 100644 index 0000000000..26b504dfb3 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/data/MaterialReferenceColorData.java @@ -0,0 +1,58 @@ +package se.llbit.chunky.ui.data; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import se.llbit.math.Vector3; +import se.llbit.math.Vector4; + +public class MaterialReferenceColorData { + private final Vector4 referenceColor; + private final SimpleStringProperty red; + private final SimpleStringProperty green; + private final SimpleStringProperty blue; + private final SimpleStringProperty range; + + public MaterialReferenceColorData(Vector4 referenceColor) { + this.referenceColor = referenceColor; + this.red = new SimpleStringProperty(String.valueOf((int) (referenceColor.x * 255))); + this.green = new SimpleStringProperty(String.valueOf((int) (referenceColor.y * 255))); + this.blue = new SimpleStringProperty(String.valueOf((int) (referenceColor.z * 255))); + this.range = new SimpleStringProperty(String.valueOf((int) (referenceColor.w * 255))); + } + + public void setReferenceColor(Vector3 color) { + this.referenceColor.x = color.x; + this.referenceColor.y = color.y; + this.referenceColor.z = color.z; + + this.red.set(String.valueOf((int) (referenceColor.x * 255))); + this.green.set(String.valueOf((int) (referenceColor.y * 255))); + this.blue.set(String.valueOf((int) (referenceColor.z * 255))); + } + + public Vector4 getReferenceColor() { + return referenceColor; + } + + public void setRange(int value) { + this.referenceColor.w = value / 255d; + + this.range.set(String.valueOf(value)); + } + + public ObservableValue redProperty() { + return this.red; + } + + public ObservableValue greenProperty() { + return this.green; + } + + public ObservableValue blueProperty() { + return this.blue; + } + + public ObservableValue rangeProperty() { + return this.range; + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/AddEntityDialog.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/AddEntityDialog.java new file mode 100644 index 0000000000..e2d1ff58cd --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/AddEntityDialog.java @@ -0,0 +1,86 @@ +package se.llbit.chunky.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import se.llbit.chunky.ui.DoubleTextField; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +import se.llbit.chunky.ui.render.tabs.EntitiesTab; +import se.llbit.math.Vector3; + +public class AddEntityDialog extends Dialog { + protected ChoiceBox entityType = new ChoiceBox<>(); + protected ChoiceBox entityPlacement = new ChoiceBox<>(); + protected GridPane positionPane = new GridPane(); + protected DoubleTextField posX = new DoubleTextField(); + protected DoubleTextField posY = new DoubleTextField(); + protected DoubleTextField posZ = new DoubleTextField(); + + public AddEntityDialog() { + this.setTitle("Add an entity to the scene"); + + Label entityTypeLabel = new Label("Entity type:"); + entityType.getItems().addAll(EntitiesTab.entityTypes.keySet()); + HBox entityTypeBox = new HBox(10, entityTypeLabel, entityType); + + Label entityPlacementLabel = new Label("Entity placement:"); + entityPlacement.getItems().addAll(EntitiesTab.EntityPlacement.values()); + entityPlacement.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + boolean disablePositionControls = newValue != EntitiesTab.EntityPlacement.POSITION; + positionPane.setDisable(disablePositionControls); + } + ); + HBox entityPlacementBox = new HBox(10, entityPlacementLabel, entityPlacement); + + TextFieldLabelWrapper posXWrapper = new TextFieldLabelWrapper(); + posXWrapper.setLabelText("x:"); + posXWrapper.setTextField(posX); + posX.setText("0.0"); + + TextFieldLabelWrapper posYWrapper = new TextFieldLabelWrapper(); + posYWrapper.setLabelText("y:"); + posYWrapper.setTextField(posY); + posY.setText("0.0"); + + TextFieldLabelWrapper posZWrapper = new TextFieldLabelWrapper(); + posZWrapper.setLabelText("z:"); + posZWrapper.setTextField(posZ); + posZ.setText("0.0"); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(120); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + positionPane.setHgap(6); + positionPane.getColumnConstraints().addAll(labelConstraints, posFieldConstraints, posFieldConstraints, posFieldConstraints); + positionPane.addRow(0, new Label("Specific position:"), posXWrapper, posYWrapper, posZWrapper); + positionPane.setDisable(true); + + DialogPane dialogPane = this.getDialogPane(); + VBox vBox = new VBox(10, entityTypeBox, entityPlacementBox, positionPane); + vBox.setPadding(new Insets(10)); + + dialogPane.setContent(vBox); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + } + + public EntitiesTab.EntityType getType() { + return EntitiesTab.entityTypes.get(entityType.getSelectionModel().getSelectedItem()); + } + + public EntitiesTab.EntityPlacement getPlacement() { + return entityPlacement.getValue(); + } + + public Vector3 getPosition() { + return new Vector3( + posX.valueProperty().doubleValue(), + posY.valueProperty().doubleValue(), + posZ.valueProperty().doubleValue() + ); + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/EditMaterialDialog.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/EditMaterialDialog.java new file mode 100644 index 0000000000..00638a05f2 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/EditMaterialDialog.java @@ -0,0 +1,18 @@ +package se.llbit.chunky.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.world.Material; + +public class EditMaterialDialog extends Dialog { + public EditMaterialDialog(Material material, Scene scene) { + DialogPane dialogPane = this.getDialogPane(); + VBox vBox = Material.getControls(material, scene); + vBox.setPadding(new Insets(10)); + + dialogPane.setContent(vBox); + dialogPane.getButtonTypes().add(ButtonType.CLOSE); + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeShapeSelectorDialog.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeShapeSelectorDialog.java new file mode 100644 index 0000000000..7fcf8da249 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeShapeSelectorDialog.java @@ -0,0 +1,32 @@ +package se.llbit.chunky.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolumeShape; + +public class FogVolumeShapeSelectorDialog extends Dialog { + protected ChoiceBox choiceBox = new ChoiceBox<>(); + + public FogVolumeShapeSelectorDialog() { + this.setTitle("Select fog volume type"); + + DialogPane dialogPane = this.getDialogPane(); + VBox vBox = new VBox(); + + choiceBox.getItems().addAll(FogVolumeShape.values()); + + vBox.getChildren().add(choiceBox); + vBox.setPadding(new Insets(10)); + + dialogPane.setContent(vBox); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + } + + public FogVolumeShape getShape() { + return choiceBox.getSelectionModel().getSelectedItem(); + } +} \ No newline at end of file diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/Poser.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/Poser.java index 9e7f7a9e8a..ecbd74468e 100644 --- a/chunky/src/java/se/llbit/chunky/ui/dialogs/Poser.java +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/Poser.java @@ -37,11 +37,12 @@ import se.llbit.chunky.renderer.scene.PlayerModel; import se.llbit.chunky.ui.DoubleAdjuster; import se.llbit.chunky.ui.render.tabs.EntitiesTab; +import se.llbit.math.IntersectionRecord; +import se.llbit.math.Ray; import se.llbit.math.bvh.BVH; import se.llbit.math.ColorUtil; import se.llbit.math.Matrix3; import se.llbit.math.QuickMath; -import se.llbit.math.Ray; import se.llbit.math.Vector3; import java.io.File; @@ -191,6 +192,7 @@ private void redraw() { buildBvh(); GraphicsContext gc = preview.getGraphicsContext2D(); Ray ray = new Ray(); + IntersectionRecord intersectionRecord = new IntersectionRecord(); double aspect = width / (double) height; double fovTan = Camera.clampedFovTan(70); camPos.set(0, 1, -2); @@ -198,32 +200,30 @@ private void redraw() { double rayy = fovTan * (.5 - ((double) y) / height); for (int x = 0; x < width; ++x) { double rayx = fovTan * aspect * (.5 - ((double) x) / width); - ray.setDefault(); - ray.t = Double.MAX_VALUE; ray.d.set(rayx, rayy, 1); ray.d.normalize(); ray.o.set(camPos); while (true) { - if (bvh.closestIntersection(ray)) { - if (ray.color.w > 0.9) { + if (bvh.closestIntersection(ray, intersectionRecord)) { + if (intersectionRecord.color.w > 0.9) { break; } - ray.o.scaleAdd(ray.t, ray.d); + ray.o.scaleAdd(intersectionRecord.distance, ray.d); } else { if (x % 20 == 0 || y % 20 == 0) { - ray.color.set(0.7, 0.7, 0.7, 1); + intersectionRecord.color.set(0.7, 0.7, 0.7, 1); } else { - ray.color.set(1, 1, 1, 1); + intersectionRecord.color.set(1, 1, 1, 1); } break; } } - ray.color.x = QuickMath.min(1, FastMath.sqrt(ray.color.x)); - ray.color.y = QuickMath.min(1, FastMath.sqrt(ray.color.y)); - ray.color.z = QuickMath.min(1, FastMath.sqrt(ray.color.z)); - pixels[y * width + x] = ColorUtil.getRGB(ray.color); + intersectionRecord.color.x = QuickMath.min(1, FastMath.sqrt(intersectionRecord.color.x)); + intersectionRecord.color.y = QuickMath.min(1, FastMath.sqrt(intersectionRecord.color.y)); + intersectionRecord.color.z = QuickMath.min(1, FastMath.sqrt(intersectionRecord.color.z)); + pixels[y * width + x] = ColorUtil.getRGB(intersectionRecord.color); } } image.getPixelWriter().setPixels(0, 0, width, height, PIXEL_FORMAT, pixels, 0, width); diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/PostprocessingFilterChooser.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/PostprocessingFilterChooser.java new file mode 100644 index 0000000000..30b1737f55 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/PostprocessingFilterChooser.java @@ -0,0 +1,52 @@ +package se.llbit.chunky.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; +import se.llbit.chunky.renderer.postprocessing.PostProcessingFilter; +import se.llbit.chunky.renderer.postprocessing.PostProcessingFilters; +import se.llbit.chunky.renderer.scene.Scene; + +public class PostprocessingFilterChooser extends Dialog { + + private final ChoiceBox> choiceBox = new ChoiceBox<>(); + + public PostprocessingFilterChooser() { + this.setTitle("Select Postprocessing Filter"); + + DialogPane dialogPane = this.getDialogPane(); + VBox vBox = new VBox(); + + choiceBox.getItems().addAll(PostProcessingFilters.getFilterClasses()); + choiceBox.setConverter(new StringConverter>() { + @Override + public String toString(Class filterClass) { + return filterClass == null ? null : PostProcessingFilters.getFilterName(filterClass); + } + + @Override + public Class fromString(String string) { + return PostProcessingFilters.getPostProcessingFilterFromName(string) + .get(); + } + }); + choiceBox.getSelectionModel().selectedItemProperty() + .addListener(((observable, oldValue, newValue) -> choiceBox.setTooltip( + new Tooltip(PostProcessingFilters.getSampleFilterFromClass(newValue).getDescription())))); + + vBox.getChildren().addAll(choiceBox); + vBox.setPadding(new Insets(10)); + + dialogPane.setContent(vBox); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + } + + public Class getFilter() { + return choiceBox.getSelectionModel().getSelectedItem(); + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/SettingsExport.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/SettingsExport.java index 81396973b3..cf822efe08 100644 --- a/chunky/src/java/se/llbit/chunky/ui/dialogs/SettingsExport.java +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/SettingsExport.java @@ -29,7 +29,6 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import se.llbit.chunky.ui.Icons; diff --git a/chunky/src/java/se/llbit/chunky/ui/elements/AngleAdjuster.java b/chunky/src/java/se/llbit/chunky/ui/elements/AngleAdjuster.java index 1ec1bdbab5..04d170fa93 100644 --- a/chunky/src/java/se/llbit/chunky/ui/elements/AngleAdjuster.java +++ b/chunky/src/java/se/llbit/chunky/ui/elements/AngleAdjuster.java @@ -50,7 +50,7 @@ public AngleAdjuster() { dimple.setDisable(true); StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(knob, dimple); - getChildren().setAll(nameLbl, valueField, stackPane); + getChildren().setAll(stackPane, valueField, nameLbl); angle.bindBidirectional(value); angle.addListener((observable, oldValue, newValue) -> { double angle = newValue.doubleValue(); diff --git a/chunky/src/java/se/llbit/chunky/ui/elements/GradientEditor.java b/chunky/src/java/se/llbit/chunky/ui/elements/GradientEditor.java index 24c35cd2a8..64f235235e 100644 --- a/chunky/src/java/se/llbit/chunky/ui/elements/GradientEditor.java +++ b/chunky/src/java/se/llbit/chunky/ui/elements/GradientEditor.java @@ -33,8 +33,8 @@ import javafx.scene.image.WritablePixelFormat; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import se.llbit.chunky.ui.render.tabs.EnvironmentTab; import se.llbit.chunky.renderer.scene.sky.Sky; -import se.llbit.chunky.ui.render.tabs.SkyTab; import se.llbit.fx.LuxColorPicker; import se.llbit.json.JsonParser; import se.llbit.math.ColorUtil; @@ -56,7 +56,7 @@ public class GradientEditor extends VBox implements Initializable { private static final WritablePixelFormat PIXEL_FORMAT = PixelFormat.getIntArgbInstance(); - private final SkyTab sky; + private final EnvironmentTab sky; // The array order determines the order of the preset list. // Sorted alphabetically, including "The" prefixes. @@ -89,7 +89,7 @@ public class GradientEditor extends VBox implements Initializable { gradientChanged(); }; - public GradientEditor(SkyTab sky) throws IOException { + public GradientEditor(EnvironmentTab sky) throws IOException { this.sky = sky; FXMLLoader loader = new FXMLLoader(getClass().getResource("GradientEditor.fxml")); loader.setRoot(this); @@ -133,9 +133,7 @@ public GradientEditor(SkyTab sky) throws IOException { dialog.setHeaderText("Gradient Import"); dialog.setContentText("Graident JSON:"); Optional result = dialog.showAndWait(); - if (result.isPresent()) { - importGradient(result.get()); - } + result.ifPresent(this::importGradient); }); exportBtn.setOnAction(e -> { TextInputDialog dialog = new TextInputDialog(Sky.gradientJson(gradient).toCompactString()); diff --git a/chunky/src/java/se/llbit/chunky/ui/elements/SizeInput.java b/chunky/src/java/se/llbit/chunky/ui/elements/SizeInput.java index 0d1c5937d9..e53671acb1 100644 --- a/chunky/src/java/se/llbit/chunky/ui/elements/SizeInput.java +++ b/chunky/src/java/se/llbit/chunky/ui/elements/SizeInput.java @@ -35,7 +35,6 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.shape.SVGPath; -import javafx.scene.shape.Shape; import se.llbit.chunky.ui.Icons; import se.llbit.chunky.ui.IntegerTextField; import se.llbit.chunky.ui.ValidatingNumberStringConverter; diff --git a/chunky/src/java/se/llbit/chunky/ui/render/RenderControlsTab.java b/chunky/src/java/se/llbit/chunky/ui/render/RenderControlsTab.java index 990a60e53e..75f6c78327 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/RenderControlsTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/RenderControlsTab.java @@ -16,17 +16,20 @@ */ package se.llbit.chunky.ui.render; -import javafx.scene.Node; +import javafx.scene.layout.VBox; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.ui.controller.RenderControlsFxController; /** - * Tabs in the Render Controls dialog implement this interface. + * Tabs in the Render Controls dialog extend this class. * *

The update method is called to update the active tab with the * current scene state. */ -public interface RenderControlsTab { +public abstract class RenderControlsTab extends VBox { + protected Scene scene; + protected RenderControlsFxController controller; + /** * Called when the tab should update itself because something in the * scene state changed. @@ -35,21 +38,35 @@ public interface RenderControlsTab { * * @param scene the current scene state */ - void update(Scene scene); + public abstract void update(Scene scene); - String getTabTitle(); + public abstract String getTabTitle(); /** * @return the JavaFX tab component for this render controls tab */ - Node getTabContent(); + public abstract VBox getTabContent(); /** * Called after chunks have been loaded. */ - default void onChunksLoaded() { + public void onChunksLoaded() { + } + + public final void setController(RenderControlsFxController controller) { + this.controller = controller; + scene = controller.getRenderController().getSceneManager().getScene(); + onSetController(controller); + } + + protected void onSetController(RenderControlsFxController controller) { + } + + public final Scene getChunkyScene() { + return this.scene; } - default void setController(RenderControlsFxController controller) { + public final RenderControlsFxController getController() { + return this.controller; } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/settings/LayeredFogSettings.java b/chunky/src/java/se/llbit/chunky/ui/render/settings/LayeredFogSettings.java index 48ccf26164..745ffe9be8 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/settings/LayeredFogSettings.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/settings/LayeredFogSettings.java @@ -79,13 +79,13 @@ public void disableControls() { }); removeLayer.setOnAction(e -> { - if (scene.fog.getFogLayers().size() > 0) { + if (!scene.fog.getFogLayers().isEmpty()) { scene.fog.removeLayer(layers.getSelectionModel().getSelectedIndex()); update(scene); } }); - if (!(layers.getItems().size() > 0)) { + if (layers.getItems().isEmpty()) { disableControls(); } @@ -109,6 +109,7 @@ public void disableControls() { layerDensity.onValueChange(value -> scene.fog.setDensity(layers.getSelectionModel().getSelectedIndex(), value)); layerDensity.setName("Fog density"); + fogColor.setText("Fog color"); fogColor.colorProperty().addListener(fogColorListener); } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/settings/SkyboxSettings.java b/chunky/src/java/se/llbit/chunky/ui/render/settings/SkyboxSettings.java index d67c17d157..f452df5785 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/settings/SkyboxSettings.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/settings/SkyboxSettings.java @@ -22,6 +22,7 @@ import javafx.scene.control.Button; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.renderer.RenderController; import se.llbit.chunky.renderer.SceneIOProvider; import se.llbit.chunky.renderer.scene.Scene; @@ -47,6 +48,7 @@ public class SkyboxSettings extends VBox implements Initializable { @FXML private DoubleAdjuster skyboxYaw; @FXML private DoubleAdjuster skyboxPitch; @FXML private DoubleAdjuster skyboxRoll; + @FXML private ToggleSwitch textureInterpolation; private File skyboxDirectory; @@ -79,6 +81,9 @@ public SkyboxSettings() throws IOException { skyboxRoll.setRange(0, 360); skyboxRoll .onValueChange(value -> scene.sky().setRoll(QuickMath.degToRad(value))); + textureInterpolation.selectedProperty().addListener((observable, oldValue, newValue) -> { + scene.sky().setTextureInterpolation(newValue); + }); } private void selectSkyboxTexture(int textureIndex) { @@ -105,5 +110,6 @@ public void update(Scene scene) { skyboxYaw.set(QuickMath.radToDeg(scene.sky().getYaw())); skyboxPitch.set(QuickMath.radToDeg(scene.sky().getPitch())); skyboxRoll.set(QuickMath.radToDeg(scene.sky().getRoll())); + textureInterpolation.setSelected(scene.sky().getTextureInterpolation()); } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/settings/SkymapSettings.java b/chunky/src/java/se/llbit/chunky/ui/render/settings/SkymapSettings.java index 572fc619fb..6d557df4ef 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/settings/SkymapSettings.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/settings/SkymapSettings.java @@ -24,6 +24,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.renderer.RenderController; import se.llbit.chunky.renderer.SceneIOProvider; import se.llbit.chunky.renderer.scene.Scene; @@ -46,6 +47,7 @@ public class SkymapSettings extends VBox implements Initializable { @FXML private DoubleAdjuster skymapRoll; @FXML private ToggleButton v90; @FXML private HBox panoSpecific; + @FXML private ToggleSwitch textureInterpolation; public SkymapSettings() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("SkymapSettings.fxml")); @@ -91,6 +93,9 @@ public void setPanoramic(boolean pano) { .onValueChange(value -> scene.sky().setRoll(QuickMath.degToRad(value))); v90.selectedProperty().addListener( (observable, oldValue, newValue) -> scene.sky().setMirrored(newValue)); + textureInterpolation.selectedProperty().addListener((observable, oldValue, newValue) -> { + scene.sky().setTextureInterpolation(newValue); + }); } public void update(Scene scene) { @@ -98,6 +103,7 @@ public void update(Scene scene) { skymapPitch.set(QuickMath.radToDeg(scene.sky().getPitch())); skymapRoll.set(QuickMath.radToDeg(scene.sky().getRoll())); v90.setSelected(scene.sky().isMirrored()); + textureInterpolation.setSelected(scene.sky().getTextureInterpolation()); } public void setRenderController(RenderController controller) { diff --git a/chunky/src/java/se/llbit/chunky/ui/render/settings/UniformFogSettings.java b/chunky/src/java/se/llbit/chunky/ui/render/settings/UniformFogSettings.java index bd7afd9492..3084ae621d 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/settings/UniformFogSettings.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/settings/UniformFogSettings.java @@ -51,7 +51,7 @@ public UniformFogSettings() throws IOException { @Override public void initialize(URL location, ResourceBundle resources) { fogDensity.setTooltip("Fog thickness. Set to 0 to disable volumetric fog effect."); - fogDensity.setRange(0, 1); + fogDensity.setRange(0.000001, 1); fogDensity.setMaximumFractionDigits(6); fogDensity.makeLogarithmic(); fogDensity.clampMin(); @@ -65,6 +65,7 @@ public UniformFogSettings() throws IOException { skyFogDensity.onValueChange(value -> scene.setSkyFogDensity(value)); skyFogDensity.setName("Sky fog blending"); + fogColor.setText("Fog color"); fogColor.colorProperty().addListener(fogColorListener); } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/AdvancedTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/AdvancedTab.java index 150da3b4f7..de1c822d6b 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/AdvancedTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/AdvancedTab.java @@ -20,15 +20,15 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.util.StringConverter; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.launcher.LauncherSettings; import se.llbit.chunky.main.Chunky; -import se.llbit.chunky.renderer.EmitterSamplingStrategy; -import se.llbit.chunky.renderer.RenderController; +import se.llbit.chunky.renderer.scene.EmitterSamplingStrategy; import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.export.PictureExportFormat; import se.llbit.chunky.renderer.export.PictureExportFormats; @@ -56,20 +56,14 @@ import java.util.Map; import java.util.ResourceBundle; -public class AdvancedTab extends ScrollPane implements RenderControlsTab, Initializable { - private RenderControlsFxController renderControls; - private RenderController controller; - private Scene scene; - +public class AdvancedTab extends RenderControlsTab implements Initializable { @FXML private IntegerAdjuster renderThreads; @FXML private IntegerAdjuster cpuLoad; @FXML private IntegerAdjuster rayDepth; @FXML private IntegerAdjuster branchCount; @FXML private Button mergeRenderDump; @FXML private CheckBox shutdown; - @FXML private CheckBox fastFog; - @FXML private CheckBox fancierTranslucency; - @FXML private DoubleAdjuster transmissivityCap; + @FXML private ToggleSwitch fastFog; @FXML private IntegerAdjuster cacheResolution; @FXML private DoubleAdjuster animationTime; @FXML private ChoiceBox outputMode; @@ -77,9 +71,7 @@ public class AdvancedTab extends ScrollPane implements RenderControlsTab, Initia @FXML private Button octreeSwitchImplementation; @FXML private ChoiceBox bvhMethod; @FXML private ChoiceBox biomeStructureImplementation; - @FXML private IntegerAdjuster gridSize; - @FXML private CheckBox preventNormalEmitterWithSampling; - @FXML private CheckBox hideUnknownBlocks; + @FXML private ToggleSwitch hideUnknownBlocks; @FXML private ChoiceBox rendererSelect; @FXML private ChoiceBox previewSelect; @FXML private CheckBox showLauncher; @@ -101,7 +93,7 @@ public void initialize(URL location, ResourceBundle resources) { cpuLoad.clampBoth(); cpuLoad.onValueChange(value -> { PersistentSettings.setCPULoad(value); - controller.getRenderManager().setCPULoad(value); + controller.getRenderController().getRenderManager().setCPULoad(value); }); rayDepth.setName("Ray depth"); rayDepth.setTooltip("Sets the minimum recursive ray depth."); @@ -119,6 +111,8 @@ public void initialize(URL location, ResourceBundle resources) { scene.setBranchCount(value); PersistentSettings.setBranchCountDefault(value); }); + branchCount.setVisible(false); + branchCount.setManaged(false); mergeRenderDump .setTooltip(new Tooltip("Merge an existing render dump with the current render.")); @@ -131,7 +125,7 @@ public void initialize(URL location, ResourceBundle resources) { List dumps = fileChooser.showOpenMultipleDialog(getScene().getWindow()); if (dumps != null) { // TODO: remove cast. - AsynchronousSceneManager sceneManager = ((AsynchronousSceneManager) controller.getSceneManager()); + AsynchronousSceneManager sceneManager = ((AsynchronousSceneManager) controller.getRenderController().getSceneManager()); for (File dump : dumps) { sceneManager.mergeRenderDump(dump); } @@ -156,21 +150,6 @@ public PictureExportFormat fromString(String string) { fastFog.setTooltip(new Tooltip("Enable faster fog rendering algorithm.")); fastFog.selectedProperty() .addListener((observable, oldValue, newValue) -> scene.setFastFog(newValue)); - fancierTranslucency.setTooltip(new Tooltip("Enable more sophisticated algorithm for computing color changes through translucent materials.")); - fancierTranslucency.selectedProperty() - .addListener((observable, oldValue, newValue) -> { - scene.setFancierTranslucency(newValue); - transmissivityCap.setVisible(newValue); - transmissivityCap.setManaged(newValue); - }); - boolean tcapVisible = scene != null && scene.getFancierTranslucency(); - transmissivityCap.setVisible(tcapVisible); - transmissivityCap.setManaged(tcapVisible); - transmissivityCap.setName("Transmissivity cap"); - transmissivityCap.setRange(Scene.MIN_TRANSMISSIVITY_CAP, Scene.MAX_TRANSMISSIVITY_CAP); - transmissivityCap.clampBoth(); - transmissivityCap.setTooltip("Maximum amplification of one color channel as a ray passes through a translucent block (stained glass, ice, etc.).\nA value of 1 prevents amplification entirely; higher values result in more vibrant colors."); - transmissivityCap.onValueChange(value -> scene.setTransmissivityCap(value)); cacheResolution.setName("Sky cache resolution"); cacheResolution.setTooltip("Resolution of the sky cache. Lower values will use less memory and improve performance but can cause sky artifacts."); cacheResolution.setRange(1, 4096); @@ -193,7 +172,7 @@ public PictureExportFormat fromString(String string) { renderThreads.clampMin(); renderThreads.onValueChange(value -> { PersistentSettings.setNumRenderThreads(value); - controller.getRenderManager().setThreadCount(value); + controller.getRenderController().getRenderManager().setThreadCount(value); Chunky.setCommonThreadsCount(value); }); @@ -219,14 +198,11 @@ public PictureExportFormat fromString(String string) { octreeImplementation.setTooltip(new Tooltip(tooltipTextBuilder.toString())); octreeSwitchImplementation.setOnAction(event -> Chunky.getCommonThreads().submit(() -> { - TaskTracker tracker = controller.getSceneManager().getTaskTracker(); + TaskTracker tracker = controller.getRenderController().getSceneManager().getTaskTracker(); try { - try (TaskTracker.Task task = tracker.task("(1/2) Converting world octree", 1000)) { + try (TaskTracker.Task task = tracker.task("(1/1) Converting world octree", 1000)) { scene.getWorldOctree().switchImplementation(octreeImplementation.getValue(), task); } - try (TaskTracker.Task task = tracker.task("(2/2) Converting water octree")) { - scene.getWaterOctree().switchImplementation(octreeImplementation.getValue(), task); - } } catch (IOException e) { Log.error("Switching octrees failed. Reload the scene.\n", e); } @@ -270,36 +246,6 @@ public PictureExportFormat fromString(String string) { }); biomeStructureImplementation.setTooltip(new Tooltip(biomeStructureTooltipBuilder.toString())); - gridSize.setRange(4, 64); - gridSize.setName("Emitter grid size"); - gridSize.setTooltip("Size of the cells of the emitter grid. " + - "The bigger, the more emitter will be sampled. " + - "Need the chunks to be reloaded to apply"); - gridSize.onValueChange(value -> { - scene.setGridSize(value); - PersistentSettings.setGridSizeDefault(value); - }); - gridSize.addEventHandler(Adjuster.AFTER_VALUE_CHANGE, e -> { - if (scene.getEmitterSamplingStrategy() != EmitterSamplingStrategy.NONE && scene.haveLoadedChunks()) { - Alert warning = Dialogs.createAlert(Alert.AlertType.CONFIRMATION); - warning.setContentText("The selected chunks need to be reloaded to update the emitter grid size."); - warning.getButtonTypes().setAll( - ButtonType.CANCEL, - new ButtonType("Reload chunks", ButtonBar.ButtonData.FINISH)); - warning.setTitle("Chunk reload required"); - ButtonType result = warning.showAndWait().orElse(ButtonType.CANCEL); - if (result.getButtonData() == ButtonBar.ButtonData.FINISH) { - controller.getSceneManager().reloadChunks(); - } - } - }); - - preventNormalEmitterWithSampling.setTooltip(new Tooltip("Prevent usual emitter contribution when emitter sampling is used")); - preventNormalEmitterWithSampling.selectedProperty().addListener((observable, oldvalue, newvalue) -> { - scene.setPreventNormalEmitterWithSampling(newvalue); - PersistentSettings.setPreventNormalEmitterWithSampling(newvalue); - }); - hideUnknownBlocks.setTooltip(new Tooltip("Hide unknown blocks instead of rendering them as question marks.")); hideUnknownBlocks.selectedProperty().addListener((observable, oldValue, newValue) -> { scene.setHideUnknownBlocks(newValue); @@ -333,9 +279,7 @@ public boolean shutdownAfterCompletedRender() { @Override public void update(Scene scene) { outputMode.getSelectionModel().select(scene.getPictureExportFormat()); - fastFog.setSelected(scene.fog.fastFog()); - fancierTranslucency.setSelected(scene.getFancierTranslucency()); - transmissivityCap.set(scene.getTransmissivityCap()); + fastFog.setSelected(scene.fog.isFastFog()); renderThreads.set(PersistentSettings.getNumThreads()); cpuLoad.set(PersistentSettings.getCPULoad()); rayDepth.set(scene.getRayDepth()); @@ -343,8 +287,6 @@ public void update(Scene scene) { octreeImplementation.getSelectionModel().select(scene.getOctreeImplementation()); bvhMethod.getSelectionModel().select(scene.getBvhImplementation()); biomeStructureImplementation.getSelectionModel().select(scene.getBiomeStructureImplementation()); - gridSize.set(scene.getGridSize()); - preventNormalEmitterWithSampling.setSelected(scene.isPreventNormalEmitterWithSampling()); animationTime.set(scene.getAnimationTime()); hideUnknownBlocks.setSelected(scene.getHideUnknownBlocks()); rendererSelect.getSelectionModel().select(scene.getRenderer()); @@ -357,16 +299,13 @@ public String getTabTitle() { } @Override - public Node getTabContent() { + public VBox getTabContent() { return this; } @Override - public void setController(RenderControlsFxController controls) { - this.renderControls = controls; - this.controller = controls.getRenderController(); - scene = controller.getSceneManager().getScene(); - controller.getRenderManager().setOnRenderCompleted((time, sps) -> { + public void onSetController(RenderControlsFxController controls) { + controller.getRenderController().getRenderManager().setOnRenderCompleted((time, sps) -> { if(shutdownAfterCompletedRender()) { // TODO: rewrite the shutdown alert in JavaFX. new ShutdownAlert(null); @@ -375,7 +314,7 @@ public void setController(RenderControlsFxController controls) { // Set the renderers rendererSelect.getItems().clear(); - RenderManager renderManager = controller.getRenderManager(); + RenderManager renderManager = controller.getRenderController().getRenderManager(); ArrayList ids = new ArrayList<>(); for (Registerable renderer : renderManager.getRenderers()) diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/CameraTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/CameraTab.java index 0c875d7490..81ece935e6 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/CameraTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/CameraTab.java @@ -22,12 +22,13 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.map.MapView; import se.llbit.chunky.renderer.ApertureShape; @@ -50,14 +51,12 @@ import java.net.URL; import java.util.ResourceBundle; -public class CameraTab extends ScrollPane implements RenderControlsTab, Initializable { - private Scene scene; - +public class CameraTab extends RenderControlsTab implements Initializable { @FXML private MenuButton loadPreset; @FXML private ComboBox cameras; @FXML private Button duplicate; @FXML private Button removeCamera; - @FXML private CheckBox lockCamera; + @FXML private ToggleSwitch lockCamera; @FXML private TitledPane positionOrientation; @FXML private DoubleTextField posX; @FXML private DoubleTextField posY; @@ -106,7 +105,7 @@ public CameraTab() throws IOException { return "Camera"; } - @Override public Node getTabContent() { + @Override public VBox getTabContent() { return this; } @@ -405,7 +404,8 @@ private void updateCameraDirection() { update(scene); } - @Override public void setController(RenderControlsFxController controller) { + @Override + protected void onSetController(RenderControlsFxController controller) { this.mapView = controller.getChunkyController().getMapView(); this.cameraViewListener = controller.getChunkyController(); diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/EmittersTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EmittersTab.java new file mode 100644 index 0000000000..22bc589965 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EmittersTab.java @@ -0,0 +1,143 @@ +/* Copyright (c) 2016-2022 Jesper Öqvist + * Copyright (c) 2016-2022 Chunky Contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.ui.render.tabs; + +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.layout.VBox; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.PersistentSettings; +import se.llbit.chunky.renderer.scene.EmitterMappingType; +import se.llbit.chunky.renderer.scene.EmitterSamplingStrategy; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.Adjuster; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.fxutil.Dialogs; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class EmittersTab extends RenderControlsTab implements Initializable { + @FXML private DoubleAdjuster emitterIntensity; + @FXML private ChoiceBox emitterMappingType; + @FXML private DoubleAdjuster emitterMappingExponent; + @FXML private ChoiceBox emitterSamplingStrategy; + @FXML private IntegerAdjuster gridSize; + @FXML private ToggleSwitch preventNormalEmitterWithSampling; + + public EmittersTab() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("EmittersTab.fxml")); + loader.setRoot(this); + loader.setController(this); + loader.load(); + } + + @Override public void initialize(URL location, ResourceBundle resources) { + emitterIntensity.setName("Emitter intensity"); + emitterIntensity.setTooltip("Modifies the intensity of emitter light."); + emitterIntensity.setRange(Scene.MIN_EMITTER_INTENSITY, Scene.MAX_EMITTER_INTENSITY); + emitterIntensity.makeLogarithmic(); + emitterIntensity.clampMin(); + emitterIntensity.onValueChange(value -> scene.setEmitterIntensity(value)); + + emitterSamplingStrategy.getItems().addAll(EmitterSamplingStrategy.values()); + emitterSamplingStrategy.getSelectionModel().selectedItemProperty() + .addListener((observable, oldvalue, newvalue) -> { + scene.setEmitterSamplingStrategy(newvalue); + if (newvalue != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() == null && scene.haveLoadedChunks()) { + Alert warning = Dialogs.createAlert(AlertType.CONFIRMATION); + warning.setContentText("The selected chunks need to be reloaded in order for emitter sampling to work."); + warning.getButtonTypes().setAll( + ButtonType.CANCEL, + new ButtonType("Reload chunks", ButtonData.FINISH)); + warning.setTitle("Chunk reload required"); + ButtonType result = warning.showAndWait().orElse(ButtonType.CANCEL); + if (result.getButtonData() == ButtonData.FINISH) { + controller.getRenderController().getSceneManager().reloadChunks(); + } + } + }); + emitterSamplingStrategy.setTooltip(new Tooltip("Determine how emitters are sampled at each bounce.")); + + gridSize.setRange(4, 64); + gridSize.setName("Emitter grid size"); + gridSize.setTooltip("Size of the cells of the emitter grid. " + + "The bigger, the more emitter will be sampled. " + + "Need the chunks to be reloaded to apply"); + gridSize.onValueChange(value -> { + scene.setGridSize(value); + PersistentSettings.setGridSizeDefault(value); + }); + gridSize.addEventHandler(Adjuster.AFTER_VALUE_CHANGE, e -> { + if (scene.getEmitterSamplingStrategy() != EmitterSamplingStrategy.NONE && scene.haveLoadedChunks()) { + Alert warning = Dialogs.createAlert(Alert.AlertType.CONFIRMATION); + warning.setContentText("The selected chunks need to be reloaded to update the emitter grid size."); + warning.getButtonTypes().setAll( + ButtonType.CANCEL, + new ButtonType("Reload chunks", ButtonBar.ButtonData.FINISH)); + warning.setTitle("Chunk reload required"); + ButtonType result = warning.showAndWait().orElse(ButtonType.CANCEL); + if (result.getButtonData() == ButtonBar.ButtonData.FINISH) { + controller.getRenderController().getSceneManager().reloadChunks(); + } + } + }); + + preventNormalEmitterWithSampling.setTooltip(new Tooltip("Prevent usual emitter contribution when emitter sampling is used")); + preventNormalEmitterWithSampling.selectedProperty().addListener((observable, oldvalue, newvalue) -> { + scene.setPreventNormalEmitterWithSampling(newvalue); + PersistentSettings.setPreventNormalEmitterWithSampling(newvalue); + }); + + emitterMappingType.getItems().addAll(EmitterMappingType.values()); + emitterMappingType.getItems().remove(EmitterMappingType.NONE); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> scene.setEmitterMappingType(newValue)); + emitterMappingType.setTooltip(new Tooltip("Determines how per-pixel light emission is computed.")); + + emitterMappingExponent.setName("Emitter mapping exponent"); + emitterMappingExponent.setTooltip("Determines how much light is emitted from darker or lighter pixels.\nHigher values will result in darker pixels emitting less light."); + emitterMappingExponent.setRange(Scene.MIN_EMITTER_MAPPING_EXPONENT, Scene.MAX_EMITTER_MAPPING_EXPONENT); + emitterMappingExponent.clampMin(); + emitterMappingExponent.onValueChange(value -> scene.setEmitterMappingExponent(value)); + } + + @Override public void update(Scene scene) { + emitterIntensity.set(scene.getEmitterIntensity()); + emitterSamplingStrategy.getSelectionModel().select(scene.getEmitterSamplingStrategy()); + gridSize.set(scene.getGridSize()); + preventNormalEmitterWithSampling.setSelected(scene.isPreventNormalEmitterWithSampling()); + emitterMappingExponent.set(scene.getEmitterMappingExponent()); + emitterMappingType.getSelectionModel().select(scene.getEmitterMappingType()); + } + + @Override public String getTabTitle() { + return "Emitters"; + } + + @Override public VBox getTabContent() { + return this; + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/EntitiesTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EntitiesTab.java index 5b58605e50..673ac3dc33 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/EntitiesTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EntitiesTab.java @@ -16,60 +16,47 @@ */ package se.llbit.chunky.ui.render.tabs; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.function.Consumer; + import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; +import javafx.scene.layout.*; import se.llbit.chunky.entity.*; -import se.llbit.chunky.renderer.scene.PlayerModel; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.ui.DoubleAdjuster; import se.llbit.chunky.ui.DoubleTextField; -import se.llbit.chunky.ui.IntegerAdjuster; -import se.llbit.chunky.ui.IntegerTextField; -import se.llbit.chunky.ui.controller.RenderControlsFxController; -import se.llbit.chunky.ui.dialogs.ValidatingTextInputDialog; +import se.llbit.chunky.ui.dialogs.AddEntityDialog; +import se.llbit.chunky.ui.dialogs.EditMaterialDialog; import se.llbit.chunky.ui.elements.AngleAdjuster; +import se.llbit.chunky.ui.DoubleAdjuster; import se.llbit.chunky.ui.render.RenderControlsTab; -import se.llbit.chunky.world.material.BeaconBeamMaterial; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import se.llbit.chunky.entity.*; import se.llbit.chunky.world.material.DyedTextureMaterial; import se.llbit.fx.LuxColorPicker; -import se.llbit.fxutil.Dialogs; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; -import se.llbit.log.Log; import se.llbit.math.ColorUtil; import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import se.llbit.util.mojangapi.MinecraftProfile; -import se.llbit.util.mojangapi.MinecraftSkin; -import se.llbit.util.mojangapi.MojangApi; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.Set; -import java.util.function.Consumer; - -public class EntitiesTab extends ScrollPane implements RenderControlsTab, Initializable { - private static final Map> entityTypes = new HashMap<>(); +public class EntitiesTab extends RenderControlsTab implements Initializable { + public static final Map> entityTypes = new HashMap<>(); static { entityTypes.put("Player", (position, scene) -> { @@ -93,6 +80,7 @@ public class EntitiesTab extends ScrollPane implements RenderControlsTab, Initia entityTypes.put("Lectern", (position, scene) -> new Lectern(position, "north", true)); entityTypes.put("Book", (position, scene) -> new Book(position, Math.PI - Math.PI / 16, Math.toRadians(30), Math.toRadians(180 - 30))); entityTypes.put("Beacon beam", (position, scene) -> new BeaconBeam(position)); + entityTypes.put("Sphere", (position, scene) -> new SphereEntity(position, 0.5)); entityTypes.put("Sheep", (position, scene) -> new SheepEntity(position, new CompoundTag())); entityTypes.put("Cow", (position, scene) -> new CowEntity(position, new CompoundTag())); entityTypes.put("Chicken", (position, scene) -> new ChickenEntity(position, new CompoundTag())); @@ -101,7 +89,22 @@ public class EntitiesTab extends ScrollPane implements RenderControlsTab, Initia entityTypes.put("Squid", (position, scene) -> new SquidEntity(position, new CompoundTag())); } - private Scene scene; + public enum EntityPlacement { + TARGET("Preview target position"), + CAMERA("Camera position"), + POSITION("Specific position"); + + private final String name; + + EntityPlacement(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } public interface EntityType { T createInstance(Vector3 position, Scene scene); @@ -160,6 +163,7 @@ public String getKind() { @FXML private TableColumn kindCol; @FXML private Button delete; @FXML private Button add; + @FXML private Button clear; @FXML private Button cameraToEntity; @FXML private Button entityToCamera; @FXML private Button entityToTarget; @@ -170,7 +174,8 @@ public String getKind() { @FXML private DoubleTextField posY; @FXML private DoubleTextField posZ; @FXML private VBox controls; - @FXML private ComboBox entityType; + + private final AddEntityDialog addEntityDialog = new AddEntityDialog(); public EntitiesTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("EntitiesTab.fxml")); @@ -190,6 +195,13 @@ public void update(Scene scene) { } missing.remove(data); } + for (Entity entity : scene.getEntities()) { + EntityData data = new EntityData(entity, scene); + if (!entityTable.getItems().contains(data)) { + entityTable.getItems().add(data); + } + missing.remove(data); + } entityTable.getItems().removeAll(missing); } @@ -199,310 +211,50 @@ public String getTabTitle() { } @Override - public Node getTabContent() { + public VBox getTabContent() { return this; } private void updateEntity(Entity entity) { controls.getChildren().clear(); - if (entity instanceof Poseable) { - Poseable poseable = (Poseable) entity; - if (entity instanceof PlayerEntity) { - PlayerEntity player = (PlayerEntity) entity; - ChoiceBox playerModel = new ChoiceBox<>(); - playerModel.getSelectionModel().select(((PlayerEntity) entity).model); - playerModel.getItems().addAll(PlayerModel.values()); - playerModel.getSelectionModel().selectedItemProperty().addListener( - (observable, oldValue, newValue) -> { - player.model = newValue; - scene.rebuildActorBvh(); - }); - HBox modelBox = new HBox(); - modelBox.setSpacing(10.0); - modelBox.setAlignment(Pos.CENTER_LEFT); - modelBox.getChildren().addAll(new Label("Player model:"), playerModel); - - HBox skinBox = new HBox(); - skinBox.setSpacing(10.0); - skinBox.setAlignment(Pos.CENTER_LEFT); - TextField skinField = new TextField(); - skinField.setText(((PlayerEntity) entity).skin); - Button selectSkin = new Button("Select skin..."); - selectSkin.setOnAction(e -> { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Load Skin"); - fileChooser - .getExtensionFilters() - .add(new FileChooser.ExtensionFilter("Minecraft skin", "*.png")); - File skinFile = fileChooser.showOpenDialog(getScene().getWindow()); - if (skinFile != null) { - player.setTexture(skinFile.getAbsolutePath()); - skinField.setText(skinFile.getAbsolutePath()); - scene.rebuildActorBvh(); - } - }); - Button downloadSkin = new Button("Download skin..."); - downloadSkin.setOnAction(e -> { - TextInputDialog playerIdentifierInput = new ValidatingTextInputDialog(input -> input != null && !input.isEmpty()); - playerIdentifierInput.setTitle("Input player identifier"); - playerIdentifierInput.setHeaderText("Please enter the UUID or name of the player."); - playerIdentifierInput.setContentText("UUID / player name:"); - Dialogs.setupDialogDesign(playerIdentifierInput, getScene()); - playerIdentifierInput.showAndWait().map(playerIdentifier -> { - try { - // TODO: refactor this (deduplicate code, check UUID format, trim input, better error handling) - MinecraftProfile profile = MojangApi.fetchProfile(playerIdentifier); //Search by uuid - Optional skin = profile.getSkin(); - if (skin.isPresent()) { // If it found a skin, pass it back to caller - downloadAndApplySkinForPlayer( - skin.get(), - player, - playerModel, - skinField - ); - return true; - } else { // Otherwise, search by Username - String uuid = MojangApi.usernameToUUID(playerIdentifier); - profile = MojangApi.fetchProfile(uuid); - skin = profile.getSkin(); - if (skin.isPresent()) { - downloadAndApplySkinForPlayer( - skin.get(), - player, - playerModel, - skinField - ); - return true; - } else { //If still not found, warn user. - Log.warn("Could not find player with that identifier"); - } - } - } catch (IOException ex) { - Log.warn("Could not download skin", ex); - } - return false; - }); - }); - skinBox.getChildren().addAll(new Label("Skin:"), skinField, selectSkin, downloadSkin); - CheckBox showOuterLayer = new CheckBox("Show second layer"); - showOuterLayer.setSelected(player.showOuterLayer); - showOuterLayer.selectedProperty().addListener(((observable, oldValue, newValue) -> { - player.showOuterLayer = newValue; - scene.rebuildActorBvh(); - })); - HBox layerBox = new HBox(); - layerBox.setSpacing(10.0); - layerBox.setAlignment(Pos.CENTER_LEFT); - layerBox.getChildren().addAll(showOuterLayer); + updatePositionFields(entity); + posX.valueProperty().addListener((observable, oldValue, newValue) -> { + withEntity(e -> { + Vector3 currentPosition = e.getPosition(); + e.setPosition(new Vector3(newValue.doubleValue(), currentPosition.y, currentPosition.z)); + }); + scene.rebuildBvh(); + scene.rebuildActorBvh(); + }); + posY.valueProperty().addListener((observable, oldValue, newValue) -> { + withEntity(e -> { + Vector3 currentPosition = e.getPosition(); + e.setPosition(new Vector3(currentPosition.x, newValue.doubleValue(), currentPosition.z)); + }); + scene.rebuildBvh(); + scene.rebuildActorBvh(); + }); + posZ.valueProperty().addListener((observable, oldValue, newValue) -> { + withEntity(e -> { + Vector3 currentPosition = e.getPosition(); + e.setPosition(new Vector3(currentPosition.x, currentPosition.y, newValue.doubleValue())); + }); + scene.rebuildBvh(); + scene.rebuildActorBvh(); + }); - controls.getChildren().addAll(modelBox, skinBox, layerBox); - } - else if (entity instanceof Book || entity instanceof Lectern) { - Book book; - if (entity instanceof Lectern) { - book = ((Lectern) entity).getBook(); - } else { - book = (Book) entity; - } + controls.getChildren().add(position); + position.setVisible(true); - if (book != null) { - DoubleAdjuster openingAngle = new DoubleAdjuster(); - openingAngle.setName("Opening angle"); - openingAngle.setTooltip("Modifies the book's opening angle."); - openingAngle.set(Math.toDegrees(book.getOpenAngle())); - openingAngle.setRange(0, 180); - openingAngle.onValueChange(value -> { - book.setOpenAngle(Math.toRadians(value)); - scene.rebuildActorBvh(); - }); - controls.getChildren().add(openingAngle); - - DoubleAdjuster page1Angle = new DoubleAdjuster(); - page1Angle.setName("Page 1 angle"); - page1Angle.setTooltip("Modifies the book's first visible page's angle."); - page1Angle.set(Math.toDegrees(book.getPageAngleA())); - page1Angle.setRange(0, 180); - page1Angle.onValueChange(value -> { - book.setPageAngleA(Math.toRadians(value)); - scene.rebuildActorBvh(); - }); - controls.getChildren().add(page1Angle); - - DoubleAdjuster page2Angle = new DoubleAdjuster(); - page2Angle.setName("Page 2 angle"); - page2Angle.setTooltip("Modifies the book's second visible page's angle."); - page2Angle.set(Math.toDegrees(book.getPageAngleB())); - page2Angle.setRange(0, 180); - page2Angle.onValueChange(value -> { - book.setPageAngleB(Math.toRadians(value)); - scene.rebuildActorBvh(); - }); - controls.getChildren().add(page2Angle); - } - } - else if (entity instanceof BeaconBeam) { - BeaconBeam beam = (BeaconBeam) entity; - IntegerAdjuster height = new IntegerAdjuster(); - height.setName("Height"); - height.setTooltip("Modifies the height of the beam. Useful if your scene is taller than the world height."); - height.set(beam.getHeight()); - height.setRange(1, 512); - height.onValueChange(value -> { - beam.setHeight(value); - scene.rebuildActorBvh(); - }); - controls.getChildren().add(height); - - HBox beamColor = new HBox(); - VBox listControls = new VBox(); - VBox propertyControls = new VBox(); - - listControls.setMaxWidth(200); - beamColor.setPadding(new Insets(10)); - beamColor.setSpacing(15); - propertyControls.setSpacing(10); - - DoubleAdjuster emittance = new DoubleAdjuster(); - emittance.setName("Emittance"); - emittance.setRange(0, 100); - - DoubleAdjuster specular = new DoubleAdjuster(); - specular.setName("Specular"); - specular.setRange(0, 1); - - DoubleAdjuster ior = new DoubleAdjuster(); - ior.setName("IoR"); - ior.setRange(0, 5); - - DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); - perceptualSmoothness.setName("Smoothness"); - perceptualSmoothness.setRange(0, 1); - - DoubleAdjuster metalness = new DoubleAdjuster(); - metalness.setName("Metalness"); - metalness.setRange(0, 1); - - LuxColorPicker beamColorPicker = new LuxColorPicker(); - - ObservableList colorHeights = FXCollections.observableArrayList(); - colorHeights.addAll(beam.getMaterials().keySet()); - ListView colorHeightList = new ListView<>(colorHeights); - colorHeightList.setMaxHeight(150.0); - colorHeightList.getSelectionModel().selectedItemProperty().addListener( - (observable, oldValue, heightIndex) -> { - - BeaconBeamMaterial beamMat = beam.getMaterials().get(heightIndex); - emittance.set(beamMat.emittance); - specular.set(beamMat.specular); - ior.set(beamMat.ior); - perceptualSmoothness.set(beamMat.getPerceptualSmoothness()); - metalness.set(beamMat.metalness); - beamColorPicker.setColor(ColorUtil.toFx(beamMat.getColorInt())); - - emittance.onValueChange(value -> { - beamMat.emittance = value.floatValue(); - scene.rebuildActorBvh(); - }); - specular.onValueChange(value -> { - beamMat.specular = value.floatValue(); - scene.rebuildActorBvh(); - }); - ior.onValueChange(value -> { - beamMat.ior = value.floatValue(); - scene.rebuildActorBvh(); - }); - perceptualSmoothness.onValueChange(value -> { - beamMat.setPerceptualSmoothness(value); - scene.rebuildActorBvh(); - }); - metalness.onValueChange(value -> { - beamMat.metalness = value.floatValue(); - scene.rebuildActorBvh(); - }); - } - ); - beamColorPicker.colorProperty().addListener( - (observableColor, oldColorValue, newColorValue) -> { - Integer index = colorHeightList.getSelectionModel().getSelectedItem(); - if (index != null) { - beam.getMaterials().get(index).updateColor(ColorUtil.getRGB(ColorUtil.fromFx(newColorValue))); - scene.rebuildActorBvh(); - } - } - ); - - HBox listButtons = new HBox(); - listButtons.setPadding(new Insets(10)); - listButtons.setSpacing(15); - Button deleteButton = new Button("Delete"); - deleteButton.setOnAction(e -> { - Integer index = colorHeightList.getSelectionModel().getSelectedItem(); - if (index != null && index != 0) { //Prevent removal of the bottom layer - beam.getMaterials().remove(index); - colorHeightList.getItems().removeAll(index); - scene.rebuildActorBvh(); - } - }); - IntegerTextField layerInput = new IntegerTextField(); - layerInput.setMaxWidth(50); - Button addButton = new Button("Add"); - addButton.setOnAction(e -> { - if (!beam.getMaterials().containsKey(layerInput.valueProperty().get())) { //Don't allow duplicate indices - beam.getMaterials().put(layerInput.valueProperty().get(), new BeaconBeamMaterial(BeaconBeamMaterial.DEFAULT_COLOR)); - colorHeightList.getItems().add(layerInput.valueProperty().get()); - scene.rebuildActorBvh(); - } - }); + controls.getChildren().add(new Separator()); - listButtons.getChildren().addAll(deleteButton, layerInput, addButton); - propertyControls.getChildren().addAll(emittance, specular, perceptualSmoothness, ior, metalness, beamColorPicker); - listControls.getChildren().addAll(new Label("Start Height:"), colorHeightList, listButtons); - beamColor.getChildren().addAll(listControls, propertyControls); - controls.getChildren().add(beamColor); - } - else if (entity instanceof SheepEntity) { - SheepEntity sheep = (SheepEntity) entity; - CheckBox showOuterLayer = new CheckBox("Is Sheared?"); - showOuterLayer.setPadding(new Insets(10)); - showOuterLayer.setSelected(sheep.sheared); - showOuterLayer.selectedProperty().addListener(((observable, oldValue, newValue) -> { - sheep.sheared = newValue; - scene.rebuildActorBvh(); - })); - HBox layerBox = new HBox(); - layerBox.setSpacing(10.0); - layerBox.setAlignment(Pos.CENTER_LEFT); - layerBox.getChildren().addAll(showOuterLayer); + controls.getChildren().add(entity.getControls(this)); - controls.getChildren().addAll(layerBox); - } + controls.getChildren().add(new Separator()); - updatePositionFields(entity); - posX.valueProperty().addListener((observable, oldValue, newValue) -> { - withEntity(e -> { - Vector3 currentPosition = e.getPosition(); - e.setPosition(new Vector3(newValue.doubleValue(), currentPosition.y, currentPosition.z)); - }); - scene.rebuildActorBvh(); - }); - posY.valueProperty().addListener((observable, oldValue, newValue) -> { - withEntity(e -> { - Vector3 currentPosition = e.getPosition(); - e.setPosition(new Vector3(currentPosition.x, newValue.doubleValue(), currentPosition.z)); - }); - scene.rebuildActorBvh(); - }); - posZ.valueProperty().addListener((observable, oldValue, newValue) -> { - withEntity(e -> { - Vector3 currentPosition = e.getPosition(); - e.setPosition(new Vector3(currentPosition.x, currentPosition.y, newValue.doubleValue())); - }); - scene.rebuildActorBvh(); - }); - - controls.getChildren().add(position); - position.setVisible(true); + if (entity instanceof Poseable) { + Poseable poseable = (Poseable) entity; DoubleAdjuster scale = new DoubleAdjuster(); scale.setName("Scale"); @@ -588,6 +340,8 @@ else if (entity instanceof SheepEntity) { } } + controls.getChildren().add(new Separator()); + if (entity instanceof Geared) { Geared geared = (Geared) entity; controls.getChildren().addAll(new Label("Gear:")); @@ -631,148 +385,96 @@ else if (entity instanceof SheepEntity) { variantHBox.getChildren().addAll(new Label("Variant:"), variantBox); controls.getChildren().addAll(variantHBox); - - if (entity instanceof MooshroomEntity) { - MooshroomEntity mooshroom = (MooshroomEntity) entity; - CheckBox showMushrooms = new CheckBox("Show Mushrooms?"); - showMushrooms.setPadding(new Insets(10)); - showMushrooms.setSelected(mooshroom.showMushrooms); - showMushrooms.selectedProperty().addListener(((observable, oldValue, newValue) -> { - mooshroom.showMushrooms = newValue; - scene.rebuildActorBvh(); - })); - HBox mushroomBox = new HBox(); - mushroomBox.setSpacing(10.0); - mushroomBox.setAlignment(Pos.CENTER_LEFT); - mushroomBox.getChildren().addAll(showMushrooms); - - controls.getChildren().addAll(mushroomBox); - } } if (entity instanceof Dyeable) { Dyeable dyedEntity = (Dyeable) entity; - HBox colorBox = new HBox(); - VBox propertyControls = new VBox(); - - colorBox.setPadding(new Insets(10)); - colorBox.setSpacing(15); - propertyControls.setSpacing(10); - - DoubleAdjuster emittance = new DoubleAdjuster(); - emittance.setName("Emittance"); - emittance.setRange(0, 100); - - DoubleAdjuster specular = new DoubleAdjuster(); - specular.setName("Specular"); - specular.setRange(0, 1); - - DoubleAdjuster ior = new DoubleAdjuster(); - ior.setName("IoR"); - ior.setRange(0, 5); - - DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); - perceptualSmoothness.setName("Smoothness"); - perceptualSmoothness.setRange(0, 1); - - DoubleAdjuster metalness = new DoubleAdjuster(); - metalness.setName("Metalness"); - metalness.setRange(0, 1); + DyedTextureMaterial material = dyedEntity.getMaterial(); LuxColorPicker sheepColorPicker = new LuxColorPicker(); - - DyedTextureMaterial material = dyedEntity.getMaterial(); - emittance.set(material.emittance); - specular.set(material.specular); - ior.set(material.ior); - perceptualSmoothness.set(material.getPerceptualSmoothness()); - metalness.set(material.metalness); sheepColorPicker.setColor(ColorUtil.toFx(material.getColorInt())); - - emittance.onValueChange(value -> { - material.emittance = value.floatValue(); - scene.rebuildActorBvh(); - }); - specular.onValueChange(value -> { - material.specular = value.floatValue(); - scene.rebuildActorBvh(); - }); - ior.onValueChange(value -> { - material.ior = value.floatValue(); - scene.rebuildActorBvh(); - }); - perceptualSmoothness.onValueChange(value -> { - material.setPerceptualSmoothness(value); - scene.rebuildActorBvh(); - }); - metalness.onValueChange(value -> { - material.metalness = value.floatValue(); - scene.rebuildActorBvh(); - }); sheepColorPicker.colorProperty().addListener( (observableColor, oldColorValue, newColorValue) -> { dyedEntity.getMaterial().updateColor(ColorUtil.getRGB(ColorUtil.fromFx(newColorValue))); - scene.rebuildActorBvh(); } ); - propertyControls.getChildren().addAll(emittance, specular, perceptualSmoothness, ior, metalness, sheepColorPicker); - colorBox.getChildren().addAll(propertyControls); - controls.getChildren().add(colorBox); + Button editMaterialButton = new Button("Edit material"); + editMaterialButton.setOnAction(e -> new EditMaterialDialog(material, scene).showAndWait()); + + controls.getChildren().addAll(sheepColorPicker, editMaterialButton); } if (entity instanceof Saddleable) { Saddleable saddleable = (Saddleable) entity; CheckBox showOuterLayer = new CheckBox("Is Saddled?"); - showOuterLayer.setPadding(new Insets(10)); showOuterLayer.setSelected(saddleable.isSaddled()); showOuterLayer.selectedProperty().addListener(((observable, oldValue, newValue) -> { saddleable.setIsSaddled(newValue); scene.rebuildActorBvh(); })); - HBox layerBox = new HBox(); - layerBox.setSpacing(10.0); - layerBox.setAlignment(Pos.CENTER_LEFT); - layerBox.getChildren().addAll(showOuterLayer); - controls.getChildren().addAll(layerBox); + controls.getChildren().addAll(showOuterLayer); } } @Override public void initialize(URL location, ResourceBundle resources) { - entityType.getItems().addAll(entityTypes.keySet()); - entityType.setValue("Player"); add.setTooltip(new Tooltip("Add an entity at the target position.")); add.setOnAction(e -> { - Vector3 position = scene.getTargetPosition(); - if (position == null) { - position = new Vector3(scene.camera().getPosition()); - } + if (addEntityDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + EntityType entityType = addEntityDialog.getType(); + EntityPlacement entityPlacement = addEntityDialog.getPlacement(); + + Vector3 position; + switch (entityPlacement) { + case POSITION: + position = addEntityDialog.getPosition(); + break; + + case TARGET: + position = scene.getTargetPosition(); + if (position == null) { + position = new Vector3(scene.camera().getPosition()); + } + break; - Entity entity = entityTypes.get(entityType.getValue()).createInstance(position, scene); - if (entity instanceof PlayerEntity) { - PlayerEntity player = (PlayerEntity) entity; - withEntity(selected -> { - if (selected instanceof PlayerEntity) { - player.skin = ((PlayerEntity) selected).skin; - player.model = ((PlayerEntity) selected).model; - } - }); - scene.addPlayer(player); - } else { - scene.addActor(entity); + case CAMERA: + default: + position = new Vector3(scene.camera().getPosition()); + } + + Entity entity = entityType.createInstance(position, scene); + if (entity instanceof PlayerEntity) { + PlayerEntity player = (PlayerEntity) entity; + withEntity(selected -> { + if (selected instanceof PlayerEntity) { + player.skin = ((PlayerEntity) selected).skin; + player.model = ((PlayerEntity) selected).model; + } + }); + scene.addPlayer(player); + + } else { + scene.addActor(entity); + } + + EntityData data = new EntityData(entity, scene); + entityTable.getItems().add(data); + entityTable.getSelectionModel().select(data); } - EntityData data = new EntityData(entity, scene); - entityTable.getItems().add(data); - entityTable.getSelectionModel().select(data); }); delete.setTooltip(new Tooltip("Delete the selected entity.")); delete.setOnAction(e -> withEntity(entity -> { scene.removeEntity(entity); update(scene); })); + clear.setTooltip(new Tooltip("Remove all entities from the scene.")); + clear.setOnAction(e -> { + scene.clearEntities(); + update(scene); + }); // TODO: remove or update the pose editing dialog. /*entityTable.setRowFactory(tbl -> { TableRow row = new TableRow<>(); @@ -866,25 +568,4 @@ private void updatePositionFields(Entity entity) { posY.valueProperty().set(entity.position.y); posZ.valueProperty().set(entity.position.z); } - - @Override - public void setController(RenderControlsFxController controller) { - scene = controller.getRenderController().getSceneManager().getScene(); - } - - private void downloadAndApplySkinForPlayer( - MinecraftSkin skin, - PlayerEntity player, - ChoiceBox playerModelSelector, - TextField skinField - ) throws IOException { - if (skin != null) { - String filePath = MojangApi.downloadSkin(skin.getSkinUrl()).getAbsolutePath(); - player.setTexture(filePath); - playerModelSelector.getSelectionModel().select(skin.getPlayerModel()); - skinField.setText(filePath); - Log.info("Successfully set skin"); - scene.rebuildActorBvh(); - } - } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EnvironmentTab.java similarity index 59% rename from chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java rename to chunky/src/java/se/llbit/chunky/ui/render/tabs/EnvironmentTab.java index ea604b6d70..35f2646aa7 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/EnvironmentTab.java @@ -24,31 +24,32 @@ import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; -import javafx.scene.control.ComboBox; import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; import javafx.util.StringConverter; -import se.llbit.chunky.renderer.scene.FogMode; -import se.llbit.chunky.renderer.scene.Scene; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.SunSamplingStrategy; +import se.llbit.chunky.renderer.scene.*; import se.llbit.chunky.renderer.scene.sky.SimulatedSky; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.renderer.scene.sky.Sky; +import se.llbit.chunky.renderer.scene.sky.Sun; import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.elements.AngleAdjuster; import se.llbit.chunky.ui.elements.GradientEditor; import se.llbit.chunky.ui.controller.RenderControlsFxController; import se.llbit.chunky.ui.render.RenderControlsTab; -import se.llbit.chunky.ui.render.settings.LayeredFogSettings; -import se.llbit.chunky.ui.render.settings.UniformFogSettings; import se.llbit.chunky.ui.render.settings.SkyboxSettings; import se.llbit.chunky.ui.render.settings.SkymapSettings; import se.llbit.fx.LuxColorPicker; import se.llbit.math.ColorUtil; +import se.llbit.math.QuickMath; import se.llbit.math.Vector4; import java.io.IOException; @@ -56,61 +57,67 @@ import java.util.List; import java.util.ResourceBundle; -public class SkyTab extends ScrollPane implements RenderControlsTab, Initializable { - private Scene scene; - +public class EnvironmentTab extends RenderControlsTab implements Initializable { + @FXML private DoubleAdjuster skyEmittance; @FXML private ChoiceBox skyMode; @FXML private TitledPane detailsPane; @FXML private VBox skyModeSettings; @FXML private CheckBox transparentSkyEnabled; - @FXML private CheckBox cloudsEnabled; - @FXML private DoubleAdjuster cloudSize; - @FXML private DoubleAdjuster cloudX; - @FXML private DoubleAdjuster cloudY; - @FXML private DoubleAdjuster cloudZ; - @FXML private ComboBox fogMode; - @FXML private TitledPane fogDetailsPane; - @FXML private VBox fogDetailsBox; + + @FXML private DoubleAdjuster sunIntensity; + @FXML private ToggleSwitch drawSun; + @FXML private ToggleSwitch useFlatTexture; + @FXML private ChoiceBox sunSamplingStrategy; + @FXML private DoubleAdjuster sunRadius; + @FXML private AngleAdjuster sunAzimuth; + @FXML private AngleAdjuster sunAltitude; + @FXML private LuxColorPicker sunColor; + private final VBox simulatedSettings = new VBox(); - private DoubleAdjuster horizonOffset = new DoubleAdjuster(); - private ChoiceBox simulatedSky = new ChoiceBox<>(); + private final ChoiceBox simulatedSky = new ChoiceBox<>(); private final GradientEditor gradientEditor = new GradientEditor(this); private final LuxColorPicker colorPicker = new LuxColorPicker(); private final VBox colorEditor = new VBox(colorPicker); private final SkyboxSettings skyboxSettings = new SkyboxSettings(); private final SkymapSettings skymapSettings = new SkymapSettings(); - private final UniformFogSettings uniformFogSettings = new UniformFogSettings(); - private final LayeredFogSettings layeredFogSettings = new LayeredFogSettings(); - private ChangeListener skyColorListener = + private final ChangeListener skyColorListener = (observable, oldValue, newValue) -> scene.sky().setColor(ColorUtil.fromFx(newValue)); - private EventHandler simSkyListener = event -> { + + private final ChangeListener sunColorListener = + (observable, oldValue, newValue) -> scene.sun().setColor(ColorUtil.fromFx(newValue)); + private final EventHandler simSkyListener = event -> { int selected = simulatedSky.getSelectionModel().getSelectedIndex(); scene.sky().setSimulatedSkyMode(selected); + getSimulatedSkySettings(); }; - public SkyTab() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("SkyTab.fxml")); + private void getSimulatedSkySettings() { + if (simulatedSettings.getChildren().size() == 2) { + simulatedSettings.getChildren().remove(1); + } + simulatedSettings.getChildren().add(scene.sky().getSimulatedSky().getControls(this)); + } + + public EnvironmentTab() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("EnvironmentTab.fxml")); loader.setRoot(this); loader.setController(this); loader.load(); } - @Override public void setController(RenderControlsFxController controller) { - scene = controller.getRenderController().getSceneManager().getScene(); + @Override protected void onSetController(RenderControlsFxController controller) { skyboxSettings.setRenderController(controller.getRenderController()); skymapSettings.setRenderController(controller.getRenderController()); - uniformFogSettings.setRenderController(controller.getRenderController()); - layeredFogSettings.setRenderController(controller.getRenderController()); } @Override public void initialize(URL location, ResourceBundle resources) { - simulatedSettings.getChildren().add(horizonOffset); - horizonOffset.setName("Horizon offset"); - horizonOffset.setTooltip("Moves the simulated horizon."); - horizonOffset.setRange(0, 1); - horizonOffset.clampBoth(); - horizonOffset.onValueChange(value -> scene.sky().setHorizonOffset(value)); + skyEmittance.setName("Sky intensity"); + skyEmittance.setTooltip("Changes the intensity of the sky light."); + skyEmittance.setRange(Sky.MIN_INTENSITY, Sky.MAX_INTENSITY); + skyEmittance.makeLogarithmic(); + skyEmittance.clampMin(); + skyEmittance.onValueChange(value -> scene.sky().setSkyEmittance(value)); HBox simulatedSkyBox = new HBox(new Label("Sky Mode:"), simulatedSky); simulatedSkyBox.setSpacing(10); @@ -137,42 +144,40 @@ public SimulatedSky fromString(String string) { simulatedSky.setOnAction(simSkyListener); simulatedSky.setTooltip(new Tooltip(skiesTooltip(Sky.skies))); - cloudSize.setName("Cloud size"); - cloudSize.setTooltip("Cloud size, measured in blocks per pixel of clouds.png texture"); - cloudSize.setRange(0.1, 128); - cloudSize.clampMin(); - cloudSize.makeLogarithmic(); - cloudSize.onValueChange(value -> scene.sky().setCloudSize(value)); + sunIntensity.setName("Sunlight intensity"); + sunIntensity.setTooltip("Changes the intensity of sunlight. Only used when Sun Sampling Strategy is set to FAST or HIGH_QUALITY."); + sunIntensity.setRange(Sun.MIN_INTENSITY, Sun.MAX_INTENSITY); + sunIntensity.makeLogarithmic(); + sunIntensity.clampMin(); + sunIntensity.onValueChange(value -> scene.sun().setIntensity(value)); - cloudX.setTooltip("Cloud X offset."); - cloudX.setRange(-256, 256); - cloudX.onValueChange(value -> scene.sky().setCloudXOffset(value)); - cloudY.setTooltip("Cloud Y offset."); - cloudY.setRange(-64, 320); - cloudY.onValueChange(value -> scene.sky().setCloudYOffset(value)); - cloudZ.setTooltip("Cloud Z offset."); - cloudZ.setRange(-256, 256); - cloudZ.onValueChange(value -> scene.sky().setCloudZOffset(value)); + drawSun.selectedProperty().addListener((observable, oldValue, newValue) -> scene.sun().setDrawTexture(newValue)); + drawSun.setTooltip(new Tooltip("Draws the sun texture on top of the skymap.")); - fogMode.getItems().addAll(FogMode.values()); - fogMode.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - scene.setFogMode(newValue); - switch (newValue) { - case NONE: { - fogDetailsBox.getChildren().setAll(new Label("Selected mode has no settings.")); - break; - } - case UNIFORM: { - fogDetailsBox.getChildren().setAll(uniformFogSettings); - break; - } - case LAYERED: { - fogDetailsBox.getChildren().setAll(layeredFogSettings); - break; - } - } - fogDetailsPane.setExpanded(true); - }); + useFlatTexture.selectedProperty().addListener((observable, oldValue, newValue) -> scene.sun().setUseFlatTexture(newValue)); + useFlatTexture.setTooltip(new Tooltip("Use a solid color as the sun texture.")); + + sunSamplingStrategy.getItems().addAll(SunSamplingStrategy.values()); + sunSamplingStrategy.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> scene.setSunSamplingStrategy(newValue)); + sunSamplingStrategy.setTooltip(new Tooltip("Determines how the sun is sampled at each bounce.")); + + sunRadius.setName("Sun size"); + sunRadius.setTooltip("Sun radius in degrees."); + sunRadius.setRange(0.01, 20); + sunRadius.clampMin(); + sunRadius.onValueChange(value -> scene.sun().setSunRadius(Math.toRadians(value))); + + sunAzimuth.setName("Sun azimuth"); + sunAzimuth.setTooltip("Changes the horizontal direction of the sun from a reference direction of East."); + sunAzimuth.onValueChange(value -> scene.sun().setAzimuth(-QuickMath.degToRad(value))); + + sunAltitude.setName("Sun altitude"); + sunAltitude.setTooltip("Changes the vertical direction of the sun from a reference altitude of the horizon."); + sunAltitude.onValueChange(value -> scene.sun().setAltitude(QuickMath.degToRad(value))); + + sunColor.setText("Sunlight color"); + sunColor.colorProperty().addListener(sunColorListener); skyMode.setTooltip(new Tooltip("Set the type of sky to be used in the scene.")); skyMode.getItems().addAll(Sky.SkyMode.values()); @@ -210,27 +215,27 @@ public SimulatedSky fromString(String string) { .setTooltip(new Tooltip("Disables sky rendering for background compositing.")); transparentSkyEnabled.selectedProperty().addListener( (observable, oldValue, newValue) -> scene.setTransparentSky(newValue)); - cloudsEnabled.setTooltip(new Tooltip("Toggle visibility of Minecraft-style clouds.")); - cloudsEnabled.selectedProperty().addListener((observable, oldValue, newValue) -> { - - scene.sky().setCloudsEnabled(newValue); - }); colorPicker.colorProperty().addListener(skyColorListener); } @Override public void update(Scene scene) { + skyEmittance.set(scene.sky().getSkyEmittance()); skyMode.getSelectionModel().select(scene.sky().getSkyMode()); simulatedSky.setOnAction(null); simulatedSky.getSelectionModel().select(scene.sky().getSimulatedSky()); simulatedSky.setOnAction(simSkyListener); - cloudsEnabled.setSelected(scene.sky().cloudsEnabled()); + sunIntensity.set(scene.sun().getIntensity()); + sunRadius.set(Math.toDegrees(scene.sun().getSunRadius())); + sunAzimuth.set(-QuickMath.radToDeg(scene.sun().getAzimuth())); + sunAltitude.set(QuickMath.radToDeg(scene.sun().getAltitude())); + sunSamplingStrategy.getSelectionModel().select(scene.getSunSamplingStrategy()); + drawSun.setSelected(scene.sun().getDrawTexture()); + useFlatTexture.setSelected(scene.sun().getUseFlatTexture()); + sunColor.colorProperty().removeListener(sunColorListener); + sunColor.setColor(ColorUtil.toFx(scene.sun().getColor())); + sunColor.colorProperty().addListener(sunColorListener); + getSimulatedSkySettings(); transparentSkyEnabled.setSelected(scene.transparentSky()); - cloudSize.set(scene.sky().cloudSize()); - cloudX.set(scene.sky().cloudXOffset()); - cloudY.set(scene.sky().cloudYOffset()); - cloudZ.set(scene.sky().cloudZOffset()); - fogMode.getSelectionModel().select(scene.fog.getFogMode()); - horizonOffset.set(scene.sky().getHorizonOffset()); simulatedSky.setValue(scene.sky().getSimulatedSky()); gradientEditor.setGradient(scene.sky().getGradient()); colorPicker.colorProperty().removeListener(skyColorListener); @@ -238,15 +243,13 @@ public SimulatedSky fromString(String string) { colorPicker.colorProperty().addListener(skyColorListener); skyboxSettings.update(scene); skymapSettings.update(scene); - uniformFogSettings.update(scene); - layeredFogSettings.update(scene); } @Override public String getTabTitle() { - return "Sky & Fog"; + return "Environment"; } - @Override public Node getTabContent() { + @Override public VBox getTabContent() { return this; } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/FogTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/FogTab.java new file mode 100644 index 0000000000..f4e8c5f5b8 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/FogTab.java @@ -0,0 +1,251 @@ +package se.llbit.chunky.ui.render.tabs; + +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.CloudLayer; +import se.llbit.chunky.renderer.scene.fog.FogMode; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolume; +import se.llbit.chunky.renderer.scene.volumetricfog.FogVolumeShape; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.dialogs.FogVolumeShapeSelectorDialog; +import se.llbit.chunky.ui.render.RenderControlsTab; +import se.llbit.chunky.ui.render.settings.LayeredFogSettings; +import se.llbit.chunky.ui.render.settings.UniformFogSettings; +import se.llbit.fx.LuxColorPicker; +import se.llbit.math.ColorUtil; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class FogTab extends RenderControlsTab implements Initializable { + private static class FogVolumeData { + final String shape; + + FogVolumeData(FogVolume fogVolume) { + this.shape = fogVolume.getShape().name(); + } + } + + @FXML private ChoiceBox fogMode; + @FXML private TitledPane fogDetailsPane; + @FXML private VBox fogDetailsBox; + + @FXML private TableView fogVolumeTable; + @FXML private TableColumn typeCol; + @FXML private Button addVolume; + @FXML private Button removeVolume; + @FXML private VBox volumeSpecificControls; + + @FXML private TableView cloudLayerTable; + @FXML private TableColumn scaleXColumn; + @FXML private TableColumn scaleYColumn; + @FXML private TableColumn scaleZColumn; + @FXML private TableColumn offsetXColumn; + @FXML private TableColumn offsetYColumn; + @FXML private TableColumn offsetZColumn; + @FXML private Button addCloudLayer; + @FXML private Button removeCloudLayer; + @FXML private VBox layerSpecificControls; + + private final UniformFogSettings uniformFogSettings = new UniformFogSettings(); + private final LayeredFogSettings layeredFogSettings = new LayeredFogSettings(); + private final FogVolumeShapeSelectorDialog fogVolumeShapeSelectorDialog = new FogVolumeShapeSelectorDialog(); + + public FogTab() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("FogTab.fxml")); + loader.setRoot(this); + loader.setController(this); + loader.load(); + } + + @Override protected void onSetController(RenderControlsFxController controller) { + uniformFogSettings.setRenderController(controller.getRenderController()); + layeredFogSettings.setRenderController(controller.getRenderController()); + } + + @Override public void initialize(URL location, ResourceBundle resources) { + fogMode.getItems().addAll(FogMode.values()); + fogMode.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + scene.setFogMode(newValue); + switch (newValue) { + case NONE: { + fogDetailsBox.getChildren().setAll(new Label("Selected mode has no settings.")); + break; + } + case UNIFORM: { + fogDetailsBox.getChildren().setAll(uniformFogSettings); + break; + } + case LAYERED: { + fogDetailsBox.getChildren().setAll(layeredFogSettings); + break; + } + } + fogDetailsPane.setExpanded(true); + }); + + fogVolumeTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + updateControls(); + }); + fogVolumeTable.refresh(); + typeCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().shape)); + typeCol.setSortable(false); + + addVolume.setOnAction(e -> { + if (fogVolumeShapeSelectorDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + FogVolumeShape shape = fogVolumeShapeSelectorDialog.getShape(); + scene.addFogVolume(shape); + rebuildFogVolumeList(); + fogVolumeTable.getSelectionModel().selectLast(); + } + }); + + removeVolume.setOnAction(e -> { + int index = fogVolumeTable.getSelectionModel().getSelectedIndex(); + scene.removeFogVolume(index); + rebuildFogVolumeList(); + }); + + cloudLayerTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + layerSpecificControls.getChildren().clear(); + if (newValue != null) { + layerSpecificControls.getChildren().add(scene.getCloudLayers().get(cloudLayerTable.getSelectionModel().getSelectedIndex()).getControls(this)); + } + }); + cloudLayerTable.refresh(); + scaleXColumn.setCellValueFactory(new PropertyValueFactory("scaleX")); + scaleYColumn.setCellValueFactory(new PropertyValueFactory("scaleY")); + scaleZColumn.setCellValueFactory(new PropertyValueFactory("scaleZ")); + offsetXColumn.setCellValueFactory(new PropertyValueFactory("offsetX")); + offsetYColumn.setCellValueFactory(new PropertyValueFactory("offsetY")); + offsetZColumn.setCellValueFactory(new PropertyValueFactory("offsetZ")); + + scaleXColumn.setSortable(false); + scaleYColumn.setSortable(false); + scaleZColumn.setSortable(false); + offsetXColumn.setSortable(false); + offsetYColumn.setSortable(false); + offsetZColumn.setSortable(false); + + addCloudLayer.setOnAction(e -> { + scene.addCloudLayer(); + rebuildCloudLayerList(); + cloudLayerTable.getSelectionModel().selectLast(); + }); + + removeCloudLayer.setOnAction(e -> { + int index = cloudLayerTable.getSelectionModel().getSelectedIndex(); + scene.removeCloudLayer(index); + rebuildCloudLayerList(); + }); + } + + @Override public void update(Scene scene) { + fogMode.getSelectionModel().select(scene.fog.getFogMode()); + uniformFogSettings.update(scene); + layeredFogSettings.update(scene); + rebuildFogVolumeList(); + rebuildCloudLayerList(); + } + + @Override + public String getTabTitle() { + return "Fog & Clouds"; + } + + @Override + public VBox getTabContent() { + return this; + } + + private void rebuildFogVolumeList() { + fogVolumeTable.getSelectionModel().clearSelection(); + fogVolumeTable.getItems().clear(); + scene.getFogVolumes().forEach(fogVolume -> { + FogVolumeData fogVolumeData = new FogVolumeData(fogVolume); + fogVolumeTable.getItems().add(fogVolumeData); + }); + } + + private void rebuildCloudLayerList() { + cloudLayerTable.getSelectionModel().clearSelection(); + cloudLayerTable.getItems().clear(); + scene.getCloudLayers().forEach(cloudLayer -> cloudLayerTable.getItems().add(cloudLayer)); + } + + private void updateControls() { + volumeSpecificControls.getChildren().clear(); + if (!fogVolumeTable.getSelectionModel().isEmpty()) { + int index = fogVolumeTable.getSelectionModel().getSelectedIndex(); + FogVolume fogVolume = scene.getFogVolumes().get(index); + + HBox fogColorPickerBox = new HBox(); + fogColorPickerBox.setSpacing(10); + Label label = new Label("Fog color:"); + LuxColorPicker luxColorPicker = new LuxColorPicker(); + luxColorPicker.setColor(ColorUtil.toFx(fogVolume.getMaterial().volumeColor)); + luxColorPicker.colorProperty().addListener( + (observable, oldValue, newValue) -> { + fogVolume.getMaterial().volumeColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }); + fogColorPickerBox.getChildren().addAll(label, luxColorPicker); + + DoubleAdjuster density = new DoubleAdjuster(); + density.setName("Fog density"); + density.setTooltip("Fog thickness"); + density.setMaximumFractionDigits(6); + density.setRange(0.000001, 1); + density.clampMin(); + density.set(fogVolume.getMaterial().volumeDensity); + density.onValueChange(value -> { + fogVolume.getMaterial().volumeDensity = value.floatValue(); + scene.refresh(); + }); + + DoubleAdjuster anisotropy = new DoubleAdjuster(); + anisotropy.setName("Anisotropy"); + anisotropy.setTooltip("Changes the direction light is more likely to be scattered.\n" + + "Positive values increase the chance light scatters into its original direction of travel.\n" + + "Negative values increase the chance light scatters away from its original direction of travel"); + anisotropy.set(fogVolume.getMaterial().volumeAnisotropy); + anisotropy.setRange(-1, 1); + anisotropy.clampBoth(); + anisotropy.onValueChange(value -> { + fogVolume.getMaterial().volumeAnisotropy = value.floatValue(); + scene.refresh(); + }); + + DoubleAdjuster emittance = new DoubleAdjuster(); + emittance.setName("Emittance"); + emittance.setRange(0, 100); + emittance.clampMin(); + emittance.set(fogVolume.getMaterial().volumeEmittance); + emittance.onValueChange(value -> { + fogVolume.getMaterial().volumeEmittance = value.floatValue(); + scene.refresh(); + }); + + Separator separator = new Separator(); + + volumeSpecificControls.getChildren().addAll( + fogColorPickerBox, + density, + anisotropy, + emittance, + separator + ); + + volumeSpecificControls.getChildren().add(fogVolume.getControls(this)); + } + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java index 6cf2cd9f62..43437b8d6c 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java @@ -22,7 +22,6 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.image.ImageView; @@ -30,6 +29,7 @@ import javafx.scene.layout.VBox; import javafx.scene.shape.SVGPath; import javafx.scene.text.Text; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.entity.*; import se.llbit.chunky.map.WorldMapLoader; @@ -61,10 +61,7 @@ import java.util.Optional; import java.util.ResourceBundle; -public class GeneralTab extends ScrollPane implements RenderControlsTab, Initializable { - private Scene scene; - private final Node wrapper; - +public class GeneralTab extends RenderControlsTab implements Initializable { @FXML private Button openSceneDirBtn; @FXML private Button exportSettings; @FXML private Button importSettings; @@ -77,7 +74,7 @@ public class GeneralTab extends ScrollPane implements RenderControlsTab, Initial @FXML private Button makeDefaultSize; @FXML private Button flipAxesBtn; @FXML private Pane scaleButtonArea; - @FXML private CheckBox renderRegions; + @FXML private ToggleSwitch renderRegions; @FXML private Pane renderRegionsArea; @FXML private IntegerTextField cameraCropWidth; @FXML private IntegerTextField cameraCropHeight; @@ -107,19 +104,16 @@ public class GeneralTab extends ScrollPane implements RenderControlsTab, Initial private final Double[] scaleButtonValues = {0.5, 1.5, 2.0}; - private RenderController controller; + private RenderController renderController; private WorldMapLoader mapLoader; - private RenderControlsFxController renderControls; private ChunkyFxController chunkyFxController; - private ChangeListener canvasCropListener = (observable, oldValue, newValue) -> updateCanvasCrop(); + private final ChangeListener canvasCropListener = (observable, oldValue, newValue) -> updateCanvasCrop(); public GeneralTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("GeneralTab.fxml")); loader.setRoot(this); loader.setController(this); loader.load(); - - this.wrapper = new VBox(this); } @Override public void update(Scene scene) { @@ -170,10 +164,10 @@ public GeneralTab() throws IOException { chunkyFxController.getChunkSelection().isEmpty() ); }); - openSceneDirBtn.setDisable(!controller.getContext().getSceneDirectory().exists()); + openSceneDirBtn.setDisable(!renderController.getContext().getSceneDirectory().exists()); openSceneDirBtn.setTooltip(new Tooltip("Open the directory of the scene, if it has been saved.")); - ((AsynchronousSceneManager) controller.getSceneManager()).setOnSceneSaved(() -> { - openSceneDirBtn.setDisable(!controller.getContext().getSceneDirectory().exists()); + ((AsynchronousSceneManager) renderController.getSceneManager()).setOnSceneSaved(() -> { + openSceneDirBtn.setDisable(!renderController.getContext().getSceneDirectory().exists()); }); cameraCropWidth.valueProperty().removeListener(canvasCropListener); @@ -199,8 +193,8 @@ public GeneralTab() throws IOException { return "Scene"; } - @Override public Node getTabContent() { - return this.wrapper; + @Override public VBox getTabContent() { + return this; } @Override public void initialize(URL location, ResourceBundle resources) { @@ -224,8 +218,8 @@ public GeneralTab() throws IOException { try (JsonParser parser = new JsonParser(new ByteArrayInputStream(text.getBytes()))) { JsonObject json = parser.parse().object(); scene.importFromJson(json); - renderControls.getCanvas().setCanvasSize(scene.canvasConfig.getWidth(), scene.canvasConfig.getHeight()); - renderControls.refreshSettings(); + controller.getCanvas().setCanvasSize(scene.canvasConfig.getWidth(), scene.canvasConfig.getHeight()); + controller.refreshSettings(); } catch (IOException e) { Log.warn("Failed to import scene settings."); } catch (JsonParser.SyntaxError syntaxError) { @@ -242,7 +236,7 @@ public GeneralTab() throws IOException { alert.setContentText("Do you really want to reset all scene settings?"); alert.showAndWait().ifPresent(result -> { if (result == ButtonType.OK) { - scene.resetScene(scene.name, controller.getContext().getChunky().getSceneFactory()); + scene.resetScene(scene.name, controller.getRenderController().getContext().getChunky().getSceneFactory()); chunkyFxController.refreshSettings(); } }); @@ -255,7 +249,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadPlayers(newValue); }); loadPlayers.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadPlayers); }); loadArmorStands.setTooltip(new Tooltip("Enable/disable armor stand entity loading. " @@ -265,7 +259,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadArmorStands(newValue); }); loadArmorStands.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadArmorStands); }); loadBooks.setTooltip(new Tooltip("Enable/disable book entity loading. " @@ -275,7 +269,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadBooks(newValue); }); loadBooks.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadBooks); }); loadPaintings.setTooltip(new Tooltip("Enable/disable painting entity loading. " @@ -285,7 +279,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadPaintings(newValue); }); loadPaintings.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadPaintings); }); loadBeaconBeams.setTooltip(new Tooltip("Enable/disable beacon beam entity loading. " @@ -295,7 +289,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadBeaconBeams(newValue); }); loadBeaconBeams.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadBeaconBeams); }); loadSheep.setTooltip(new Tooltip("Enable/disable sheep entity loading. " @@ -305,7 +299,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadSheep(newValue); }); loadSheep.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadSheep); }); loadCows.setTooltip(new Tooltip("Enable/disable cow entity loading. " @@ -315,7 +309,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadCows(newValue); }); loadCows.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadCows); }); loadChickens.setTooltip(new Tooltip("Enable/disable chicken entity loading. " @@ -325,7 +319,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadChickens(newValue); }); loadChickens.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadChickens); }); loadPigs.setTooltip(new Tooltip("Enable/disable Pig entity loading. " @@ -335,7 +329,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadPigs(newValue); }); loadPigs.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadPigs); }); loadMooshrooms.setTooltip(new Tooltip("Enable/disable Mooshroom entity loading. " @@ -345,7 +339,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadMooshrooms(newValue); }); loadMooshrooms.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadMooshrooms); }); loadSquids.setTooltip(new Tooltip("Enable/disable Squid entity loading. " @@ -355,7 +349,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadSquids(newValue); }); loadSquids.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadSquids); }); loadOtherEntities.setTooltip(new Tooltip("Enable/disable other entity loading. " @@ -365,7 +359,7 @@ public GeneralTab() throws IOException { PersistentSettings.setLoadOtherEntities(newValue); }); loadOtherEntities.setOnAction(event -> { - renderControls.showPopup( + controller.showPopup( "This takes effect the next time a new scene is created.", loadOtherEntities); }); loadAllEntities.setOnAction(event -> { @@ -439,13 +433,13 @@ public GeneralTab() throws IOException { if (value >= yMax.get()) { yMin.setInvalid(true); yMax.setInvalid(true); - renderControls.hidePopup(); + controller.hidePopup(); } else { yMin.setInvalid(false); yMax.setInvalid(false); scene.setYClipMin(value); - renderControls.showPopup("Reload the chunks for this to take effect.", yMax); + controller.showPopup("Reload the chunks for this to take effect.", yMax); } }); @@ -455,13 +449,13 @@ public GeneralTab() throws IOException { if (yMin.get() >= value) { yMin.setInvalid(true); yMax.setInvalid(true); - renderControls.hidePopup(); + controller.hidePopup(); } else { yMin.setInvalid(false); yMax.setInvalid(false); scene.setYClipMax(value); - renderControls.showPopup("Reload the chunks for this to take effect.", yMax); + controller.showPopup("Reload the chunks for this to take effect.", yMax); } }); @@ -470,19 +464,19 @@ public GeneralTab() throws IOException { openSceneDirBtn.setTooltip( new Tooltip("Open the directory where Chunky stores the scene description and renders of this scene.")); - openSceneDirBtn.setOnAction(e -> chunkyFxController.openDirectory(chunkyFxController.getRenderController().getContext().getSceneDirectory())); + openSceneDirBtn.setOnAction(e -> chunkyFxController.openDirectory(renderController.getContext().getSceneDirectory())); loadSelectedChunks .setTooltip(new Tooltip("Load the chunks that are currently selected in the map view")); loadSelectedChunks.setOnAction(e -> { - controller.getSceneManager() + renderController.getSceneManager() .loadChunks(mapLoader.getWorld(), chunkyFxController.getChunkSelection().getSelectionByRegion()); reloadChunks.setDisable(chunkyFxController.getChunkSelection().isEmpty()); }); reloadChunks.setTooltip(new Tooltip("Reload all chunks in the scene.")); reloadChunks.setGraphic(new ImageView(Icon.reload.fxImage())); - reloadChunks.setOnAction(e -> controller.getSceneManager().reloadChunks()); + reloadChunks.setOnAction(e -> renderController.getSceneManager().reloadChunks()); canvasSizeLabel.setGraphic(new ImageView(Icon.scale.fxImage())); canvasSizeInput.getSize().addListener(this::updateCanvasSize); @@ -540,6 +534,7 @@ public GeneralTab() throws IOException { cameraCropY.valueProperty().set(0); } }); + renderRegions.setTooltip(new Tooltip("Render a subset region of the main canvas.")); cameraCropWidth.valueProperty().addListener(canvasCropListener); cameraCropHeight.valueProperty().addListener(canvasCropListener); cameraCropX.valueProperty().addListener(canvasCropListener); @@ -564,7 +559,7 @@ private void updateCanvasSize(int width, int height) { } else { scene.setCanvasCropSize(width, height, 0, 0, 0, 0); } - renderControls.getCanvas().setCanvasSize(scene.canvasConfig.getWidth(), scene.canvasConfig.getHeight()); + controller.getCanvas().setCanvasSize(scene.canvasConfig.getWidth(), scene.canvasConfig.getHeight()); } private void updateCanvasCrop() { @@ -572,17 +567,15 @@ private void updateCanvasCrop() { updateCanvasSize(size.getWidth(), size.getHeight()); } - @Override public void setController(RenderControlsFxController controls) { - this.renderControls = controls; - this.chunkyFxController = controls.getChunkyController(); + @Override protected void onSetController(RenderControlsFxController controller) { + this.chunkyFxController = this.controller.getChunkyController(); + this.renderController = this.controller.getRenderController(); this.mapLoader = chunkyFxController.getMapLoader(); mapLoader.addWorldLoadListener((world, reloaded) -> { loadSelectedChunks.setDisable(world instanceof EmptyWorld || world == null); }); mapLoader.addWorldLoadListener((world, reloaded) -> updateYClipSlidersRanges(world)); updateYClipSlidersRanges(mapLoader.getWorld()); - this.controller = controls.getRenderController(); - this.scene = this.controller.getSceneManager().getScene(); } private void updateYClipSlidersRanges(World world) { diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/HelpTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/HelpTab.java index 321a764b55..6d3de41d2b 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/HelpTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/HelpTab.java @@ -17,14 +17,13 @@ package se.llbit.chunky.ui.render.tabs; import javafx.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.ui.render.RenderControlsTab; import java.io.IOException; -public class HelpTab extends ScrollPane implements RenderControlsTab { +public class HelpTab extends RenderControlsTab { public HelpTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("HelpTab.fxml")); loader.setRoot(this); @@ -39,7 +38,7 @@ public HelpTab() throws IOException { return "Help"; } - @Override public Node getTabContent() { + @Override public VBox getTabContent() { return this; } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java deleted file mode 100644 index 1aff9fd3be..0000000000 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java +++ /dev/null @@ -1,257 +0,0 @@ -/* Copyright (c) 2016-2022 Jesper Öqvist - * Copyright (c) 2016-2022 Chunky Contributors - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ -package se.llbit.chunky.ui.render.tabs; - -import javafx.beans.value.ChangeListener; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.ButtonBar.ButtonData; -import javafx.scene.paint.Color; -import se.llbit.chunky.renderer.EmitterSamplingStrategy; -import se.llbit.chunky.renderer.SunSamplingStrategy; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.renderer.scene.sky.Sky; -import se.llbit.chunky.renderer.scene.sky.Sun; -import se.llbit.chunky.ui.elements.AngleAdjuster; -import se.llbit.chunky.ui.DoubleAdjuster; -import se.llbit.chunky.ui.controller.RenderControlsFxController; -import se.llbit.chunky.ui.render.RenderControlsTab; -import se.llbit.fx.LuxColorPicker; -import se.llbit.fxutil.Dialogs; -import se.llbit.math.ColorUtil; -import se.llbit.math.QuickMath; -import se.llbit.util.Registerable; - -import java.io.IOException; -import java.net.URL; -import java.util.ResourceBundle; - -public class LightingTab extends ScrollPane implements RenderControlsTab, Initializable { - private RenderControlsFxController controller; - private Scene scene; - - @FXML private DoubleAdjuster skyExposure; - @FXML private DoubleAdjuster skyIntensity; - @FXML private DoubleAdjuster apparentSkyBrightness; - @FXML private DoubleAdjuster emitterIntensity; - @FXML private DoubleAdjuster sunIntensity; - @FXML private CheckBox drawSun; - @FXML private ComboBox sunSamplingStrategy; - @FXML private TitledPane importanceSamplingDetailsPane; - @FXML private DoubleAdjuster importanceSampleChance; - @FXML private DoubleAdjuster importanceSampleRadius; - @FXML private DoubleAdjuster sunLuminosity; - @FXML private DoubleAdjuster apparentSunBrightness; - @FXML private DoubleAdjuster sunRadius; - @FXML private AngleAdjuster sunAzimuth; - @FXML private AngleAdjuster sunAltitude; - @FXML private CheckBox enableEmitters; - @FXML private LuxColorPicker sunColor; - @FXML private LuxColorPicker apparentSunColor; - @FXML private CheckBox modifySunTexture; - @FXML private ChoiceBox emitterSamplingStrategy; - - private ChangeListener sunColorListener = (observable, oldValue, newValue) -> scene.sun().setColor(ColorUtil.fromFx(newValue)); - - private ChangeListener apparentSunColorListener = (observable, oldValue, newValue) -> scene.sun().setApparentColor(ColorUtil.fromFx(newValue)); - - public LightingTab() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("LightingTab.fxml")); - loader.setRoot(this); - loader.setController(this); - loader.load(); - } - - @Override public void initialize(URL location, ResourceBundle resources) { - skyExposure.setName("Sky exposure"); - skyExposure.setTooltip("Changes the exposure of the sky."); - skyExposure.setRange(Sky.MIN_INTENSITY, Sky.MAX_INTENSITY); - skyExposure.makeLogarithmic(); - skyExposure.clampMin(); - skyExposure.onValueChange(value -> scene.sky().setSkyExposure(value)); - - skyIntensity.setName("Sky light intensity modifier"); - skyIntensity.setTooltip("Modifies the intensity of the light emitted by the sky."); - skyIntensity.setRange(Sky.MIN_INTENSITY, Sky.MAX_INTENSITY); - skyIntensity.makeLogarithmic(); - skyIntensity.clampMin(); - skyIntensity.onValueChange(value -> scene.sky().setSkyLight(value)); - - apparentSkyBrightness.setName("Apparent sky brightness modifier"); - apparentSkyBrightness.setTooltip("Modifies the apparent brightness of the sky."); - apparentSkyBrightness.setRange(Sky.MIN_APPARENT_INTENSITY, Sky.MAX_APPARENT_INTENSITY); - apparentSkyBrightness.makeLogarithmic(); - apparentSkyBrightness.clampMin(); - apparentSkyBrightness.onValueChange(value -> scene.sky().setApparentSkyLight(value)); - - enableEmitters.setTooltip(new Tooltip("Allow blocks to emit light based on their material settings.")); - enableEmitters.selectedProperty().addListener( - (observable, oldValue, newValue) -> scene.setEmittersEnabled(newValue)); - - emitterIntensity.setName("Emitter intensity"); - emitterIntensity.setTooltip("Modifies the intensity of emitter light."); - emitterIntensity.setRange(Scene.MIN_EMITTER_INTENSITY, Scene.MAX_EMITTER_INTENSITY); - emitterIntensity.makeLogarithmic(); - emitterIntensity.clampMin(); - emitterIntensity.onValueChange(value -> scene.setEmitterIntensity(value)); - - emitterSamplingStrategy.getItems().addAll(EmitterSamplingStrategy.values()); - emitterSamplingStrategy.getSelectionModel().selectedItemProperty() - .addListener((observable, oldvalue, newvalue) -> { - scene.setEmitterSamplingStrategy(newvalue); - if (newvalue != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() == null && scene.haveLoadedChunks()) { - Alert warning = Dialogs.createAlert(AlertType.CONFIRMATION); - warning.setContentText("The selected chunks need to be reloaded in order for emitter sampling to work."); - warning.getButtonTypes().setAll( - ButtonType.CANCEL, - new ButtonType("Reload chunks", ButtonData.FINISH)); - warning.setTitle("Chunk reload required"); - ButtonType result = warning.showAndWait().orElse(ButtonType.CANCEL); - if (result.getButtonData() == ButtonData.FINISH) { - controller.getRenderController().getSceneManager().reloadChunks(); - } - } - }); - emitterSamplingStrategy.setTooltip(new Tooltip("Determine how emitters are sampled at each bounce.")); - - drawSun.selectedProperty().addListener((observable, oldValue, newValue) -> scene.sun().setDrawTexture(newValue)); - drawSun.setTooltip(new Tooltip("Draws the sun texture on top of the skymap.")); - - for (SunSamplingStrategy strategy : SunSamplingStrategy.values()) { - if (strategy.getDeprecationStatus() != Registerable.DeprecationStatus.HIDDEN) { - sunSamplingStrategy.getItems().add(strategy); - } - } - sunSamplingStrategy.getSelectionModel().selectedItemProperty().addListener( - (observable, oldValue, newValue) -> { - scene.setSunSamplingStrategy(newValue); - - boolean visible = scene != null && scene.getSunSamplingStrategy().isImportanceSampling(); - importanceSamplingDetailsPane.setVisible(visible); - importanceSamplingDetailsPane.setExpanded(visible); - importanceSamplingDetailsPane.setManaged(visible); - }); - sunSamplingStrategy.setTooltip(new Tooltip("Determines how the sun is sampled at each bounce.")); - - boolean visible = scene != null && scene.getSunSamplingStrategy().isImportanceSampling(); - importanceSamplingDetailsPane.setVisible(visible); - importanceSamplingDetailsPane.setExpanded(visible); - importanceSamplingDetailsPane.setManaged(visible); - - importanceSampleChance.setName("Importance sample chance"); - importanceSampleChance.setTooltip("Probability of sampling the sun on each importance bounce"); - importanceSampleChance.setRange(Sun.MIN_IMPORTANCE_SAMPLE_CHANCE, Sun.MAX_IMPORTANCE_SAMPLE_CHANCE); - importanceSampleChance.clampBoth(); - importanceSampleChance.onValueChange(value -> scene.sun().setImportanceSampleChance(value)); - - importanceSampleRadius.setName("Importance sample radius"); - importanceSampleRadius.setTooltip("Radius of possible sun sampling bounces (relative to the sun's radius)"); - importanceSampleRadius.setRange(Sun.MIN_IMPORTANCE_SAMPLE_RADIUS, Sun.MAX_IMPORTANCE_SAMPLE_RADIUS); - importanceSampleRadius.clampMin(); - importanceSampleRadius.onValueChange(value -> scene.sun().setImportanceSampleRadius(value)); - - sunIntensity.setName("Sunlight intensity"); - sunIntensity.setTooltip("Changes the intensity of sunlight. Only used when Sun Sampling Strategy is set to FAST or HIGH_QUALITY."); - sunIntensity.setRange(Sun.MIN_INTENSITY, Sun.MAX_INTENSITY); - sunIntensity.makeLogarithmic(); - sunIntensity.clampMin(); - sunIntensity.onValueChange(value -> scene.sun().setIntensity(value)); - - sunLuminosity.setName("Sun luminosity"); - sunLuminosity.setTooltip("Changes the absolute brightness of the sun. Only used when Sun Sampling Strategy is set to OFF or HIGH_QUALITY."); sunLuminosity.setRange(1, 10000); - sunLuminosity.makeLogarithmic(); - sunLuminosity.clampMin(); - sunLuminosity.onValueChange(value -> scene.sun().setLuminosity(value)); - - apparentSunBrightness.setName("Apparent sun brightness"); - apparentSunBrightness.setTooltip("Changes the apparent brightness of the sun texture."); - apparentSunBrightness.setRange(Sun.MIN_APPARENT_BRIGHTNESS, Sun.MAX_APPARENT_BRIGHTNESS); - apparentSunBrightness.makeLogarithmic(); - apparentSunBrightness.clampMin(); - apparentSunBrightness.onValueChange(value -> scene.sun().setApparentBrightness(value)); - - sunRadius.setName("Sun size"); - sunRadius.setTooltip("Sun radius in degrees."); - sunRadius.setRange(0.01, 20); - sunRadius.clampMin(); - sunRadius.onValueChange(value -> scene.sun().setSunRadius(Math.toRadians(value))); - - sunColor.colorProperty().addListener(sunColorListener); - - modifySunTexture.setTooltip(new Tooltip("Changes whether the the color of the sun texture is modified by the apparent sun color.")); - modifySunTexture.selectedProperty().addListener((observable, oldValue, newValue) -> { - scene.sun().setEnableTextureModification(newValue); - apparentSunColor.setDisable(!newValue); - }); - - apparentSunColor.setDisable(true); - apparentSunColor.colorProperty().addListener(apparentSunColorListener); - - sunAzimuth.setName("Sun azimuth"); - sunAzimuth.setTooltip("Changes the horizontal direction of the sun from a reference direction of East."); - sunAzimuth.onValueChange(value -> scene.sun().setAzimuth(-QuickMath.degToRad(value))); - - sunAltitude.setName("Sun altitude"); - sunAltitude.setTooltip("Changes the vertical direction of the sun from a reference altitude of the horizon."); - sunAltitude.onValueChange(value -> scene.sun().setAltitude(QuickMath.degToRad(value))); - } - - @Override - public void setController(RenderControlsFxController controller) { - this.controller = controller; - scene = controller.getRenderController().getSceneManager().getScene(); - } - - @Override public void update(Scene scene) { - skyExposure.set(scene.sky().getSkyExposure()); - skyIntensity.set(scene.sky().getSkyLight()); - apparentSkyBrightness.set(scene.sky().getApparentSkyLight()); - emitterIntensity.set(scene.getEmitterIntensity()); - sunIntensity.set(scene.sun().getIntensity()); - sunLuminosity.set(scene.sun().getLuminosity()); - apparentSunBrightness.set(scene.sun().getApparentBrightness()); - sunRadius.set(Math.toDegrees(scene.sun().getSunRadius())); - modifySunTexture.setSelected(scene.sun().getEnableTextureModification()); - sunAzimuth.set(-QuickMath.radToDeg(scene.sun().getAzimuth())); - sunAltitude.set(QuickMath.radToDeg(scene.sun().getAltitude())); - enableEmitters.setSelected(scene.getEmittersEnabled()); - sunSamplingStrategy.getSelectionModel().select(scene.getSunSamplingStrategy()); - importanceSampleChance.set(scene.sun().getImportanceSampleChance()); - importanceSampleRadius.set(scene.sun().getImportanceSampleRadius()); - drawSun.setSelected(scene.sun().drawTexture()); - sunColor.colorProperty().removeListener(sunColorListener); - sunColor.setColor(ColorUtil.toFx(scene.sun().getColor())); - sunColor.colorProperty().addListener(sunColorListener); - apparentSunColor.colorProperty().removeListener(apparentSunColorListener); - apparentSunColor.setColor(ColorUtil.toFx(scene.sun().getApparentColor())); - apparentSunColor.colorProperty().addListener(apparentSunColorListener); - emitterSamplingStrategy.getSelectionModel().select(scene.getEmitterSamplingStrategy()); - } - - @Override public String getTabTitle() { - return "Lighting"; - } - - @Override public Node getTabContent() { - return this; - } -} diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java index a1644556ee..87c33daf9a 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java @@ -17,6 +17,8 @@ */ package se.llbit.chunky.ui.render.tabs; +import java.util.ArrayList; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -24,21 +26,27 @@ import javafx.fxml.Initializable; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListView; -import javafx.scene.control.TextField; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.block.*; +import se.llbit.chunky.block.minecraft.UnknownBlock; +import se.llbit.chunky.renderer.scene.EmitterMappingType; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.resources.Texture; import se.llbit.chunky.ui.DoubleAdjuster; -import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.data.MaterialReferenceColorData; import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.ExtraMaterials; import se.llbit.chunky.world.Material; import se.llbit.chunky.world.MaterialStore; +import se.llbit.fx.LuxColorPicker; +import se.llbit.math.ColorUtil; +import se.llbit.math.Vector3; +import se.llbit.math.Vector4; +import se.llbit.nbt.CompoundTag; import java.net.URL; import java.util.Collection; @@ -46,36 +54,168 @@ import java.util.ResourceBundle; // TODO: customization of textures, base color, etc. -public class MaterialsTab extends HBox implements RenderControlsTab, Initializable { - private Scene scene; - +public class MaterialsTab extends RenderControlsTab implements Initializable { private final DoubleAdjuster emittance = new DoubleAdjuster(); + private final LuxColorPicker emittanceColor = new LuxColorPicker(); + private final DoubleAdjuster emitterMappingOffset = new DoubleAdjuster(); + private final ChoiceBox emitterMappingType = new ChoiceBox<>(); + private final ToggleSwitch useReferenceColors = new ToggleSwitch(); + private final DoubleAdjuster alpha = new DoubleAdjuster(); + private final DoubleAdjuster subsurfaceScattering = new DoubleAdjuster(); + private final LuxColorPicker diffuseColor = new LuxColorPicker(); private final DoubleAdjuster specular = new DoubleAdjuster(); private final DoubleAdjuster ior = new DoubleAdjuster(); private final DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); + private final DoubleAdjuster perceptualTransmissionSmoothness = new DoubleAdjuster(); private final DoubleAdjuster metalness = new DoubleAdjuster(); + private final DoubleAdjuster transmissionMetalness = new DoubleAdjuster(); + private final LuxColorPicker specularColor = new LuxColorPicker(); + private final LuxColorPicker transmissionSpecularColor = new LuxColorPicker(); + private final DoubleAdjuster volumeDensity = new DoubleAdjuster(); + private final DoubleAdjuster volumeAnisotropy = new DoubleAdjuster(); + private final DoubleAdjuster volumeEmittance = new DoubleAdjuster(); + private final LuxColorPicker volumeColor = new LuxColorPicker(); + private final DoubleAdjuster absorption = new DoubleAdjuster(); + private final LuxColorPicker absorptionColor = new LuxColorPicker(); + private final CheckBox opaque = new CheckBox(); + private final ToggleSwitch hidden = new ToggleSwitch(); + private final TableView referenceColorTable = new TableView<>(); + private final Button addReferenceColor = new Button(); + private final Button removeReferenceColor = new Button(); + private final LuxColorPicker referenceColorPicker = new LuxColorPicker(); + private final IntegerAdjuster referenceColorRangeSlider = new IntegerAdjuster(); + + private ChangeListener emittanceColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener emitterMappingTypeListener = (observable, oldValue, newValue) -> {}; + private ChangeListener useReferenceColorsListener = (observable, oldValue, newValue) -> {}; + private ChangeListener specularColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener transmissionSpecularColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener diffuseColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener volumeColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener absorptionColorListener = (observable, oldValue, newValue) -> {}; + private ChangeListener opaqueListener = (observable, oldValue, newValue) -> {}; + private ChangeListener hiddenListener = (observable, oldValue, newValue) -> {}; + private ChangeListener referenceColorTableListener = (observable, oldValue, newValue) -> {}; + private ChangeListener referenceColorPickerListener = (observable, oldValue, newValue) -> {}; + + private final ListView listView; public MaterialsTab() { emittance.setName("Emittance"); emittance.setRange(0, 100); + emittance.clampMin(); emittance.setTooltip("Intensity of the light emitted from the selected material."); + + emittanceColor.setText("Emittance color"); + emittanceColor.colorProperty().addListener(diffuseColorListener); + + emitterMappingOffset.setName("Emitter mapping offset"); + emitterMappingOffset.setRange(-5, 5); + emitterMappingOffset.setTooltip("Offset applied to the global emitter mapping exponent."); + + emitterMappingType.getItems().addAll(EmitterMappingType.values()); + emitterMappingType.setTooltip(new Tooltip("Overrides the global setting for emitter mapping type.")); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener(emitterMappingTypeListener); + + VBox emitterMappingTypeBox = new VBox(6, new Label("Emitter mapping type"), emitterMappingType); + + useReferenceColors.setText("Use reference colors"); + useReferenceColors.selectedProperty().addListener(useReferenceColorsListener); + + alpha.setName("Alpha"); + alpha.setRange(0, 1); + alpha.clampBoth(); + alpha.setTooltip("Alpha (opacity) of the selected material."); + + subsurfaceScattering.setName("Subsurface scattering"); + subsurfaceScattering.setRange(0, 1); + subsurfaceScattering.clampBoth(); + subsurfaceScattering.setTooltip("Probability of a ray to be scattered behind the surface."); + + diffuseColor.setText("Diffuse color"); + diffuseColor.colorProperty().addListener(diffuseColorListener); + specular.setName("Specular"); specular.setRange(0, 1); + specular.clampBoth(); specular.setTooltip("Reflectivity of the selected material."); + ior.setName("IoR"); ior.setRange(0, 5); + ior.clampMin(); ior.setTooltip("Index of Refraction of the selected material."); + ior.setMaximumFractionDigits(6); + perceptualSmoothness.setName("Smoothness"); perceptualSmoothness.setRange(0, 1); + perceptualSmoothness.clampBoth(); perceptualSmoothness.setTooltip("Smoothness of the selected material."); + + perceptualTransmissionSmoothness.setName("Transmission smoothness"); + perceptualTransmissionSmoothness.setRange(0, 1); + perceptualTransmissionSmoothness.clampBoth(); + perceptualTransmissionSmoothness.setTooltip("Smoothness of the selected material applied to light transmission."); + metalness.setName("Metalness"); metalness.setRange(0, 1); - metalness.setTooltip("Metalness (texture-tinted reflectivity) of the selected material."); + metalness.clampBoth(); + metalness.setTooltip("Texture tinting of reflected light."); + + transmissionMetalness.setName("Transmission metalness"); + transmissionMetalness.setRange(0, 1); + transmissionMetalness.clampBoth(); + transmissionMetalness.setTooltip("Texture tinting of refracted/transmitted light."); + + specularColor.setText("Specular color"); + specularColor.colorProperty().addListener(specularColorListener); + + transmissionSpecularColor.setText("Transmission specular color"); + transmissionSpecularColor.colorProperty().addListener(transmissionSpecularColorListener); + + volumeDensity.setName("Volume density"); + volumeDensity.setRange(0, 1); + volumeDensity.clampMin(); + volumeDensity.setTooltip("Density of volume medium."); + + volumeAnisotropy.setName("Volume anisotropy"); + volumeAnisotropy.setRange(-1, 1); + volumeAnisotropy.clampBoth(); + volumeAnisotropy.setTooltip("Changes the direction light is more likely to be scattered.\n" + + "Positive values increase the chance light scatters into its original direction of travel.\n" + + "Negative values increase the chance light scatters away from its original direction of travel."); + + volumeEmittance.setName("Volume emittance"); + volumeEmittance.setRange(0, 100); + volumeEmittance.clampMin(); + volumeEmittance.setTooltip("Emittance of volume medium."); + + volumeColor.setText("Volume color"); + volumeColor.colorProperty().addListener(volumeColorListener); + + absorption.setName("Absorption"); + absorption.setRange(0, 10); + absorption.clampMin(); + absorption.makeLogarithmic(); + absorption.setTooltip("Absorption of volume medium."); + + absorptionColor.setText("Absorption color"); + absorptionColor.colorProperty().addListener(absorptionColorListener); + + opaque.setText("Opaque"); + opaque.setTooltip(new Tooltip("Blocks surrounded by opaque blocks are replaced with stone to improve performance.\n" + + "This change takes effect after chunks are loaded.")); + opaque.selectedProperty().addListener(opaqueListener); + + hidden.setText("Hidden"); + hidden.setTooltip(new Tooltip("Sets whether the block is visible.")); + hidden.selectedProperty().addListener(hiddenListener); + ObservableList blockIds = FXCollections.observableArrayList(); blockIds.addAll(MaterialStore.collections.keySet()); blockIds.addAll(ExtraMaterials.idMap.keySet()); blockIds.addAll(MaterialStore.blockIds); + FilteredList filteredList = new FilteredList<>( new SortedList<>(blockIds, Comparator.naturalOrder()) ); @@ -83,14 +223,28 @@ public MaterialsTab() { listView.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, materialName) -> updateSelectedMaterial(materialName) ); - VBox settings = new VBox(); - settings.setSpacing(10); - settings.getChildren().addAll( - new Label("Material Properties"), - emittance, specular, perceptualSmoothness, ior, metalness, - new Label("(set to zero to disable)")); - setPadding(new Insets(10)); - setSpacing(15); + + GridPane settings = new GridPane(); + + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(50); + + settings.getColumnConstraints().addAll(columnConstraints, columnConstraints); + settings.setHgap(10); + settings.setVgap(10); + + VBox diffuseSettings = new VBox(6, emittance, emittanceColor, emitterMappingOffset, emitterMappingTypeBox, useReferenceColors, alpha, subsurfaceScattering, diffuseColor); + VBox volumeSettings = new VBox(6, volumeDensity, volumeAnisotropy, volumeEmittance, volumeColor, absorption, absorptionColor); + VBox specularSettings = new VBox(6, specular, ior, perceptualSmoothness, perceptualTransmissionSmoothness); + VBox specularColorSettings = new VBox(6, metalness, transmissionMetalness, specularColor, transmissionSpecularColor); + VBox otherSettings = new VBox(6, opaque, hidden); + + settings.add(diffuseSettings, 0, 0); + settings.add(volumeSettings, 1, 0); + settings.add(specularSettings, 0, 1); + settings.add(specularColorSettings, 1, 1); + settings.add(otherSettings, 0, 2); + TextField filterField = new TextField(); filterField.textProperty().addListener((observable, oldValue, newValue) -> { if (newValue.trim().isEmpty()) { @@ -99,73 +253,305 @@ public MaterialsTab() { filteredList.setPredicate(name -> name.contains(newValue)); } }); + HBox filterBox = new HBox(); filterBox.setAlignment(Pos.BASELINE_LEFT); filterBox.setSpacing(10); filterBox.getChildren().addAll(new Label("Filter:"), filterField); + VBox listPane = new VBox(); listPane.setSpacing(10); listPane.getChildren().addAll(filterBox, listView); - getChildren().addAll(listPane, settings); + listPane.setPrefHeight(200); + + addReferenceColor.setText("Add reference color"); + removeReferenceColor.setText("Remove reference color"); + + HBox addRemoveControls = new HBox(10, addReferenceColor, removeReferenceColor); + + TableColumn redColumn = new TableColumn<>("Red"); + TableColumn greenColumn = new TableColumn<>("Green"); + TableColumn blueColumn = new TableColumn<>("Blue"); + TableColumn rangeColumn = new TableColumn<>("Range"); + + redColumn.setCellValueFactory(new PropertyValueFactory<>("red")); + greenColumn.setCellValueFactory(new PropertyValueFactory<>("green")); + blueColumn.setCellValueFactory(new PropertyValueFactory<>("blue")); + rangeColumn.setCellValueFactory(new PropertyValueFactory<>("range")); + + redColumn.setSortable(true); + greenColumn.setSortable(true); + blueColumn.setSortable(true); + rangeColumn.setSortable(true); + + referenceColorTable.getColumns().add(redColumn); + referenceColorTable.getColumns().add(greenColumn); + referenceColorTable.getColumns().add(blueColumn); + referenceColorTable.getColumns().add(rangeColumn); + referenceColorTable.getSelectionModel().selectedItemProperty().addListener( + referenceColorTableListener); + referenceColorTable.setMaxHeight(200); + + referenceColorPicker.setText("Reference color"); + referenceColorPicker.colorProperty().addListener(referenceColorPickerListener); + + referenceColorRangeSlider.setName("Range"); + referenceColorRangeSlider.setRange(0, 255); + referenceColorRangeSlider.clampBoth(); + + getChildren().addAll(listPane, settings, addRemoveControls, referenceColorTable, referenceColorPicker, referenceColorRangeSlider); + setPadding(new Insets(10)); + setSpacing(15); } private void updateSelectedMaterial(String materialName) { boolean materialExists = false; + emittanceColor.colorProperty().removeListener(emittanceColorListener); + emitterMappingType.getSelectionModel().selectedItemProperty().removeListener(emitterMappingTypeListener); + useReferenceColors.selectedProperty().removeListener(useReferenceColorsListener); + diffuseColor.colorProperty().removeListener(diffuseColorListener); + specularColor.colorProperty().removeListener(specularColorListener); + transmissionSpecularColor.colorProperty().removeListener(transmissionSpecularColorListener); + volumeColor.colorProperty().removeListener(volumeColorListener); + absorptionColor.colorProperty().removeListener(absorptionColorListener); + opaque.selectedProperty().removeListener(opaqueListener); + hidden.selectedProperty().removeListener(hiddenListener); + referenceColorTable.getSelectionModel().selectedItemProperty().removeListener(referenceColorTableListener); + referenceColorTable.getItems().clear(); + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener); if (MaterialStore.collections.containsKey(materialName)) { double emAcc = 0; + double emitterMappingOffsetAcc = 0; + double alphaAcc = 0; + double subsurfaceScatteringAcc = 0; double specAcc = 0; double iorAcc = 0; double perceptualSmoothnessAcc = 0; + double perceptualTransmissionSmoothnessAcc = 0; double metalnessAcc = 0; + double transmissionMetalnessAcc = 0; + double volumeDensityAcc = 0; + double volumeAnisotropyAcc = 0; + double volumeEmittanceAcc = 0; + double absorptionAcc = 0; + Collection blocks = MaterialStore.collections.get(materialName); for (Block block : blocks) { emAcc += block.emittance; + emitterMappingOffsetAcc += block.emitterMappingOffset; + alphaAcc += block.alpha; + subsurfaceScatteringAcc += block.subSurfaceScattering; specAcc += block.specular; iorAcc += block.ior; perceptualSmoothnessAcc += block.getPerceptualSmoothness(); + perceptualTransmissionSmoothnessAcc += block.getPerceptualTransmissionSmoothness(); metalnessAcc += block.metalness; + transmissionMetalnessAcc += block.transmissionMetalness; + volumeDensityAcc += block.volumeDensity; + volumeAnisotropyAcc += block.volumeAnisotropy; + volumeEmittanceAcc += block.volumeEmittance; + absorptionAcc += block.absorption; } + emittance.set(emAcc / blocks.size()); + emitterMappingOffset.set(emitterMappingOffsetAcc / blocks.size()); + alpha.set(alphaAcc / blocks.size()); + subsurfaceScattering.set(subsurfaceScatteringAcc / blocks.size()); specular.set(specAcc / blocks.size()); ior.set(iorAcc / blocks.size()); perceptualSmoothness.set(perceptualSmoothnessAcc / blocks.size()); + perceptualTransmissionSmoothness.set(perceptualTransmissionSmoothnessAcc / blocks.size()); metalness.set(metalnessAcc / blocks.size()); + transmissionMetalness.set(transmissionMetalnessAcc / blocks.size()); + volumeDensity.set(volumeDensityAcc / blocks.size()); + volumeAnisotropy.set(volumeAnisotropyAcc / blocks.size()); + volumeEmittance.set(volumeEmittanceAcc / blocks.size()); + absorption.set(absorptionAcc / blocks.size()); materialExists = true; + } else if (ExtraMaterials.idMap.containsKey(materialName)) { Material material = ExtraMaterials.idMap.get(materialName); if (material != null) { emittance.set(material.emittance); + emittanceColor.setColor(ColorUtil.toFx(material.emittanceColor)); + emitterMappingOffset.set(material.emitterMappingOffset); + emitterMappingType.getSelectionModel().select(material.emitterMappingType); + useReferenceColors.setSelected(material.useReferenceColors); + if (material.emitterMappingReferenceColors != null && !material.emitterMappingReferenceColors.isEmpty()) { + material.emitterMappingReferenceColors.forEach(referenceColor -> referenceColorTable.getItems().add(new MaterialReferenceColorData(referenceColor))); + } + referenceColorTableListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener); + referenceColorPicker.setColor(ColorUtil.toFx(newValue.getReferenceColor().toVec3())); + referenceColorPickerListener = (observable2, oldValue2, newValue2) -> { + Vector3 color = ColorUtil.fromFx(newValue2); + newValue.setReferenceColor(color); + setEmitterMappingReferenceColors(materialName); + }; + referenceColorPicker.colorProperty().addListener(referenceColorPickerListener); + referenceColorRangeSlider.set(newValue.getReferenceColor().w * 255); + referenceColorRangeSlider.onValueChange(value -> { + newValue.setRange(value); + setEmitterMappingReferenceColors(materialName); + }); + } else { + referenceColorRangeSlider.onValueChange(value -> {}); + } + }; + alpha.set(material.alpha); + subsurfaceScattering.set(material.subSurfaceScattering); + diffuseColor.setColor(ColorUtil.toFx(material.diffuseColor)); specular.set(material.specular); ior.set(material.ior); perceptualSmoothness.set(material.getPerceptualSmoothness()); + perceptualTransmissionSmoothness.set(material.getPerceptualTransmissionSmoothness()); metalness.set(material.metalness); + transmissionMetalness.set(material.transmissionMetalness); + specularColor.setColor(ColorUtil.toFx(material.specularColor)); + transmissionSpecularColor.setColor(ColorUtil.toFx(material.transmissionSpecularColor)); + volumeDensity.set(material.volumeDensity); + volumeAnisotropy.set(material.volumeAnisotropy); + volumeEmittance.set(material.volumeEmittance); + volumeColor.setColor(ColorUtil.toFx(material.volumeColor)); + absorption.set(material.absorption); + absorptionColor.setColor(ColorUtil.toFx(material.absorptionColor)); + opaque.setSelected(material.opaque); + hidden.setSelected(material.hidden); materialExists = true; } } else if (MaterialStore.blockIds.contains(materialName)) { - Block block = new MinecraftBlock(materialName.substring(10), Texture.air); + Block block = new UnknownBlock(materialName.substring(10)); scene.getPalette().applyMaterial(block); emittance.set(block.emittance); + emittanceColor.setColor(ColorUtil.toFx(block.emittanceColor)); + emitterMappingOffset.set(block.emitterMappingOffset); + emitterMappingType.getSelectionModel().select(block.emitterMappingType); + useReferenceColors.setSelected(block.useReferenceColors); + if (block.emitterMappingReferenceColors != null && !block.emitterMappingReferenceColors.isEmpty()) { + block.emitterMappingReferenceColors.forEach(referenceColor -> referenceColorTable.getItems().add(new MaterialReferenceColorData(referenceColor))); + } + referenceColorTableListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener); + referenceColorPicker.setColor(ColorUtil.toFx(newValue.getReferenceColor().toVec3())); + referenceColorPickerListener = (observable2, oldValue2, newValue2) -> { + Vector3 color = ColorUtil.fromFx(newValue2); + newValue.setReferenceColor(color); + setEmitterMappingReferenceColors(materialName); + }; + referenceColorPicker.colorProperty().addListener(referenceColorPickerListener); + referenceColorRangeSlider.set(newValue.getReferenceColor().w * 255); + referenceColorRangeSlider.onValueChange(value -> { + newValue.setRange(value); + setEmitterMappingReferenceColors(materialName); + }); + } else { + referenceColorRangeSlider.onValueChange(value -> {}); + } + }; + alpha.set(block.alpha); + subsurfaceScattering.set(block.subSurfaceScattering); + diffuseColor.setColor(ColorUtil.toFx(block.diffuseColor)); specular.set(block.specular); ior.set(block.ior); perceptualSmoothness.set(block.getPerceptualSmoothness()); + perceptualTransmissionSmoothness.set(block.getPerceptualTransmissionSmoothness()); metalness.set(block.metalness); + transmissionMetalness.set(block.transmissionMetalness); + specularColor.setColor(ColorUtil.toFx(block.specularColor)); + transmissionSpecularColor.setColor(ColorUtil.toFx(block.transmissionSpecularColor)); + volumeDensity.set(block.volumeDensity); + volumeAnisotropy.set(block.volumeAnisotropy); + volumeEmittance.set(block.volumeEmittance); + volumeColor.setColor(ColorUtil.toFx(block.volumeColor)); + absorption.set(block.absorption); + absorptionColor.setColor(ColorUtil.toFx(block.absorptionColor)); + opaque.setSelected(block.opaque); + hidden.setSelected(block.hidden); materialExists = true; } if (materialExists) { emittance.onValueChange(value -> scene.setEmittance(materialName, value.floatValue())); + emittanceColorListener = (observable, oldValue, newValue) -> scene.setEmittanceColor(materialName, ColorUtil.fromFx(newValue)); + emittanceColor.colorProperty().addListener(emittanceColorListener); + emitterMappingOffset.onValueChange(value -> scene.setEmitterMappingOffset(materialName, value.floatValue())); + emitterMappingTypeListener = (observable, oldValue, newValue) -> scene.setEmitterMappingTypeOverride(materialName, newValue); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener(emitterMappingTypeListener); + useReferenceColorsListener = (observable, oldValue, newValue) -> scene.setUseReferenceColors(materialName, newValue); + useReferenceColors.selectedProperty().addListener(useReferenceColorsListener); + alpha.onValueChange(value -> scene.setAlpha(materialName, value.floatValue())); + subsurfaceScattering.onValueChange(value -> scene.setSubsurfaceScattering(materialName, value.floatValue())); + diffuseColorListener = (observable, oldValue, newValue) -> scene.setDiffuseColor(materialName, ColorUtil.fromFx(newValue)); + diffuseColor.colorProperty().addListener(diffuseColorListener); specular.onValueChange(value -> scene.setSpecular(materialName, value.floatValue())); ior.onValueChange(value -> scene.setIor(materialName, value.floatValue())); - perceptualSmoothness.onValueChange(value -> scene.setPerceptualSmoothness(materialName, value.floatValue())); + perceptualSmoothness.onValueChange( + value -> scene.setPerceptualSmoothness(materialName, value.floatValue()) + ); + perceptualTransmissionSmoothness.onValueChange( + value -> scene.setPerceptualTransmissionSmoothness(materialName, value.floatValue()) + ); metalness.onValueChange(value -> scene.setMetalness(materialName, value.floatValue())); + transmissionMetalness.onValueChange(value -> scene.setTransmissionMetalness(materialName, value.floatValue())); + specularColorListener = (observable, oldValue, newValue) -> scene.setSpecularColor(materialName, ColorUtil.fromFx(newValue)); + specularColor.colorProperty().addListener(specularColorListener); + transmissionSpecularColorListener = (observable, oldValue, newValue) -> scene.setTransmissionSpecularColor(materialName, ColorUtil.fromFx(newValue)); + transmissionSpecularColor.colorProperty().addListener(transmissionSpecularColorListener); + volumeDensity.onValueChange(value -> scene.setVolumeDensity(materialName, value.floatValue())); + volumeAnisotropy.onValueChange(value -> scene.setVolumeAnisotropy(materialName, value.floatValue())); + volumeEmittance.onValueChange(value -> scene.setVolumeEmittance(materialName, value.floatValue())); + volumeColorListener = (observable, oldValue, newValue) -> scene.setVolumeColor(materialName, ColorUtil.fromFx(newValue)); + volumeColor.colorProperty().addListener(volumeColorListener); + absorption.onValueChange(value -> scene.setAbsorption(materialName, value.floatValue())); + absorptionColorListener = (observable, oldValue, newValue) -> scene.setAbsorptionColor(materialName, ColorUtil.fromFx(newValue)); + absorptionColor.colorProperty().addListener(absorptionColorListener); + opaqueListener = (observable, oldValue, newValue) -> scene.setOpaque(materialName, newValue); + opaque.selectedProperty().addListener(opaqueListener); + hiddenListener = (observable, oldValue, newValue) -> scene.setHidden(materialName, newValue); + hidden.selectedProperty().addListener(hiddenListener); + addReferenceColor.setOnAction(e -> { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener); + referenceColorRangeSlider.onValueChange(value -> {}); + referenceColorTable.getItems().add(new MaterialReferenceColorData(new Vector4(1, 1, 1, 1))); + setEmitterMappingReferenceColors(materialName); + referenceColorTable.getSelectionModel().selectLast(); + }); + removeReferenceColor.setOnAction(e -> { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener); + referenceColorRangeSlider.onValueChange(value -> {}); + int index = referenceColorTable.getSelectionModel().getSelectedIndex(); + referenceColorTable.getItems().remove(index); + setEmitterMappingReferenceColors(materialName); + }); + referenceColorTable.getSelectionModel().selectedItemProperty().addListener(referenceColorTableListener); } else { emittance.onValueChange(value -> {}); + emitterMappingOffset.onValueChange(value -> {}); + alpha.onValueChange(value -> {}); + subsurfaceScattering.onValueChange(value -> {}); specular.onValueChange(value -> {}); ior.onValueChange(value -> {}); perceptualSmoothness.onValueChange(value -> {}); + perceptualTransmissionSmoothness.onValueChange(value -> {}); metalness.onValueChange(value -> {}); + transmissionMetalness.onValueChange(value -> {}); + volumeDensity.onValueChange(value -> {}); + volumeAnisotropy.onValueChange(value -> {}); + volumeEmittance.onValueChange(value -> {}); + absorption.onValueChange(value -> {}); + referenceColorRangeSlider.onValueChange(value -> {}); + addReferenceColor.setOnAction(e -> {}); + removeReferenceColor.setOnAction(e -> {}); } } + private void setEmitterMappingReferenceColors(String materialName) { + ArrayList referenceColors = new ArrayList<>(referenceColorTable.getItems().size()); + referenceColorTable.getItems().forEach(data -> referenceColors.add(data.getReferenceColor())); + scene.setEmitterMappingReferenceColors(materialName, referenceColors); + } + @Override public void update(Scene scene) { String material = listView.getSelectionModel().getSelectedItem(); updateSelectedMaterial(material); @@ -175,14 +561,10 @@ private void updateSelectedMaterial(String materialName) { return "Materials"; } - @Override public Node getTabContent() { + @Override public VBox getTabContent() { return this; } @Override public void initialize(URL location, ResourceBundle resources) { } - - @Override public void setController(RenderControlsFxController controller) { - scene = controller.getRenderController().getSceneManager().getScene(); - } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java index 2381d704c5..619981358a 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java @@ -16,62 +16,39 @@ */ package se.llbit.chunky.ui.render.tabs; -import javafx.event.EventHandler; +import javafx.beans.property.ReadOnlyStringWrapper; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.VBox; -import javafx.util.StringConverter; import se.llbit.chunky.renderer.RenderMode; -import se.llbit.chunky.renderer.postprocessing.HableToneMappingFilter; import se.llbit.chunky.renderer.postprocessing.PostProcessingFilter; -import se.llbit.chunky.renderer.postprocessing.PostProcessingFilters; -import se.llbit.chunky.renderer.postprocessing.UE4ToneMappingFilter; import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.resources.BitmapImage; import se.llbit.chunky.ui.DoubleAdjuster; -import se.llbit.chunky.ui.DoubleTextField; -import se.llbit.chunky.ui.controller.RenderControlsFxController; +import se.llbit.chunky.ui.dialogs.PostprocessingFilterChooser; import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.util.ProgressListener; import se.llbit.util.TaskTracker; -import se.llbit.util.TaskTracker.Task; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; -public class PostprocessingTab extends ScrollPane implements RenderControlsTab, Initializable { - private Scene scene; - private RenderControlsFxController controller; - - @FXML private DoubleAdjuster exposure; - @FXML private ChoiceBox postprocessingFilter; - - @FXML private VBox hableCurveSettings; - @FXML private DoubleTextField hableShoulderStrength; - @FXML private DoubleTextField hableLinearStrength; - @FXML private DoubleTextField hableLinearAngle; - @FXML private DoubleTextField hableToeStrength; - @FXML private DoubleTextField hableToeNumerator; - @FXML private DoubleTextField hableToeDenominator; - @FXML private DoubleTextField hableLinearWhitePointValue; - @FXML private Button gdcPreset; - @FXML private Button fwPreset; - - @FXML private VBox ue4CurveSettings; - @FXML private DoubleTextField ue4Saturation; - @FXML private DoubleTextField ue4Slope; - @FXML private DoubleTextField ue4Toe; - @FXML private DoubleTextField ue4Shoulder; - @FXML private DoubleTextField ue4BlackClip; - @FXML private DoubleTextField ue4WhiteClip; - @FXML private Button acesPreset; - @FXML private Button ue4LegacyPreset; +public class PostprocessingTab extends RenderControlsTab implements Initializable { + + @FXML + private DoubleAdjuster exposure; + @FXML + private TableView filterTable; + @FXML + private TableColumn filterTypeCol; + @FXML + private Button addFilter; + @FXML + private Button removeFilter; + @FXML + private TitledPane filterControls; public PostprocessingTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("PostprocessingTab.fxml")); @@ -80,168 +57,80 @@ public PostprocessingTab() throws IOException { loader.load(); } - @Override public void setController(RenderControlsFxController controller) { - this.controller = controller; - scene = controller.getRenderController().getSceneManager().getScene(); - } - - @Override public void update(Scene scene) { - postprocessingFilter.getSelectionModel().select(scene.getPostProcessingFilter()); - hableCurveSettings.setVisible(scene.getPostProcessingFilter() instanceof HableToneMappingFilter); - ue4CurveSettings.setVisible(scene.getPostProcessingFilter() instanceof UE4ToneMappingFilter); + @Override + public void update(Scene scene) { exposure.set(scene.getExposure()); + rebuildList(); + } - if (scene.getPostProcessingFilter() instanceof HableToneMappingFilter) { - HableToneMappingFilter filter = (HableToneMappingFilter) scene.getPostProcessingFilter(); - hableShoulderStrength.valueProperty().set(filter.getShoulderStrength()); - hableLinearStrength.valueProperty().set(filter.getLinearStrength()); - hableLinearAngle.valueProperty().set(filter.getLinearAngle()); - hableToeStrength.valueProperty().set(filter.getToeStrength()); - hableToeNumerator.valueProperty().set(filter.getToeNumerator()); - hableToeDenominator.valueProperty().set(filter.getToeDenominator()); - hableLinearWhitePointValue.valueProperty().set(filter.getLinearWhitePointValue()); - } else if (scene.getPostProcessingFilter() instanceof UE4ToneMappingFilter) { - UE4ToneMappingFilter filter = (UE4ToneMappingFilter) scene.getPostProcessingFilter(); - ue4Saturation.valueProperty().set(filter.getSaturation()); - ue4Slope.valueProperty().set(filter.getSlope()); - ue4Toe.valueProperty().set(filter.getToe()); - ue4Shoulder.valueProperty().set(filter.getShoulder()); - ue4BlackClip.valueProperty().set(filter.getBlackClip()); - ue4WhiteClip.valueProperty().set(filter.getWhiteClip()); - } + private void rebuildList() { + filterTable.getSelectionModel().clearSelection(); + filterTable.getItems().clear(); + scene.postprocessingFilters.forEach(filterTable.getItems()::add); } - @Override public String getTabTitle() { + @Override + public String getTabTitle() { return "Postprocessing"; } - @Override public Node getTabContent() { + @Override + public VBox getTabContent() { return this; } - @Override public void initialize(URL location, ResourceBundle resources) { - postprocessingFilter.setTooltip(new Tooltip("Set the postprocessing filter to be used on the image.")); - postprocessingFilter.getItems().add(PostProcessingFilters.NONE); - postprocessingFilter.getItems().add(new PostprocessingSeparator()); - for (PostProcessingFilter filter : PostProcessingFilters.getFilters()) { - if (filter != PostProcessingFilters.NONE) { - postprocessingFilter.getItems().add(filter); - } - } - postprocessingFilter.getSelectionModel().select(Scene.DEFAULT_POSTPROCESSING_FILTER); - postprocessingFilter.getSelectionModel().selectedItemProperty().addListener( - (observable, oldValue, newValue) -> { - scene.setPostprocess(newValue); - applyChangedSettings(false); - }); - postprocessingFilter.setConverter(new StringConverter() { - @Override - public String toString(PostProcessingFilter object) { - return object == null ? null : object.getName(); - } - - @Override - public PostProcessingFilter fromString(String string) { - return PostProcessingFilters.getPostProcessingFilterFromName(string).orElse(Scene.DEFAULT_POSTPROCESSING_FILTER); - } - }); + @Override + public void initialize(URL location, ResourceBundle resources) { exposure.setName("Exposure"); - exposure.setTooltip("Linear exposure of the image."); + exposure.setTooltip("Exposure of the image. Applied before postprocessing filters."); exposure.setRange(Scene.MIN_EXPOSURE, Scene.MAX_EXPOSURE); - exposure.makeLogarithmic(); exposure.clampMin(); exposure.onValueChange(value -> { scene.setExposure(value); applyChangedSettings(false); }); - hableCurveSettings.managedProperty().bind(hableCurveSettings.visibleProperty()); - gdcPreset.setOnAction((e) -> { - if (scene.postProcessingFilter instanceof HableToneMappingFilter) { - ((HableToneMappingFilter) scene.postProcessingFilter).applyPreset(HableToneMappingFilter.Preset.GDC); - applyChangedSettings(true); - } - }); - fwPreset.setOnAction((e) -> { - if (scene.postProcessingFilter instanceof HableToneMappingFilter) { - ((HableToneMappingFilter) scene.postProcessingFilter).applyPreset(HableToneMappingFilter.Preset.FILMIC_WORLDS); - applyChangedSettings(true); - } - }); - ue4CurveSettings.managedProperty().bind(ue4CurveSettings.visibleProperty()); - acesPreset.setOnAction((e) -> { - if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { - ((UE4ToneMappingFilter) scene.postProcessingFilter).applyPreset(UE4ToneMappingFilter.Preset.ACES); - applyChangedSettings(true); + + filterTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + filterControls.setContent(newValue.getControls(this)); + } else { + filterControls.setContent(null); } }); - ue4LegacyPreset.setOnAction((e) -> { - if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { - ((UE4ToneMappingFilter) scene.postProcessingFilter).applyPreset(UE4ToneMappingFilter.Preset.LEGACY_UE4); + filterTable.refresh(); + filterTypeCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().getName())); + filterTypeCol.setSortable(false); + + addFilter.setOnAction(e -> { + PostprocessingFilterChooser dialog = new PostprocessingFilterChooser(); + if (dialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + Class filterClass = dialog.getFilter(); + try { + scene.addPostprocessingFilter(filterClass.newInstance()); + } catch (InstantiationException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } applyChangedSettings(true); + filterTable.getSelectionModel().selectLast(); } }); - EventHandler postprocessingSettingsHandler = e -> { - if (e.getCode() == KeyCode.ENTER) { - if (scene.postProcessingFilter instanceof HableToneMappingFilter) { - HableToneMappingFilter filter = (HableToneMappingFilter) scene.postProcessingFilter; - filter.setShoulderStrength(hableShoulderStrength.valueProperty().floatValue()); - filter.setLinearStrength(hableLinearStrength.valueProperty().floatValue()); - filter.setLinearAngle(hableLinearAngle.valueProperty().floatValue()); - filter.setToeStrength(hableToeStrength.valueProperty().floatValue()); - filter.setToeNumerator(hableToeNumerator.valueProperty().floatValue()); - filter.setToeDenominator(hableToeDenominator.valueProperty().floatValue()); - filter.setLinearWhitePointValue(hableLinearWhitePointValue.valueProperty().floatValue()); - } else if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { - UE4ToneMappingFilter filter = (UE4ToneMappingFilter) scene.postProcessingFilter; - filter.setSaturation(ue4Saturation.valueProperty().floatValue()); - filter.setSlope(ue4Slope.valueProperty().floatValue()); - filter.setToe(ue4Toe.valueProperty().floatValue()); - filter.setShoulder(ue4Shoulder.valueProperty().floatValue()); - filter.setBlackClip(ue4BlackClip.valueProperty().floatValue()); - filter.setWhiteClip(ue4WhiteClip.valueProperty().floatValue()); - } - applyChangedSettings(true); - } - }; - hableShoulderStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableLinearStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableLinearAngle.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableToeStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableToeNumerator.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableToeDenominator.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - hableLinearWhitePointValue.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4Saturation.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4Slope.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4Toe.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4Shoulder.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4BlackClip.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); - ue4WhiteClip.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + removeFilter.setOnAction(e -> { + int index = filterTable.getSelectionModel().getSelectedIndex(); + scene.removePostprocessingFilter(index); + applyChangedSettings(true); + }); } - private void applyChangedSettings(boolean refreshScene) { - if (refreshScene && scene.getMode() == RenderMode.PREVIEW) { + private void applyChangedSettings(boolean update) { + if (scene.getMode() == RenderMode.PREVIEW) { // Don't interrupt the render if we are currently rendering. scene.refresh(); } - update(scene); + if (update) { + update(scene); + } scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); controller.getCanvas().forceRepaint(); } - - /** - * Fake post processing filter that is also a separator for the combobox. - */ - private static class PostprocessingSeparator extends Separator implements PostProcessingFilter { - - @Override - public void processFrame(int width, int height, double[] input, BitmapImage output, - double exposure, Task task) { - } - - @Override - public String getName() { - return ""; - } - } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/TexturesTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/TexturesTab.java index 5ed9bfb79e..778f7bf073 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/TexturesTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/TexturesTab.java @@ -20,15 +20,14 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ScrollPane; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import org.controlsfx.control.ToggleSwitch; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.renderer.RenderController; import se.llbit.chunky.renderer.scene.Scene; @@ -46,16 +45,16 @@ import java.net.URL; import java.util.ResourceBundle; -public class TexturesTab extends ScrollPane implements RenderControlsTab, Initializable { - private RenderController controller; +public class TexturesTab extends RenderControlsTab implements Initializable { + private RenderController renderController; private SceneManager sceneManager; @FXML - private CheckBox biomeColors; + private ToggleSwitch biomeColors; @FXML private IntegerAdjuster biomeBlendingRadiusInput; @FXML - private CheckBox singleColorBtn; + private ToggleSwitch singleColorBtn; @FXML private Button editResourcePacks; @@ -139,14 +138,14 @@ private void alertIfReloadNeeded(String changedFeature) { warning.setTitle("Chunk reload required"); ButtonType result = warning.showAndWait().orElse(ButtonType.CANCEL); if (result.getButtonData() == ButtonBar.ButtonData.FINISH) { - controller.getSceneManager().reloadChunks(); + renderController.getSceneManager().reloadChunks(); } } @Override - public void setController(RenderControlsFxController fxController) { - controller = fxController.getRenderController(); - sceneManager = controller.getSceneManager(); + protected void onSetController(RenderControlsFxController fxController) { + renderController = fxController.getRenderController(); + sceneManager = renderController.getSceneManager(); } @Override @@ -162,7 +161,7 @@ public String getTabTitle() { } @Override - public Node getTabContent() { + public VBox getTabContent() { return this; } } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java index 6efb2a7653..80e5854f51 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java @@ -17,57 +17,29 @@ */ package se.llbit.chunky.ui.render.tabs; -import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.paint.Color; -import se.llbit.chunky.PersistentSettings; -import se.llbit.chunky.renderer.RenderController; -import se.llbit.chunky.renderer.WaterShadingStrategy; +import javafx.scene.layout.VBox; +import se.llbit.chunky.renderer.scene.watershading.WaterShadingStrategy; import se.llbit.chunky.renderer.scene.*; import se.llbit.chunky.ui.DoubleAdjuster; -import se.llbit.chunky.ui.IntegerAdjuster; -import se.llbit.chunky.ui.controller.RenderControlsFxController; import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.chunky.world.World; -import se.llbit.fx.LuxColorPicker; -import se.llbit.math.ColorUtil; -import se.llbit.math.Vector3; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; -public class WaterTab extends ScrollPane implements RenderControlsTab, Initializable { - private Scene scene; - +public class WaterTab extends RenderControlsTab implements Initializable { @FXML private ChoiceBox waterShader; - @FXML private DoubleAdjuster waterVisibility; - @FXML private DoubleAdjuster waterOpacity; - @FXML private CheckBox useCustomWaterColor; - @FXML private LuxColorPicker waterColor; - @FXML private Button saveDefaults; @FXML private CheckBox waterPlaneEnabled; @FXML private DoubleAdjuster waterPlaneHeight; @FXML private CheckBox waterPlaneOffsetEnabled; @FXML private CheckBox waterPlaneClip; @FXML private TitledPane waterWorldModeDetailsPane; - @FXML private IntegerAdjuster proceduralWaterIterations; - @FXML private DoubleAdjuster proceduralWaterFrequency; - @FXML private DoubleAdjuster proceduralWaterAmplitude; - @FXML private DoubleAdjuster proceduralWaterAnimationSpeed; - @FXML private TitledPane proceduralWaterDetailsPane; - - private RenderControlsFxController renderControls; - private RenderController controller; - private final ChangeListener waterColorListener = - (observable, oldValue, newValue) -> { - scene.setWaterColor(ColorUtil.fromFx(newValue)); - useCustomWaterColor.setSelected(true); - }; + @FXML private TitledPane waterShaderControls; public WaterTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("WaterTab.fxml")); @@ -76,43 +48,16 @@ public WaterTab() throws IOException { loader.load(); } - @Override - public void setController(RenderControlsFxController controller) { - renderControls = controller; - this.controller = controller.getRenderController(); - scene = this.controller.getSceneManager().getScene(); - } - @Override public void update(Scene scene) { - useCustomWaterColor.setSelected(scene.getUseCustomWaterColor()); waterShader.getSelectionModel().select(scene.getWaterShadingStrategy()); - waterVisibility.set(scene.getWaterVisibility()); - waterOpacity.set(scene.getWaterOpacity()); - - // Update water color without modifying the useCustomColor value. - waterColor.colorProperty().removeListener(waterColorListener); - waterColor.setColor(ColorUtil.toFx(scene.getWaterColor())); - waterColor.colorProperty().addListener(waterColorListener); + waterShaderControls.setContent(scene.getCurrentWaterShader().getControls(this)); waterPlaneEnabled.setSelected(scene.isWaterPlaneEnabled()); waterPlaneHeight.setRange(scene.yClipMin, scene.yClipMax); waterPlaneHeight.set(scene.getWaterPlaneHeight()); waterPlaneOffsetEnabled.setSelected(scene.isWaterPlaneOffsetEnabled()); waterPlaneClip.setSelected(scene.getWaterPlaneChunkClip()); - - if(scene.getCurrentWaterShader() instanceof SimplexWaterShader) { - SimplexWaterShader simplexWaterShader = (SimplexWaterShader) scene.getCurrentWaterShader(); - proceduralWaterIterations.set(simplexWaterShader.iterations); - proceduralWaterFrequency.set(simplexWaterShader.baseFrequency); - proceduralWaterAmplitude.set(simplexWaterShader.baseAmplitude); - proceduralWaterAnimationSpeed.set(simplexWaterShader.animationSpeed); - } else { - proceduralWaterIterations.set(4); - proceduralWaterFrequency.set(0.4); - proceduralWaterAmplitude.set(0.025); - proceduralWaterAnimationSpeed.set(1); - } } @Override @@ -121,40 +66,16 @@ public String getTabTitle() { } @Override - public Node getTabContent() { + public VBox getTabContent() { return this; } @Override public void initialize(URL location, ResourceBundle resources) { - waterVisibility.setName("Water visibility"); - waterVisibility.setTooltip("Distance of visibility past the water surface."); - waterVisibility.setRange(0, 50); - waterVisibility.clampMin(); - waterVisibility.onValueChange(value -> scene.setWaterVisibility(value)); - - waterOpacity.setName("Water opacity"); - waterOpacity.setTooltip("Opacity of the water surface."); - waterOpacity.setRange(0, 1); - waterOpacity.clampBoth(); - waterOpacity.onValueChange(value -> scene.setWaterOpacity(value)); - waterShader.getItems().addAll(WaterShadingStrategy.values()); waterShader.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { scene.setWaterShadingStrategy(newValue); - switch (newValue) { - case STILL: - case TILED_NORMALMAP: - proceduralWaterDetailsPane.setVisible(false); - proceduralWaterDetailsPane.setExpanded(false); - proceduralWaterDetailsPane.setManaged(false); - break; - case SIMPLEX: - proceduralWaterDetailsPane.setVisible(true); - proceduralWaterDetailsPane.setExpanded(true); - proceduralWaterDetailsPane.setManaged(true); - break; - } + waterShaderControls.setContent(scene.getCurrentWaterShader().getControls(this)); }); StringBuilder waterShaderOptions = new StringBuilder("\n\n"); for (WaterShadingStrategy strategy : WaterShadingStrategy.values()) { @@ -162,26 +83,6 @@ public void initialize(URL location, ResourceBundle resources) { } waterShader.setTooltip(new Tooltip("Change how the water surface is rendered." + waterShaderOptions)); - useCustomWaterColor.setTooltip(new Tooltip("Disable biome tinting for water, and use a custom color instead.")); - useCustomWaterColor.selectedProperty().addListener((observable, oldValue, newValue) -> - scene.setUseCustomWaterColor(newValue) - ); - - waterColor.colorProperty().addListener(waterColorListener); - - saveDefaults.setTooltip(new Tooltip("Save the current water settings as new defaults.")); - saveDefaults.setOnAction(e -> { - PersistentSettings.setWaterShadingStrategy(scene.getWaterShadingStrategy().getId()); - PersistentSettings.setWaterOpacity(scene.getWaterOpacity()); - PersistentSettings.setWaterVisibility(scene.getWaterVisibility()); - boolean useCustomWaterColor = scene.getUseCustomWaterColor(); - PersistentSettings.setUseCustomWaterColor(useCustomWaterColor); - if (useCustomWaterColor) { - Vector3 color = scene.getWaterColor(); - PersistentSettings.setWaterColor(color.x, color.y, color.z); - } - }); - waterWorldModeDetailsPane.setVisible(waterPlaneEnabled.isSelected()); waterWorldModeDetailsPane.setExpanded(waterPlaneEnabled.isSelected()); waterWorldModeDetailsPane.setManaged(waterPlaneEnabled.isSelected()); @@ -207,51 +108,6 @@ public void initialize(URL location, ResourceBundle resources) { waterPlaneClip.selectedProperty().addListener((observable, oldValue, newValue) -> scene.setWaterPlaneChunkClip(newValue) ); - - proceduralWaterIterations.setName("Iterations"); - proceduralWaterIterations.setTooltip("The number of iterations (layers) of noise used"); - proceduralWaterIterations.setRange(1, 10); - proceduralWaterIterations.onValueChange(iter -> { - WaterShader shader = scene.getCurrentWaterShader(); - if(shader instanceof SimplexWaterShader) { - ((SimplexWaterShader) shader).iterations = iter; - scene.refresh(); - } - }); - - proceduralWaterFrequency.setName("Frequency"); - proceduralWaterFrequency.setTooltip("The frequency of the noise"); - proceduralWaterFrequency.setRange(0, 1); - proceduralWaterFrequency.onValueChange(freq -> { - WaterShader shader = scene.getCurrentWaterShader(); - if(shader instanceof SimplexWaterShader) { - ((SimplexWaterShader) shader).baseFrequency = freq; - } - scene.refresh(); - }); - - proceduralWaterAmplitude.setName("Amplitude"); - proceduralWaterAmplitude.setTooltip("The amplitude of the noise"); - proceduralWaterAmplitude.setRange(0, 1); - proceduralWaterAmplitude.onValueChange(amp -> { - WaterShader shader = scene.getCurrentWaterShader(); - if(shader instanceof SimplexWaterShader) { - ((SimplexWaterShader) shader).baseAmplitude = amp; - } - scene.refresh(); - }); - - proceduralWaterAnimationSpeed.setName("Animation speed"); - proceduralWaterAnimationSpeed.setTooltip("Animation speed of the water. " - + " Only relevant when rendering animation by varying animation time."); - proceduralWaterAnimationSpeed.setRange(0, 10); - proceduralWaterAnimationSpeed.onValueChange(speed -> { - WaterShader shader = scene.getCurrentWaterShader(); - if(shader instanceof SimplexWaterShader) { - ((SimplexWaterShader) shader).animationSpeed = speed; - } - scene.refresh(); - }); } } diff --git a/chunky/src/java/se/llbit/chunky/world/Chunk.java b/chunky/src/java/se/llbit/chunky/world/Chunk.java index 092955ba8a..4b256119a3 100644 --- a/chunky/src/java/se/llbit/chunky/world/Chunk.java +++ b/chunky/src/java/se/llbit/chunky/world/Chunk.java @@ -16,6 +16,7 @@ */ package se.llbit.chunky.world; +import se.llbit.chunky.block.Void; import se.llbit.chunky.block.minecraft.Air; import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; import se.llbit.chunky.block.Block; @@ -362,7 +363,7 @@ protected static void updateHeightmap(Heightmap heightmap, ChunkPosition pos, Ch int y = Math.max(chunkData.minY()+1, Math.min(chunkHeightmap[z * 16 + x] - 1, yMax)); for (; y > chunkData.minY()+1; --y) { Block block = palette.get(chunkData.getBlockAt(x, y, z)); - if (block != Air.INSTANCE && !block.isWater()) + if (block != Air.INSTANCE && block != Void.INSTANCE && !block.isWater()) break; } heightmap.set(y, pos.x * 16 + x, pos.z * 16 + z); diff --git a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java index 1357b0bb1c..8716c5e265 100644 --- a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java +++ b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java @@ -22,10 +22,10 @@ import se.llbit.chunky.entity.CalibratedSculkSensorAmethyst; import se.llbit.chunky.entity.Campfire; import se.llbit.chunky.entity.SporeBlossom; -import se.llbit.chunky.world.material.CloudMaterial; import java.util.HashMap; import java.util.Map; +import se.llbit.chunky.world.material.WaterPlaneMaterial; public class ExtraMaterials { @@ -33,12 +33,12 @@ public class ExtraMaterials { public static final Map idMap = new HashMap<>(); static { - idMap.put("cloud", CloudMaterial.INSTANCE); idMap.put("candle_flame", Candle.flameMaterial); idMap.put("campfire_flame", Campfire.flameMaterial); idMap.put("soul_campfire_flame", Campfire.soulFlameMaterial); idMap.put("calibrated_sculk_sensor_amethyst_active", CalibratedSculkSensorAmethyst.activeMaterial); idMap.put("calibrated_sculk_sensor_amethyst_inactive", CalibratedSculkSensorAmethyst.inactiveMaterial); + idMap.put("water_plane", WaterPlaneMaterial.INSTANCE); idMap.put("spore_blossom (base)", SporeBlossom.baseMaterial); idMap.put("spore_blossom (blossom)", SporeBlossom.blossomMaterial); idMap.put("open_eyeblossom (emissive)", OpenEyeblossom.emissiveMaterial); @@ -46,8 +46,6 @@ public class ExtraMaterials { } public static void loadDefaultMaterialProperties() { - CloudMaterial.INSTANCE.restoreDefaults(); - Candle.flameMaterial.restoreDefaults(); Candle.flameMaterial.emittance = 1.0f; @@ -58,17 +56,18 @@ public static void loadDefaultMaterialProperties() { Campfire.soulFlameMaterial.emittance = 0.6f; CalibratedSculkSensorAmethyst.activeMaterial.restoreDefaults(); - CalibratedSculkSensorAmethyst.activeMaterial.emittance = 1.0f / 15; + CalibratedSculkSensorAmethyst.activeMaterial.setLightLevel(1f); CalibratedSculkSensorAmethyst.inactiveMaterial.restoreDefaults(); + WaterPlaneMaterial.INSTANCE.restoreDefaults(); SporeBlossom.blossomMaterial.restoreDefaults(); SporeBlossom.baseMaterial.restoreDefaults(); OpenEyeblossom.emissiveMaterial.restoreDefaults(); - OpenEyeblossom.emissiveMaterial.emittance = 1.0f / 15; + OpenEyeblossom.emissiveMaterial.setLightLevel(1f); FireflyBush.emissiveMaterial.restoreDefaults(); - FireflyBush.emissiveMaterial.emittance = 1.0f / 15; + FireflyBush.emissiveMaterial.setLightLevel(1f); } } diff --git a/chunky/src/java/se/llbit/chunky/world/Heightmap.java b/chunky/src/java/se/llbit/chunky/world/Heightmap.java index e4cb392214..7a10fb363a 100644 --- a/chunky/src/java/se/llbit/chunky/world/Heightmap.java +++ b/chunky/src/java/se/llbit/chunky/world/Heightmap.java @@ -19,9 +19,6 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import java.util.HashMap; -import java.util.Map; - /** * Chunk heightmap. * diff --git a/chunky/src/java/se/llbit/chunky/world/Material.java b/chunky/src/java/se/llbit/chunky/world/Material.java index 572af56774..e685099e26 100644 --- a/chunky/src/java/se/llbit/chunky/world/Material.java +++ b/chunky/src/java/se/llbit/chunky/world/Material.java @@ -16,11 +16,36 @@ */ package se.llbit.chunky.world; +import java.util.ArrayList; +import javafx.beans.value.ChangeListener; +import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.apache.commons.math3.util.FastMath; +import org.controlsfx.control.ToggleSwitch; +import se.llbit.chunky.renderer.scene.EmitterMappingType; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.IntegerAdjuster; +import se.llbit.chunky.ui.data.MaterialReferenceColorData; +import se.llbit.fx.LuxColorPicker; +import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonString; import se.llbit.json.JsonValue; -import se.llbit.math.Ray; +import se.llbit.math.*; + +import java.util.Random; public abstract class Material { @@ -53,12 +78,33 @@ public abstract class Material { /** * The specular coefficient controlling how shiny the block appears. */ - public float specular = 0; + public float specular = 0f; /** * The amount of light the material emits. */ - public float emittance = 0; + public float emittance = 0f; + + /** + * Offset to apply to the global emitter mapping exponent (the resulting value will be constrained to be >= 0). + */ + public float emitterMappingOffset = 0; + + /** + * Overrides the global emitter mapping type unless set to NONE. + */ + public EmitterMappingType emitterMappingType = EmitterMappingType.NONE; + + /** + * Whether to use reference colors. + */ + public boolean useReferenceColors = false; + + /** + * (x, y, z): The color to use for the REFERENCE_COLORS emitter mapping type. + * w: The range surrounding the specified color to apply full brightness. + */ + public ArrayList emitterMappingReferenceColors = null; /** * The (linear) roughness controlling how rough a shiny block appears. A value of 0 makes the @@ -66,6 +112,11 @@ public abstract class Material { */ public float roughness = 0f; + /** + * The (linear) roughness applied to transmitted / refracted rays. + */ + public float transmissionRoughness = 0f; + /** * The metalness value controls how metal-y a block appears. In reality this is a boolean value * but in practice usually a float is used in PBR to allow adding dirt or scratches on metals @@ -73,20 +124,89 @@ public abstract class Material { * Metals only do specular reflection for certain wavelengths (effectively tinting the reflection) * and have no diffuse reflection. The albedo color is used for tinting. */ - public float metalness = 0; + public float metalness = 0f; /** - * Subsurface scattering property. + * The amount of texture tinting applied to transmitted rays. */ - public boolean subSurfaceScattering = false; + public float transmissionMetalness = 0f; + + /** + * Additional color tinting applied to specularly-reflected rays. + */ + public final Vector3 specularColor = new Vector3(1, 1, 1); + + /** + * Additional color tinting applied to specularly-transmitted or -refracted rays. + */ + public final Vector3 transmissionSpecularColor = new Vector3(1, 1, 1); + + /** + * Texture alpha multiplier. + */ + public float alpha = 1f; + + /** + * Subsurface scattering property. This is extremely basic subsurface scattering that simply + * inverts the normal of the surface to diffusely reflect rays to the other side of the surface. + */ + public float subSurfaceScattering = 0f; + + /** + * Color tinting applied to diffusely-reflected rays. + */ + public final Vector3 diffuseColor = new Vector3(1, 1, 1); + + /** + * Color tinting applied to emitted light. + */ + public final Vector3 emittanceColor = new Vector3(1, 1, 1); + + /** + * The density of the volume medium of the material. Rays that are traversing the medium are more + * likely to hit the volume medium the higher the volume density. + */ + public float volumeDensity = 0f; + + /** + * The phase function that is applied to rays scattering off the volume medium. + */ + public float volumeAnisotropy = 0f; + + /** + * The emittance applied from rays intersecting the volume medium. + */ + public float volumeEmittance = 0f; + + /** + * The color of the volume medium. + */ + public Vector3 volumeColor = new Vector3(1, 1, 1); + + /** + * Rays traversing this material will have their light attenuated. The strength of the + * attenuation is controlled by this value. + */ + public float absorption = 0f; + + /** + * The color of light absorbed by the medium. + */ + public Vector3 absorptionColor = new Vector3(1, 1, 1); + + /** + * Whether the block is intersected. + */ + public boolean hidden = false; /** * Base texture. */ public final Texture texture; - public boolean refractive = false; - + /** + * Whether this block is waterlogged. + */ public boolean waterlogged = false; public Material(String name, Texture texture) { @@ -101,14 +221,33 @@ public void restoreDefaults() { ior = DEFAULT_IOR; opaque = false; solid = true; - specular = 0; - emittance = 0; - roughness = 0; - subSurfaceScattering = false; + specular = 0f; + metalness = 0f; + transmissionMetalness = 0f; + emittance = 0f; + emitterMappingOffset = 0; + emitterMappingType = EmitterMappingType.NONE; + useReferenceColors = false; + emitterMappingReferenceColors = null; + roughness = 0f; + transmissionRoughness = 0f; + subSurfaceScattering = 0f; + alpha = 1f; + specularColor.set(1, 1, 1); + transmissionSpecularColor.set(1, 1, 1); + diffuseColor.set(1, 1, 1); + emittanceColor.set(1); + volumeDensity = 0f; + volumeAnisotropy = 0f; + volumeEmittance = 0f; + volumeColor.set(1, 1, 1); + absorption = 0f; + absorptionColor.set(1, 1, 1); + hidden = false; } - public void getColor(Ray ray) { - texture.getColor(ray); + public void getColor(IntersectionRecord intersectionRecord) { + texture.getColor(intersectionRecord); } public float[] getColor(double u, double v) { @@ -122,15 +261,95 @@ public JsonValue toJson() { public void loadMaterialProperties(JsonObject json) { ior = json.get("ior").floatValue(ior); specular = json.get("specular").floatValue(specular); + metalness = json.get("metalness").floatValue(metalness); + transmissionMetalness = json.get("transmissionMetalness").floatValue(transmissionMetalness); emittance = json.get("emittance").floatValue(emittance); + emitterMappingOffset = json.get("emitterMappingOffset").floatValue(emitterMappingOffset); + emitterMappingType = EmitterMappingType.valueOf(json.get("emitterMappingType").asString(emitterMappingType.getId())); + useReferenceColors = json.get("useReferenceColors").boolValue(useReferenceColors); + JsonArray referenceColorsArray = json.get("emitterMappingReferenceColors").array(); + if (!referenceColorsArray.isEmpty()) { + emitterMappingReferenceColors = new ArrayList<>(); + for (JsonValue referenceColorJson : referenceColorsArray.elements) { + JsonObject referenceColorObject = referenceColorJson.object(); + emitterMappingReferenceColors.add(new Vector4( + referenceColorObject.get("red").doubleValue(0), + referenceColorObject.get("green").doubleValue(0), + referenceColorObject.get("blue").doubleValue(0), + referenceColorObject.get("range").doubleValue(0))); + } + } else { + emitterMappingReferenceColors = null; + } roughness = json.get("roughness").floatValue(roughness); - metalness = json.get("metalness").floatValue(metalness); + transmissionRoughness = json.get("transmissionRoughness").floatValue(transmissionRoughness); + alpha = json.get("alpha").floatValue(alpha); + specularColor.set(ColorUtil.jsonToRGB(json.get("specularColor").asObject(), specularColor)); + transmissionSpecularColor.set(ColorUtil.jsonToRGB(json.get("transmissionSpecularColor").asObject(), transmissionSpecularColor)); + subSurfaceScattering = json.get("subsurfaceScattering").floatValue(subSurfaceScattering); + diffuseColor.set(ColorUtil.jsonToRGB(json.get("diffuseColor").asObject(), diffuseColor)); + emittanceColor.set(ColorUtil.jsonToRGB(json.get("emittanceColor").asObject(), emittanceColor)); + volumeDensity = json.get("volumeDensity").floatValue(volumeDensity); + volumeAnisotropy = json.get("volumeAnisotropy").floatValue(volumeAnisotropy); + volumeEmittance = json.get("volumeEmittance").floatValue(volumeEmittance); + volumeColor.set(ColorUtil.jsonToRGB(json.get("volumeColor").asObject(), volumeColor)); + absorption = json.get("absorption").floatValue(absorption); + absorptionColor.set(ColorUtil.jsonToRGB(json.get("absorptionColor").asObject(), absorptionColor)); + opaque = json.get("opaque").boolValue(opaque); + hidden = json.get("hidden").boolValue(hidden); + } + + public JsonObject saveMaterialProperties() { + JsonObject properties = new JsonObject(); + properties.add("ior", ior); + properties.add("specular", specular); + properties.add("metalness", metalness); + properties.add("transmissionMetalness", transmissionMetalness); + properties.add("emittance", emittance); + properties.add("emitterMappingOffset", emitterMappingOffset); + properties.add("emitterMappingType", emitterMappingType.getId()); + properties.add("useReferenceColors", useReferenceColors); + JsonArray referenceColorsArray = new JsonArray(0); + if (emitterMappingReferenceColors != null) { + emitterMappingReferenceColors.forEach(referenceColor -> { + JsonObject referenceColorJson = new JsonObject(); + referenceColorJson.add("red", referenceColor.x); + referenceColorJson.add("green", referenceColor.y); + referenceColorJson.add("blue", referenceColor.z); + referenceColorJson.add("range", referenceColor.w); + referenceColorsArray.add(referenceColorJson); + }); + } + properties.add("emitterMappingReferenceColors", referenceColorsArray); + properties.add("roughness", roughness); + properties.add("transmissionRoughness", transmissionRoughness); + properties.add("alpha", alpha); + properties.add("specularColor", ColorUtil.rgbToJson(specularColor)); + properties.add("transmissionSpecularColor", ColorUtil.rgbToJson(transmissionSpecularColor)); + properties.add("subsurfaceScattering", subSurfaceScattering); + properties.add("diffuseColor", ColorUtil.rgbToJson(diffuseColor)); + properties.add("emittanceColor", ColorUtil.rgbToJson(emittanceColor)); + properties.add("volumeDensity", volumeDensity); + properties.add("volumeAnisotropy", volumeAnisotropy); + properties.add("volumeEmittance", volumeEmittance); + properties.add("volumeColor", ColorUtil.rgbToJson(volumeColor)); + properties.add("absorption", absorption); + properties.add("absorptionColor", ColorUtil.rgbToJson(absorptionColor)); + properties.add("opaque", opaque); + properties.add("hidden", hidden); + return properties; } + /** + * Whether this material is a water block. + */ public boolean isWater() { return false; } + /** + * Whether this material is waterlogged or a water block. + */ public boolean isWaterFilled() { return waterlogged || isWater(); } @@ -146,4 +365,774 @@ public double getPerceptualSmoothness() { public void setPerceptualSmoothness(double perceptualSmoothness) { roughness = (float) Math.pow(1 - perceptualSmoothness, 2); } + + public double getPerceptualTransmissionSmoothness() { + return 1 - Math.sqrt(transmissionRoughness); + } + + public void setPerceptualTransmissionSmoothness(double perceptualTransmissionSmoothness) { + transmissionRoughness = (float) Math.pow(1 - perceptualTransmissionSmoothness, 2); + } + + /** + * Sets the emittance of the material based on its in-game light level. + * @param level The light level of the block in Minecraft. + */ + public void setLightLevel(float level) { + emittance = level / 15; + } + + public void addRefColorGammaCorrected(float r, float g, float b, float delta) { + if (emitterMappingReferenceColors == null) { + emitterMappingReferenceColors = new ArrayList<>(); + } + emitterMappingReferenceColors.add(new Vector4(Math.pow(r/255, Scene.DEFAULT_GAMMA), Math.pow(g/255, Scene.DEFAULT_GAMMA), Math.pow(b/255, Scene.DEFAULT_GAMMA), delta)); + } + + /** + * Intersect with the volume medium of this material. + */ + public boolean volumeIntersect(IntersectionRecord intersectionRecord, Random random) { + if (volumeDensity < Constants.EPSILON) { + return false; + } + + intersectionRecord.distance = fogDistance(volumeDensity, random); + intersectionRecord.material = this; + intersectionRecord.color.set(volumeColor.x, volumeColor.y, volumeColor.z, 1); + intersectionRecord.setVolumeIntersect(true); + return true; + } + + /** + * Finds the distance to the volume medium intersection. + * @param density The density of the volume medium + * @param random Random number generator + * @return The distance to the volume medium intersection. + */ + public static double fogDistance(double density, Random random) { + return -FastMath.log(1 - random.nextDouble()) / density; + } + + /** + * Attenuates the color based on the {@code absorption} of this material, and the distance the + * ray has travelled through it. + * @param color The color to be attenuated. + * @param distance The distance the ray has travelled through this material. + */ + public void absorption(Vector3 color, double distance) { + if (absorption < Constants.EPSILON) { + return; + } + color.x *= FastMath.exp((1 - absorptionColor.x) * absorption * -distance); + color.y *= FastMath.exp((1 - absorptionColor.y) * absorption * -distance); + color.z *= FastMath.exp((1 - absorptionColor.z) * absorption * -distance); + } + + /** + * Sets the {@code emittance} based on the {@code emitterMappingType} and + * {@code emitterMappingReferenceColors}. + * @param emittance The emittance color to be set. + * @param color The intersection color. + * @param scene The scene state. + */ + public void doEmitterMapping(Vector3 emittance, Vector4 color, Scene scene) { + double exp = Math.max(scene.getEmitterMappingExponent() + emitterMappingOffset, 0); + EmitterMappingType emitterMappingType = this.emitterMappingType == EmitterMappingType.NONE ? scene.getEmitterMappingType() : this.emitterMappingType; + + boolean emit = !useReferenceColors; + if (useReferenceColors) { + if (emitterMappingReferenceColors == null) { + emittance.set(0, 0, 0); + return; + } + + for (Vector4 referenceColor : emitterMappingReferenceColors) { + emit |= (Math.max(Math.abs(color.x - referenceColor.x), Math.max(Math.abs(color.y - referenceColor.y), Math.abs(color.z - referenceColor.z))) <= referenceColor.w); + } + } + if (emit) { + switch (emitterMappingType) { + case BRIGHTEST_CHANNEL: + double val = FastMath.pow(Math.max(color.x, Math.max(color.y, color.z)), exp); + emittance.set(color.x * val, color.y * val, color.z * val); + break; + case INDEPENDENT_CHANNELS: + emittance.set(FastMath.pow(color.x, exp), FastMath.pow(color.y, exp), FastMath.pow(color.z, exp)); + break; + } + emittance.scale(this.emittance); + } else { + emittance.set(0, 0, 0); + } + } + + /** + * Scatter the incoming ray and update the {@code emittance} based on the material properties. + * @param ray The incoming ray. + * @param intersectionRecord The {@code intersectionRecord} containing the intersection details. + * @param scene The scene state. + * @param emittance The emittance color to be updated. + * @param random Random number generator. + * @return Whether to update the ray material to the intersection material. + */ + public boolean scatter(Ray ray, IntersectionRecord intersectionRecord, Scene scene, final Vector3 emittance, Random random) { + boolean mediumChanged = false; + boolean throughSurface = false; + + Vector3 direction; + + double n2 = ior; + double n1 = ray.getCurrentMedium().ior; + + double pDiffuse = intersectionRecord.color.w * alpha; + + if (random.nextDouble() < pDiffuse) { + // Reflection + if (random.nextDouble() < specular) { + // Specular reflection with roughness + + // For rough specular reflections, we interpolate linearly between the diffuse ray direction and the specular direction, + // which is inspired by https://blog.demofox.org/2020/06/06/casual-shadertoy-path-tracing-2-image-improvement-and-glossy-reflections/ + // This gives good-looking results, although a microfacet-based model would be more physically correct. + direction = specularReflection(ray.d, intersectionRecord.shadeN); + if (roughness > Constants.EPSILON) { + Vector3 roughnessDirection = lambertianReflection(intersectionRecord.n, random); + roughnessDirection.scale(roughness); + roughnessDirection.scaleAdd(1 - roughness, direction); + roughnessDirection.normalize(); + direction = roughnessDirection; + } + tintColor(intersectionRecord.color, metalness, specularColor); + ray.setSpecular(true); + Vector4 emittanceColor1 = new Vector4(intersectionRecord.color); + tintColor(emittanceColor1, 1, emittanceColor); + doEmitterMapping(emittance, emittanceColor1, scene); + } else { + // Lambertian reflection + if (random.nextDouble() < subSurfaceScattering) { + // Subsurface scattering + intersectionRecord.shadeN.scale(-1); + intersectionRecord.n.scale(-1); + ray.d.scale(-1); // This is to prevent direction from being inverted later. + if (!intersectionRecord.isNoMediumChange()) { + mediumChanged = true; + } + } + direction = lambertianReflection(intersectionRecord.shadeN, random); + tintColor(intersectionRecord.color, 1, diffuseColor); + ray.setDiffuse(true); + ray.setIndirect(true); + Vector4 emittanceColor1 = new Vector4(intersectionRecord.color); + tintColor(emittanceColor1, 1, emittanceColor); + doEmitterMapping(emittance, emittanceColor1, scene); + } + } else { + // Transmission / Refraction + if (FastMath.abs(n2 - n1) > Constants.EPSILON) { + // Refraction / Total internal reflection + boolean front_face = ray.d.dot(intersectionRecord.shadeN) < 0.0; + double ri = (front_face) ? (n1 / n2) : (n2 / n1); + + Vector3 unitDirection = ray.d.normalized(); + double cosTheta = FastMath.min(unitDirection.rScale(-1).dot(intersectionRecord.shadeN), 1.0); + double sinTheta = FastMath.sqrt(1.0 - cosTheta * cosTheta); + + boolean cannotRefract = ri * sinTheta > 1.0; + + if (cannotRefract || schlickReflectance(cosTheta, ri) > random.nextDouble()) { + // Total internal reflection + direction = specularReflection(unitDirection, intersectionRecord.shadeN); + double interfaceRoughness = FastMath.max(roughness, ray.getCurrentMedium().roughness); + if (interfaceRoughness > Constants.EPSILON) { + Vector3 roughnessDirection = lambertianReflection(intersectionRecord.n, random); + roughnessDirection.scale(interfaceRoughness); + roughnessDirection.scaleAdd(1 - interfaceRoughness, direction); + roughnessDirection.normalize(); + direction = roughnessDirection; + } + tintColor(intersectionRecord.color, metalness, specularColor); + ray.setSpecular(true); + } else { + // Refraction + if (intersectionRecord.isNoMediumChange()) { + direction = new Vector3(ray.d); + } else { + direction = specularRefraction(unitDirection, intersectionRecord.shadeN, ri); + double interfaceTransmissionRoughness = FastMath.max(transmissionRoughness, ray.getCurrentMedium().transmissionRoughness); + if (interfaceTransmissionRoughness > Constants.EPSILON) { + Vector3 roughnessDirection = lambertianReflection(intersectionRecord.n.rScale(-1), random); + roughnessDirection.scale(interfaceTransmissionRoughness); + roughnessDirection.scaleAdd(1 - interfaceTransmissionRoughness, direction); + roughnessDirection.normalize(); + direction = roughnessDirection; + } + mediumChanged = true; + } + tintColor(intersectionRecord.color, transmissionMetalness, transmissionSpecularColor); + ray.setSpecular(true); + throughSurface = true; + } + } else { + // Transmission + direction = new Vector3(ray.d); + tintColor(intersectionRecord.color, transmissionMetalness, transmissionSpecularColor); + ray.setSpecular(true); + throughSurface = true; + if (!intersectionRecord.isNoMediumChange()) { + mediumChanged = true; + } + } + } + + int sign = throughSurface ? -1 : 1; + + // After reflection, the dot product between the direction and the real surface normal + // should have the opposite sign as the dot product between the incoming direction + // and the normal (because the incoming is going toward the volume enclosed + // by the surface and the reflected ray is going away) + // If this is not the case, we need to fix that + if (QuickMath.signum(intersectionRecord.n.dot(direction)) == sign * QuickMath.signum(intersectionRecord.n.dot(ray.d))) { + // The reflected ray goes is going through the geometry, + // we need to alter its direction so it doesn't. + // The way we do that is by adding the geometry normal multiplied by some factor + // The factor can be determined by projecting the direction on the normal, + // ie doing a dot product because, for every unit vector d and n, + // we have the relation: + // `(d - d.n * n) . n = 0` + // This tells us that if we chose `-d.n` as the factor we would have a dot product + // equals to 0, as we want something positive or negative, + // we will use the factor `-d.n +/- epsilon` + double factor = QuickMath.signum(intersectionRecord.n.dot(ray.d)) * -Constants.EPSILON - direction.dot(intersectionRecord.n); + direction.scaleAdd(factor, intersectionRecord.n); + direction.normalize(); + } + ray.d.set(direction); + + // Offset the ray so that it does not intersect the same spot again. + ray.o.scaleAdd(sign * Constants.OFFSET, intersectionRecord.n); + + return mediumChanged; + } + + /** + * Scatter the ray based on the Henyey-Greenstein phase function for volume intersections. + * @param ray The ray whose direction will be updated. + * @param random Random number generator. + */ + public void volumeScatter(Ray ray, Random random) { + Vector3 invDir = ray.d.rScale(-1); + Vector3 outDir = new Vector3(); + double x1 = random.nextDouble(); + double x2 = random.nextDouble(); + henyeyGreensteinSampleP(volumeAnisotropy, invDir, outDir, x1, x2); + outDir.normalize(); + ray.d.set(outDir); + ray.setDiffuse(true); + ray.setIndirect(true); + } + + /** + * Henyey-Greenstein phase function. + * @param cosTheta Dot product between incoming and outgoing directions. + * @param g Anisotropy of the volume medium. + */ + public static double phaseHG(double cosTheta, double g) { + double denominator = 1 + (g * g) + (2 * g * cosTheta); + return Constants.INV_4_PI * (1 - g * g) / (denominator * FastMath.sqrt(denominator)); + } + + /** + * Code adapted from pbrt + */ + private static double henyeyGreensteinSampleP(double g, Vector3 wo, Vector3 wi, double x1, double x2) { + double cosTheta; + if (FastMath.abs(g) < 1e-3) { + cosTheta = 1 - 2 * x1; + } else { + double sqrTerm = (1 - g * g) / (1 + g - 2 * g * x1); + cosTheta = -(1 + g * g - sqrTerm * sqrTerm) / (2 * g); + } + + double sinTheta = FastMath.sqrt(FastMath.max(0d, 1 - cosTheta * cosTheta)); + double phi = 2 * FastMath.PI * x2; + Vector3 v1 = new Vector3(); + Vector3 v2 = new Vector3(); + coordinateSystem(wo, v1, v2); + wi.set(sphericalDirection(sinTheta, cosTheta, phi, v1, v2, wo)); + return phaseHG(cosTheta, g); + } + + /** + * Code adapted from pbrt + */ + private static void coordinateSystem(Vector3 v1, Vector3 v2, Vector3 v3) { + Vector3 x; + if (FastMath.abs(v1.x) > FastMath.abs(v1.y)) { + x = new Vector3(-v1.z, 0, v1.x); + x.scale(1 / FastMath.sqrt(v1.x * v1.x + v1.z * v1.z)); + } else { + x = new Vector3(0, v1.z, -v1.y); + x.scale(1 / FastMath.sqrt(v1.y * v1.y + v1.z * v1.z)); + } + v2.set(x); + v3.cross(v1, v2); + } + + /** + * Code adapted from pbrt + */ + private static Vector3 sphericalDirection(double sinTheta, double cosTheta, double phi, Vector3 x, Vector3 y, Vector3 z) { + Vector3 x1 = new Vector3(x); + Vector3 y1 = new Vector3(y); + Vector3 z1 = new Vector3(z); + x1.scale(sinTheta * FastMath.cos(phi)); + y1.scale(sinTheta * FastMath.sin(phi)); + z1.scale(cosTheta); + x1.add(y1); + x1.add(z1); + return x1; + } + + /** + * Return a random cosine-weighted Vector3 in the hemisphere defined by {@code normal}. + * @param normal The normal defining the hemisphere. + * @param random Random number generator. + */ + private static Vector3 randomHemisphereDir(Vector3 normal, Random random) { + double x1 = random.nextDouble(); + double x2 = random.nextDouble(); + double r = FastMath.sqrt(x1); + double theta = 2 * FastMath.PI * x2; + + // project to point on hemisphere in tangent space + double tx = r * FastMath.cos(theta); + double ty = r * FastMath.sin(theta); + double tz = FastMath.sqrt(1 - x1); + + // Transform from tangent space to world space + double xx, xy, xz; + double ux, uy, uz; + double vx, vy, vz; + + if (QuickMath.abs(normal.x) > 0.1) { + xx = 0; + xy = 1; + } else { + xx = 1; + xy = 0; + } + xz = 0; + + ux = xy * normal.z - xz * normal.y; + uy = xz * normal.x - xx * normal.z; + uz = xx * normal.y - xy * normal.x; + + r = 1 / FastMath.sqrt(ux*ux + uy*uy + uz*uz); + + ux *= r; + uy *= r; + uz *= r; + + vx = uy * normal.z - uz * normal.y; + vy = uz * normal.x - ux * normal.z; + vz = ux * normal.y - uy * normal.x; + + return new Vector3( + ux * tx + vx * ty + normal.x * tz, + uy * tx + vy * ty + normal.y * tz, + uz * tx + vz * ty + normal.z * tz + ); + } + + /** + * Return a Lambertian (diffuse) reflection in the hemisphere defined by {@code n}, the normal. + * @param n The normal of the reflecting surface. + * @param random Random number generator. + */ + private static Vector3 lambertianReflection(Vector3 n, Random random) { + Vector3 direction = randomHemisphereDir(n, random); + direction.normalize(); + return direction; + } + + private static Vector3 specularReflection(Vector3 v, Vector3 n) { + return v.rSub(n.rScale(2 * v.dot(n))); + } + + private static double schlickReflectance(double cosine, double refractionIndex) { + double r0 = (1 - refractionIndex) / (1 + refractionIndex); + r0 = r0 * r0; + return r0 + (1 - r0) * FastMath.pow((1 - cosine), 5); + } + + private static Vector3 specularRefraction(Vector3 uv, Vector3 n, double etaiOverEtat) { + double cosTheta = FastMath.min(uv.rScale(-1).dot(n), 1.0); + Vector3 rOutPerp = n.rScale(cosTheta).rAdd(uv).rScale(etaiOverEtat); + Vector3 rOutParallel = n.rScale(-FastMath.sqrt(FastMath.abs(1.0 - rOutPerp.lengthSquared()))); + return rOutPerp.rAdd(rOutParallel); + } + + public static void tintColor(Vector4 color, float tintStrength, Vector3 colorModifier) { + color.x = 1 - tintStrength * (1 - color.x); + color.y = 1 - tintStrength * (1 - color.y); + color.z = 1 - tintStrength * (1 - color.z); + color.x *= colorModifier.x; + color.y *= colorModifier.y; + color.z *= colorModifier.z; + } + + /** + * Return a {@code VBox} containing a set of controls that change the properties of + * {@code material}. + */ + public static VBox getControls(Material material, Scene scene) { + DoubleAdjuster emittanceAdjuster = new DoubleAdjuster(); + LuxColorPicker emittanceColorPicker = new LuxColorPicker(); + DoubleAdjuster emitterMappingOffset = new DoubleAdjuster(); + ChoiceBox emitterMappingType = new ChoiceBox<>(); + ToggleSwitch useReferenceColors = new ToggleSwitch(); + DoubleAdjuster alphaAdjuster = new DoubleAdjuster(); + DoubleAdjuster subsurfaceScatteringAdjuster = new DoubleAdjuster(); + LuxColorPicker diffuseColorPicker = new LuxColorPicker(); + DoubleAdjuster specularAdjuster = new DoubleAdjuster(); + DoubleAdjuster iorAdjuster = new DoubleAdjuster(); + DoubleAdjuster smoothnessAdjuster = new DoubleAdjuster(); + DoubleAdjuster transmissionSmoothnessAdjuster = new DoubleAdjuster(); + DoubleAdjuster metalnessAdjuster = new DoubleAdjuster(); + DoubleAdjuster transmissionMetalnessAdjuster = new DoubleAdjuster(); + LuxColorPicker specularColorPicker = new LuxColorPicker(); + LuxColorPicker transmissionSpecularColorPicker = new LuxColorPicker(); + DoubleAdjuster volumeDensityAdjuster = new DoubleAdjuster(); + DoubleAdjuster volumeAnisotropyAdjuster = new DoubleAdjuster(); + DoubleAdjuster volumeEmittanceAdjuster = new DoubleAdjuster(); + LuxColorPicker volumeColorPicker = new LuxColorPicker(); + DoubleAdjuster absorptionAdjuster = new DoubleAdjuster(); + LuxColorPicker absorptionColorPicker = new LuxColorPicker(); + + TableView referenceColorTable = new TableView<>(); + Button addReferenceColor = new Button(); + Button removeReferenceColor = new Button(); + LuxColorPicker referenceColorPicker = new LuxColorPicker(); + IntegerAdjuster referenceColorRangeSlider = new IntegerAdjuster(); + + emittanceAdjuster.setName("Emittance"); + emittanceAdjuster.setTooltip("Intensity of the light emitted from the selected material."); + emittanceAdjuster.setRange(0, 100); + emittanceAdjuster.clampMin(); + emittanceAdjuster.set(material.emittance); + emittanceAdjuster.onValueChange(value -> { + material.emittance = value.floatValue(); + scene.refresh(); + }); + + emittanceColorPicker.setText("Emittance color"); + emittanceColorPicker.setColor(ColorUtil.toFx(material.emittanceColor)); + emittanceColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.emittanceColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + emitterMappingOffset.setName("Emitter mapping offset"); + emitterMappingOffset.setRange(-5, 5); + emitterMappingOffset.setTooltip("Offset applied to the global emitter mapping exponent."); + emitterMappingOffset.set(material.emitterMappingOffset); + emitterMappingOffset.onValueChange(value -> { + material.emitterMappingOffset = value.floatValue(); + scene.refresh(); + }); + + emitterMappingType.getItems().addAll(EmitterMappingType.values()); + emitterMappingType.setTooltip(new Tooltip("Overrides the global setting for emitter mapping type.")); + emitterMappingType.getSelectionModel().select(material.emitterMappingType); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + material.emitterMappingType = newValue; + scene.refresh(); + }); + + VBox emitterMappingTypeBox = new VBox(6, new Label("Emitter mapping type"), emitterMappingType); + + useReferenceColors.setText("Use reference colors"); + useReferenceColors.setSelected(material.useReferenceColors); + useReferenceColors.selectedProperty().addListener((observable, oldValue, newValue) -> { + material.useReferenceColors = newValue; + scene.refresh(); + }); + + alphaAdjuster.setName("Alpha"); + alphaAdjuster.setTooltip("Alpha (opacity) of the selected material."); + alphaAdjuster.setRange(0, 1); + alphaAdjuster.clampBoth(); + alphaAdjuster.set(material.alpha); + alphaAdjuster.onValueChange(value -> { + material.alpha = value.floatValue(); + scene.refresh(); + }); + + subsurfaceScatteringAdjuster.setName("Subsurface scattering"); + subsurfaceScatteringAdjuster.setTooltip("Probability of a ray to be scattered behind the surface."); + subsurfaceScatteringAdjuster.setRange(0, 1); + subsurfaceScatteringAdjuster.clampBoth(); + subsurfaceScatteringAdjuster.set(material.subSurfaceScattering); + subsurfaceScatteringAdjuster.onValueChange(value -> { + material.subSurfaceScattering = value.floatValue(); + scene.refresh(); + }); + + diffuseColorPicker.setText("Diffuse color"); + diffuseColorPicker.setColor(ColorUtil.toFx(material.diffuseColor)); + diffuseColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.diffuseColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + specularAdjuster.setName("Specular"); + specularAdjuster.setTooltip("Reflectivity of the selected material."); + specularAdjuster.setRange(0, 1); + specularAdjuster.clampBoth(); + specularAdjuster.set(material.specular); + specularAdjuster.onValueChange(value -> { + material.specular = value.floatValue(); + scene.refresh(); + }); + + iorAdjuster.setName("IoR"); + iorAdjuster.setTooltip("Index of Refraction of the selected material."); + iorAdjuster.setRange(0, 5); + iorAdjuster.clampMin(); + iorAdjuster.setMaximumFractionDigits(6); + iorAdjuster.set(material.ior); + iorAdjuster.onValueChange(value -> { + material.ior = value.floatValue(); + scene.refresh(); + }); + + smoothnessAdjuster.setName("Smoothness"); + smoothnessAdjuster.setTooltip("Smoothness of the selected material."); + smoothnessAdjuster.setRange(0, 1); + smoothnessAdjuster.clampBoth(); + smoothnessAdjuster.set(material.getPerceptualSmoothness()); + smoothnessAdjuster.onValueChange(value -> { + material.setPerceptualSmoothness(value); + scene.refresh(); + }); + + transmissionSmoothnessAdjuster.setName("Transmission smoothness"); + transmissionSmoothnessAdjuster.setTooltip("Transmission smoothness of the selected material."); + transmissionSmoothnessAdjuster.setRange(0, 1); + transmissionSmoothnessAdjuster.clampBoth(); + transmissionSmoothnessAdjuster.set(material.getPerceptualTransmissionSmoothness()); + transmissionSmoothnessAdjuster.onValueChange(value -> { + material.setPerceptualTransmissionSmoothness(value); + scene.refresh(); + }); + + metalnessAdjuster.setName("Metalness"); + metalnessAdjuster.setTooltip("Texture tinting of reflected light."); + metalnessAdjuster.setRange(0, 1); + metalnessAdjuster.clampBoth(); + metalnessAdjuster.set(material.metalness); + metalnessAdjuster.onValueChange(value -> { + material.metalness = value.floatValue(); + scene.refresh(); + }); + + transmissionMetalnessAdjuster.setName("Transmission metalness"); + transmissionMetalnessAdjuster.setTooltip("Texture tinting of refracted/transmitted light."); + transmissionMetalnessAdjuster.setRange(0, 1); + transmissionMetalnessAdjuster.clampBoth(); + transmissionMetalnessAdjuster.set(material.transmissionMetalness); + transmissionMetalnessAdjuster.onValueChange(value -> { + material.transmissionMetalness = value.floatValue(); + scene.refresh(); + }); + + specularColorPicker.setText("Specular color"); + specularColorPicker.setColor(ColorUtil.toFx(material.specularColor)); + specularColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.specularColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + transmissionSpecularColorPicker.setText("Transmission specular color"); + transmissionSpecularColorPicker.setColor(ColorUtil.toFx(material.transmissionSpecularColor)); + transmissionSpecularColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.transmissionSpecularColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + volumeDensityAdjuster.setName("Volume density"); + volumeDensityAdjuster.setTooltip("Density of volume medium."); + volumeDensityAdjuster.setRange(0, 1); + volumeDensityAdjuster.clampMin(); + volumeDensityAdjuster.set(material.volumeDensity); + volumeDensityAdjuster.onValueChange( value -> { + material.volumeDensity = value.floatValue(); + scene.refresh(); + }); + + volumeAnisotropyAdjuster.setName("Volume anisotropy"); + volumeAnisotropyAdjuster.setTooltip("Changes the direction light is more likely to be scattered.\n" + + "Positive values increase the chance light scatters into its original direction of travel.\n" + + "Negative values increase the chance light scatters away from its original direction of travel."); + volumeAnisotropyAdjuster.setRange(-1, 1); + volumeAnisotropyAdjuster.clampBoth(); + volumeAnisotropyAdjuster.set(material.volumeAnisotropy); + volumeAnisotropyAdjuster.onValueChange( value -> { + material.volumeAnisotropy = value.floatValue(); + scene.refresh(); + }); + + volumeEmittanceAdjuster.setName("Volume emittance"); + volumeEmittanceAdjuster.setTooltip("Emittance of volume medium."); + volumeEmittanceAdjuster.setRange(0, 100); + volumeEmittanceAdjuster.clampMin(); + volumeEmittanceAdjuster.set(material.volumeEmittance); + volumeEmittanceAdjuster.onValueChange( value -> { + material.volumeEmittance = value.floatValue(); + scene.refresh(); + }); + + volumeColorPicker.setText("Volume color"); + volumeColorPicker.setColor(ColorUtil.toFx(material.volumeColor)); + volumeColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.volumeColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + absorptionAdjuster.setName("Absorption"); + absorptionAdjuster.setTooltip("Absorption of volume medium."); + absorptionAdjuster.setRange(0, 10); + absorptionAdjuster.clampMin(); + absorptionAdjuster.makeLogarithmic(); + absorptionAdjuster.set(material.absorption); + absorptionAdjuster.onValueChange( value -> { + material.absorption = value.floatValue(); + scene.refresh(); + }); + + absorptionColorPicker.setText("Absorption color"); + absorptionColorPicker.setColor(ColorUtil.toFx(material.absorptionColor)); + absorptionColorPicker.colorProperty().addListener( + ((observable, oldValue, newValue) -> { + material.absorptionColor.set(ColorUtil.fromFx(newValue)); + scene.refresh(); + }) + ); + + GridPane settings = new GridPane(); + + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(50); + + settings.getColumnConstraints().addAll(columnConstraints, columnConstraints); + settings.setHgap(10); + settings.setVgap(10); + + VBox diffuseSettings = new VBox(6, emittanceAdjuster, emittanceColorPicker, emitterMappingOffset, emitterMappingTypeBox, useReferenceColors, alphaAdjuster, subsurfaceScatteringAdjuster, diffuseColorPicker); + VBox volumeSettings = new VBox(6, volumeDensityAdjuster, volumeAnisotropyAdjuster, volumeEmittanceAdjuster, volumeColorPicker, absorptionAdjuster, absorptionColorPicker); + VBox specularSettings = new VBox(6, specularAdjuster, iorAdjuster, smoothnessAdjuster, transmissionSmoothnessAdjuster); + VBox specularColorSettings = new VBox(6, metalnessAdjuster, transmissionMetalnessAdjuster, specularColorPicker, transmissionSpecularColorPicker); + + settings.add(diffuseSettings, 0, 0); + settings.add(volumeSettings, 1, 0); + settings.add(specularSettings, 0, 1); + settings.add(specularColorSettings, 1, 1); + + final ArrayList> referenceColorPickerListener = new ArrayList<>(1); + referenceColorPickerListener.add((observable, oldValue, newValue) -> {}); + + referenceColorPicker.setText("Reference color"); + referenceColorPicker.colorProperty().addListener(referenceColorPickerListener.get(0)); + + referenceColorRangeSlider.setName("Range"); + referenceColorRangeSlider.setRange(0, 255); + referenceColorRangeSlider.clampBoth(); + + TableColumn redColumn = new TableColumn<>("Red"); + TableColumn greenColumn = new TableColumn<>("Green"); + TableColumn blueColumn = new TableColumn<>("Blue"); + TableColumn rangeColumn = new TableColumn<>("Range"); + + redColumn.setCellValueFactory(new PropertyValueFactory<>("red")); + greenColumn.setCellValueFactory(new PropertyValueFactory<>("green")); + blueColumn.setCellValueFactory(new PropertyValueFactory<>("blue")); + rangeColumn.setCellValueFactory(new PropertyValueFactory<>("range")); + + redColumn.setSortable(true); + greenColumn.setSortable(true); + blueColumn.setSortable(true); + rangeColumn.setSortable(true); + + referenceColorTable.getColumns().add(redColumn); + referenceColorTable.getColumns().add(greenColumn); + referenceColorTable.getColumns().add(blueColumn); + referenceColorTable.getColumns().add(rangeColumn); + referenceColorTable.setMaxHeight(200); + + if (material.emitterMappingReferenceColors != null && !material.emitterMappingReferenceColors.isEmpty()) { + material.emitterMappingReferenceColors.forEach(referenceColor -> { + referenceColorTable.getItems().add(new MaterialReferenceColorData(referenceColor)); + }); + } + + referenceColorTable.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener.get(0)); + if (newValue != null) { + referenceColorPicker.setColor(ColorUtil.toFx(newValue.getReferenceColor().toVec3())); + referenceColorPickerListener.set(0, (observable2, oldValue2, newValue2) -> { + Vector3 color = ColorUtil.fromFx(newValue2); + newValue.setReferenceColor(color); + scene.refresh(); + }); + referenceColorPicker.colorProperty().addListener(referenceColorPickerListener.get(0)); + referenceColorRangeSlider.set(newValue.getReferenceColor().w * 255); + referenceColorRangeSlider.onValueChange(value -> { + newValue.setRange(value); + scene.refresh(); + }); + } else { + referenceColorRangeSlider.onValueChange(value -> {}); + } + }); + + addReferenceColor.setText("Add reference color"); + addReferenceColor.setOnAction(e -> { + Vector4 referenceColor = new Vector4(1, 1, 1, 1); + if (material.emitterMappingReferenceColors == null) { + material.emitterMappingReferenceColors = new ArrayList<>(1); + } + material.emitterMappingReferenceColors.add(referenceColor); + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener.get(0)); + referenceColorRangeSlider.onValueChange(value -> {}); + referenceColorTable.getItems().add(new MaterialReferenceColorData(referenceColor)); + referenceColorTable.getSelectionModel().selectLast(); + }); + removeReferenceColor.setText("Remove reference color"); + removeReferenceColor.setOnAction(e -> { + referenceColorPicker.colorProperty().removeListener(referenceColorPickerListener.get(0)); + referenceColorRangeSlider.onValueChange(value -> {}); + Vector4 referenceColor = referenceColorTable.getSelectionModel().getSelectedItem().getReferenceColor(); + material.emitterMappingReferenceColors.remove(referenceColor); + if (material.emitterMappingReferenceColors.isEmpty()) { + material.emitterMappingReferenceColors = null; + } + referenceColorTable.getItems().remove(referenceColorTable.getSelectionModel().getSelectedItem()); + }); + + HBox addRemoveControls = new HBox(10, addReferenceColor, removeReferenceColor); + + return new VBox(10, settings, addRemoveControls, referenceColorTable, referenceColorPicker, referenceColorRangeSlider); + } } diff --git a/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java b/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java index 4b2500a901..0d0cf561c4 100644 --- a/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java +++ b/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java @@ -22,8 +22,9 @@ import se.llbit.chunky.resources.Texture; import se.llbit.log.Log; import se.llbit.math.ColorUtil; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.QuickMath; -import se.llbit.math.Ray; import se.llbit.math.Vector4; import se.llbit.util.ImageTools; @@ -128,8 +129,8 @@ public SkymapTexture(BitmapImage image) { } @Override public void getColor(double u, double v, Vector4 c) { - ColorUtil.getRGBComponents(image.getPixel((int) (u * width - Ray.EPSILON), - (int) ((1 - v) * height - Ray.EPSILON)), c); + ColorUtil.getRGBComponents(image.getPixel((int) (u * width - Constants.EPSILON), + (int) ((1 - v) * height - Constants.EPSILON)), c); } /** @@ -139,7 +140,7 @@ public void getColor(int x, int y, Vector4 c) { ColorUtil.getRGBComponents(image.getPixel(x, y), c); } - @Override public void getColor(Ray ray) { + @Override public void getColor(IntersectionRecord intersectionRecord) { throw new UnsupportedOperationException(); } diff --git a/chunky/src/java/se/llbit/chunky/world/material/BeaconBeamMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/BeaconBeamMaterial.java index e49341176e..454b432a8d 100644 --- a/chunky/src/java/se/llbit/chunky/world/material/BeaconBeamMaterial.java +++ b/chunky/src/java/se/llbit/chunky/world/material/BeaconBeamMaterial.java @@ -6,15 +6,15 @@ import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.math.ColorUtil; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.util.JsonUtil; public class BeaconBeamMaterial extends Material { public static final int DEFAULT_COLOR = 0xF9FFFE; private int color; - private float[] beamColor = new float[3]; + private final float[] beamColor = new float[4]; public BeaconBeamMaterial(int color) { super("beacon_beam", Texture.beaconBeam); @@ -33,19 +33,19 @@ public int getColorInt() { } @Override - public void getColor(Ray ray) { - super.getColor(ray); - if (ray.color.w > Ray.EPSILON) { - ray.color.x *= beamColor[0]; - ray.color.y *= beamColor[1]; - ray.color.z *= beamColor[2]; + public void getColor(IntersectionRecord intersectionRecord) { + super.getColor(intersectionRecord); + if (intersectionRecord.color.w > Constants.EPSILON) { + intersectionRecord.color.x *= beamColor[0]; + intersectionRecord.color.y *= beamColor[1]; + intersectionRecord.color.z *= beamColor[2]; } } @Override public float[] getColor(double u, double v) { float[] color = super.getColor(u, v); - if (color[3] > Ray.EPSILON) { + if (color[3] > Constants.EPSILON) { color = color.clone(); color[0] *= beamColor[0]; color[1] *= beamColor[1]; @@ -66,14 +66,10 @@ public void loadMaterialProperties(JsonObject json) { } } - public void saveMaterialProperties(JsonObject json) { - json.add("ior", this.ior); - json.add("specular", this.specular); - json.add("emittance", this.emittance); - json.add("roughness", this.roughness); - json.add("metalness", this.metalness); - Vector3 color = new Vector3(); - ColorUtil.getRGBComponents(this.color, color); - json.add("color", JsonUtil.rgbToJson(color)); + @Override + public JsonObject saveMaterialProperties() { + JsonObject json = super.saveMaterialProperties(); + json.add("color", this.color); + return json; } -} \ No newline at end of file +} diff --git a/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java deleted file mode 100644 index 9040b2dacd..0000000000 --- a/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) 2019 Jesper Öqvist - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ -package se.llbit.chunky.world.material; - -import se.llbit.chunky.resources.Texture; -import se.llbit.chunky.world.Material; - -public class CloudMaterial extends Material { - public static final CloudMaterial INSTANCE = new CloudMaterial(); - public static float[] color = {1, 1, 1, 1}; - - private CloudMaterial() { - super("cloud", Texture.air); - } -} diff --git a/chunky/src/java/se/llbit/chunky/world/material/DyedTextureMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/DyedTextureMaterial.java index cf71493061..229f954ef5 100644 --- a/chunky/src/java/se/llbit/chunky/world/material/DyedTextureMaterial.java +++ b/chunky/src/java/se/llbit/chunky/world/material/DyedTextureMaterial.java @@ -4,6 +4,8 @@ import se.llbit.chunky.world.Material; import se.llbit.json.JsonObject; import se.llbit.math.ColorUtil; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.Ray; public class DyedTextureMaterial extends Material { @@ -84,19 +86,19 @@ public int getColorInt() { } @Override - public void getColor(Ray ray) { - super.getColor(ray); - if (ray.color.w > Ray.EPSILON) { - ray.color.x *= colorRGBA[0]; - ray.color.y *= colorRGBA[1]; - ray.color.z *= colorRGBA[2]; + public void getColor(IntersectionRecord intersectionRecord) { + super.getColor(intersectionRecord); + if (intersectionRecord.color.w > Constants.EPSILON) { + intersectionRecord.color.x *= colorRGBA[0]; + intersectionRecord.color.y *= colorRGBA[1]; + intersectionRecord.color.z *= colorRGBA[2]; } } @Override public float[] getColor(double u, double v) { float[] color = super.getColor(u, v); - if (color[3] > Ray.EPSILON) { + if (color[3] > Constants.EPSILON) { color = color.clone(); color[0] *= colorRGBA[0]; color[1] *= colorRGBA[1]; @@ -104,19 +106,4 @@ public float[] getColor(double u, double v) { } return color; } - - @Override - public void loadMaterialProperties(JsonObject json) { - super.loadMaterialProperties(json); - updateColor(json.get("color").asInt(DyeColor.WHITE.colorDecimal)); - } - - public void saveMaterialProperties(JsonObject json) { - json.add("ior", this.ior); - json.add("specular", this.specular); - json.add("emittance", this.emittance); - json.add("roughness", this.roughness); - json.add("metalness", this.metalness); - json.add("color", this.color); - } } \ No newline at end of file diff --git a/chunky/src/java/se/llbit/chunky/world/material/LilyPadMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/LilyPadMaterial.java index 10b4a28c03..872bf1e5f2 100644 --- a/chunky/src/java/se/llbit/chunky/world/material/LilyPadMaterial.java +++ b/chunky/src/java/se/llbit/chunky/world/material/LilyPadMaterial.java @@ -19,7 +19,8 @@ import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.Material; import se.llbit.math.ColorUtil; -import se.llbit.math.Ray; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; public class LilyPadMaterial extends Material { @@ -36,19 +37,19 @@ public LilyPadMaterial() { } @Override - public void getColor(Ray ray) { - super.getColor(ray); - if (ray.color.w > Ray.EPSILON) { - ray.color.x *= lilyPadColor[0]; - ray.color.y *= lilyPadColor[1]; - ray.color.z *= lilyPadColor[2]; + public void getColor(IntersectionRecord intersectionRecord) { + super.getColor(intersectionRecord); + if (intersectionRecord.color.w > Constants.EPSILON) { + intersectionRecord.color.x *= lilyPadColor[0]; + intersectionRecord.color.y *= lilyPadColor[1]; + intersectionRecord.color.z *= lilyPadColor[2]; } } @Override public float[] getColor(double u, double v) { float[] color = super.getColor(u, v); - if (color[3] > Ray.EPSILON) { + if (color[3] > Constants.EPSILON) { color = color.clone(); color[0] *= lilyPadColor[0]; color[1] *= lilyPadColor[1]; diff --git a/chunky/src/java/se/llbit/chunky/world/material/WaterPlaneMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/WaterPlaneMaterial.java new file mode 100644 index 0000000000..5d86378698 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/material/WaterPlaneMaterial.java @@ -0,0 +1,22 @@ +package se.llbit.chunky.world.material; + +import se.llbit.chunky.block.Block; +import se.llbit.chunky.resources.SolidColorTexture; + +/** + * The material used for the water plane. + */ +public class WaterPlaneMaterial extends Block { + + public static final WaterPlaneMaterial INSTANCE = new WaterPlaneMaterial(); + + private WaterPlaneMaterial() { + super("water_plane", SolidColorTexture.EMPTY); + } + + @Override + public void restoreDefaults() { + ior = 1.333f; + alpha = 0f; + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/model/CubeModel.java b/chunky/src/java/se/llbit/chunky/world/model/CubeModel.java index af730a6ed9..43eb3fc71e 100644 --- a/chunky/src/java/se/llbit/chunky/world/model/CubeModel.java +++ b/chunky/src/java/se/llbit/chunky/world/model/CubeModel.java @@ -18,20 +18,15 @@ package se.llbit.chunky.world.model; import se.llbit.chunky.resources.Texture; -import se.llbit.chunky.resources.TextureCache; -import se.llbit.chunky.resources.TexturePackLoader; -import se.llbit.chunky.resources.texturepack.SimpleTexture; -import se.llbit.chunky.resources.texturepack.TextureLoader; import se.llbit.log.Log; +import se.llbit.math.Constants; import se.llbit.math.Quad; -import se.llbit.math.Ray; import se.llbit.math.Vector2; import se.llbit.math.Vector3; import se.llbit.math.Vector4; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Map; /** @@ -84,7 +79,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 0, 0 }, { 0, 1 } }, new int[][] { { 2, 1 }, { 2, 0 } }, uv, - Math.abs(cube.start.y - cube.end.y) > Ray.EPSILON); + Math.abs(cube.start.y - cube.end.y) > Constants.EPSILON); break; case "down": addFace(theFaces, face, @@ -93,7 +88,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 0, 0 }, { 0, 1 } }, new int[][] { { 2, 0 }, { 2, 1 } }, uv, - Math.abs(cube.start.y - cube.end.y) > Ray.EPSILON); + Math.abs(cube.start.y - cube.end.y) > Constants.EPSILON); break; case "north": addFace(theFaces, face, @@ -102,7 +97,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 0, 1 }, { 0, 0 } }, new int[][] { { 1, 0 }, { 1, 1 } }, uv, - Math.abs(cube.start.z - cube.end.z) > Ray.EPSILON); + Math.abs(cube.start.z - cube.end.z) > Constants.EPSILON); break; case "south": addFace(theFaces, face, @@ -111,7 +106,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 0, 0 }, { 0, 1 } }, new int[][] { { 1, 0 }, { 1, 1 } }, uv, - Math.abs(cube.start.z - cube.end.z) > Ray.EPSILON); + Math.abs(cube.start.z - cube.end.z) > Constants.EPSILON); break; case "east": addFace(theFaces, face, @@ -120,7 +115,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 2, 1 }, { 2, 0 } }, new int[][] { { 1, 0 }, { 1, 1 } }, uv, - Math.abs(cube.start.x - cube.end.x) > Ray.EPSILON); + Math.abs(cube.start.x - cube.end.x) > Constants.EPSILON); break; case "west": addFace(theFaces, face, @@ -129,7 +124,7 @@ public CubeModel(Collection cubes, double uvScale, Map te new int[][] { { 2, 0 }, { 2, 1 } }, new int[][] { { 1, 0 }, { 1, 1 } }, uv, - Math.abs(cube.start.x - cube.end.x) > Ray.EPSILON); + Math.abs(cube.start.x - cube.end.x) > Constants.EPSILON); break; } } diff --git a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java index e6cee2dda4..b88044d67d 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java +++ b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java @@ -428,7 +428,7 @@ public static synchronized void writeRegion(File regionDirectory, RegionPosition int numSectors = loc & 0xFF; int sectorOffset = loc >> 8; - file.seek(sectorOffset * SECTOR_SIZE); + file.seek((long) sectorOffset * SECTOR_SIZE); byte[] buffer = new byte[SECTOR_SIZE]; for (int j = 0; j < numSectors; ++j) { file.read(buffer); diff --git a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java index b25f0771c0..c619e91154 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java +++ b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java @@ -17,8 +17,6 @@ package se.llbit.chunky.world.region; import se.llbit.chunky.chunk.ChunkData; -import se.llbit.chunky.chunk.GenericChunkData; -import se.llbit.chunky.chunk.SimpleChunkData; import se.llbit.chunky.map.MapView; import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.world.*; diff --git a/chunky/src/java/se/llbit/imageformats/png/PngFileWriter.java b/chunky/src/java/se/llbit/imageformats/png/PngFileWriter.java index 1bd4877a6c..a4a6bcf25b 100644 --- a/chunky/src/java/se/llbit/imageformats/png/PngFileWriter.java +++ b/chunky/src/java/se/llbit/imageformats/png/PngFileWriter.java @@ -21,7 +21,6 @@ import java.io.FileOutputStream; import java.io.OutputStream; -import se.llbit.chunky.renderer.scene.AlphaBuffer; import se.llbit.util.TaskTracker; import java.io.DataOutputStream; diff --git a/chunky/src/java/se/llbit/imageformats/tiff/TiffFileWriter.java b/chunky/src/java/se/llbit/imageformats/tiff/TiffFileWriter.java index e075f82604..bf99a7026c 100644 --- a/chunky/src/java/se/llbit/imageformats/tiff/TiffFileWriter.java +++ b/chunky/src/java/se/llbit/imageformats/tiff/TiffFileWriter.java @@ -25,10 +25,11 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; import se.llbit.chunky.main.Version; import se.llbit.chunky.renderer.postprocessing.PixelPostProcessingFilter; import se.llbit.chunky.renderer.postprocessing.PostProcessingFilter; -import se.llbit.chunky.renderer.postprocessing.PostProcessingFilters; import se.llbit.chunky.renderer.scene.AlphaBuffer; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.log.Log; @@ -119,7 +120,7 @@ private FinalizableBFCOutputStream.UnfinalizedData.Int writePrimaryIDF( idf.addTag(IFDTag.TAG_DATETIME, DATETIME_FORMAT.format(LocalDateTime.now())); return idf.write(out, ifdOffset, (out) -> { - PixelPostProcessingFilter filter = requirePixelPostProcessingFilter(scene); + List filters = requirePixelPostProcessingFilter(scene); double[] sampleBuffer = scene.getSampleBuffer(); AlphaBuffer alpha = scene.getAlphaBuffer(); FloatBuffer buffer = null; @@ -132,7 +133,13 @@ private FinalizableBFCOutputStream.UnfinalizedData.Int writePrimaryIDF( task.update(height, y); for (int x = 0; x < width; ++x) { // TODO: refactor pixel access to remove duplicate post processing code from here - filter.processPixel(width, height, sampleBuffer, x, y, scene.getExposure(), pixelBuffer); + + int index = (y * width + x) * 3; + System.arraycopy(sampleBuffer, index, pixelBuffer, 0, 3); + + for (PixelPostProcessingFilter filter : filters) { + filter.processPixel(pixelBuffer); + } if(embedAlpha) { pixelBuffer[3] = buffer.get(y * width + x); } @@ -151,16 +158,18 @@ void writePixel(DataOutput out, double[] pixelBuffer) throws IOException { out.writeFloat((float) pixelBuffer[3]); } - private PixelPostProcessingFilter requirePixelPostProcessingFilter(Scene scene) { - PostProcessingFilter filter = scene.getPostProcessingFilter(); - if (filter instanceof PixelPostProcessingFilter) { - // TODO: use https://openjdk.java.net/jeps/394 - return (PixelPostProcessingFilter) filter; - } else { - Log.warn("The selected post processing filter (" + filter.getName() - + ") doesn't support pixel based processing and can't be used to export TIFF files. " + - "The TIFF will be exported without post-processing instead."); - return PostProcessingFilters.NONE; + private List requirePixelPostProcessingFilter(Scene scene) { + List filters = scene.getPostprocessingFilters(); + List pixelPostProcessingFilters = new ArrayList<>(0); + for (PostProcessingFilter filter : filters) { + if (filter instanceof PixelPostProcessingFilter) { + pixelPostProcessingFilters.add((PixelPostProcessingFilter) filter); + } else { + Log.warn("The selected post processing filter (" + filter.getName() + + ") doesn't support pixel based processing and can't be used to export TIFF files. " + + "The TIFF will be exported without post-processing instead."); + } } + return pixelPostProcessingFilters; } } diff --git a/chunky/src/java/se/llbit/math/AABB.java b/chunky/src/java/se/llbit/math/AABB.java index 34797d3514..352d871f25 100644 --- a/chunky/src/java/se/llbit/math/AABB.java +++ b/chunky/src/java/se/llbit/math/AABB.java @@ -16,6 +16,9 @@ */ package se.llbit.math; +import it.unimi.dsi.fastutil.doubles.DoubleDoubleImmutablePair; +import se.llbit.chunky.renderer.scene.Scene; + import java.util.Random; /** @@ -24,7 +27,7 @@ * * @author Jesper Öqvist */ -public class AABB { +public class AABB implements Intersectable { public double xmin; public double xmax; @@ -93,92 +96,90 @@ public double faceSurfaceArea(int face) { * * @return true if the ray intersects this AABB */ - public boolean intersect(Ray ray) { - double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Ray.OFFSET); - double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Ray.OFFSET); - double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Ray.OFFSET); + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Constants.OFFSET); + double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Constants.OFFSET); + double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Constants.OFFSET); double t; double u, v; boolean hit = false; - ray.tNext = ray.t; - t = (xmin - ix) / ray.d.x; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = iz + ray.d.z * t; v = iy + ray.d.y * t; if (u >= zmin && u <= zmax && v >= ymin && v <= ymax) { hit = true; - ray.tNext = t; - ray.u = u; - ray.v = v; - ray.setNormal(-1, 0, 0); + intersectionRecord.distance = t; + intersectionRecord.uv.x = u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(-1, 0, 0); } } t = (xmax - ix) / ray.d.x; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = iz + ray.d.z * t; v = iy + ray.d.y * t; if (u >= zmin && u <= zmax && v >= ymin && v <= ymax) { hit = true; - ray.tNext = t; - ray.u = 1 - u; - ray.v = v; - ray.setNormal(1, 0, 0); + intersectionRecord.distance = t; + intersectionRecord.uv.x = 1 - u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(1, 0, 0); } } t = (ymin - iy) / ray.d.y; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = ix + ray.d.x * t; v = iz + ray.d.z * t; if (u >= xmin && u <= xmax && v >= zmin && v <= zmax) { hit = true; - ray.tNext = t; - ray.u = u; - ray.v = v; - ray.setNormal(0, -1, 0); + intersectionRecord.distance = t; + intersectionRecord.uv.x = u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(0, -1, 0); } } t = (ymax - iy) / ray.d.y; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = ix + ray.d.x * t; v = iz + ray.d.z * t; if (u >= xmin && u <= xmax && v >= zmin && v <= zmax) { hit = true; - ray.tNext = t; - ray.u = u; - ray.v = v; - ray.setNormal(0, 1, 0); + intersectionRecord.distance = t; + intersectionRecord.uv.x = u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(0, 1, 0); } } t = (zmin - iz) / ray.d.z; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = ix + ray.d.x * t; v = iy + ray.d.y * t; if (u >= xmin && u <= xmax && v >= ymin && v <= ymax) { hit = true; - ray.tNext = t; - ray.u = 1 - u; - ray.v = v; - ray.setNormal(0, 0, -1); + intersectionRecord.distance = t; + intersectionRecord.uv.x = 1 - u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(0, 0, -1); } } t = (zmax - iz) / ray.d.z; - if (t < ray.tNext && t > -Ray.EPSILON) { + if (t < intersectionRecord.distance + Constants.OFFSET && t > -Constants.EPSILON) { u = ix + ray.d.x * t; v = iy + ray.d.y * t; if (u >= xmin && u <= xmax && v >= ymin && v <= ymax) { hit = true; - ray.tNext = t; - ray.u = u; - ray.v = v; - ray.setNormal(0, 0, 1); + intersectionRecord.distance = t; + intersectionRecord.uv.x = u; + intersectionRecord.uv.y = v; + intersectionRecord.setNormal(0, 0, 1); } } return hit; @@ -189,7 +190,7 @@ public boolean intersect(Ray ray) { * * @return {@code true} if there is an intersection */ - public boolean quickIntersect(Ray ray) { + public double quickIntersect(Ray ray) { double t1, t2; double tNear = Double.NEGATIVE_INFINITY; double tFar = Double.POSITIVE_INFINITY; @@ -249,23 +250,97 @@ public boolean quickIntersect(Ray ray) { } } - if (tNear < tFar + Ray.EPSILON && tNear >= 0 && tNear < ray.t) { - ray.tNext = tNear; - return true; + if (tNear < tFar + Constants.EPSILON && tNear >= 0) { + return tNear; + } else { + return Double.NaN; + } + } + + public DoubleDoubleImmutablePair intersectionDistance(Ray ray) { + double t1, t2; + double tNear = Double.NEGATIVE_INFINITY; + double tFar = Double.POSITIVE_INFINITY; + Vector3 d = ray.d; + Vector3 o = ray.o; + + if (d.x != 0) { + double rx = 1 / d.x; + t1 = (xmin - o.x) * rx; + t2 = (xmax - o.x) * rx; + + if (t1 > t2) { + double t = t1; + t1 = t2; + t2 = t; + } + + tNear = t1; + tFar = t2; + } + + if (d.y != 0) { + double ry = 1 / d.y; + t1 = (ymin - o.y) * ry; + t2 = (ymax - o.y) * ry; + + if (t1 > t2) { + double t = t1; + t1 = t2; + t2 = t; + } + + if (t1 > tNear) { + tNear = t1; + } + if (t2 < tFar) { + tFar = t2; + } + } + + if (d.z != 0) { + double rz = 1 / d.z; + t1 = (zmin - o.z) * rz; + t2 = (zmax - o.z) * rz; + + if (t1 > t2) { + double t = t1; + t1 = t2; + t2 = t; + } + + if (t1 > tNear) { + tNear = t1; + } + if (t2 < tFar) { + tFar = t2; + } + } + if (tNear < tFar + Constants.EPSILON && tFar >= 0) { + return new DoubleDoubleImmutablePair(tNear, tFar); } else { - return false; + return new DoubleDoubleImmutablePair(Double.NaN, Double.NaN); } } /** * Test if point is inside the bounding box. * - * @return true if p is inside this BB. + * @return true if p is inside this AABB. */ public boolean inside(Vector3 p) { - return (p.x >= xmin && p.x <= xmax) && - (p.y >= ymin && p.y <= ymax) && - (p.z >= zmin && p.z <= zmax); + return inside(p.x, p.y, p.z); + } + + /** + * Test if point is inside the bounding box. + * + * @return true if p is inside this AABB. + */ + public boolean inside(double x, double y, double z) { + return (x > xmin && x < xmax) && + (y > ymin && y < ymax) && + (z > zmin && z < zmax); } /** @@ -333,7 +408,7 @@ public boolean hitTest(Ray ray) { } } - return tNear < tFar + Ray.EPSILON && tFar > 0; + return tNear < tFar + Constants.EPSILON && tFar > 0; } /** diff --git a/chunky/src/java/se/llbit/math/ColorUtil.java b/chunky/src/java/se/llbit/math/ColorUtil.java index a022e25e39..b26183fd50 100644 --- a/chunky/src/java/se/llbit/math/ColorUtil.java +++ b/chunky/src/java/se/llbit/math/ColorUtil.java @@ -21,6 +21,7 @@ import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.json.JsonObject; /** * Collection of utility methods for converting between different color representations. @@ -234,7 +235,7 @@ public static int getRGB(double[] frgb) { * Transform from xyY colorspace to XYZ colorspace. */ public static void xyYtoXYZ(Vector3 in, Vector3 out) { - if (in.y <= Ray.EPSILON) { + if (in.y <= Constants.EPSILON) { out.set(0, 0, 0); return; } @@ -293,7 +294,7 @@ public static void RGBtoHSL(Vector3 hsl, double r, double g, double b) { hue = (((r - g) / delta) + 4) / 6.0; } - hsl.set(hue, delta < Ray.EPSILON ? 0 : delta / (1 - FastMath.abs(2*lightness - 1)), lightness); + hsl.set(hue, delta < Constants.EPSILON ? 0 : delta / (1 - FastMath.abs(2*lightness - 1)), lightness); } public static Vector3 RGBtoHSL(double r, double g, double b) { @@ -435,4 +436,28 @@ public static byte RGBComponentFromLinear(float linearValue) { public static float RGBComponentToLinear(byte value) { return toLinearLut[value & 0xFF]; } + + public static JsonObject rgbToJson(Vector3 color) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("red", color.x); + jsonObject.add("green", color.y); + jsonObject.add("blue", color.z); + return jsonObject; + } + + public static Vector3 jsonToRGB(JsonObject json) { + Vector3 color = new Vector3(); + color.x = json.get("red").doubleValue(1); + color.y = json.get("green").doubleValue(1); + color.z = json.get("blue").doubleValue(1); + return color; + } + + public static Vector3 jsonToRGB(JsonObject json, Vector3 undefined) { + Vector3 color = new Vector3(); + color.x = json.get("red").doubleValue(undefined.x); + color.y = json.get("green").doubleValue(undefined.y); + color.z = json.get("blue").doubleValue(undefined.z); + return color; + } } diff --git a/chunky/src/java/se/llbit/math/Constants.java b/chunky/src/java/se/llbit/math/Constants.java index 5aff99ae7e..4d51560fbe 100644 --- a/chunky/src/java/se/llbit/math/Constants.java +++ b/chunky/src/java/se/llbit/math/Constants.java @@ -16,10 +16,18 @@ */ package se.llbit.math; -public class Constants { +public final class Constants { + public static final double EPSILON = 5e-8; + public static final double OFFSET = 1e-6; + + public static final double INV_4_PI = 1 / (4 * Math.PI); public static final double HALF_PI = Math.PI / 2; + public static final double INV_PI = 1 / Math.PI; // TODO INV_TAU public static final double TAU = Math.PI * 2; public static final double SQRT_HALF = Math.sqrt(0.5); public static final double INV_SQRT_HALF = 1 / Math.sqrt(0.5); + public static final double SQRT_2 = Math.sqrt(2); + + private Constants() {} } diff --git a/chunky/src/java/se/llbit/math/Grid.java b/chunky/src/java/se/llbit/math/Grid.java index 985259f901..aa7bd80972 100644 --- a/chunky/src/java/se/llbit/math/Grid.java +++ b/chunky/src/java/se/llbit/math/Grid.java @@ -15,13 +15,13 @@ public class Grid { private static final int GRID_FORMAT_VERSION = 3; /** - * Holds a 3D grid of blocks cube - * Each cell of the grid holds the position of the emitters present in this cell and in neighboring cells - * As such when we want to sample emitters close from an intersection point, we only have to look at - * the cell where this intersection falls in and we will find every emitters we are interested in. - * The reason we need to hold emitter of neighboring cells is because emitters a fex block away from the intersection point - * to have an effect even if it falls in a different cell. - * With this every emitters away for cellSize or less blocks from the intersection points will always be found. + * Holds a 3D grid of blocks + * Each cell of the grid holds the position of the emitters present in this cell and in neighboring cells. + * As such, when we want to sample emitters close to an intersection point, we only have to look at + * the cell where the intersection falls in, and we will find every emitter we are interested in. + * The reason we need to hold emitter of neighboring cells is because emitters a few block away from the intersection point + * can have an effect even if it falls in a different cell. + * With this, every emitter that are cellSize or less blocks away from the intersection points will always be found. * The maximum distance where an emitter can be found in some cases is 2*cellSize-1 blocks away. */ public static class EmitterPosition { diff --git a/chunky/src/java/se/llbit/math/Intersectable.java b/chunky/src/java/se/llbit/math/Intersectable.java index 9ecd10a5da..657a61527c 100644 --- a/chunky/src/java/se/llbit/math/Intersectable.java +++ b/chunky/src/java/se/llbit/math/Intersectable.java @@ -17,6 +17,10 @@ */ package se.llbit.math; +import se.llbit.chunky.renderer.scene.Scene; + +import java.util.Random; + /** * Anything which can intersect a ray in space. */ @@ -26,5 +30,13 @@ public interface Intersectable { * * @return {@code true} if there exists any intersection */ - boolean closestIntersection(Ray ray); + boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random); + + default boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene) { + return closestIntersection(ray, intersectionRecord, scene, null); + } + + default boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord) { + return closestIntersection(ray, intersectionRecord, null); + } } diff --git a/chunky/src/java/se/llbit/math/IntersectionRecord.java b/chunky/src/java/se/llbit/math/IntersectionRecord.java new file mode 100644 index 0000000000..cbb1094f9c --- /dev/null +++ b/chunky/src/java/se/llbit/math/IntersectionRecord.java @@ -0,0 +1,58 @@ +package se.llbit.math; + +import se.llbit.chunky.block.minecraft.Air; +import se.llbit.chunky.world.Material; + +public class IntersectionRecord { + public static final int NO_MEDIUM_CHANGE = 1; + public static final int VOLUME_INTERSECT = 1 << 1; + + public double distance = Double.POSITIVE_INFINITY; + public final Vector3 n = new Vector3(0, 1, 0); + public final Vector2 uv = new Vector2(); + public Material material = Air.INSTANCE; + public final Vector3 shadeN = new Vector3(0, 1, 0); + public final Vector4 color = new Vector4(); + public int flags = 0; + + public void setNoMediumChange(boolean value) { + this.flags = value ? this.flags | NO_MEDIUM_CHANGE : this.flags & ~NO_MEDIUM_CHANGE; + } + + public void setVolumeIntersect(boolean value) { + this.flags = value ? this.flags | VOLUME_INTERSECT : this.flags & ~VOLUME_INTERSECT; + } + + public boolean isNoMediumChange() { + return (this.flags & NO_MEDIUM_CHANGE) != 0; + } + + public boolean isVolumeIntersect() { + return (this.flags & VOLUME_INTERSECT) != 0; + } + + public void reset() { + this.distance = Double.POSITIVE_INFINITY; + this.n.set(0, 1, 0); + this.uv.set(0, 0); + this.material = Air.INSTANCE; + this.shadeN.set(0, 1, 0); + this.color.set(0, 0, 0, 0); + this.flags = 0; + } + + public void setNormal(double x, double y, double z) { + n.set(x, y, z); + shadeN.set(x, y, z); + } + + public void setNormal(Vector3 normal) { + n.set(normal); + shadeN.set(normal); + } + + public void setNormal(IntersectionRecord intersectionRecord) { + n.set(intersectionRecord.n); + shadeN.set(intersectionRecord.shadeN); + } +} diff --git a/chunky/src/java/se/llbit/math/Matrix3.java b/chunky/src/java/se/llbit/math/Matrix3.java index 9fe1beaf2f..638179980e 100644 --- a/chunky/src/java/se/llbit/math/Matrix3.java +++ b/chunky/src/java/se/llbit/math/Matrix3.java @@ -29,6 +29,21 @@ public class Matrix3 { public double m21, m22, m23; public double m31, m32, m33; + public Matrix3() { + } + + public Matrix3(double m11, double m12, double m13, double m21, double m22, double m23, double m31, double m32, double m33) { + this.m11 = m11; + this.m12 = m12; + this.m13 = m13; + this.m21 = m21; + this.m22 = m22; + this.m23 = m23; + this.m31 = m31; + this.m32 = m32; + this.m33 = m33; + } + /** * Set the matrix to be a rotation matrix for rotation * around the X axis. @@ -110,7 +125,7 @@ public final void rotate(double pitch, double yaw, double roll) { */ public void transform(Vector3 o) { o.set(m11 * o.x + m12 * o.y + m13 * o.z, m21 * o.x + m22 * o.y + m23 * o.z, - m31 * o.x + m32 * o.y + m33 * o.z); + m31 * o.x + m32 * o.y + m33 * o.z); } /** diff --git a/chunky/src/java/se/llbit/math/Octree.java b/chunky/src/java/se/llbit/math/Octree.java index 897180d839..3d49a142b8 100644 --- a/chunky/src/java/se/llbit/math/Octree.java +++ b/chunky/src/java/se/llbit/math/Octree.java @@ -21,21 +21,22 @@ import java.nio.file.Files; import java.util.HashMap; import java.util.Map; +import java.util.Random; import it.unimi.dsi.fastutil.ints.IntIntMutablePair; import it.unimi.dsi.fastutil.io.FastBufferedInputStream; import it.unimi.dsi.fastutil.io.FastBufferedOutputStream; import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.block.Void; import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.Block; -import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.chunk.BlockPalette; import se.llbit.chunky.model.TexturedBlockModel; -import se.llbit.chunky.model.minecraft.WaterModel; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.material.WaterPlaneMaterial; import se.llbit.log.Log; import se.llbit.util.io.PositionalInputStream; import se.llbit.util.io.PositionalOutputStream; @@ -54,7 +55,7 @@ * * @author Jesper Öqvist (jesper@llbit.se) */ -public class Octree { +public class Octree implements Intersectable { public interface OctreeImplementation { void set(int type, int x, int y, int z); @@ -249,7 +250,7 @@ public int getData() { */ private long timestamp = 0; - private OctreeImplementation implementation; + protected OctreeImplementation implementation; /** * Create a new Octree. The dimensions of the Octree @@ -292,12 +293,12 @@ public synchronized void set(int type, int x, int y, int z) { * @param y y position * @param z z position * @param palette Block palette - * @return Material at the given position or {@link Air#INSTANCE} if the position is outside of this octree + * @return Material at the given position or {@link Void#INSTANCE} if the position is outside of this octree */ public Material getMaterial(int x, int y, int z, BlockPalette palette) { int size = (1 << implementation.getDepth()); if(x < 0 || y < 0 || z < 0 || x >= size || y >= size || z >= size) - return Air.INSTANCE; + return Void.INSTANCE; return implementation.getMaterial(x, y, z, palette); } @@ -346,7 +347,7 @@ public boolean isInside(Vector3 o) { * @param ray Ray that will be moved to the boundary of the octree. The origin, distance and normals will be modified. * @return {@code false} if the ray doesn't intersect the octree. */ - private boolean enterOctree(Ray ray) { + private double enterOctree(Ray ray, IntersectionRecord intersectionRecord) { double nx = 0, ny = 0, nz = 0; double octree_size = 1 << getDepth(); @@ -408,7 +409,7 @@ private boolean enterOctree(Ray ray) { } if ((tMin > tXMax) || (tXMin > tMax)) - return false; + return Double.NaN; if (tXMin > tMin) { tMin = tXMin; @@ -431,7 +432,7 @@ private boolean enterOctree(Ray ray) { } if ((tMin > tYMax) || (tYMin > tMax)) - return false; + return Double.NaN; if (tYMin > tMin) { tMin = tYMin; @@ -454,7 +455,7 @@ private boolean enterOctree(Ray ray) { } if ((tMin > tZMax) || (tZMin > tMax)) - return false; + return Double.NaN; if (tZMin > tMin) { tMin = tZMin; @@ -464,12 +465,10 @@ private boolean enterOctree(Ray ray) { } if (tMin < 0) - return false; + return Double.NaN; - ray.o.scaleAdd(tMin, ray.d); - ray.setNormal(nx, ny, nz); - ray.distance += tMin; - return true; + intersectionRecord.setNormal(nx, ny, nz); + return tMin; } /** @@ -477,25 +476,32 @@ private boolean enterOctree(Ray ray) { * * @return {@code false} if the ray did not hit the geometry */ - public boolean enterBlock(Scene scene, Ray ray, BlockPalette palette) { - if (!isInside(ray.o) && !enterOctree(ray)) - return false; - - int depth = implementation.getDepth(); - + @Override + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + BlockPalette palette = scene.getPalette(); double distance = 0; - - // floating point division are slower than multiplication so we cache them - // We also try to limit the number of time the ray origin is updated - // as it would require to recompute those values - double invDx = 1 / ray.d.x; - double invDy = 1 / ray.d.y; - double invDz = 1 / ray.d.z; - double offsetX = -ray.o.x * invDx; - double offsetY = -ray.o.y * invDy; - double offsetZ = -ray.o.z * invDz; - IntIntMutablePair typeAndLevel = new IntIntMutablePair(0, 0); + Vector3 invDir = new Vector3( + 1.0 / ray.d.x, + 1.0 / ray.d.y, + 1.0 / ray.d.z + ); + Vector3 offset = new Vector3( + -ray.o.x * invDir.x, + -ray.o.y * invDir.y, + -ray.o.z * invDir.z + ); + Vector3 pos = new Vector3(ray.o); + Ray testRay = new Ray(ray); + + // Check if we are in-bounds + if (!isInside(ray.o)) { + double dist = enterOctree(ray, intersectionRecord); + if (Double.isNaN(dist)) { + return false; + } + distance += dist; + } // Marching is done in a top-down fashion: at each step, the octree is descended from the root to find the leaf // node the ray is in. Terminating the march is then decided based on the block type in that leaf node. Finally the @@ -503,68 +509,68 @@ public boolean enterBlock(Scene scene, Ray ray, BlockPalette palette) { while (true) { // Add small offset past the intersection to avoid // recursion to the same octree node! - int x = (int) Math.floor(ray.o.x + ray.d.x * (distance + Ray.OFFSET)); - int y = (int) Math.floor(ray.o.y + ray.d.y * (distance + Ray.OFFSET)); - int z = (int) Math.floor(ray.o.z + ray.d.z * (distance + Ray.OFFSET)); - - int lx = x >>> depth; - int ly = y >>> depth; - int lz = z >>> depth; - - if (lx != 0 || ly != 0 || lz != 0) - return false; // outside of octree! + pos.set(ray.o); + pos.scaleAdd(distance + Constants.OFFSET, ray.d); + int bx = (int) Math.floor(pos.x); + int by = (int) Math.floor(pos.y); + int bz = (int) Math.floor(pos.z); + + if (!isInside(pos)) { + Block currentBlock = scene.isUnderWaterPlane(pos) ? WaterPlaneMaterial.INSTANCE : Void.INSTANCE; + Material prevBlock = ray.getCurrentMedium(); + if (currentBlock.isSameMaterial(prevBlock) + || currentBlock == Void.INSTANCE && prevBlock == Air.INSTANCE) { + return false; + } + testRay.o.set(ray.o); + testRay.o.scaleAdd(distance, ray.d); + intersectionRecord.material = currentBlock; + currentBlock.getColor(intersectionRecord); + intersectionRecord.distance = distance; + return true; + } - implementation.getWithLevel(typeAndLevel, x, y, z); + implementation.getWithLevel(typeAndLevel, bx, by, bz); int type = typeAndLevel.leftInt(); int level = typeAndLevel.rightInt(); - lx = x >>> level; - ly = y >>> level; - lz = z >>> level; + int lx = bx >>> level; + int ly = by >>> level; + int lz = bz >>> level; // Test intersection Block currentBlock = palette.get(type); - Material prevBlock = ray.getCurrentMaterial(); - - ray.setPrevMaterial(prevBlock, ray.getCurrentData()); - ray.setCurrentMaterial(currentBlock); - - if (currentBlock.localIntersect) { - // Other functions expect the ray origin to be in the block they test so here time - // to update it - // Updating the origin also means that new offsetX/offsetY/offsetZ must be computed - // but that is done a after the intersection test only if necessary - // and not if we are leaving the function anyway - ray.o.scaleAdd(distance, ray.d); - ray.distance += distance; - distance = 0; - if (currentBlock.intersect(ray, scene)) { - if (prevBlock != currentBlock) - return true; + if (scene.isUnderWaterPlane(pos) && (currentBlock == Air.INSTANCE || currentBlock == Void.INSTANCE)) { + currentBlock = WaterPlaneMaterial.INSTANCE; + } + Material prevBlock = ray.getCurrentMedium(); - ray.o.scaleAdd(Ray.OFFSET, ray.d); - offsetX = -ray.o.x * invDx; - offsetY = -ray.o.y * invDy; - offsetZ = -ray.o.z * invDz; - continue; - } else { - // Exit ray from this local block. - ray.setCurrentMaterial(Air.INSTANCE); // Current material is air. - ray.exitBlock(x, y, z); - offsetX = -ray.o.x * invDx; - offsetY = -ray.o.y * invDy; - offsetZ = -ray.o.z * invDz; - continue; - } - } else if (!currentBlock.isSameMaterial(prevBlock) && currentBlock != Air.INSTANCE) { - // Origin and distance of ray need to be updated - ray.o.scaleAdd(distance, ray.d); - ray.distance += distance; - TexturedBlockModel.getIntersectionColor(ray); - if (currentBlock.opaque) { - ray.color.w = 1; + intersectionRecord.material = currentBlock; + + if (!currentBlock.hidden) { + if (currentBlock.localIntersect) { + testRay.o.set(ray.o); + testRay.o.scaleAdd(distance, ray.d); + testRay.setCurrentMedium(ray.getCurrentMedium()); + if (currentBlock.intersect(testRay, intersectionRecord, scene)) { + intersectionRecord.distance += distance; + return true; + } else { + intersectionRecord.distance = Double.POSITIVE_INFINITY; + distance += exitBlock(testRay, intersectionRecord, bx, by, bz); + continue; + } + } else if (!currentBlock.isSameMaterial(prevBlock)) { + if (!(currentBlock == Void.INSTANCE && prevBlock == Air.INSTANCE || currentBlock == Air.INSTANCE && prevBlock == Void.INSTANCE)) { + testRay.o.set(ray.o); + testRay.o.scaleAdd(distance, ray.d); + TexturedBlockModel.getIntersectionColor(testRay, intersectionRecord); + intersectionRecord.distance = distance; + return true; + } + // Set ray medium to currentBlock (which is either Air or Void), but don't intersect. + ray.setCurrentMedium(currentBlock); } - return true; } // No intersection, exit current octree leaf. @@ -575,172 +581,106 @@ public boolean enterBlock(Scene scene, Ray ray, BlockPalette palette) { // Every side is unconditionally tested because the origin of the ray can be outside the block // The computation involves a multiplication and an addition so we could use a fma (need java 9+) // but according to measurement, performance are identical - double t = (lx << level) * invDx + offsetX; - if (t > distance + Ray.EPSILON) { + double t = (lx << level) * invDir.x + offset.x; + if (t > distance + Constants.OFFSET) { tNear = t; nx = 1; } - t = ((lx + 1) << level) * invDx + offsetX; - if (t < tNear && t > distance + Ray.EPSILON) { + t = ((lx + 1) << level) * invDir.x + offset.x; + if (t < tNear && t > distance + Constants.OFFSET) { tNear = t; nx = -1; } - t = (ly << level) * invDy + offsetY; - if (t < tNear && t > distance + Ray.EPSILON) { + t = (ly << level) * invDir.y + offset.y; + if (t < tNear && t > distance + Constants.OFFSET) { tNear = t; ny = 1; nx = 0; } - t = ((ly + 1) << level) * invDy + offsetY; - if (t < tNear && t > distance + Ray.EPSILON) { + t = ((ly + 1) << level) * invDir.y + offset.y; + if (t < tNear && t > distance + Constants.OFFSET) { tNear = t; ny = -1; nx = 0; } - t = (lz << level) * invDz + offsetZ; - if (t < tNear && t > distance + Ray.EPSILON) { + t = (lz << level) * invDir.z + offset.z; + if (t < tNear && t > distance + Constants.OFFSET) { tNear = t; nz = 1; nx = ny = 0; } - t = ((lz + 1) << level) * invDz + offsetZ; - if (t < tNear && t > distance + Ray.EPSILON) { + t = ((lz + 1) << level) * invDir.z + offset.z; + if (t < tNear && t > distance + Constants.OFFSET) { tNear = t; nz = -1; nx = ny = 0; } - ray.setNormal(nx, ny, nz); + intersectionRecord.setNormal(nx, ny, nz); distance = tNear; } } /** - * Advance the ray until it leaves the current water body. + * Find the exit point from the given block for this ray. This marches the ray forward - i.e. + * updates ray origin directly. * - * @return {@code false} if the ray doesn't hit anything + * @param bx block x coordinate + * @param by block y coordinate + * @param bz block z coordinate */ - public boolean exitWater(Scene scene, Ray ray, BlockPalette palette) { - if (!isInside(ray.o) && !enterOctree(ray)) - return false; - - int depth = getDepth(); - // Marching is done in a top-down fashion: at each step, the octree is descended from the root to find the leaf - // node the ray is in. Terminating the march is then decided based on the block type in that leaf node. Finally the - // ray is advanced to the boundary of the current leaf node and the next, ready for the next iteration. - - IntIntMutablePair typeAndLevel = new IntIntMutablePair(0, 0); - while (true) { - // Add small offset past the intersection to avoid - // recursion to the same octree node! - int x = (int) QuickMath.floor(ray.o.x + ray.d.x * Ray.OFFSET); - int y = (int) QuickMath.floor(ray.o.y + ray.d.y * Ray.OFFSET); - int z = (int) QuickMath.floor(ray.o.z + ray.d.z * Ray.OFFSET); - - int lx = x >>> depth; - int ly = y >>> depth; - int lz = z >>> depth; - - if (lx != 0 || ly != 0 || lz != 0) - return false; // outside of octree! - - // Descend the tree to find the current leaf node - implementation.getWithLevel(typeAndLevel, x, y, z); - int type = typeAndLevel.leftInt(); - int level = typeAndLevel.rightInt(); - - lx = x >>> level; - ly = y >>> level; - lz = z >>> level; - - // Test intersection - Block currentBlock = palette.get(type); - Material prevBlock = ray.getCurrentMaterial(); - - ray.setPrevMaterial(prevBlock, ray.getCurrentData()); - ray.setCurrentMaterial(currentBlock); - - if (!currentBlock.isWater()) { - if (currentBlock.localIntersect) { - if (!currentBlock.intersect(ray, scene)) { - ray.setCurrentMaterial(Air.INSTANCE); - } - return true; - } else if (currentBlock != Air.INSTANCE) { - TexturedBlockModel.getIntersectionColor(ray); - if (currentBlock.opaque) { - ray.color.w = 1; - } - return true; - } else { - return true; - } - } - - if (!(currentBlock instanceof Water && ((Water) currentBlock).isFullBlock())) { - if (WaterModel.intersectTop(ray)) { - ray.setCurrentMaterial(Air.INSTANCE); - return true; - } else { - ray.exitBlock(x, y, z); - continue; - } - } - - // No intersection, exit current octree leaf. - int nx = 0, ny = 0, nz = 0; - double tNear = Double.POSITIVE_INFINITY; - - // Testing all six sides of the current leaf node and advancing to the closest intersection - double t = ((lx << level) - ray.o.x) / ray.d.x; - if (t > Ray.EPSILON) { - tNear = t; - nx = 1; + public double exitBlock(Ray ray, IntersectionRecord intersectionRecord, int bx, int by, int bz) { + int nx = 0; + int ny = 0; + int nz = 0; + double tNext = Double.POSITIVE_INFINITY; + double t = (bx - ray.o.x) / ray.d.x; + if (t > Constants.EPSILON) { + tNext = t; + nx = 1; + ny = nz = 0; + } else { + t = ((bx + 1) - ray.o.x) / ray.d.x; + if (t < tNext && t > Constants.EPSILON) { + tNext = t; + nx = -1; ny = nz = 0; - } else { - t = (((lx + 1) << level) - ray.o.x) / ray.d.x; - if (t < tNear && t > Ray.EPSILON) { - tNear = t; - nx = -1; - ny = nz = 0; - } } + } - t = ((ly << level) - ray.o.y) / ray.d.y; - if (t < tNear && t > Ray.EPSILON) { - tNear = t; - ny = 1; + t = (by - ray.o.y) / ray.d.y; + if (t < tNext && t > Constants.EPSILON) { + tNext = t; + ny = 1; + nx = nz = 0; + } else { + t = ((by + 1) - ray.o.y) / ray.d.y; + if (t < tNext && t > Constants.EPSILON) { + tNext = t; + ny = -1; nx = nz = 0; - } else { - t = (((ly + 1) << level) - ray.o.y) / ray.d.y; - if (t < tNear && t > Ray.EPSILON) { - tNear = t; - ny = -1; - nx = nz = 0; - } } + } - t = ((lz << level) - ray.o.z) / ray.d.z; - if (t < tNear && t > Ray.EPSILON) { - tNear = t; - nz = 1; + t = (bz - ray.o.z) / ray.d.z; + if (t < tNext && t > Constants.EPSILON) { + tNext = t; + nz = 1; + nx = ny = 0; + } else { + t = ((bz + 1) - ray.o.z) / ray.d.z; + if (t < tNext && t > Constants.EPSILON) { + tNext = t; + nz = -1; nx = ny = 0; - } else { - t = (((lz + 1) << level) - ray.o.z) / ray.d.z; - if (t < tNear && t > Ray.EPSILON) { - tNear = t; - nz = -1; - nx = ny = 0; - } } - - ray.o.scaleAdd(tNear, ray.d); - ray.setNormal(nx, ny, nz); - ray.distance += tNear; } + + intersectionRecord.setNormal(nx, ny, nz); + return tNext; } /** diff --git a/chunky/src/java/se/llbit/math/Quad.java b/chunky/src/java/se/llbit/math/Quad.java index 336ed7a489..3eea231bfb 100644 --- a/chunky/src/java/se/llbit/math/Quad.java +++ b/chunky/src/java/se/llbit/math/Quad.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.Random; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.world.Material; import se.llbit.math.primitive.Primitive; import se.llbit.math.primitive.TexturedTriangle; @@ -28,7 +29,7 @@ * * @author Jesper Öqvist */ -public class Quad { +public class Quad implements Intersectable { /** Note: This is public for some plugins. Stability is not guaranteed. */ public Vector3 o = new Vector3(); @@ -156,20 +157,20 @@ public void sample(Vector3 loc, Random rand) { * * @return true if the ray intersects this quad */ - public boolean intersect(Ray ray) { + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { double u, v; - double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Ray.OFFSET); - double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Ray.OFFSET); - double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Ray.OFFSET); + double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Constants.OFFSET); + double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Constants.OFFSET); + double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Constants.OFFSET); // Test that the ray is heading toward the plane of this quad. double denom = ray.d.dot(n); - if (denom < -Ray.EPSILON || (doubleSided && denom > Ray.EPSILON)) { + if (denom < -Constants.EPSILON || (doubleSided && denom > Constants.EPSILON)) { // Test for intersection with the plane at origin. double t = -(ix * n.x + iy * n.y + iz * n.z + d) / denom; - if (t > -Ray.EPSILON && t < ray.t) { + if (t > -Constants.EPSILON && t < intersectionRecord.distance) { // Plane intersection confirmed. // Translate to get hit point relative to the quad origin. @@ -181,9 +182,9 @@ public boolean intersect(Ray ray) { v = ix * yv.x + iy * yv.y + iz * yv.z; v *= yvl; if (u >= 0 && u <= 1 && v >= 0 && v <= 1) { - ray.u = uv.x + u * uv.y; - ray.v = uv.z + v * uv.w; - ray.tNext = t; + intersectionRecord.uv.x = uv.x + u * uv.y; + intersectionRecord.uv.y = uv.z + v * uv.w; + intersectionRecord.distance = t; return true; } } diff --git a/chunky/src/java/se/llbit/math/Ray.java b/chunky/src/java/se/llbit/math/Ray.java index 9684fdbae5..b3546b774b 100644 --- a/chunky/src/java/se/llbit/math/Ray.java +++ b/chunky/src/java/se/llbit/math/Ray.java @@ -1,680 +1,82 @@ -/* Copyright (c) 2012-2014 Jesper Öqvist - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ package se.llbit.math; -import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.block.minecraft.Air; -import se.llbit.chunky.block.minecraft.Lava; -import se.llbit.chunky.block.minecraft.Water; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.renderer.scene.sky.Sun; import se.llbit.chunky.world.Material; -import java.util.Random; - /** * The ray representation used for ray tracing. - * - * @author Jesper Öqvist */ public class Ray { + public static final int DIFFUSE = 1; + public static final int SPECULAR = 1 << 1; + public static final int INDIRECT = 1 << 2; - public static final double EPSILON = 0.00000005; - - public static final double OFFSET = 0.000001; - - /** - * Ray direction. - */ - public Vector3 d = new Vector3(); - - /** - * Intersection point. - */ public Vector3 o = new Vector3(); + public Vector3 d = new Vector3(); + private Material currentMedium = Air.INSTANCE; + public int flags = 0; - /** - * Intersection normal. - */ - private Vector3 n = new Vector3(); - - /** - * Geometry normal, almost always the same as normal except when a normal map is used - * This stays the real normal of the geometry - */ - private Vector3 geomN = new Vector3(); - - /** - * Distance traveled in current medium. This is updated after all intersection tests have run and - * the final t value has been found. - */ - public double distance; - - /** - * Accumulated color value. - */ - public Vector4 color = new Vector4(); - - /** - * Previous material. - */ - private Material prevMaterial = Air.INSTANCE; - - /** - * Current material. - */ - private Material currentMaterial = Air.INSTANCE; - - /** - * Previous block metadata. - */ - private int prevData; - - /** - * Current block metadata. - */ - private int currentData; - - /** - * Recursive ray depth - */ - public int depth; - - /** - * Distance to closest intersection. - */ - public double t; - - /** - * Distance to next potential intersection. The tNext value is stored by subroutines when - * calculating a potential next hit point. This can then be stored in the t variable based on - * further decision making. - */ - public double tNext; - - /** - * Texture coordinate. - */ - public double u; - - /** - * Texture coordinate. - */ - public double v; - - /** - * Is the ray specularly reflected - */ - public boolean specular; - - /** - * Builds an uninitialized ray. - */ - public Ray() { - } - - /** - * Create a copy of the given ray - * - * @param other ray to copy - */ - public Ray(Ray other) { - set(other); - } - - /** - * Set default values for this ray. - */ - public void setDefault() { - distance = 0; - prevMaterial = Air.INSTANCE; - currentMaterial = Air.INSTANCE; - depth = 0; - color.set(0, 0, 0, 0); - specular = true; - } - - /** - * Copy state from another ray. - */ - public void set(Ray other) { - prevMaterial = other.prevMaterial; - currentMaterial = other.currentMaterial; - depth = other.depth; - distance = 0; - o.set(other.o); - d.set(other.d); - n.set(other.n); - geomN.set(other.geomN); - color.set(0, 0, 0, 0); - specular = other.specular; - } - - /** - * Initialize a ray with origin and direction. - * - * @param o origin - * @param d direction - */ - public final void set(Vector3 o, Vector3 d) { - setDefault(); - this.o.set(o); - this.d.set(d); - } - - /** - * Find the exit point from the given block for this ray. This marches the ray forward - i.e. - * updates ray origin directly. - * - * @param bx block x coordinate - * @param by block y coordinate - * @param bz block z coordinate - */ - public final void exitBlock(int bx, int by, int bz) { - int nx = 0; - int ny = 0; - int nz = 0; - double tNext = Double.POSITIVE_INFINITY; - double t = (bx - o.x) / d.x; - if (t > Ray.EPSILON) { - tNext = t; - nx = 1; - ny = nz = 0; - } else { - t = ((bx + 1) - o.x) / d.x; - if (t < tNext && t > Ray.EPSILON) { - tNext = t; - nx = -1; - ny = nz = 0; - } - } - - t = (by - o.y) / d.y; - if (t < tNext && t > Ray.EPSILON) { - tNext = t; - ny = 1; - nx = nz = 0; - } else { - t = ((by + 1) - o.y) / d.y; - if (t < tNext && t > Ray.EPSILON) { - tNext = t; - ny = -1; - nx = nz = 0; - } - } - - t = (bz - o.z) / d.z; - if (t < tNext && t > Ray.EPSILON) { - tNext = t; - nz = 1; - nx = ny = 0; - } else { - t = ((bz + 1) - o.z) / d.z; - if (t < tNext && t > Ray.EPSILON) { - tNext = t; - nz = -1; - nx = ny = 0; - } - } - - o.scaleAdd(tNext, d); - n.set(nx, ny, nz); - geomN.set(nx, ny, nz); - distance += tNext; - } - - /** - * @return foliage color for the current block - */ - public float[] getBiomeFoliageColor(Scene scene) { - return scene.getFoliageColor((int) (o.x + d.x * OFFSET), (int) (o.y + d.y * OFFSET), (int) (o.z + d.z * OFFSET)); - } - - /** - * @return foliage color for the current block - */ - public float[] getBiomeDryFoliageColor(Scene scene) { - return scene.getDryFoliageColor((int) (o.x + d.x * OFFSET), (int) (o.y + d.y * OFFSET), (int) (o.z + d.z * OFFSET)); - } - - /** - * @return grass color for the current block - */ - public float[] getBiomeGrassColor(Scene scene) { - return scene.getGrassColor((int) (o.x + d.x * OFFSET), (int) (o.y + d.y * OFFSET), (int) (o.z + d.z * OFFSET)); - } - - /** - * @return water color for the current block - */ - public float[] getBiomeWaterColor(Scene scene) { - return scene.getWaterColor((int) (o.x + d.x * OFFSET), (int) (o.y + d.y * OFFSET), (int) (o.z + d.z * OFFSET)); - } - - /** - * Set this ray to a random diffuse reflection of the input ray. - */ - public final void diffuseReflection(Ray ray, Random random, Scene scene) { - - set(ray); - - // get random point on unit disk - double x1 = random.nextDouble(); - double x2 = random.nextDouble(); - double r = FastMath.sqrt(x1); - double theta = 2 * Math.PI * x2; - - // project to point on hemisphere in tangent space - double tx = r * FastMath.cos(theta); - double ty = r * FastMath.sin(theta); - double tz; // to be initialized later, after potentially changing tx and ty - - // importance sampling, see PR #1604 - // https://github.com/chunky-dev/chunky/pull/1604/ - if(scene.getSunSamplingStrategy().isImportanceSampling()) { - - // constants - final double sun_az = scene.sun().getAzimuth(); - final double sun_alt_fake = scene.sun().getAltitude(); - final double sun_alt = Math.abs(sun_alt_fake) > Math.PI / 2 ? Math.signum(sun_alt_fake) * Math.PI - sun_alt_fake : sun_alt_fake; - final double sun_dx = FastMath.cos(sun_az)*FastMath.cos(sun_alt); - final double sun_dz = FastMath.sin(sun_az)*FastMath.cos(sun_alt); - final double sun_dy = FastMath.sin(sun_alt); - - // determine the sun direction in tangent space - // since we know the sun's direction in world space easily, we must reverse the algebra done later in this method - // (I calculated the inverse matrix by hand and it was not fun) - double sun_tx, sun_ty, sqrt; - double sun_tz = sun_dx*n.x + sun_dy*n.y + sun_dz*n.z; - if(QuickMath.abs(n.x) > .1) { - sun_tx = sun_dx * n.z - sun_dz * n.x; - sun_ty = sun_dx * n.x * n.y - sun_dy * (n.x * n.x + n.z * n.z) + sun_dz * n.y * n.z; - sqrt = FastMath.hypot(n.x, n.z); - } else { - sun_tx = sun_dz * n.y - sun_dy * n.z; - sun_ty = sun_dy * n.x * n.y - sun_dx * (n.y * n.y + n.z * n.z) + sun_dz * n.x * n.z; - sqrt = FastMath.hypot(n.z, n.y); - } - sun_tx /= sqrt; - sun_ty /= sqrt; - double circle_radius = scene.sun().getSunRadius() * scene.sun().getImportanceSampleRadius(); - double sample_chance = scene.sun().getImportanceSampleChance(); - double sun_alt_relative = FastMath.asin(sun_tz); - // check if there is any chance of the sun being visible - if(sun_alt_relative + circle_radius > Ray.EPSILON) { - // if the sun is not at too shallow of an angle, then sample a circular region - if(FastMath.hypot(sun_tx, sun_ty) + circle_radius + Ray.EPSILON < 1) { - if (random.nextDouble() < sample_chance) { - // sun sampling - tx = sun_tx + tx * circle_radius; - ty = sun_ty + ty * circle_radius; - // diminish the contribution of the ray based on the circle area and the sample chance - ray.color.scale(circle_radius * circle_radius / sample_chance); - } else { - // non-sun sampling - // now, rather than guaranteeing that the ray is cast within a circle, instead guarantee that it does not - while (FastMath.hypot(tx - sun_tx, ty - sun_ty) < circle_radius) { - tx -= sun_tx; - ty -= sun_ty; - // avoid very unlikely infinite loop - if (tx == 0 && ty == 0) { - break; - } - tx /= circle_radius; - ty /= circle_radius; - } - // correct for the fact that we are now undersampling everything but the sun - ray.color.scale((1 - circle_radius * circle_radius) / (1 - sample_chance)); - } - } else { - // the sun is at a shallow angle, so instead we're using a "rectangular-ish segment" - // it is important that we sample from a shape which we can easily calculate the area of - double minr = FastMath.cos(sun_alt_relative + circle_radius); - double maxr = FastMath.cos(FastMath.max(sun_alt_relative - circle_radius, 0)); - double sun_theta = FastMath.atan2(sun_ty, sun_tx); - double segment_area_proportion = ((maxr * maxr - minr * minr) * circle_radius) / Math.PI; - sample_chance *= segment_area_proportion / (circle_radius * circle_radius); - sample_chance = FastMath.min(sample_chance, Sun.MAX_IMPORTANCE_SAMPLE_CHANCE); - if(random.nextDouble() < sample_chance) { - // sun sampling - r = FastMath.sqrt(minr * minr * x1 + maxr * maxr * (1 - x1)); - theta = sun_theta + (2 * x2 - 1) * circle_radius; - tx = r * FastMath.cos(theta); - ty = r * FastMath.sin(theta); - // diminish the contribution of the ray based on the segment area and the sample chance - ray.color.scale(segment_area_proportion / sample_chance); - } else { - // non-sun sampling - // basically, if we are going to sample the sun segment, reset the rng until we don't - while(r > minr && r < maxr && angleDistance(theta, sun_theta) < circle_radius) { - x1 = random.nextDouble(); - x2 = random.nextDouble(); - r = FastMath.sqrt(x1); - theta = 2 * Math.PI * x2; - } - tx = r * FastMath.cos(theta); - ty = r * FastMath.sin(theta); - // correct for the fact that we are now undersampling everything but the sun - ray.color.scale((1 - segment_area_proportion) / (1 - sample_chance)); - } - } - } - } - - tz = FastMath.sqrt(1 - tx*tx - ty*ty); - - // transform from tangent space to world space - double xx, xy, xz; - double ux, uy, uz; - double vx, vy, vz; - - if (QuickMath.abs(n.x) > .1) { - xx = 0; - xy = 1; - xz = 0; - } else { - xx = 1; - xy = 0; - xz = 0; - } - - ux = xy * n.z - xz * n.y; - uy = xz * n.x - xx * n.z; - uz = xx * n.y - xy * n.x; - - r = 1 / FastMath.sqrt(ux * ux + uy * uy + uz * uz); - - ux *= r; - uy *= r; - uz *= r; - - vx = uy * n.z - uz * n.y; - vy = uz * n.x - ux * n.z; - vz = ux * n.y - uy * n.x; - - d.x = ux * tx + vx * ty + n.x * tz; - d.y = uy * tx + vy * ty + n.y * tz; - d.z = uz * tx + vz * ty + n.z * tz; - - o.scaleAdd(Ray.OFFSET, d); - currentMaterial = prevMaterial; - specular = false; - - // See specularReflection for explanation of why this is needed - if(QuickMath.signum(geomN.dot(d)) == QuickMath.signum(geomN.dot(ray.d))) { - double factor = QuickMath.signum(geomN.dot(ray.d)) * -Ray.EPSILON - d.dot(geomN); - d.scaleAdd(factor, geomN); - d.normalize(); - } - } - private double angleDistance(double a1, double a2) { - double diff = Math.abs(a1 - a2) % (2*Math.PI); - return diff > Math.PI ? 2*Math.PI - diff : diff; - } - - /** - * Set this ray to the specular reflection of the input ray. - */ - public final void specularReflection(Ray ray, Random random) { - set(ray); - currentMaterial = prevMaterial; - - double roughness = ray.getCurrentMaterial().roughness; - if (roughness > Ray.EPSILON) { - // For rough specular reflections, we interpolate linearly between the diffuse ray direction and the specular direction, - // which is inspired by https://blog.demofox.org/2020/06/06/casual-shadertoy-path-tracing-2-image-improvement-and-glossy-reflections/ - // This gives good-looking results, although a microfacet-based model would be more physically correct. - - // 1. get specular reflection direction - Vector3 specularDirection = new Vector3(d); - specularDirection.scaleAdd(-2 * ray.d.dot(ray.n), ray.n, ray.d); - - // 2. get diffuse reflection direction (stored in this.d) - // get random point on unit disk - double x1 = random.nextDouble(); - double x2 = random.nextDouble(); - double r = FastMath.sqrt(x1); - double theta = 2 * Math.PI * x2; - - // project to point on hemisphere in tangent space - double tx = r * FastMath.cos(theta); - double ty = r * FastMath.sin(theta); - double tz = FastMath.sqrt(1 - x1); - - // transform from tangent space to world space - double xx, xy, xz; - double ux, uy, uz; - double vx, vy, vz; - - if (QuickMath.abs(n.x) > .1) { - xx = 0; - xy = 1; - xz = 0; - } else { - xx = 1; - xy = 0; - xz = 0; - } - - ux = xy * n.z - xz * n.y; - uy = xz * n.x - xx * n.z; - uz = xx * n.y - xy * n.x; - - r = 1 / FastMath.sqrt(ux * ux + uy * uy + uz * uz); - - ux *= r; - uy *= r; - uz *= r; - - vx = uy * n.z - uz * n.y; - vy = uz * n.x - ux * n.z; - vz = ux * n.y - uy * n.x; - - d.x = ux * tx + vx * ty + n.x * tz; - d.y = uy * tx + vy * ty + n.y * tz; - d.z = uz * tx + vz * ty + n.z * tz; - - // 3. scale d to be roughness * dDiffuse + (1 - roughness) * dSpecular - d.scale(roughness); - d.scaleAdd(1 - roughness, specularDirection); - d.normalize(); - o.scaleAdd(0.00001, d); - } else { - // roughness is zero, do a specular reflection - d.scaleAdd(-2 * ray.d.dot(ray.n), ray.n, ray.d); - o.scaleAdd(0.00001, ray.n); - } - - // After reflection, the dot product between the direction and the real surface normal - // should have the opposite sign as the dot product between the incoming direction - // and the normal (because the incoming is going toward the volume enclosed - // by the surface and the reflected ray is going away) - // If this is not the case, we need to fix that - if(QuickMath.signum(geomN.dot(d)) == QuickMath.signum(geomN.dot(ray.d))) { - // The reflected ray goes is going through the geometry, - // we need to alter its direction so it doesn't. - // The way we do that is by adding the geometry normal multiplied by some factor - // The factor can be determined by projecting the direction on the normal, - // ie doing a dot product because, for every unit vector d and n, - // we have the relation: - // `(d - d.n * n) . n = 0` - // This tells us that if we chose `-d.n` as the factor we would have a dot product - // equals to 0, as we want something positive or negative, - // we will use the factor `-d.n +/- epsilon` - double factor = QuickMath.signum(geomN.dot(ray.d)) * -Ray.EPSILON - d.dot(geomN); - d.scaleAdd(factor, geomN); - d.normalize(); - } - } - - /** - * Scatter ray normal - * - * @param random random number source - */ - public final void scatterNormal(Random random) { - // get random point on unit disk - double x1 = random.nextDouble(); - double x2 = random.nextDouble(); - double r = FastMath.sqrt(x1); - double theta = 2 * Math.PI * x2; - - // project to point on hemisphere in tangent space - double tx = r * FastMath.cos(theta); - double ty = r * FastMath.sin(theta); - double tz = FastMath.sqrt(1 - x1); - - // transform from tangent space to world space - double xx, xy, xz; - double ux, uy, uz; - double vx, vy, vz; - - if (QuickMath.abs(n.x) > .1) { - xx = 0; - xy = 1; - xz = 0; - } else { - xx = 1; - xy = 0; - xz = 0; - } - - ux = xy * n.z - xz * n.y; - uy = xz * n.x - xx * n.z; - uz = xx * n.y - xy * n.x; - - r = 1 / FastMath.sqrt(ux * ux + uy * uy + uz * uz); - - ux *= r; - uy *= r; - uz *= r; - - vx = uy * n.z - uz * n.y; - vy = uz * n.x - ux * n.z; - vz = ux * n.y - uy * n.x; - - n.set(ux * tx + vx * ty + n.x * tz, uy * tx + vy * ty + n.y * tz, uz * tx + vz * ty + n.z * tz); + public void setDiffuse(boolean value) { + this.flags = value ? this.flags | DIFFUSE : this.flags & ~DIFFUSE; } - public void setPrevMaterial(Material mat, int data) { - this.prevMaterial = mat; - this.prevData = data; + public void setSpecular(boolean value) { + this.flags = value ? this.flags | SPECULAR : this.flags & ~SPECULAR; } - public void setCurrentMaterial(Material mat) { - this.currentMaterial = mat; - if (mat instanceof Water) { - this.currentData = ((Water) mat).data; - } else if (mat instanceof Lava) { - this.currentData = ((Lava) mat).data; - } else { - this.currentData = 0; - } + public void setIndirect(boolean value) { + this.flags = value ? this.flags | INDIRECT : this.flags & ~INDIRECT; } - public void setCurrentMaterial(Material mat, int data) { - this.currentMaterial = mat; - this.currentData = data; + public boolean isDiffuse() { + return (this.flags & DIFFUSE) != 0; } - public Material getPrevMaterial() { - return prevMaterial; + public boolean isSpecular() { + return (this.flags & SPECULAR) != 0; } - public Material getCurrentMaterial() { - return currentMaterial; + public boolean isIndirect() { + return (this.flags & INDIRECT) != 0; } - /** - * Get the data of the previous block. This used to contain the block data but as of Chunky 2, - * every block variant gets its own Block instance and this field is only used for water and lava - * levels. - * - * @return Data of the previous block (if water or lava), 0 otherwise - */ - public int getPrevData() { - return prevData; - } + public Ray() {} - /** - * Get the data of the current block. This used to contain the block data but as of Chunky 2, - * every block variant gets its own Block instance and this field is only used for water and lava - * levels. - * - * @return Data of the current block (if water or lava), 0 otherwise - */ - public int getCurrentData() { - return currentData; + public Ray(Ray ray) { + o.set(ray.o); + d.set(ray.d); + currentMedium = ray.currentMedium; + flags = ray.flags; } - /** - * Get the normal of the previously hit surface. - */ - public Vector3 getNormal() { - return n; - } - /** - * Get the geometric normal of the previously hit surface. When using normal maps, - * this is the normal of the geometry that was hit, not taking the normal map into account. - */ - public Vector3 getGeometryNormal() { - return geomN; + public Ray(Vector3 origin, Vector3 direction) { + o.set(origin); + d.set(direction); } - /** - * Set the geometry normal (not taking normal mapping into account) - * and the shading normal (taking normal mapping into account) - */ - public void setNormal(double x, double y, double z) { - n.set(x, y, z); - geomN.set(x, y, z); + public void set(Ray other) { + o.set(other.o); + d.set(other.d); + currentMedium = other.currentMedium; + flags = other.flags; } - /** - * Set the geometry normal (not taking normal mapping into account) - * and the shading normal (taking normal mapping into account) - */ - public void setNormal(Vector3 newN) { - n.set(newN); - geomN.set(newN); + public void reset() { + o.set(0); + d.set(0); + currentMedium = Air.INSTANCE; + flags = 0; } - /** - * Sets n to the given value and optionally flip it so the normal - * is in the opposite direction as the ray direction (dot(d, n) < 0) - */ - public void orientNormal(Vector3 normal) { - if(d.dot(normal) > 0) { - n.set(-normal.x, -normal.y, -normal.z); - } else { - n.set(normal); - } - geomN.set(n); + public void clearReflectionFlags() { + flags &= ~DIFFUSE & ~SPECULAR; } - /** - * Set the shading normal (taking normal mapping into account) - */ - public void setShadingNormal(double x, double y, double z) { - n.set(x, y, z); + public Material getCurrentMedium() { + return currentMedium; } - public void invertNormal() { - n.scale(-1); - geomN.scale(-1); + public void setCurrentMedium(Material material) { + this.currentMedium = material; } } diff --git a/chunky/src/java/se/llbit/math/SimpleSphere.java b/chunky/src/java/se/llbit/math/SimpleSphere.java new file mode 100644 index 0000000000..fedb0cc5cc --- /dev/null +++ b/chunky/src/java/se/llbit/math/SimpleSphere.java @@ -0,0 +1,57 @@ +package se.llbit.math; + +import org.apache.commons.math3.util.FastMath; + +public class SimpleSphere { + private final double radius; + private final Vector3 center; + + public SimpleSphere(Vector3 center, double radius) { + this.center = new Vector3(center); + this.radius = radius; + } + + public boolean isInside(Vector3 point) { + double distance = this.center.rSub(point).length(); + return distance < this.radius; + } + + public double intersect(Ray ray) { + double t0; + double t1; + + double radiusSquared = radius * radius; + + Vector3 l = center.rSub(ray.o); + double tca = ray.d.dot(l); + if (tca < 0.0 && l.length() > radius) { + return -1; + } + + double d2 = l.dot(l) - tca * tca; + if (d2 > radiusSquared) { + return -1; + } + double thc = FastMath.sqrt(radiusSquared - d2); + t0 = tca - thc; + t1 = tca + thc; + + if (t0 > t1) { + double tmp = t0; + t0 = t1; + t1 = tmp; + } + + if (t0 < 0) { + t0 = t1; + if (t0 < 0) { + return -1; + } + } + if (t0 > Constants.EPSILON) { + return t0; + } + + return -1; + } +} diff --git a/chunky/src/java/se/llbit/math/SimplexNoise.java b/chunky/src/java/se/llbit/math/SimplexNoise.java index 6d7fe56c9f..a040ed6ab8 100644 --- a/chunky/src/java/se/llbit/math/SimplexNoise.java +++ b/chunky/src/java/se/llbit/math/SimplexNoise.java @@ -26,6 +26,8 @@ package se.llbit.math; +import java.util.Random; + /** * A Simplex noise generator with true analytic derivative in 3D, based on sdnoise1234. * @@ -43,7 +45,7 @@ public final class SimplexNoise public float ddz; // 3D simplex noise - public final float calculate(float x, float y, float z) + public float calculate(float x, float y, float z) { // Skewing factors for 3D simplex grid: // F3 = 1/3 @@ -245,17 +247,17 @@ else if (x0 < z0) // Helper functions to compute gradients in 1D to 4D // and gradients-dot-residualvectors in 2D to 4D. - static final float grad3x(int hash) + static float grad3x(int hash) { return grad3lut[hash & 15][0]; } - static final float grad3y(int hash) + static float grad3y(int hash) { return grad3lut[hash & 15][1]; } - static final float grad3z(int hash) + static float grad3z(int hash) { return grad3lut[hash & 15][2]; } @@ -275,34 +277,51 @@ static final float grad3z(int hash) { 1, 0, 1 }, {-1, 0, 1 }, { 0, 1,-1 }, { 0,-1,-1 } // 4 repeats to make 16 }; + public SimplexNoise(long seed) { + Random random = new Random(seed); + perm = new int[512]; + for (int i = 0; i < 512 / 2; i++) { + perm[i] = i; + } + for (int i = 255; i > 0; i--) { + int index = random.nextInt(i + 1); + int a = perm[index]; + perm[index] = perm[index + 256] = perm[i]; + perm[i] = perm[i + 256] = a; + } + } + + public SimplexNoise() { + perm = new int[] { + 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142, + 8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203, + 117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74, + 165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220, + 105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132, + 187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3, + 64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227, + 47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221, + 153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185, + 112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51, + 145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121, + 50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78, + 66,215,61,156,180,151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225, + 140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197, + 62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136, + 171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60, + 211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80, + 73,209,76,132,187,208, 89,18,169,200,196,135,130,116,188,159,86,164,100,109, + 198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212, + 207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44, + 154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113, + 224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191, + 179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184, + 84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72, + 243,141,128,195,78,66,215,61,156,180 + }; + } + // Permutation table. This is just a random jumble of all numbers 0-255, // repeated twice to avoid wrapping the index at 255 for each lookup. - static final int [] perm = - { - 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142, - 8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203, - 117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74, - 165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220, - 105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132, - 187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3, - 64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227, - 47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221, - 153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185, - 112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51, - 145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121, - 50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78, - 66,215,61,156,180,151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225, - 140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197, - 62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136, - 171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60, - 211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80, - 73,209,76,132,187,208, 89,18,169,200,196,135,130,116,188,159,86,164,100,109, - 198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212, - 207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44, - 154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113, - 224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191, - 179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184, - 84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72, - 243,141,128,195,78,66,215,61,156,180 - }; + private final int[] perm; } \ No newline at end of file diff --git a/chunky/src/java/se/llbit/math/Triangle.java b/chunky/src/java/se/llbit/math/Triangle.java index ce6eca7c67..d75b3cb522 100644 --- a/chunky/src/java/se/llbit/math/Triangle.java +++ b/chunky/src/java/se/llbit/math/Triangle.java @@ -82,18 +82,18 @@ public Triangle(Triangle other, Vector3 offset) { * * @return true if the ray intersects the triangle */ - public boolean intersect(Ray ray) { - double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Ray.OFFSET); - double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Ray.OFFSET); - double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Ray.OFFSET); + public boolean intersect(Ray ray, IntersectionRecord intersectionRecord) { + double ix = ray.o.x - QuickMath.floor(ray.o.x + ray.d.x * Constants.OFFSET); + double iy = ray.o.y - QuickMath.floor(ray.o.y + ray.d.y * Constants.OFFSET); + double iz = ray.o.z - QuickMath.floor(ray.o.z + ray.d.z * Constants.OFFSET); // test that the ray is heading toward the plane double denom = ray.d.dot(n); - if (QuickMath.abs(denom) > Ray.EPSILON) { + if (QuickMath.abs(denom) > Constants.EPSILON) { // test for intersection with the plane at origin double t = -(ix * n.x + iy * n.y + iz * n.z + d) / denom; - if (t > -Ray.EPSILON && t < ray.t) { + if (t > -Constants.EPSILON && t < intersectionRecord.distance) { // plane intersection confirmed // translate to get hit point relative to the triangle origin @@ -106,9 +106,9 @@ public boolean intersect(Ray ray) { double si = (uv * wv - vv * wu) / (uv2 - uu * vv); double ti = (uv * wu - uu * wv) / (uv2 - uu * vv); if ((si >= 0) && (ti >= 0) && (si + ti <= 1)) { - ray.tNext = t; - ray.u = si; - ray.v = ti; + intersectionRecord.distance = t; + intersectionRecord.uv.x = si; + intersectionRecord.uv.y = ti; return true; } } diff --git a/chunky/src/java/se/llbit/math/Vector2.java b/chunky/src/java/se/llbit/math/Vector2.java index 420892f1f8..7c4584a258 100644 --- a/chunky/src/java/se/llbit/math/Vector2.java +++ b/chunky/src/java/se/llbit/math/Vector2.java @@ -31,7 +31,7 @@ public class Vector2 { public double x, y; /** - * Creates a new vector (0, 0, 0) + * Creates a new vector (0, 0) */ public Vector2() { this(0, 0); @@ -184,4 +184,12 @@ public JsonValue toJson() { object.add("y", y); return object; } + + /** + * Unmarshals a vector from JSON. + */ + public void fromJson(JsonObject object) { + x = object.get("x").doubleValue(0); + y = object.get("y").doubleValue(0); + } } diff --git a/chunky/src/java/se/llbit/math/Vector3.java b/chunky/src/java/se/llbit/math/Vector3.java index 5536e5b477..f59c205819 100644 --- a/chunky/src/java/se/llbit/math/Vector3.java +++ b/chunky/src/java/se/llbit/math/Vector3.java @@ -19,14 +19,17 @@ import org.apache.commons.math3.util.FastMath; import se.llbit.json.JsonObject; +import java.util.Random; + /** * A 3D vector of doubles. * * @author Jesper Öqvist */ public class Vector3 { - - public double x, y, z; + public double x; + public double y; + public double z; /** * Creates a new vector (0, 0, 0). @@ -53,6 +56,27 @@ public Vector3(Vector3 o) { z = o.z; } + public Vector3(double v) { + x = v; + y = v; + z = v; + } + + public static Vector3 randomUnitVector(Random random) { + Vector3 randomUnitVector = new Vector3(); + while (true) { + randomUnitVector.set(random.nextDouble(-1, 1), random.nextDouble(-1, 1), random.nextDouble(-1, 1)); + if (randomUnitVector.lengthSquared() < 1) { + randomUnitVector.normalize(); + return randomUnitVector; + } + } + } + + public boolean nearZero() { + return (FastMath.abs(x) < Constants.EPSILON) && (FastMath.abs(y) < Constants.EPSILON) && (FastMath.abs(z) < Constants.EPSILON); + } + /** * Set this vector equal to other vector. */ @@ -62,6 +86,15 @@ public final void set(Vector3 o) { z = o.z; } + /** + * Set this vector equal to a. + */ + public final void set(Vector3i o) { + x = o.x; + y = o.y; + z = o.z; + } + /** * Set this vector equal to (d, e, f). */ @@ -71,6 +104,13 @@ public final void set(double d, double e, double f) { z = f; } + public final void set(double a) { + x = a; + y = a; + z = a; + } + + /** * @return The dot product of this vector and o vector */ @@ -78,15 +118,6 @@ public final double dot(Vector3 o) { return x * o.x + y * o.y + z * o.z; } - /** - * Set this vector equal to a-b. - */ - public final void sub(Vector3 a, Vector3 b) { - x = a.x - b.x; - y = a.y - b.y; - z = a.z - b.z; - } - /** * @return The length of this vector, squared */ @@ -129,6 +160,11 @@ public final void normalize() { z *= s; } + public final Vector3 normalized() { + double s = 1 / FastMath.sqrt(lengthSquared()); + return new Vector3(x * s, y * s, z * s); + } + /** * Set this vector equal to s*d + o. */ @@ -138,6 +174,10 @@ public final void scaleAdd(double s, Vector3 d, Vector3 o) { z = s * d.z + o.z; } + public final Vector3 rScaleAdd(double s, Vector3 d, Vector3 o) { + return new Vector3(s * d.x + o.x, s * d.y + o.y, s * d.z + o.z); + } + /** * Add s*d to this vector. */ @@ -147,6 +187,10 @@ public final void scaleAdd(double s, Vector3 d) { z += s * d.z; } + public final Vector3 rScaleAdd(double s, Vector3 d) { + return new Vector3(x + s * d.x, y + s * d.y, z + s * d.z); + } + /** * Scale this vector by s. */ @@ -156,6 +200,20 @@ public final void scale(double s) { z *= s; } + public final Vector3 rScale(double s) { + return new Vector3(x * s, y * s, z * s); + } + + public final Vector3 rMultiplyEntrywise(Vector3 other) { + return new Vector3(this.x * other.x, this.y * other.y, this.z * other.z); + } + + public final void multiplyEntrywise(Vector3 other) { + this.x *= other.x; + this.y *= other.y; + this.z *= other.z; + } + /** * Scale each component of this vector by v */ @@ -174,6 +232,10 @@ public final void add(Vector3 a, Vector3 b) { z = a.z + b.z; } + public final Vector3 rAdd(Vector3 a, Vector3 b) { + return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); + } + /** * Add a to this vector. */ @@ -183,6 +245,10 @@ public final void add(Vector3 a) { z += a.z; } + public final Vector3 rAdd(Vector3 a) { + return new Vector3(x + a.x, y + a.y, z + a.z); + } + /** * Add a to this vector. */ @@ -192,6 +258,10 @@ public final void add(Vector3i a) { z += a.z; } + public final Vector3 rAdd(Vector3i a) { + return new Vector3(x + a.x, y + a.y, z + a.z); + } + /** * Add vector (a, b, c) to this vector. */ @@ -201,6 +271,26 @@ public final void add(double a, double b, double c) { z += c; } + public final Vector3 rAdd(double a, double b, double c) { + return new Vector3(x + a, y + b, z + c); + } + + /** + * Set this vector equal to a-b. + */ + public final void sub(Vector3 a, Vector3 b) { + x = a.x - b.x; + y = a.y - b.y; + z = a.z - b.z; + } + + /** + * @return difference of vector a and b. + */ + public final Vector3 rSub(Vector3 a, Vector3 b) { + return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); + } + /** * Subtract a from this vector. */ @@ -210,13 +300,8 @@ public final void sub(Vector3 a) { z -= a.z; } - /** - * Subtract vector (a, b, c) from this vector. - */ - public final void sub(double a, double b, double c) { - x -= a; - y -= b; - z -= c; + public final Vector3 rSub(Vector3 a) { + return new Vector3(x - a.x, y - a.y, z - a.z); } /** @@ -228,13 +313,25 @@ public final void sub(Vector3i a) { z -= a.z; } + public final Vector3 rSub(Vector3i a) { + return new Vector3(x - a.x, y - a.y, z - a.z); + } + /** - * Set this vector equal to a. + * Subtract vector (a, b, c) from this vector. */ - public void set(Vector3i a) { - x = a.x; - y = a.y; - z = a.z; + public final void sub(double a, double b, double c) { + x -= a; + y -= b; + z -= c; + } + + public final Vector3 rSub(double a, double b, double c) { + return new Vector3(x - a, y - b, z - c); + } + + public final double distance(Vector3 other) { + return this.rSub(other).length(); } @Override public String toString() { @@ -262,4 +359,11 @@ public JsonObject toJson() { object.add("z", z); return object; } + + public static Vector3 orientNormal(Vector3 direction, Vector3 normal) { + if(direction.dot(normal) > 0) { + return new Vector3(-normal.x, -normal.y, -normal.z); + } + return normal; + } } diff --git a/chunky/src/java/se/llbit/math/Vector4.java b/chunky/src/java/se/llbit/math/Vector4.java index 77919c99d8..3ef3329603 100644 --- a/chunky/src/java/se/llbit/math/Vector4.java +++ b/chunky/src/java/se/llbit/math/Vector4.java @@ -48,6 +48,13 @@ public Vector4(double i, double j, double k, double l) { w = l; } + public Vector4(double i) { + x = i; + y = i; + z = i; + w = i; + } + /** * Set the vector equal to other vector. */ @@ -68,6 +75,16 @@ public final void set(double i, double j, double k, double l) { w = l; } + /** + * Set the vector. + */ + public final void set(double a) { + x = a; + y = a; + z = a; + w = a; + } + /** * Scale the vector. */ diff --git a/chunky/src/java/se/llbit/math/bvh/BVH.java b/chunky/src/java/se/llbit/math/bvh/BVH.java index 2f19317a40..38768955b3 100644 --- a/chunky/src/java/se/llbit/math/bvh/BVH.java +++ b/chunky/src/java/se/llbit/math/bvh/BVH.java @@ -17,8 +17,10 @@ */ package se.llbit.math.bvh; -import se.llbit.chunky.entity.Entity; import se.llbit.chunky.plugin.PluginApi; +import se.llbit.chunky.renderer.HasPrimitives; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; import se.llbit.log.Log; import se.llbit.math.Intersectable; import se.llbit.math.Ray; @@ -28,25 +30,26 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Random; /** * Bounding Volume Hierarchy based on AABBs. */ public interface BVH extends Intersectable { - BVH EMPTY = ray -> false; + BVH EMPTY = (ray, intersectionRecord, scene, random) -> false; /** - * Find closest intersection between the ray and any object in the BVH + * Find the closest intersection between the ray and any object in the BVH * * @return {@code true} if there exists any intersection */ @Override - boolean closestIntersection(Ray ray); + boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random); final class Factory { public interface BVHBuilder { - BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task); + BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task); String getName(); String getDescription(); @@ -95,7 +98,7 @@ public static void addBVHBuilder(BVHBuilder builder) { * Construct a new BVH containing the given entities. This will generate the BVH using the * persistent BVH method (default is SAH_MA). */ - public static BVH create(String implementation, Collection entities, Vector3 worldOffset, TaskTracker.Task task) { + public static BVH create(String implementation, Collection entities, Vector3 worldOffset, TaskTracker.Task task) { if (entities.isEmpty()) { return BVH.EMPTY; } else { diff --git a/chunky/src/java/se/llbit/math/bvh/BinaryBVH.java b/chunky/src/java/se/llbit/math/bvh/BinaryBVH.java index 81287ee9d9..ec3f7fe360 100644 --- a/chunky/src/java/se/llbit/math/bvh/BinaryBVH.java +++ b/chunky/src/java/se/llbit/math/bvh/BinaryBVH.java @@ -20,15 +20,18 @@ import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntStack; import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; import se.llbit.math.AABB; import se.llbit.math.Ray; import se.llbit.math.primitive.Primitive; import java.util.ArrayList; import java.util.Comparator; +import java.util.Random; import java.util.Stack; -import static se.llbit.math.Ray.OFFSET; +import static se.llbit.math.Constants.OFFSET; /** * An abstract class for BinaryBVHs. This provides helper methods for packing a node based BVH into a more compact @@ -212,7 +215,7 @@ private void packAabb(AABB box, IntArrayList data) { * @return {@code true} if there exists any intersection */ @Override - public boolean closestIntersection(Ray ray) { + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { boolean hit = false; int currentNode = 0; IntStack nodesToVisit = new IntArrayList(depth/2); @@ -226,7 +229,7 @@ public boolean closestIntersection(Ray ray) { // Is leaf int primIndex = -packed[currentNode]; for (Primitive primitive : packedPrimitives[primIndex]) { - hit = primitive.intersect(ray) | hit; + hit = primitive.closestIntersection(ray, intersectionRecord, scene, random) | hit; } if (nodesToVisit.isEmpty()) break; @@ -244,14 +247,14 @@ public boolean closestIntersection(Ray ray) { Float.intBitsToFloat(packed[offset+5]), Float.intBitsToFloat(packed[offset+6]), rx, ry, rz); - if (t1 > ray.t | t1 == -1) { - if (t2 > ray.t | t2 == -1) { + if (t1 > intersectionRecord.distance | t1 == -1) { + if (t2 > intersectionRecord.distance | t2 == -1) { if (nodesToVisit.isEmpty()) break; currentNode = nodesToVisit.popInt(); } else { currentNode = packed[currentNode]; } - } else if (t2 > ray.t | t2 == -1) { + } else if (t2 > intersectionRecord.distance | t2 == -1) { currentNode += 7; } else if (t1 < t2) { nodesToVisit.push(packed[currentNode]); diff --git a/chunky/src/java/se/llbit/math/bvh/MidpointBVH.java b/chunky/src/java/se/llbit/math/bvh/MidpointBVH.java index 537963285b..ca76777879 100644 --- a/chunky/src/java/se/llbit/math/bvh/MidpointBVH.java +++ b/chunky/src/java/se/llbit/math/bvh/MidpointBVH.java @@ -17,8 +17,8 @@ */ package se.llbit.math.bvh; -import se.llbit.chunky.entity.Entity; import se.llbit.chunky.main.Chunky; +import se.llbit.chunky.renderer.HasPrimitives; import se.llbit.log.Log; import se.llbit.math.AABB; import se.llbit.math.Vector3; @@ -32,13 +32,13 @@ public class MidpointBVH extends BinaryBVH { public static void registerImplementation() { Factory.addBVHBuilder(new Factory.BVHBuilder() { @Override - public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { + public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { task.update(1000, 0); double entityScaler = 500.0 / entities.size(); int done = 0; List primitives = new ArrayList<>(); - for (Entity entity : entities) { + for (HasPrimitives entity : entities) { primitives.addAll(entity.primitives(worldOffset)); done++; diff --git a/chunky/src/java/se/llbit/math/bvh/SahBVH.java b/chunky/src/java/se/llbit/math/bvh/SahBVH.java index 1e8c69e7e9..045ab12daf 100644 --- a/chunky/src/java/se/llbit/math/bvh/SahBVH.java +++ b/chunky/src/java/se/llbit/math/bvh/SahBVH.java @@ -17,8 +17,8 @@ */ package se.llbit.math.bvh; -import se.llbit.chunky.entity.Entity; import se.llbit.chunky.main.Chunky; +import se.llbit.chunky.renderer.HasPrimitives; import se.llbit.log.Log; import se.llbit.math.Vector3; import se.llbit.math.primitive.MutableAABB; @@ -32,13 +32,13 @@ public class SahBVH extends BinaryBVH { public static void registerImplementation() { Factory.addBVHBuilder(new Factory.BVHBuilder() { @Override - public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { + public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { task.update(1000, 0); double entityScaler = 500.0 / entities.size(); int done = 0; List primitives = new ArrayList<>(); - for (Entity entity : entities) { + for (HasPrimitives entity : entities) { primitives.addAll(entity.primitives(worldOffset)); done++; diff --git a/chunky/src/java/se/llbit/math/bvh/SahMaBVH.java b/chunky/src/java/se/llbit/math/bvh/SahMaBVH.java index c047893a6e..8c373b87b3 100644 --- a/chunky/src/java/se/llbit/math/bvh/SahMaBVH.java +++ b/chunky/src/java/se/llbit/math/bvh/SahMaBVH.java @@ -17,8 +17,8 @@ */ package se.llbit.math.bvh; -import se.llbit.chunky.entity.Entity; import se.llbit.chunky.main.Chunky; +import se.llbit.chunky.renderer.HasPrimitives; import se.llbit.log.Log; import se.llbit.math.AABB; import se.llbit.math.Vector3; @@ -33,13 +33,13 @@ public class SahMaBVH extends BinaryBVH { public static void registerImplementation() { Factory.addBVHBuilder(new Factory.BVHBuilder() { @Override - public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { + public BVH create(Collection entities, Vector3 worldOffset, TaskTracker.Task task) { task.update(1000, 0); double entityScaler = 500.0 / entities.size(); int done = 0; List primitives = new ArrayList<>(); - for (Entity entity : entities) { + for (HasPrimitives entity : entities) { primitives.addAll(entity.primitives(worldOffset)); done++; diff --git a/chunky/src/java/se/llbit/math/primitive/Box.java b/chunky/src/java/se/llbit/math/primitive/Box.java index 45bec12b62..7107139912 100644 --- a/chunky/src/java/se/llbit/math/primitive/Box.java +++ b/chunky/src/java/se/llbit/math/primitive/Box.java @@ -17,7 +17,10 @@ package se.llbit.math.primitive; import java.util.Collection; +import java.util.Random; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.IntersectionRecord; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.Material; import se.llbit.chunky.world.material.TextureMaterial; @@ -145,7 +148,7 @@ public void addBottomFaces(Collection primitives, Texture texture, Ve new Vector2(uv.y, uv.z), material)); } - @Override public boolean intersect(Ray ray) { + @Override public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { // TODO Auto-generated method stub return false; } diff --git a/chunky/src/java/se/llbit/math/primitive/MutableAABB.java b/chunky/src/java/se/llbit/math/primitive/MutableAABB.java index c45f385bfe..cbaa62884f 100644 --- a/chunky/src/java/se/llbit/math/primitive/MutableAABB.java +++ b/chunky/src/java/se/llbit/math/primitive/MutableAABB.java @@ -16,10 +16,15 @@ */ package se.llbit.math.primitive; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.Constants; +import se.llbit.math.IntersectionRecord; import se.llbit.math.AABB; import se.llbit.math.Ray; import se.llbit.math.Vector3; +import java.util.Random; + /** * Axis-Aligned Bounding Box. Does not compute intersection normals. * @@ -110,10 +115,10 @@ public boolean hitTest(Ray ray) { } } - return tNear < tFar + Ray.EPSILON && tFar > 0; + return tNear < tFar + Constants.EPSILON && tFar > 0; } - @Override public boolean intersect(Ray ray) { + @Override public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { double t1, t2; double tNear = Double.NEGATIVE_INFINITY; double tFar = Double.POSITIVE_INFINITY; @@ -173,8 +178,8 @@ public boolean hitTest(Ray ray) { } } - if (tNear < tFar + Ray.EPSILON && tNear >= 0 && tNear < ray.t) { - ray.tNext = tNear; + if (tNear < tFar + Constants.EPSILON && tNear >= 0 && tNear < intersectionRecord.distance) { + intersectionRecord.distance = tNear; return true; } else { return false; diff --git a/chunky/src/java/se/llbit/math/primitive/Primitive.java b/chunky/src/java/se/llbit/math/primitive/Primitive.java index 6e18c1e8d8..49aa2f35f7 100644 --- a/chunky/src/java/se/llbit/math/primitive/Primitive.java +++ b/chunky/src/java/se/llbit/math/primitive/Primitive.java @@ -16,22 +16,15 @@ */ package se.llbit.math.primitive; +import se.llbit.math.Intersectable; import se.llbit.math.AABB; -import se.llbit.math.Ray; /** * An intersectable primitive piece of geometry * * @author Jesper Öqvist */ -public interface Primitive { - - /** - * Intersect the ray with this geometry. - * - * @return {@code true} if there was an intersection - */ - boolean intersect(Ray ray); +public interface Primitive extends Intersectable { /** * @return axis-aligned bounding box for the primitive diff --git a/chunky/src/java/se/llbit/math/primitive/Sphere.java b/chunky/src/java/se/llbit/math/primitive/Sphere.java new file mode 100644 index 0000000000..4d6f7ae3df --- /dev/null +++ b/chunky/src/java/se/llbit/math/primitive/Sphere.java @@ -0,0 +1,91 @@ +package se.llbit.math.primitive; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.block.minecraft.Air; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.world.Material; +import se.llbit.math.*; + +import java.util.Random; + +public class Sphere implements Primitive { + private final double radius; + private final Vector3 center; + + public Material material; + + public Sphere(Vector3 center, double radius, Material material) { + this.center = new Vector3(center); + this.radius = radius; + this.material = material; + } + + public boolean isInside(Vector3 point) { + double distance = this.center.rSub(point).length(); + return distance < this.radius; + } + + public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { + double t0; + double t1; + + double radiusSquared = radius * radius; + + Vector3 l = center.rSub(ray.o); + double tca = ray.d.dot(l); + if (tca < 0.0 && l.length() > radius) { + return false; + } + + double d2 = l.dot(l) - tca * tca; + if (d2 > radiusSquared) { + return false; + } + double thc = FastMath.sqrt(radiusSquared - d2); + t0 = tca - thc; + t1 = tca + thc; + + if (t0 > t1) { + double tmp = t0; + t0 = t1; + t1 = tmp; + } + + if (t0 < 0) { + t0 = t1; + if (t0 < 0) { + return false; + } + } + if (t0 > Constants.EPSILON && t0 < intersectionRecord.distance) { + intersectionRecord.distance = t0; + Vector3 nHit = ray.o.rScaleAdd(t0, ray.d); + nHit.sub(center); + nHit.normalize(); + intersectionRecord.setNormal(nHit); + if (ray.d.dot(nHit) > 0) { + intersectionRecord.material = Air.INSTANCE; + intersectionRecord.n.scale(-1); + intersectionRecord.shadeN.scale(-1); + intersectionRecord.color.set(1, 1, 1, 0); + } else { + intersectionRecord.material = material; + material.getColor(intersectionRecord); + } + return true; + } + return false; + } + + @Override + public AABB bounds() { + return new AABB( + center.x - radius, + center.x + radius, + center.y - radius, + center.y + radius, + center.z - radius, + center.z + radius + ); + } +} diff --git a/chunky/src/java/se/llbit/math/primitive/TexturedTriangle.java b/chunky/src/java/se/llbit/math/primitive/TexturedTriangle.java index 146e74a4a0..6ca1fccf52 100644 --- a/chunky/src/java/se/llbit/math/primitive/TexturedTriangle.java +++ b/chunky/src/java/se/llbit/math/primitive/TexturedTriangle.java @@ -16,11 +16,11 @@ */ package se.llbit.math.primitive; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.*; import se.llbit.chunky.world.Material; -import se.llbit.math.AABB; -import se.llbit.math.Ray; -import se.llbit.math.Vector2; -import se.llbit.math.Vector3; + +import java.util.Random; /** * A simple triangle primitive. @@ -29,8 +29,6 @@ */ public class TexturedTriangle implements Primitive { - private static final double EPSILON = 0.000001; - /** Note: This is public for some plugins. Stability is not guaranteed. */ public final Vector3 e1 = new Vector3(0, 0, 0); public final Vector3 e2 = new Vector3(0, 0, 0); @@ -80,7 +78,7 @@ public TexturedTriangle(Vector3 c1, Vector3 c2, Vector3 c3, Vector2 t1, Vector2 bounds = AABB.bounds(c1, c2, c3); } - @Override public boolean intersect(Ray ray) { + @Override public boolean closestIntersection(Ray ray, IntersectionRecord intersectionRecord, Scene scene, Random random) { // Möller-Trumbore triangle intersection algorithm! Vector3 pvec = new Vector3(); Vector3 qvec = new Vector3(); @@ -89,10 +87,10 @@ public TexturedTriangle(Vector3 c1, Vector3 c2, Vector3 c3, Vector2 t1, Vector2 pvec.cross(ray.d, e2); double det = e1.dot(pvec); if (doubleSided) { - if (det > -EPSILON && det < EPSILON) { + if (det > -Constants.EPSILON && det < Constants.EPSILON) { return false; } - } else if (det > -EPSILON) { + } else if (det > -Constants.EPSILON) { return false; } double recip = 1 / det; @@ -115,18 +113,16 @@ public TexturedTriangle(Vector3 c1, Vector3 c2, Vector3 c3, Vector2 t1, Vector2 double t = e2.dot(qvec) * recip; - if (t > EPSILON && t < ray.t) { + if (t > Constants.EPSILON && t < intersectionRecord.distance) { double w = 1 - u - v; - ray.u = t1u * u + t2u * v + t3u * w; - ray.v = t1v * u + t2v * v + t3v * w; - float[] color = material.getColor(ray.u, ray.v); - if (color[3] > 0) { - ray.color.set(color); - ray.setCurrentMaterial(material); - ray.t = t; - ray.setNormal(n); - return true; - } + intersectionRecord.uv.x = t1u * u + t2u * v + t3u * w; + intersectionRecord.uv.y = t1v * u + t2v * v + t3v * w; + material.getColor(intersectionRecord); + intersectionRecord.material = material; + intersectionRecord.distance = t; + intersectionRecord.setNormal(Vector3.orientNormal(ray.d, n)); + intersectionRecord.setNoMediumChange(true); + return true; } return false; } diff --git a/chunky/src/java/se/llbit/math/structures/Position3d2IntPackedArray.java b/chunky/src/java/se/llbit/math/structures/Position3d2IntPackedArray.java index 5ac7bf4b04..5988eb1459 100644 --- a/chunky/src/java/se/llbit/math/structures/Position3d2IntPackedArray.java +++ b/chunky/src/java/se/llbit/math/structures/Position3d2IntPackedArray.java @@ -4,7 +4,6 @@ import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; import java.util.Arrays; -import java.util.Objects; public class Position3d2IntPackedArray implements Position2IntStructure { diff --git a/chunky/src/java/se/llbit/util/Configurable.java b/chunky/src/java/se/llbit/util/Configurable.java index fe180d155a..2df26d5d04 100644 --- a/chunky/src/java/se/llbit/util/Configurable.java +++ b/chunky/src/java/se/llbit/util/Configurable.java @@ -6,21 +6,14 @@ * This interface specifies an object that can be configured by the user. * This would be, for example, a post processing method. */ -public interface Configurable { +public interface Configurable extends JsonSerializable { /** - * Load the configuration from the given JSON object that may have been created by {@link #storeConfiguration(JsonObject)} + * Load the configuration from the given JSON object that may have been created by {@link #toJson()} * but may as well have been created by external tools. * * @param json Source object */ - void loadConfiguration(JsonObject json); - - /** - * Store the configuration in the given JSON object such that it can be loaded later with {@link #loadConfiguration(JsonObject)}. - * - * @param json Destination object - */ - void storeConfiguration(JsonObject json); + void fromJson(JsonObject json); /** * Restore the default configuration. diff --git a/chunky/src/java/se/llbit/util/HasControls.java b/chunky/src/java/se/llbit/util/HasControls.java new file mode 100644 index 0000000000..72ec236c60 --- /dev/null +++ b/chunky/src/java/se/llbit/util/HasControls.java @@ -0,0 +1,19 @@ +package se.llbit.util; + +import javafx.scene.layout.VBox; +import se.llbit.chunky.ui.render.RenderControlsTab; + +/** + * This interface specifies an object that has its own set of controls. + */ +public interface HasControls { + /** + * Get controls for this object. + */ + default VBox getControls(RenderControlsTab parent) { + VBox vBox = new VBox(); + vBox.setVisible(false); + vBox.setManaged(false); + return vBox; + } +} diff --git a/chunky/src/res/se/llbit/chunky/ui/Chunky.fxml b/chunky/src/res/se/llbit/chunky/ui/Chunky.fxml index 10d7196383..e755a27153 100644 --- a/chunky/src/res/se/llbit/chunky/ui/Chunky.fxml +++ b/chunky/src/res/se/llbit/chunky/ui/Chunky.fxml @@ -7,7 +7,6 @@ - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + +