diff --git a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java index 9fc15279d2..b4bf968d8e 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java +++ b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java @@ -347,10 +347,10 @@ public static Map> getDefaultMaterialProperties() { } }); materialProperties.put("minecraft:torch", block -> { - block.emittance = 50.0f; + block.emittance = 1.0f; }); materialProperties.put("minecraft:wall_torch", block -> { - block.emittance = 50.0f; + block.emittance = 1.0f; }); materialProperties.put("minecraft:fire", block -> { block.emittance = 1.0f; @@ -448,16 +448,16 @@ public static Map> getDefaultMaterialProperties() { block.emittance = 0.6f; }); materialProperties.put("minecraft:soul_fire_torch", block -> { // MC 20w06a-20w16a - block.emittance = 35.0f; + block.emittance = 0.6f; }); materialProperties.put("minecraft:soul_torch", block -> { // MC >= 20w17a - block.emittance = 35.0f; + block.emittance = 0.6f; }); materialProperties.put("minecraft:soul_fire_wall_torch", block -> { // MC 20w06a-20w16a - block.emittance = 35.0f; + block.emittance = 0.6f; }); materialProperties.put("minecraft:soul_wall_torch", block -> { // MC >= 20w17a - block.emittance = 35.0f; + block.emittance = 0.6f; }); materialProperties.put("minecraft:soul_fire", block -> { block.emittance = 0.6f; @@ -523,16 +523,24 @@ public static Map> getDefaultMaterialProperties() { } }); materialProperties.put("minecraft:small_amethyst_bud", block -> { - block.emittance = 1.0f / 15f; + if (block instanceof AmethystCluster && ((AmethystCluster) block).isLit()) { + block.emittance = 1.0f / 15f; + } }); materialProperties.put("minecraft:medium_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 2; + if (block instanceof AmethystCluster && ((AmethystCluster) block).isLit()) { + block.emittance = 1.0f / 15f * 2; + } }); materialProperties.put("minecraft:large_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 4; + if (block instanceof AmethystCluster && ((AmethystCluster) block).isLit()) { + block.emittance = 1.0f / 15f * 4; + } }); materialProperties.put("minecraft:amethyst_cluster", block -> { - block.emittance = 1.0f / 15f * 5; + if (block instanceof AmethystCluster && ((AmethystCluster) block).isLit()) { + block.emittance = 1.0f / 15f * 5; + } }); materialProperties.put("minecraft:tinted_glass", glassConfig); materialProperties.put("minecraft:sculk_sensor", block -> { diff --git a/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java index 48773593df..6e280a1bc1 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java +++ b/chunky/src/java/se/llbit/chunky/renderer/SunSamplingStrategy.java @@ -19,10 +19,11 @@ 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); + 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), + HIGH_QUALITY("High Quality", "High quality sun sampling. More noise but correctly models visual effects such as caustics.", true, true, true, true, false), + DIFFUSE("Diffuse", "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); private final String displayName; private final String description; @@ -31,8 +32,9 @@ public enum SunSamplingStrategy implements Registerable { private final boolean diffuseSun; private final boolean strictDirectLight; private final boolean sunLuminosity; + private final boolean diffuseSampling; - SunSamplingStrategy(String displayName, String description, boolean sunSampling, boolean diffuseSun, boolean strictDirectLight, boolean sunLuminosity) { + SunSamplingStrategy(String displayName, String description, boolean sunSampling, boolean diffuseSun, boolean strictDirectLight, boolean sunLuminosity, boolean diffuseSampling) { this.displayName = displayName; this.description = description; @@ -40,6 +42,7 @@ public enum SunSamplingStrategy implements Registerable { this.diffuseSun = diffuseSun; this.strictDirectLight = strictDirectLight; this.sunLuminosity = sunLuminosity; + this.diffuseSampling = diffuseSampling; } @Override @@ -72,4 +75,6 @@ public boolean isStrictDirectLight() { public boolean isSunLuminosity() { return sunLuminosity; } + + public boolean isDiffuseSampling() { return diffuseSampling; } } 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..8c38e12f79 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/CloudLayer.java @@ -0,0 +1,542 @@ +package se.llbit.chunky.renderer.scene; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.block.minecraft.Air; +import se.llbit.chunky.world.Clouds; +import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.VolumeMaterial; +import se.llbit.chunky.world.material.CloudMaterial; +import se.llbit.chunky.world.material.VolumeCloudMaterial; +import se.llbit.json.JsonObject; +import se.llbit.math.ColorUtil; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; +import se.llbit.util.JsonSerializable; +import se.llbit.util.Pair; + +import java.util.Random; + +public class CloudLayer implements JsonSerializable { + /** + * Default cloud y-position + */ + protected static final int DEFAULT_CLOUD_HEIGHT = 128; + + protected static final int DEFAULT_CLOUD_SIZE = 12; + + protected static final double DEFAULT_CLOUD_DENSITY = 0.2; + + private final Vector3 size = new Vector3(DEFAULT_CLOUD_SIZE, 4, DEFAULT_CLOUD_SIZE); + private final Vector3 offset = new Vector3(0, DEFAULT_CLOUD_HEIGHT, 0); + private final Vector3 color = new Vector3(1, 1, 1); + private boolean volumetricClouds = false; + private double density = DEFAULT_CLOUD_DENSITY; + private final Material material = new CloudMaterial(); + private final VolumeMaterial volumeMaterial = new VolumeCloudMaterial(); + + public Vector3 getCloudColor() { + return new Vector3(color); + } + + public void setCloudColor(Vector3 color) { + this.color.set(color); + } + + + public boolean getVolumetricClouds() { + return volumetricClouds; + } + + public void setVolumetricClouds(boolean value) { + volumetricClouds = value; + } + + + public double getCloudDensity() { + return density; + } + + public void setCloudDensity(double value) { + density = value; + } + + + public double getCloudSizeX() { + return size.x; + } + + public void setCloudSizeX(double newValue) { + if (newValue != size.x) { + size.x = newValue; + } + } + + + public double getCloudSizeY() { + return size.y; + } + + public void setCloudSizeY(double newValue) { + if (newValue != size.y) { + size.y = newValue; + } + } + + + public double getCloudSizeZ() { + return size.z; + } + + public void setCloudSizeZ(double newValue) { + if (newValue != size.z) { + size.z = newValue; + } + } + + + public double getCloudXOffset() { + return offset.x; + } + + public void setCloudXOffset(double newValue) { + if (newValue != offset.x) { + offset.x = newValue; + } + } + + + /** + * @return The current cloud height + */ + public double getCloudYOffset() { + return offset.y; + } + + /** + * Change the cloud height + */ + public void setCloudYOffset(double newValue) { + if (newValue != offset.y) { + offset.y = newValue; + } + } + + + public double getCloudZOffset() { + return offset.z; + } + + public void setCloudZOffset(double newValue) { + if (newValue != offset.z) { + offset.z = newValue; + } + } + + public float getEmittance() { + return volumetricClouds ? volumeMaterial.emittance : material.emittance; + } + + public void setEmittance(float value) { + if (volumetricClouds) { + volumeMaterial.emittance = value; + } else { + material.emittance = value; + } + } + + public float getSpecular() { + return material.specular; + } + + public void setSpecular(float value) { + material.specular = value; + } + + public float getSmoothness() { + return (float) material.getPerceptualSmoothness(); + } + + public void setSmoothness(float value) { + material.setPerceptualSmoothness(value); + } + + public float getIor() { + return material.ior; + } + + public void setIor(float value) { + material.ior = value; + } + + public float getMetalness() { + return material.metalness; + } + + public void setMetalness(float value) { + material.emittance = value; + } + + public float getAnisotropy() { + return volumeMaterial.anisotropy; + } + + public void setAnisotropy(float value) { + volumeMaterial.anisotropy = value; + } + + private Pair getCloudDistance(Scene scene, Ray ray) { + Pair cloudIntersectionTest = cloudIntersection(scene, ray); + boolean hitCloud = cloudIntersectionTest.thing1; + boolean insideCloud = cloudIntersectionTest.thing2; + if (!hitCloud) { + return new Pair<>(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } else { + if (!volumetricClouds) { + double t = ray.t; + double tExit; + if (insideCloud) { + tExit = 1d; + } else { + tExit = 0d; + } + return new Pair<>(t, tExit); + } else { + if (insideCloud) { + double t; + double tExit; + t = 0; + tExit = ray.t; + return new Pair<>(t, tExit); + } else { + double t = ray.t; + double tExit; + double depth = 0; + while (true) { + if (depth >= 1000) { + return new Pair<>(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + Ray nextIntersection = new Ray(ray); + nextIntersection.t = Double.POSITIVE_INFINITY; + nextIntersection.o.scaleAdd(t + Ray.OFFSET, ray.d); + Pair testSecondIntersection = cloudIntersection(scene, nextIntersection); + if (!testSecondIntersection.thing1) { + return new Pair<>(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } else { + if (testSecondIntersection.thing2) { + tExit = t + Ray.OFFSET + nextIntersection.t; + break; + } else { + t += Ray.OFFSET + nextIntersection.t; + depth++; + } + } + } + return new Pair<>(t, tExit); + } + } + } + } + + public boolean intersect(Scene scene, Ray ray, Random random) { + if (random == null && volumetricClouds) { + return false; + } + Ray test = new Ray(ray); + test.t = ray.t; + Pair cloudDistance = getCloudDistance(scene, test); + double firstIntersection = cloudDistance.thing1; + if (firstIntersection == Double.POSITIVE_INFINITY) { + return false; + } + double secondIntersection = cloudDistance.thing2; + double t; + if (volumetricClouds) { + double testFirstIntersection = firstIntersection; + double testSecondIntersection = secondIntersection; + int depth = 0; + while (true) { + if (depth >= 1000) { + return false; + } + double fogPenetrated = -FastMath.log(1 - random.nextDouble()); + double fogDistance = fogPenetrated / density; + if (testFirstIntersection + fogDistance < testSecondIntersection) { + t = testFirstIntersection + fogDistance; + + if (t >= ray.t) { + return false; + } + + volumeMaterial.setRandomSphericalNormal(ray, random); + + ray.setCurrentMaterial(volumeMaterial); + ray.specular = false; + break; + } else { + Ray test2 = new Ray(ray); + test2.t = Double.POSITIVE_INFINITY; + test2.o.scaleAdd(testSecondIntersection + Ray.OFFSET, ray.d); + Pair testCloudDistance = getCloudDistance(scene, test2); + if (testCloudDistance.thing1 == Double.POSITIVE_INFINITY) { + return false; + } + testFirstIntersection += (testSecondIntersection - testFirstIntersection) + Ray.OFFSET + testCloudDistance.thing1; + testSecondIntersection += Ray.OFFSET + testCloudDistance.thing2; + depth++; + } + } + } else { + t = firstIntersection; + if (t >= ray.t) { + return false; + } + ray.setNormal(test.getNormal()); + if (secondIntersection == 1) { + ray.setCurrentMaterial(Air.INSTANCE); + } else { + ray.setCurrentMaterial(material); + } + } + ray.t = t; + ray.color.set(color.x, color.y, color.z, 1); + return true; + } + + /** + * 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(Scene scene, Ray ray) { + 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 = offset.x; + double offsetY = offset.y; + double offsetZ = offset.z; + double invSizeX = 1 / size.x; + double invSizeZ = 1 / size.z; + double cloudTop = offsetY + size.y; + 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)) { + ray.setNormal(0, -Math.signum(ray.d.y), 0); + ray.t = 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 (ray.t < tExit) { + tExit = ray.t; + } + 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); + } + ray.setNormal(nx, ny, nz); + ray.t = 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); + } + ray.setNormal(nx, ny, nz); + ray.t = 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 cloudLayerJson = new JsonObject(); + cloudLayerJson.add("size", size.toJson()); + cloudLayerJson.add("offset", offset.toJson()); + cloudLayerJson.add("color", ColorUtil.rgbToJson(color)); + cloudLayerJson.add("volumetricClouds", volumetricClouds); + cloudLayerJson.add("density", density); + cloudLayerJson.add("flatMaterialProperties", flatMaterialToJson()); + cloudLayerJson.add("volumeMaterialProperties", volumeMaterialToJson()); + return cloudLayerJson; + } + + private JsonObject flatMaterialToJson() { + JsonObject flatMaterial = new JsonObject(); + flatMaterial.add("emittance", material.emittance); + flatMaterial.add("specular", material.specular); + flatMaterial.add("roughness", material.roughness); + flatMaterial.add("ior", material.ior); + flatMaterial.add("metalness", material.metalness); + return flatMaterial; + } + + private JsonObject volumeMaterialToJson() { + JsonObject volumeMaterialObject = new JsonObject(); + volumeMaterialObject.add("emittance", volumeMaterial.emittance); + volumeMaterialObject.add("anisotropy", volumeMaterial.anisotropy); + return volumeMaterialObject; + } + + public void importFromJson(JsonObject json) { + size.fromJson(json.get("size").asObject()); + offset.fromJson(json.get("offset").asObject()); + color.set(ColorUtil.jsonToRGB(json.get("color").asObject())); + volumetricClouds = json.get("volumetricClouds").boolValue(volumetricClouds); + density = json.get("density").doubleValue(density); + JsonObject flatMaterialProperties = json.get("flatMaterialProperties").asObject(); + importFlatMaterial(flatMaterialProperties); + JsonObject volumeMaterialProperties = json.get("volumeMaterialProperties").asObject(); + importVolumeMaterial(volumeMaterialProperties); + } + + private void importFlatMaterial(JsonObject json) { + material.emittance = json.get("emittance").floatValue(material.emittance); + material.specular = json.get("specular").floatValue(material.specular); + material.roughness = json.get("roughness").floatValue(material.roughness); + material.ior = json.get("ior").floatValue(material.ior); + material.metalness = json.get("metalness").floatValue(material.metalness); + } + + private void importVolumeMaterial(JsonObject json) { + volumeMaterial.emittance = json.get("emittance").floatValue(volumeMaterial.emittance); + volumeMaterial.anisotropy = json.get("anisotropy").floatValue(volumeMaterial.anisotropy); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/CuboidFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/CuboidFogVolume.java new file mode 100644 index 0000000000..cc112f9904 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/CuboidFogVolume.java @@ -0,0 +1,113 @@ +package se.llbit.chunky.renderer.scene; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.json.JsonObject; +import se.llbit.math.AABB; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class CuboidFogVolume extends FogVolume { + 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 AABB aabb; + + @Override + public boolean intersect(Scene scene, Ray ray, Random random) { + double distance; + double fogPenetrated = -FastMath.log(1 - random.nextDouble()); + double fogDistance = fogPenetrated / density; + AABB aabbTranslated = aabb.getTranslated(-scene.origin.x, -scene.origin.y, -scene.origin.z); + if (!aabbTranslated.inside(ray.o)) { + Ray test = new Ray(ray); + if (aabbTranslated.hitTest(test)) { + distance = test.tNext; + distance += fogDistance; + } else { + return false; + } + } else { + distance = fogDistance; + } + + if (distance >= ray.t) { + return false; + } + + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + if (aabbTranslated.inside(o)) { + ray.t = distance; + // pick a random normal vector based on a spherical particle. + // This is done only to prevent fog from looking ugly in the render preview and + // should have no effect on the path-traced render. + setRandomNormal(ray, random); + setRayMaterialAndColor(ray); + ray.specular = false; + return true; + } else { + return false; + } + } + + public void setBounds(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax) { + setBounds(new AABB(xmin, xmax, ymin, ymax, zmin, zmax)); + } + + private void setBounds(AABB aabb) { + this.aabb = aabb; + } + + public AABB getBounds() { + return new AABB(this.aabb.xmin, this.aabb.xmax, this.aabb.ymin, this.aabb.ymax, this.aabb.zmin, this.aabb.zmax); + } + + public CuboidFogVolume(Vector3 color, double density, double xmin, double xmax, double ymin, double ymax, double zmin, double zmax) { + this(color, density, new AABB(xmin, xmax, ymin, ymax, zmin, zmax)); + } + + public CuboidFogVolume(Vector3 color, double density, AABB aabb) { + this.type = FogVolumeType.CUBOID; + this.aabb = aabb; + this.color = new Vector3(color); + this.density = density; + } + + public CuboidFogVolume(Vector3 color, double density) { + this(color, density, new AABB(DEFAULT_XMIN, DEFAULT_XMAX, DEFAULT_YMIN, DEFAULT_YMAX, DEFAULT_ZMIN, DEFAULT_ZMAX)); + } + + @Override + public JsonObject volumeSpecificPropertiesToJson() { + JsonObject properties = new JsonObject(); + JsonObject pos1 = new JsonObject(); + pos1.add("x", aabb.xmin); + pos1.add("y", aabb.ymin); + pos1.add("z", aabb.zmin); + properties.add("pos1", pos1); + JsonObject pos2 = new JsonObject(); + pos2.add("x", aabb.xmax); + pos2.add("y", aabb.ymax); + pos2.add("z", aabb.zmax); + properties.add("pos2", pos2); + return properties; + } + + @Override + public void importVolumeSpecificProperties(JsonObject json) { + JsonObject pos1 = json.get("pos1").object(); + double x1 = pos1.get("x").doubleValue(aabb.xmin); + double y1 = pos1.get("y").doubleValue(aabb.ymin); + double z1 = pos1.get("z").doubleValue(aabb.zmin); + JsonObject pos2 = json.get("pos2").object(); + double x2 = pos2.get("x").doubleValue(aabb.xmax); + double y2 = pos2.get("y").doubleValue(aabb.ymax); + double z2 = pos2.get("z").doubleValue(aabb.zmax); + aabb = new AABB(x1, x2, y1, y2, z1, z2); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/ExponentialFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/ExponentialFogVolume.java new file mode 100644 index 0000000000..1c1559be63 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/ExponentialFogVolume.java @@ -0,0 +1,83 @@ +package se.llbit.chunky.renderer.scene; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.json.JsonObject; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class ExponentialFogVolume extends FogVolume { + public static final double DEFAULT_SCALE_HEIGHT = 20; + public static final double DEFAULT_Y_OFFSET = 0; + private double scaleHeight; + private double yOffset; + public ExponentialFogVolume(Vector3 color, double density, double scaleHeight, double yOffset) { + this.type = FogVolumeType.EXPONENTIAL; + this.color = new Vector3(color); + this.density = density; + this.scaleHeight = scaleHeight; + this.yOffset = yOffset; + } + + public ExponentialFogVolume(Vector3 color, double density) { + this(color, density, DEFAULT_SCALE_HEIGHT, DEFAULT_Y_OFFSET); + } + + @Override + public boolean intersect(Scene scene, Ray ray, Random random) { + // 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 * density); + double expYfHs = FastMath.exp(-(ray.o.y + scene.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 - (ray.o.y + scene.origin.y)) / ray.d.y; + if(dist >= ray.t) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + ray.t = dist; + // pick a random normal vector based on a spherical particle. + // This is done only to prevent fog from looking ugly in the render preview and + // should have no effect on the path-traced render. + setRandomNormal(ray, random); + setRayMaterialAndColor(ray); + ray.specular = false; + return true; + } + + public void setScaleHeight(double s) { + scaleHeight = s; + } + + public double getScaleHeight() { + return scaleHeight; + } + + public void setYOffset(double y) { + yOffset = y; + } + + public double getYOffset() { + return yOffset; + } + + @Override + public JsonObject volumeSpecificPropertiesToJson() { + JsonObject properties = new JsonObject(); + properties.add("scaleHeight", scaleHeight); + properties.add("yOffset", yOffset); + return properties; + } + + @Override + public void importVolumeSpecificProperties(JsonObject json) { + scaleHeight = json.get("scaleHeight").doubleValue(scaleHeight); + yOffset = json.get("yOffset").doubleValue(yOffset); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java index 76f16794e3..e95c45b1d0 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Fog.java @@ -8,10 +8,7 @@ import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; -import se.llbit.math.QuickMath; -import se.llbit.math.Ray; -import se.llbit.math.Vector3; -import se.llbit.math.Vector4; +import se.llbit.math.*; import se.llbit.util.JsonSerializable; public final class Fog implements JsonSerializable { @@ -21,6 +18,7 @@ public final class Fog implements JsonSerializable { protected double uniformDensity = Scene.DEFAULT_FOG_DENSITY; protected double skyFogDensity = 1; protected ArrayList layers = new ArrayList<>(0); + protected ArrayList volumes = new ArrayList<>(0); protected Vector3 fogColor = new Vector3(PersistentSettings.getFogColorRed(), PersistentSettings.getFogColorGreen(), PersistentSettings.getFogColorBlue()); private static final double EXTINCTION_FACTOR = 0.04; @@ -58,6 +56,9 @@ public boolean fastFog() { public ArrayList getFogLayers() { return layers; } + public ArrayList getFogVolumes() { + return volumes; + } public void addLayer() { layers.add(new FogLayer(scene)); @@ -69,6 +70,34 @@ public void removeLayer(int index) { scene.refresh(); } + private FogVolume getVolumeFromType(FogVolumeType fogVolumeType) { + switch (fogVolumeType) { + case LAYER: + return new LayerFogVolume(fogColor, uniformDensity); + case SPHERE: + return new SphericalFogVolume(fogColor, uniformDensity); + case CUBOID: + return new CuboidFogVolume(fogColor, uniformDensity); + case EXPONENTIAL: + default: + return new ExponentialFogVolume(fogColor, uniformDensity); + } + } + + public void addVolume(FogVolumeType fogVolumeType) { + addVolume(getVolumeFromType(fogVolumeType)); + scene.refresh(); + } + + public void addVolume(FogVolume fogVolume) { + volumes.add(fogVolume); + } + + public void removeVolume(int index) { + volumes.remove(index); + scene.refresh(); + } + public void setY(int index, double value) { layers.get(index).setY(value); scene.refresh(); @@ -98,6 +127,7 @@ private static double clampDy(double dy) { } public void addSkyFog(Ray ray, Vector4 scatterLight) { + if (mode == FogMode.UNIFORM) { if (uniformDensity > 0.0) { double fog; @@ -118,6 +148,7 @@ public void addSkyFog(Ray ray, Vector4 scatterLight) { double y2 = y1 + dy * FOG_LIMIT; addLayeredFog(ray.color, dy, y1, y2, scatterLight); } + } public void addGroundFog(Ray ray, Vector3 ox, double airDistance, Vector4 scatterLight, double scatterOffset) { @@ -199,6 +230,17 @@ private double sampleLayeredScatterOffset(Random random, double y1, double y2, d return (offsetY - y1) / clampDy(dy); } + public boolean particleFogIntersection(Scene scene, Ray ray, Random random) { + if (random == null) { + return false; + } + boolean hit = false; + for (FogVolume v : volumes) { + hit |= v.intersect(scene, ray, random); + } + return hit; + } + @Override public JsonObject toJson() { JsonObject fogObj = new JsonObject(); fogObj.add("mode", mode.name()); @@ -213,11 +255,12 @@ private double sampleLayeredScatterOffset(Random random, double y1, double y2, d jsonLayers.add(jsonLayer); } fogObj.add("layers", jsonLayers); - JsonObject colorObj = new JsonObject(); - colorObj.add("red", fogColor.x); - colorObj.add("green", fogColor.y); - colorObj.add("blue", fogColor.z); - fogObj.add("color", colorObj); + JsonArray jsonVolumes = new JsonArray(); + for (FogVolume volume : volumes) { + jsonVolumes.add(volume.toJson()); + } + fogObj.add("volumes", jsonVolumes); + fogObj.add("color", ColorUtil.rgbToJson(fogColor)); fogObj.add("fastFog", fastFog); return fogObj; } @@ -231,10 +274,16 @@ public void importFromJson(JsonObject json, Scene scene) { o.get("breadth").doubleValue(0), o.get("density").doubleValue(0), scene)).collect(Collectors.toCollection(ArrayList::new)); - JsonObject colorObj = json.get("color").object(); - fogColor.x = colorObj.get("red").doubleValue(fogColor.x); - fogColor.y = colorObj.get("green").doubleValue(fogColor.y); - fogColor.z = colorObj.get("blue").doubleValue(fogColor.z); + JsonArray jsonVolumes = json.get("volumes").array(); + volumes.clear(); + for (JsonValue jsonVolume : jsonVolumes) { + JsonObject jsonVolumeObject = jsonVolume.asObject(); + FogVolumeType fogVolumeType = FogVolumeType.valueOf(jsonVolumeObject.get("type").asString("")); + FogVolume fogVolume = getVolumeFromType(fogVolumeType); + fogVolume.importFromJson(jsonVolumeObject); + addVolume(fogVolume); + } + fogColor.set(ColorUtil.jsonToRGB(json.get("color").object())); fastFog = json.get("fastFog").boolValue(fastFog); } @@ -242,11 +291,9 @@ public void importFromLegacy(JsonObject json) { mode = json.get("fogEnabled").boolValue(mode != FogMode.NONE) ? FogMode.NONE : FogMode.UNIFORM; uniformDensity = json.get("fogDensity").doubleValue(uniformDensity); skyFogDensity = json.get("skyFogDensity").doubleValue(skyFogDensity); - layers = new ArrayList<>(0); - JsonObject colorObj = json.get("fogColor").object(); - fogColor.x = colorObj.get("red").doubleValue(fogColor.x); - fogColor.y = colorObj.get("green").doubleValue(fogColor.y); - fogColor.z = colorObj.get("blue").doubleValue(fogColor.z); + layers.clear(); + volumes.clear(); + fogColor.set(ColorUtil.jsonToRGB(json.get("fogColor").asObject())); fastFog = json.get("fastFog").boolValue(fastFog); } @@ -255,6 +302,7 @@ public void set(Fog other) { uniformDensity = other.uniformDensity; skyFogDensity = other.skyFogDensity; layers = new ArrayList<>(other.layers); + volumes = new ArrayList<>(other.volumes); fogColor.set(other.fogColor); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolume.java new file mode 100644 index 0000000000..2905e2335a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolume.java @@ -0,0 +1,104 @@ +package se.llbit.chunky.renderer.scene; + +import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.VolumeMaterial; +import se.llbit.chunky.world.material.ParticleFogMaterial; +import se.llbit.json.JsonObject; +import se.llbit.math.ColorUtil; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; +import se.llbit.util.JsonSerializable; + +import java.util.Random; + +public abstract class FogVolume implements JsonSerializable { + protected FogVolumeType type; + protected Vector3 color; + protected double density; + protected VolumeMaterial material = new ParticleFogMaterial(); + + public abstract boolean intersect(Scene scene, Ray ray, Random random); + + public void setRandomNormal(Ray ray, Random random) { + material.setRandomSphericalNormal(ray, random); + } + + public void setRayMaterialAndColor(Ray ray) { + ray.setCurrentMaterial(material); + ray.color.set(color.x, color.y, color.z, 1); + } + + public Material getMaterial() { + return material; + } + + public double getEmittance() { + return this.material.emittance; + } + + public void setEmittance(float value) { + this.material.emittance = value; + } + + public double getAnisotropy() { + return this.material.anisotropy; + } + + public void setAnisotropy(float value) { + this.material.anisotropy = value; + } + + public void setDensity(double value) { + this.density = value; + } + + public double getDensity() { + return density; + } + + public void setColor(Vector3 value) { + this.color.set(value); + } + + public Vector3 getColor() { + return new Vector3(color); + } + + public FogVolumeType getType() { + return type; + } + + protected abstract JsonObject volumeSpecificPropertiesToJson(); + + @Override + public JsonObject toJson() { + JsonObject properties = volumeSpecificPropertiesToJson(); + properties.add("type", type.name()); + properties.add("color", ColorUtil.rgbToJson(color)); + properties.add("density", density); + properties.add("materialProperties", materialPropertiesToJson()); + return properties; + } + + protected JsonObject materialPropertiesToJson() { + JsonObject materialProperties = new JsonObject(); + materialProperties.add("emittance", material.emittance); + materialProperties.add("anisotropy", material.anisotropy); + return materialProperties; + } + + protected abstract void importVolumeSpecificProperties(JsonObject jsonObject); + + public void importFromJson(JsonObject json) { + color.set(ColorUtil.jsonToRGB(json.get("color").asObject())); + density = json.get("density").doubleValue(Scene.DEFAULT_FOG_DENSITY); + JsonObject materialProperties = json.get("materialProperties").object(); + importMaterialProperties(materialProperties); + importVolumeSpecificProperties(json); + } + + protected void importMaterialProperties(JsonObject json) { + material.emittance = json.get("emittance").floatValue(material.emittance); + material.anisotropy = json.get("anisotropy").floatValue(material.anisotropy); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolumeType.java b/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolumeType.java new file mode 100644 index 0000000000..c798be7065 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/FogVolumeType.java @@ -0,0 +1,6 @@ +package se.llbit.chunky.renderer.scene; + +public enum FogVolumeType { + EXPONENTIAL, LAYER, SPHERE, CUBOID + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/IntersectionConfig.java b/chunky/src/java/se/llbit/chunky/renderer/scene/IntersectionConfig.java new file mode 100644 index 0000000000..526870150a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/IntersectionConfig.java @@ -0,0 +1,19 @@ +package se.llbit.chunky.renderer.scene; + +public class IntersectionConfig { + public final boolean cloudIntersect; + public final boolean fogIntersect; + public final boolean waterPlaneIntersect; + public final boolean sceneIntersect; + + public IntersectionConfig(boolean cloudIntersect, boolean fogIntersect, boolean waterPlaneIntersect, boolean sceneIntersect) { + this.cloudIntersect = cloudIntersect; + this.fogIntersect = fogIntersect; + this.waterPlaneIntersect = waterPlaneIntersect; + this.sceneIntersect = sceneIntersect; + } + + public static IntersectionConfig defaultIntersect(Scene scene, boolean isRenderPreview) { + return new IntersectionConfig(true, !isRenderPreview || scene.getPreviewParticleFog(), scene.isWaterPlaneEnabled(), true); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/LayerFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/LayerFogVolume.java new file mode 100644 index 0000000000..3f150b0384 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/LayerFogVolume.java @@ -0,0 +1,82 @@ +package se.llbit.chunky.renderer.scene; + +import se.llbit.json.JsonObject; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class LayerFogVolume extends FogVolume { + public static final double DEFAULT_Y_OFFSET = 62; + public static final double DEFAULT_BREADTH = 5; + private double layerBreadth; + private double yOffset; + public LayerFogVolume(Vector3 color, double density, double layerBreadth, double yOffset) { + this.type = FogVolumeType.LAYER; + this.color = new Vector3(color); + this.density = density; + this.layerBreadth = layerBreadth; + this.yOffset = yOffset; + } + + public LayerFogVolume(Vector3 color, double density) { + this(color, density, DEFAULT_BREADTH, DEFAULT_Y_OFFSET); + } + + @Override + public boolean intersect(Scene scene, Ray ray, Random random) { + // Amount of fog the ray should pass through before being scattered + // Sampled from an exponential distribution + double fogPenetrated = -Math.log(1 - random.nextDouble()); + double atanHeightDiff = fogPenetrated * ray.d.y / (layerBreadth * density); + double atanYfHs = Math.atan((ray.o.y + scene.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 - (ray.o.y + scene.origin.y)) / ray.d.y; + if(dist >= ray.t) { + // The ray would have encountered enough fog to be scattered, but something is in the way. + return false; + } + ray.t = dist; + // pick a random normal vector based on a spherical particle. + // This is done only to prevent fog from looking ugly in the render preview and + // should have no effect on the path-traced render. + setRandomNormal(ray, random); + setRayMaterialAndColor(ray); + ray.specular = false; + return true; + } + + public void setLayerBreadth(double l) { + layerBreadth = l; + } + + public double getLayerBreadth() { + return layerBreadth; + } + + public void setYOffset(double y) { + yOffset = y; + } + + public double getYOffset() { + return yOffset; + } + + @Override + public JsonObject volumeSpecificPropertiesToJson() { + JsonObject properties = new JsonObject(); + properties.add("layerBreadth", layerBreadth); + properties.add("yOffset", yOffset); + return properties; + } + + @Override + public void importVolumeSpecificProperties(JsonObject json) { + layerBreadth = json.get("layerBreadth").doubleValue(layerBreadth); + yOffset = json.get("yOffset").doubleValue(yOffset); + } +} 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 7e80122b2f..58a91641d7 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -23,6 +23,7 @@ import se.llbit.chunky.renderer.EmitterSamplingStrategy; import se.llbit.chunky.renderer.WorkerState; import se.llbit.chunky.world.Material; +import se.llbit.chunky.world.VolumeMaterial; import se.llbit.math.*; import java.util.List; @@ -45,7 +46,7 @@ public class PathTracer implements RayTracer { } else { ray.setCurrentMaterial(Air.INSTANCE); } - pathTrace(scene, ray, state, 1, true); + pathTrace(scene, ray, state, true); } /** @@ -54,7 +55,7 @@ public class PathTracer implements RayTracer { * @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, int addEmitted, + public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, boolean firstReflection) { boolean hit = false; @@ -65,7 +66,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add while (true) { - if (!PreviewRayTracer.nextIntersection(scene, ray)) { + if (!PreviewRayTracer.nextIntersection(scene, ray, random, IntersectionConfig.defaultIntersect(scene, false))) { if (ray.getPrevMaterial().isWater()) { ray.color.set(0, 0, 0, 1); hit = true; @@ -84,7 +85,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add } else { // Indirect sky hit - diffuse color. scene.sky.getSkyColorDiffuseSun(ray, scene.getSunSamplingStrategy().isDiffuseSun()); - // Skip sky fog - likely not noticeable in diffuse reflection. + addSkyFog(scene, ray, state, ox, od); hit = true; } break; @@ -138,10 +139,13 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add 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)) { + + if (currentMat instanceof VolumeMaterial) { + hit |= doParticleFogReflection(ray, next, (VolumeMaterial) currentMat, cumulativeColor, random, state, scene); + } else 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, addEmitted, random, state, scene); + hit |= doDiffuseReflection(ray, next, currentMat, cumulativeColor, random, state, scene); } else if (n1 != n2) { hit |= doRefraction(ray, next, currentMat, prevMat, cumulativeColor, n1, n2, pDiffuse, random, state, scene); } else { @@ -201,13 +205,82 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add return hit; } + private static boolean doParticleFogReflection(Ray ray, Ray next, VolumeMaterial currentMat, Vector4 cumulativeColor, Random random, WorkerState state, Scene scene) { + boolean hit = false; + Vector3 emittance = new Vector3(); + next.set(ray); + Vector3 inboundDirection = new Vector3(ray.d); + inboundDirection.scale(-1); + + if (scene.emittersEnabled && 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; + } + + if (scene.getSunSamplingStrategy().doSunSampling()) { + scene.sun.getRandomSunDirection(next, random); + double cosTheta = inboundDirection.dot(next.d); + + double directLightR = 0; + double directLightG = 0; + double directLightB = 0; + + next.setCurrentMaterial(next.getPrevMaterial(), next.getPrevData()); + + getDirectLightAttenuation(scene, next, state); + + Vector4 attenuation = state.attenuation; + if (attenuation.w > 0) { + double mult = phaseHG(cosTheta, currentMat.anisotropy) * (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.set(ray); + Vector3 outboundDirection = new Vector3(); + double x1 = random.nextDouble(); + double x2 = random.nextDouble(); + henyeyGreensteinSampleP(currentMat.anisotropy, inboundDirection, outboundDirection, x1, x2); + next.d.set(outboundDirection); + next.d.normalize(); + + hit |= pathTrace(scene, next, state, false); + if (hit) { + cumulativeColor.x += emittance.x + ray.color.x * (directLightR * scene.sun.emittance.x + next.color.x); + cumulativeColor.y += emittance.y + ray.color.y * (directLightG * scene.sun.emittance.y + next.color.y); + cumulativeColor.z += emittance.z + ray.color.z * (directLightB * scene.sun.emittance.z + next.color.z); + } + } else { + Vector4 rayColor = new Vector4(ray.color); + + Vector3 outboundDirection = new Vector3(); + double x1 = random.nextDouble(); + double x2 = random.nextDouble(); + henyeyGreensteinSampleP(currentMat.anisotropy, inboundDirection, outboundDirection, x1, x2); + next.d.set(outboundDirection); + next.d.normalize(); + + hit |= pathTrace(scene, next, state, false); + if (hit) { + cumulativeColor.x += emittance.x + ray.color.x * (next.color.x); + cumulativeColor.y += emittance.y + ray.color.y * (next.color.y); + cumulativeColor.z += emittance.z + ray.color.z * (next.color.z); + } + ray.color.set(rayColor); + + } + return hit; + } + 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, 1, false)) { - ray.emittance.x = ray.color.x * next.emittance.x; - ray.emittance.y = ray.color.y * next.emittance.y; - ray.emittance.z = ray.color.z * next.emittance.z; + if (pathTrace(scene, next, state, false)) { if (doMetal) { // use the albedo color as specular color @@ -224,20 +297,17 @@ private static boolean doSpecularReflection(Ray ray, Ray next, Vector4 cumulativ return hit; } - private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMat, Vector4 cumulativeColor, int addEmitted, Random random, WorkerState state, Scene scene) { + private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMat, Vector4 cumulativeColor, Random random, WorkerState state, Scene scene) { boolean hit = false; - float emittance = 0; + Vector3 emittance = new Vector3(); Vector4 indirectEmitterColor = new Vector4(0, 0, 0, 0); if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && currentMat.emittance > Ray.EPSILON) { - emittance = addEmitted; - ray.emittance.x = ray.color.x * ray.color.x * - currentMat.emittance * scene.emitterIntensity; - ray.emittance.y = ray.color.y * ray.color.y * - currentMat.emittance * scene.emitterIntensity; - ray.emittance.z = ray.color.z * ray.color.z * - currentMat.emittance * scene.emitterIntensity; + // 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) { @@ -293,15 +363,12 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa } } - next.diffuseReflection(ray, random); - hit = pathTrace(scene, next, state, 0, false) || hit; + next.diffuseReflection(ray, random, scene); + hit = pathTrace(scene, next, state, false) || hit; if (hit) { - cumulativeColor.x += ray.color.x * (emittance + directLightR * scene.sun.emittance.x + ( - next.color.x + next.emittance.x) + (indirectEmitterColor.x)); - cumulativeColor.y += ray.color.y * (emittance + directLightG * scene.sun.emittance.y + ( - next.color.y + next.emittance.y) + (indirectEmitterColor.y)); - cumulativeColor.z += ray.color.z * (emittance + directLightB * scene.sun.emittance.z + ( - next.color.z + next.emittance.z) + (indirectEmitterColor.z)); + cumulativeColor.x += emittance.x + ray.color.x * (directLightR * scene.sun.emittance.x + next.color.x + indirectEmitterColor.x); + cumulativeColor.y += emittance.y + ray.color.y * (directLightG * scene.sun.emittance.y + next.color.y + indirectEmitterColor.y); + cumulativeColor.z += emittance.z + ray.color.z * (directLightB * scene.sun.emittance.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; @@ -310,19 +377,22 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa } } else { - next.diffuseReflection(ray, random); + // 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, 0, false) || hit; + hit = pathTrace(scene, next, state, false) || hit; if (hit) { - cumulativeColor.x += ray.color.x * (emittance + (next.color.x + next.emittance.x) + (indirectEmitterColor.x)); - cumulativeColor.y += ray.color.y * (emittance + (next.color.y + next.emittance.y) + (indirectEmitterColor.y)); - cumulativeColor.z += ray.color.z * (emittance + (next.color.z + next.emittance.z) + (indirectEmitterColor.z)); + 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); } return hit; } @@ -338,10 +408,7 @@ private static boolean doRefraction(Ray ray, Ray next, Material currentMat, Mate if (doRefraction && radicand < Ray.EPSILON) { // Total internal reflection. next.specularReflection(ray, random); - if (pathTrace(scene, next, state, 1, false)) { - ray.emittance.x = ray.color.x * next.emittance.x; - ray.emittance.y = ray.color.y * next.emittance.y; - ray.emittance.z = ray.color.z * next.emittance.z; + if (pathTrace(scene, next, state, false)) { cumulativeColor.x += next.color.x; cumulativeColor.y += next.color.y; @@ -362,10 +429,7 @@ private static boolean doRefraction(Ray ray, Ray next, Material currentMat, Mate if (random.nextFloat() < Rtheta) { next.specularReflection(ray, random); - if (pathTrace(scene, next, state, 1, false)) { - ray.emittance.x = ray.color.x * next.emittance.x; - ray.emittance.y = ray.color.y * next.emittance.y; - ray.emittance.z = ray.color.z * next.emittance.z; + if (pathTrace(scene, next, state, false)) { cumulativeColor.x += next.color.x; cumulativeColor.y += next.color.y; @@ -401,7 +465,7 @@ private static boolean doRefraction(Ray ray, Ray next, Material currentMat, Mate next.o.scaleAdd(Ray.OFFSET, next.d); } - if (pathTrace(scene, next, state, 1, false)) { + if (pathTrace(scene, next, state, false)) { // Calculate the color and emittance of the refracted ray translucentRayColor(scene, ray, next, cumulativeColor, pDiffuse); hit = true; @@ -416,7 +480,7 @@ private static boolean doTransmission(Ray ray, Ray next, Vector4 cumulativeColor next.set(ray); next.o.scaleAdd(Ray.OFFSET, next.d); - if (pathTrace(scene, next, state, 1, false)) { + if (pathTrace(scene, next, state, false)) { // Calculate the color and emittance of the refracted ray translucentRayColor(scene, ray, next, cumulativeColor, pDiffuse); hit = true; @@ -481,8 +545,6 @@ private static void translucentRayColor(Scene scene, Ray ray, Ray next, Vector4 Vector4 outputColor = new Vector4(0, 0, 0, 0); outputColor.multiplyEntrywise(new Vector4(rgbTrans, 1), next.color); cumulativeColor.add(outputColor); - // Use emittance from next ray - ray.emittance.multiplyEntrywise(rgbTrans, next.emittance); } private static double reassignTransmissivity(double from, double to, double other, double trans, double cap) { @@ -517,7 +579,7 @@ private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition emitterRay.o.scaleAdd(Ray.OFFSET, emitterRay.d); emitterRay.distance += Ray.OFFSET; - PreviewRayTracer.nextIntersection(scene, emitterRay); + PreviewRayTracer.nextIntersection(scene, emitterRay, random, IntersectionConfig.defaultIntersect(scene, false)); if (Math.abs(emitterRay.distance - distance) < Ray.OFFSET) { double e = Math.abs(emitterRay.d.dot(emitterRay.getNormal())); e /= Math.max(distance * distance, 1); @@ -573,7 +635,7 @@ public static void getDirectLightAttenuation(Scene scene, Ray ray, WorkerState s attenuation.w = 1; while (attenuation.w > 0) { ray.o.scaleAdd(Ray.OFFSET, ray.d); - if (!PreviewRayTracer.nextIntersection(scene, ray)) { + if (!PreviewRayTracer.nextIntersection(scene, ray, state.random, IntersectionConfig.defaultIntersect(scene, false))) { break; } double mult = 1 - ray.color.w; @@ -595,4 +657,66 @@ public static void getDirectLightAttenuation(Scene scene, Ray ray, WorkerState s } } + private static final double INV_4_PI = 1 / (4 * FastMath.PI); + + /** + * Code adapted from pbrt + */ + private static double phaseHG(double cosTheta, double g) { + double denominator = 1 + g * g + 2 * g * cosTheta; + return 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; + } + } 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 c2446e9c23..4e066044d3 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java @@ -25,6 +25,8 @@ import se.llbit.math.Vector3; import se.llbit.math.Vector4; +import java.util.Random; + /** * @author Jesper Öqvist */ @@ -41,7 +43,7 @@ public class PreviewRayTracer implements RayTracer { ray.setCurrentMaterial(Air.INSTANCE); } while (true) { - if (!nextIntersection(scene, ray)) { + if (!nextIntersection(scene, ray, state.random, IntersectionConfig.defaultIntersect(scene, true))) { if (mapIntersection(scene, ray)) { break; } @@ -68,7 +70,7 @@ public static double skyOcclusion(Scene scene, WorkerState state) { Ray ray = state.ray; double occlusion = 1.0; while (true) { - if (!nextIntersection(scene, ray)) { + if (!nextIntersection(scene, ray, state.random, IntersectionConfig.defaultIntersect(scene, false))) { break; } else { occlusion *= (1 - ray.color.w); @@ -83,21 +85,27 @@ public static double skyOcclusion(Scene scene, WorkerState state) { /** * Find next ray intersection. + * @param random Used for particle fog, can be null if particleFog is false * @return Next intersection */ - public static boolean nextIntersection(Scene scene, Ray ray) { + public static boolean nextIntersection(Scene scene, Ray ray, Random random, IntersectionConfig config) { 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 (config.cloudIntersect) { + hit |= cloudIntersection(scene, ray, random); } - if (scene.isWaterPlaneEnabled()) { - hit = waterPlaneIntersection(scene, ray) || hit; + if (config.waterPlaneIntersect) { + hit |= waterPlaneIntersection(scene, ray); } - if (scene.intersect(ray)) { + if (config.fogIntersect) { + hit |= fogIntersection(scene, ray, random); + } + if (config.sceneIntersect) { // Octree tracer handles updating distance. - return true; + if (sceneIntersection(scene, ray)) { + return true; + } } if (hit) { ray.distance += ray.t; @@ -110,6 +118,18 @@ public static boolean nextIntersection(Scene scene, Ray ray) { } } + public static boolean nextIntersection(Scene scene, Ray ray) { + return nextIntersection(scene, ray, null, new IntersectionConfig(true, false, scene.waterPlaneEnabled, true)); + } + + private static boolean cloudIntersection(Scene scene, Ray ray, Random random) { + return scene.sky.cloudIntersection(scene, ray, random); + } + + private static boolean fogIntersection(Scene scene, Ray ray, Random random) { + return scene.fog.particleFogIntersection(scene, ray, random); + } + private static boolean waterPlaneIntersection(Scene scene, Ray ray) { double t = (scene.getEffectiveWaterPlaneHeight() - ray.o.y - scene.origin.y) / ray.d.y; if (scene.getWaterPlaneChunkClip()) { @@ -139,6 +159,10 @@ private static boolean waterPlaneIntersection(Scene scene, Ray ray) { return false; } + private static boolean sceneIntersection(Scene scene, Ray ray) { + return scene.intersect(ray); + } + // Chunk pattern config private static final double chunkPatternLineWidth = 0.5; // in blocks private static final double chunkPatternLinePosition = 8 - chunkPatternLineWidth / 2; 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 32b6b7340a..ec85a87a17 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -149,7 +149,7 @@ public class Scene implements JsonSerializable, Refreshable { /** * Default fog density. */ - public static final double DEFAULT_FOG_DENSITY = 0.0; + public static final double DEFAULT_FOG_DENSITY = 0.05; /** * Default post processing filter. @@ -343,6 +343,8 @@ public class Scene implements JsonSerializable, Refreshable { protected volatile boolean isLoading = false; + private boolean previewParticleFog = PersistentSettings.getPreviewParticleFog(); + /** * Creates a scene with all default settings. * @@ -454,6 +456,7 @@ public synchronized void copyState(Scene other, boolean copyChunks) { preventNormalEmitterWithSampling = other.preventNormalEmitterWithSampling; fancierTranslucency = other.fancierTranslucency; transmissivityCap = other.transmissivityCap; + previewParticleFog = other.previewParticleFog; transparentSky = other.transparentSky; yClipMin = other.yClipMin; yClipMax = other.yClipMax; @@ -1682,7 +1685,9 @@ public boolean traceTarget(Ray ray) { ray.o.x -= origin.x; ray.o.y -= origin.y; ray.o.z -= origin.z; - while (PreviewRayTracer.nextIntersection(this, ray)) { + while (PreviewRayTracer.nextIntersection(this, ray, null, + new IntersectionConfig(true, + false, waterPlaneEnabled, true))) { if (ray.getCurrentMaterial() != Air.INSTANCE) { return true; } @@ -2653,11 +2658,7 @@ public void setUseCustomWaterColor(boolean value) { json.add("waterVisibility", waterVisibility); json.add("useCustomWaterColor", useCustomWaterColor); if (useCustomWaterColor) { - JsonObject colorObj = new JsonObject(); - colorObj.add("red", waterColor.x); - colorObj.add("green", waterColor.y); - colorObj.add("blue", waterColor.z); - json.add("waterColor", colorObj); + json.add("waterColor", ColorUtil.rgbToJson(waterColor)); } waterShading.save(json); json.add("fog", fog.toJson()); @@ -2935,10 +2936,7 @@ public synchronized void importFromJson(JsonObject json) { waterVisibility = json.get("waterVisibility").doubleValue(waterVisibility); useCustomWaterColor = json.get("useCustomWaterColor").boolValue(useCustomWaterColor); if (useCustomWaterColor) { - JsonObject colorObj = json.get("waterColor").object(); - waterColor.x = colorObj.get("red").doubleValue(waterColor.x); - waterColor.y = colorObj.get("green").doubleValue(waterColor.y); - waterColor.z = colorObj.get("blue").doubleValue(waterColor.z); + waterColor.set(ColorUtil.jsonToRGB(json.get("waterColor").asObject())); } String waterShader = json.get("waterShader").stringValue("SIMPLEX"); if(waterShader.equals("LEGACY")) @@ -3248,6 +3246,13 @@ public void setMetalness(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + public void setAnisotropy(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("anisotropy", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + public int getYClipMin() { return yClipMin; } @@ -3431,4 +3436,14 @@ public void setTransmissivityCap(double value) { transmissivityCap = value; refresh(); } + + public boolean getPreviewParticleFog() { + return this.previewParticleFog; + } + + public void setPreviewParticleFog(boolean value) { + this.previewParticleFog = value; + PersistentSettings.setPreviewParticleFog(value); + refresh(); + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java index fce0a6abda..1b5f79f6ce 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java @@ -25,6 +25,7 @@ import se.llbit.chunky.world.Clouds; import se.llbit.chunky.world.SkymapTexture; import se.llbit.chunky.world.material.CloudMaterial; +import se.llbit.chunky.world.material.VolumeCloudMaterial; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; @@ -34,6 +35,7 @@ import se.llbit.resources.ImageLoader; import se.llbit.util.JsonSerializable; import se.llbit.util.JsonUtil; +import se.llbit.util.Pair; import se.llbit.util.annotation.NotNull; import se.llbit.util.annotation.Nullable; @@ -56,13 +58,6 @@ public class Sky implements JsonSerializable { */ 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; - /** * Minimum sky light intensity */ @@ -154,9 +149,8 @@ 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 ArrayList cloudLayers = new ArrayList<>(0); private double skyExposure = DEFAULT_INTENSITY; private double skyLightModifier = DEFAULT_INTENSITY; @@ -227,9 +221,7 @@ public void loadSkymap(String fileName, @Nullable File sceneDirectory) { * Set the sky equal to other sky. */ public void set(Sky other) { - cloudsEnabled = other.cloudsEnabled; - cloudOffset.set(other.cloudOffset); - cloudSize = other.cloudSize; + cloudLayers = new ArrayList<>(other.cloudLayers); skymapFileName = other.skymapFileName; skymap = other.skymap; yaw = other.yaw; @@ -632,18 +624,16 @@ public void setSkyCacheResolution(int resolution) { sky.add("apparentSkyLight", apparentSkyLightModifier); sky.add("mode", mode.name()); sky.add("horizonOffset", horizonOffset); - sky.add("cloudsEnabled", cloudsEnabled); - sky.add("cloudSize", cloudSize); - sky.add("cloudOffset", cloudOffset.toJson()); + JsonArray cloudLayersJson = new JsonArray(); + for (CloudLayer layer : cloudLayers) { + cloudLayersJson.add(layer.toJson()); + } + sky.add("cloudLayers", cloudLayersJson); // Always save gradient. sky.add("gradient", gradientJson(gradient)); - JsonObject colorObj = new JsonObject(); - colorObj.add("red", color.x); - colorObj.add("green", color.y); - colorObj.add("blue", color.z); - sky.add("color", colorObj); + sky.add("color", ColorUtil.rgbToJson(color)); switch (mode) { case SKYMAP_EQUIRECTANGULAR: @@ -694,10 +684,30 @@ public void importFromJson(JsonObject json) { mode = SkyMode.SKYMAP_ANGULAR; } 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("cloudLayers").isUnknown()) { + boolean cloudsEnabled = json.get("cloudsEnabled").boolValue(false); + if (cloudsEnabled) { + double cloudSize = json.get("cloudSize").doubleValue(12); + Vector3 cloudOffset = JsonUtil.vec3FromJsonObject(json.get("cloudOffset").asObject()); + CloudLayer cloudLayer = new CloudLayer(); + cloudLayer.setCloudSizeX(cloudSize); + cloudLayer.setCloudSizeY(5); + cloudLayer.setCloudSizeZ(cloudSize); + cloudLayer.setCloudXOffset(cloudOffset.x); + cloudLayer.setCloudYOffset(cloudOffset.y); + cloudLayer.setCloudZOffset(cloudOffset.z); + cloudLayers.add(cloudLayer); + } + } else { + JsonArray jsonCloudLayers = json.get("cloudLayers").asArray(); + cloudLayers.clear(); + for (JsonValue layer : jsonCloudLayers) { + JsonObject layerObject = layer.asObject(); + CloudLayer cloudLayer = new CloudLayer(); + cloudLayer.importFromJson(layerObject); + cloudLayers.add(cloudLayer); + } } if (json.get("gradient").isArray()) { @@ -708,10 +718,7 @@ public void importFromJson(JsonObject json) { } if (json.get("color").isObject()) { - JsonObject colorObj = json.get("color").object(); - color.x = colorObj.get("red").doubleValue(1); - color.y = colorObj.get("green").doubleValue(1); - color.z = colorObj.get("blue").doubleValue(1); + color.set(ColorUtil.jsonToRGB(json.get("color").asObject())); } else { // Maintain backwards-compatibility with scenes saved in older Chunky versions color.set(JsonUtil.vec3FromJsonArray(json.get("color"))); @@ -911,275 +918,183 @@ public double getHorizonOffset() { return horizonOffset; } - public void setCloudSize(double newValue) { - if (newValue != cloudSize) { - cloudSize = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } + public void setColor(Vector3 color) { + this.color.set(color); + scene.refresh(); } - public double cloudSize() { - return cloudSize; + public Vector3 getColor() { + return color; } - public void setCloudXOffset(double newValue) { - if (newValue != cloudOffset.x) { - cloudOffset.x = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } + public Vector3 getCloudLayerColor(int index) { + return cloudLayers.get(index).getCloudColor(); } - /** - * Change the cloud height - */ - public void setCloudYOffset(double newValue) { - if (newValue != cloudOffset.y) { - cloudOffset.y = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } + public void setCloudLayerColor(int index, Vector3 color) { + cloudLayers.get(index).setCloudColor(color); + scene.refresh(); } - public void setCloudZOffset(double newValue) { - if (newValue != cloudOffset.z) { - cloudOffset.z = newValue; - if (cloudsEnabled) { - scene.refresh(); - } - } + + public boolean getCloudLayerVolumetricClouds(int index) { + return cloudLayers.get(index).getVolumetricClouds(); } - public double cloudXOffset() { - return cloudOffset.x; + public void setCloudLayerVolumetricClouds(int index, boolean value) { + cloudLayers.get(index).setVolumetricClouds(value); + scene.refresh(); } - /** - * @return The current cloud height - */ - public double cloudYOffset() { - return cloudOffset.y; + + public double getCloudLayerDensity(int index) { + return cloudLayers.get(index).getCloudDensity(); + } + + public void setCloudLayerDensity(int index, double value) { + cloudLayers.get(index).setCloudDensity(value); + scene.refresh(); + } + + + public double getCloudLayerSizeX(int index) { + return cloudLayers.get(index).getCloudSizeX(); + } + + public void setCloudLayerSizeX(int index, double newValue) { + cloudLayers.get(index).setCloudSizeX(newValue); + scene.refresh(); + } + + + public double getCloudLayerSizeY(int index) { + return cloudLayers.get(index).getCloudSizeY(); } - public double cloudZOffset() { - return cloudOffset.z; + public void setCloudLayerSizeY(int index, double newValue) { + cloudLayers.get(index).setCloudSizeY(newValue); + scene.refresh(); + } + + + public double getCloudLayerSizeZ(int index) { + return cloudLayers.get(index).getCloudSizeZ(); + } + + public void setCloudLayerSizeZ(int index, double newValue) { + cloudLayers.get(index).setCloudSizeZ(newValue); + scene.refresh(); + } + + + public double getCloudLayerXOffset(int index) { + return cloudLayers.get(index).getCloudXOffset(); + } + + public void setCloudLayerXOffset(int index, double newValue) { + cloudLayers.get(index).setCloudXOffset(newValue); + scene.refresh(); } /** - * Enable/disable clouds rendering. + * @return The current cloud height */ - public void setCloudsEnabled(boolean newValue) { - if (newValue != cloudsEnabled) { - cloudsEnabled = newValue; - scene.refresh(); - } + public double getCloudLayerYOffset(int index) { + return cloudLayers.get(index).getCloudYOffset(); } /** - * @return true if cloud rendering is enabled + * Change the cloud height */ - public boolean cloudsEnabled() { - return cloudsEnabled; - } - - public boolean cloudIntersection(Scene scene, Ray ray) { - 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 = 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; + public void setCloudLayerYOffset(int index, double newValue) { + cloudLayers.get(index).setCloudYOffset(newValue); + scene.refresh(); } - private static void enterCloud(Ray ray, double t) { - ray.t = t; - ray.color.set(CloudMaterial.color); - ray.setCurrentMaterial(CloudMaterial.INSTANCE); + + public double getCloudLayerZOffset(int index) { + return cloudLayers.get(index).getCloudZOffset(); } - private static void exitCloud(Ray ray, double t) { - ray.t = t; - ray.color.set(CloudMaterial.color); - ray.setCurrentMaterial(Air.INSTANCE); + public void setCloudLayerZOffset(int index, double newValue) { + cloudLayers.get(index).setCloudZOffset(newValue); + scene.refresh(); } - private static boolean inCloud(double x, double z) { - return Clouds.getCloud((int) Math.floor(x), (int) Math.floor(z)) == 1; + public float getCloudLayerEmittance(int index) { + return cloudLayers.get(index).getEmittance(); } - public void setColor(Vector3 color) { - this.color.set(color); + public void setCloudLayerEmittance(int index, float value) { + cloudLayers.get(index).setEmittance(value); scene.refresh(); } - public Vector3 getColor() { - return color; + public float getCloudLayerSpecular(int index) { + return cloudLayers.get(index).getSpecular(); + } + + public void setCloudLayerSpecular(int index, float value) { + cloudLayers.get(index).setSpecular(value); + scene.refresh(); + } + + public float getCloudLayerSmoothness(int index) { + return cloudLayers.get(index).getSmoothness(); + } + + public void setCloudLayerSmoothness(int index, float value) { + cloudLayers.get(index).setSmoothness(value); + scene.refresh(); + } + + public float getCloudLayerIor(int index) { + return cloudLayers.get(index).getIor(); + } + + public void setCloudLayerIor(int index, float value) { + cloudLayers.get(index).setIor(value); + scene.refresh(); + } + + public float getCloudLayerMetalness(int index) { + return cloudLayers.get(index).getMetalness(); + } + + public void setCloudLayerMetalness(int index, float value) { + cloudLayers.get(index).setMetalness(value); + scene.refresh(); + } + + public float getCloudLayerAnisotropy(int index) { + return cloudLayers.get(index).getAnisotropy(); + } + + public void setCloudLayerAnisotropy(int index, float value) { + cloudLayers.get(index).setAnisotropy(value); + scene.refresh(); + } + + public void addCloudLayer() { + cloudLayers.add(new CloudLayer()); + scene.refresh(); + } + + public void removeCloudLayer(int index) { + cloudLayers.remove(index); + scene.refresh(); + } + + public int getNumCloudLayers() { + return cloudLayers.size(); + } + + public boolean cloudIntersection(Scene scene, Ray ray, Random random) { + boolean hit = false; + for (CloudLayer layer : cloudLayers) { + hit |= layer.intersect(scene, ray, random); + } + return hit; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SphericalFogVolume.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SphericalFogVolume.java new file mode 100644 index 0000000000..d8d3313937 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SphericalFogVolume.java @@ -0,0 +1,161 @@ +package se.llbit.chunky.renderer.scene; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.json.JsonObject; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class SphericalFogVolume extends FogVolume { + 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 Sphere sphere; + + @Override + public boolean intersect(Scene scene, Ray ray, Random random) { + double distance; + double fogPenetrated = -Math.log(1 - random.nextDouble()); + double fogDistance = fogPenetrated / density; + Sphere sphereTranslated = sphere.getTranslated(-scene.origin.x, -scene.origin.y, -scene.origin.z); + if (!sphereTranslated.isInside(ray.o)) { + Ray test = new Ray(ray); + if (sphereTranslated.intersect(test)) { + distance = test.t; + distance += fogDistance; + } else { + return false; + } + } else { + distance = fogDistance; + } + if (distance >= ray.t) { + return false; + } + + Vector3 o = new Vector3(ray.o); + o.scaleAdd(distance, ray.d); + if (sphereTranslated.isInside(o)) { + ray.t = distance; + // pick a random normal vector based on a spherical particle. + // This is done only to prevent fog from looking ugly in the render preview and + // should have no effect on the path-traced render. + setRandomNormal(ray, random); + setRayMaterialAndColor(ray); + ray.specular = false; + return true; + } else { + return false; + } + } + + public Vector3 getCenter() { + return new Vector3(this.sphere.center); + } + + public void setCenter(Vector3 center) { + this.sphere = new Sphere(center, this.sphere.radius); + } + + public void setCenterX(double value) { + setCenter(new Vector3(value, this.sphere.center.y, this.sphere.center.z)); + } + + public void setCenterY(double value) { + setCenter(new Vector3(this.sphere.center.x, value, this.sphere.center.z)); + } + + public void setCenterZ(double value) { + setCenter(new Vector3(this.sphere.center.x, this.sphere.center.y, value)); + } + + + public double getRadius() { + return this.sphere.radius; + } + + public void setRadius(double value) { + this.sphere = new Sphere(this.sphere.center, value); + } + + public SphericalFogVolume(Vector3 color, double density, Vector3 center, double radius) { + type = FogVolumeType.SPHERE; + this.sphere = new Sphere(new Vector3(center), radius); + this.color = new Vector3(color); + this.density = density; + } + + public SphericalFogVolume(Vector3 color, double density) { + this(color, density, new Vector3(DEFAULT_X, DEFAULT_Y, DEFAULT_Z), DEFAULT_RADIUS); + } + + @Override + public JsonObject volumeSpecificPropertiesToJson() { + JsonObject properties = new JsonObject(); + JsonObject position = new JsonObject(); + position.add("x", this.sphere.center.x); + position.add("y", this.sphere.center.y); + position.add("z", this.sphere.center.z); + properties.add("position", position); + properties.add("radius", this.sphere.radius); + return properties; + } + + @Override + public void importVolumeSpecificProperties(JsonObject json) { + JsonObject position = json.get("position").object(); + double x = position.get("x").doubleValue(this.sphere.center.x); + double y = position.get("y").doubleValue(this.sphere.center.y); + double z = position.get("z").doubleValue(this.sphere.center.z); + double radius = json.get("radius").doubleValue(this.sphere.radius); + this.sphere = new Sphere(new Vector3(x, y, z), radius); + } + + // FogVolume-specific sphere implementation. + private static class Sphere { + double radius; + Vector3 center; + + Sphere(Vector3 center, double radius) { + this.center = new Vector3(center); + this.radius = radius; + } + + boolean isInside(Vector3 point) { + double distance = new Vector3(center.x - point.x, center.y - point.y, center.z - point.z).length(); + return distance <= radius; + } + + boolean intersect(Ray ray) { + Vector3 rayOrigin = new Vector3(ray.o); + Vector3 rayDirection = new Vector3(ray.d); + + double t = rayDirection.dot(new Vector3(center.x - rayOrigin.x, center.y - rayOrigin.y, center.z - rayOrigin.z)); + if (t < 0) { + return false; + } + + Vector3 tPoint = new Vector3(rayOrigin); + tPoint.scaleAdd(t, rayDirection); + + double tCenterDistance = new Vector3(center.x - tPoint.x, center.y - tPoint.y, center.z - tPoint.z).length(); + + if (isInside(tPoint)) { + double x = FastMath.sqrt(radius * radius - tCenterDistance * tCenterDistance); + double t1 = t - x; + double t2 = t + x; + if (t1 >= 0) { + ray.t = t1; + return true; + } + } + return false; + } + + Sphere getTranslated(double x, double y, double z) { + return new Sphere(new Vector3(center.x + x, center.y + y, center.z + z), this.radius); + } + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java index a368620aa4..277f4140c4 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java @@ -24,6 +24,7 @@ import se.llbit.chunky.renderer.Refreshable; import se.llbit.chunky.resources.Texture; import se.llbit.json.JsonObject; +import se.llbit.math.ColorUtil; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; @@ -61,6 +62,36 @@ public class Sun implements JsonSerializable { */ public static final double MAX_APPARENT_BRIGHTNESS = 50; + /** + * Default probability for diffuse sun sampling + */ + public static final double DEFAULT_DIFFUSE_SAMPLE_CHANCE = 0.1; + + /** + * Minimum probability for diffuse sun sampling + */ + public static final double MIN_DIFFUSE_SAMPLE_CHANCE = 0.001; + + /** + * Maximum probability for diffuse sun sampling + */ + public static final double MAX_DIFFUSE_SAMPLE_CHANCE = 0.9; + + /** + * Default radius (relative to sun) for diffuse sun sampling + */ + public static final double DEFAULT_DIFFUSE_SAMPLE_RADIUS = 1.2; + + /** + * Minimum radius (relative to sun) for diffuse sun sampling + */ + public static final double MIN_DIFFUSE_SAMPLE_RADIUS = 0.1; + + /** + * Maximum radius (relative to sun) for diffuse sun sampling + */ + public static final double MAX_DIFFUSE_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},}; @@ -137,6 +168,9 @@ public class Sun implements JsonSerializable { private Vector3 apparentTextureBrightness = new Vector3(1, 1, 1); private boolean enableTextureModification = false; + private double diffuseSampleChance = DEFAULT_DIFFUSE_SAMPLE_CHANCE; + private double diffuseSampleRadius = DEFAULT_DIFFUSE_SAMPLE_RADIUS; + private double azimuth = Math.PI / 2.5; private double altitude = Math.PI / 3; @@ -202,6 +236,8 @@ public void set(Sun other) { radius = other.radius; enableTextureModification = other.enableTextureModification; luminosityPdf = other.luminosityPdf; + diffuseSampleRadius = other.diffuseSampleRadius; + diffuseSampleChance = other.diffuseSampleChance; initSun(); } @@ -465,16 +501,12 @@ public void getRandomSunDirection(Ray reflected, Random random) { sun.add("apparentBrightness", apparentBrightness); sun.add("radius", radius); sun.add("modifySunTexture", enableTextureModification); - JsonObject colorObj = new JsonObject(); - colorObj.add("red", color.x); - colorObj.add("green", color.y); - colorObj.add("blue", color.z); - sun.add("color", colorObj); - JsonObject apparentColorObj = new JsonObject(); - apparentColorObj.add("red", apparentColor.x); - apparentColorObj.add("green", apparentColor.y); - apparentColorObj.add("blue", apparentColor.z); - sun.add("apparentColor", apparentColorObj); + sun.add("color", ColorUtil.rgbToJson(color)); + sun.add("apparentColor", ColorUtil.rgbToJson(apparentColor)); + JsonObject diffuseSamplingObj = new JsonObject(); + diffuseSamplingObj.add("chance", diffuseSampleChance); + diffuseSamplingObj.add("radius", diffuseSampleRadius); + sun.add("diffuseSampling", diffuseSamplingObj); sun.add("drawTexture", drawTexture); return sun; } @@ -489,17 +521,17 @@ public void importFromJson(JsonObject json) { enableTextureModification = json.get("modifySunTexture").boolValue(enableTextureModification); if (json.get("color").isObject()) { - JsonObject colorObj = json.get("color").object(); - color.x = colorObj.get("red").doubleValue(1); - color.y = colorObj.get("green").doubleValue(1); - color.z = colorObj.get("blue").doubleValue(1); + color.set(ColorUtil.jsonToRGB(json.get("color").asObject())); } if (json.get("apparentColor").isObject()) { - JsonObject apparentColorObj = json.get("apparentColor").object(); - apparentColor.x = apparentColorObj.get("red").doubleValue(1); - apparentColor.y = apparentColorObj.get("green").doubleValue(1); - apparentColor.z = apparentColorObj.get("blue").doubleValue(1); + apparentColor.set(ColorUtil.jsonToRGB(json.get("apparentColor").asObject())); + } + + if(json.get("diffuseSampling").isObject()) { + JsonObject diffuseSamplingObj = json.get("diffuseSampling").object(); + diffuseSampleChance = diffuseSamplingObj.get("chance").doubleValue(DEFAULT_DIFFUSE_SAMPLE_CHANCE); + diffuseSampleRadius = diffuseSamplingObj.get("radius").doubleValue(DEFAULT_DIFFUSE_SAMPLE_RADIUS); } drawTexture = json.get("drawTexture").boolValue(drawTexture); @@ -528,4 +560,18 @@ public void setDrawTexture(boolean value) { public boolean drawTexture() { return drawTexture; } + + public double getDiffuseSampleChance() { return diffuseSampleChance; } + + public void setDiffuseSampleChance(double d) { + diffuseSampleChance = d; + scene.refresh(); + } + + public double getDiffuseSampleRadius() { return diffuseSampleRadius; } + + public void setDiffuseSampleRadius(double d) { + diffuseSampleRadius = d; + scene.refresh(); + } } 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..78b3f0c158 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/RenderControlsFxController.java @@ -98,6 +98,7 @@ private void buildTabs() { tabs.add(new GeneralTab()); tabs.add(new LightingTab()); tabs.add(new SkyTab()); + tabs.add(new FogTab()); tabs.add(new WaterTab()); tabs.add(new CameraTab()); tabs.add(new EntitiesTab()); diff --git a/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeTypeSelectorDialog.java b/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeTypeSelectorDialog.java new file mode 100644 index 0000000000..46dc73d41a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/dialogs/FogVolumeTypeSelectorDialog.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.FogVolumeType; + +public class FogVolumeTypeSelectorDialog extends Dialog { + protected ChoiceBox choiceBox = new ChoiceBox<>(); + + public FogVolumeTypeSelectorDialog() { + this.setTitle("Select fog volume type"); + + DialogPane dialogPane = this.getDialogPane(); + VBox vBox = new VBox(); + + choiceBox.getItems().addAll(FogVolumeType.values()); + + vBox.getChildren().add(choiceBox); + vBox.setPadding(new Insets(10)); + + dialogPane.setContent(vBox); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + } + + public FogVolumeType getType() { + return choiceBox.getSelectionModel().getSelectedItem(); + } +} 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..1780796b02 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(); 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 8dc587d111..012d8d47e5 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 @@ -163,6 +163,7 @@ public PictureExportFormat fromString(String string) { transmissivityCap.setVisible(newValue); transmissivityCap.setManaged(newValue); }); + boolean tcapVisible = scene != null && scene.getFancierTranslucency(); transmissivityCap.setVisible(tcapVisible); transmissivityCap.setManaged(tcapVisible); 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..730f45d125 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/FogTab.java @@ -0,0 +1,568 @@ +/* Copyright (c) 2016 - 2021 Jesper Öqvist + * Copyright (c) 2016 - 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.ui.render.tabs; + +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.layout.*; +import se.llbit.chunky.renderer.scene.*; +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.FogVolumeTypeSelectorDialog; +import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; +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.AABB; +import se.llbit.math.ColorUtil; +import se.llbit.math.Vector3; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class FogTab extends ScrollPane implements RenderControlsTab, Initializable { + private Scene scene; + + private static class FogVolumeData { + final String type; + + FogVolumeData(FogVolume fogVolume) { + this.type = fogVolume.getType().name(); + } + } + + @FXML private ComboBox fogMode; + @FXML private TitledPane fogDetailsPane; + @FXML private VBox fogDetailsBox; + @FXML private CheckBox previewParticleFog; + @FXML private TableView fogVolumeTable; + @FXML private TableColumn typeCol; + @FXML private Button addVolume; + @FXML private Button removeVolume; + @FXML private VBox volumeSpecificControls; + + private final UniformFogSettings uniformFogSettings = new UniformFogSettings(); + private final LayeredFogSettings layeredFogSettings = new LayeredFogSettings(); + private final FogVolumeTypeSelectorDialog fogVolumeTypeSelectorDialog = new FogVolumeTypeSelectorDialog(); + + public FogTab() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("FogTab.fxml")); + loader.setRoot(this); + loader.setController(this); + loader.load(); + } + + @Override public void setController(RenderControlsFxController controller) { + scene = controller.getRenderController().getSceneManager().getScene(); + uniformFogSettings.setRenderController(controller.getRenderController()); + layeredFogSettings.setRenderController(controller.getRenderController()); + } + + @Override public void initialize(URL location, ResourceBundle resources) { + previewParticleFog.setTooltip(new Tooltip("Render particle fog in the render preview.")); + previewParticleFog.selectedProperty().addListener((observer, oldValue, newValue) -> + scene.setPreviewParticleFog(newValue) + ); + 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(); + }); + typeCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().type)); + typeCol.setSortable(false); + + addVolume.setOnAction(e -> { + if (fogVolumeTypeSelectorDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + FogVolumeType type = fogVolumeTypeSelectorDialog.getType(); + scene.fog.addVolume(type); + rebuildList(); + fogVolumeTable.getSelectionModel().selectLast(); + } + }); + + removeVolume.setOnAction(e -> { + int index = fogVolumeTable.getSelectionModel().getSelectedIndex(); + scene.fog.removeVolume(index); + rebuildList(); + }); + } + + @Override public void update(Scene scene) { + fogMode.getSelectionModel().select(scene.fog.getFogMode()); + uniformFogSettings.update(scene); + layeredFogSettings.update(scene); + previewParticleFog.setSelected(scene.getPreviewParticleFog()); + rebuildList(); + } + + private void rebuildList() { + fogVolumeTable.getSelectionModel().clearSelection(); + fogVolumeTable.getItems().clear(); + for (FogVolume fogVolume : scene.fog.getFogVolumes()) { + FogVolumeData fogVolumeData = new FogVolumeData(fogVolume); + fogVolumeTable.getItems().add(fogVolumeData); + } + } + + private void updateControls() { + volumeSpecificControls.getChildren().clear(); + if (!fogVolumeTable.getSelectionModel().isEmpty()) { + int index = fogVolumeTable.getSelectionModel().getSelectedIndex(); + FogVolume fogVolume = scene.fog.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.getColor())); + luxColorPicker.colorProperty().addListener( + (observable, oldValue, newValue) -> { + fogVolume.setColor(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.getDensity()); + density.onValueChange(value -> { + fogVolume.setDensity(value); + 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.getAnisotropy()); + anisotropy.setRange(-1, 1); + anisotropy.clampBoth(); + anisotropy.onValueChange(value -> { + fogVolume.setAnisotropy(value.floatValue()); + scene.refresh(); + }); + + DoubleAdjuster emittance = new DoubleAdjuster(); + emittance.setName("Emittance"); + emittance.setRange(0, 100); + emittance.clampMin(); + emittance.set(fogVolume.getEmittance()); + emittance.onValueChange(value -> { + fogVolume.setEmittance(value.floatValue()); + scene.refresh(); + }); + + Separator separator = new Separator(); + + volumeSpecificControls.getChildren().addAll( + fogColorPickerBox, + density, + anisotropy, + emittance, + separator + ); + + ColumnConstraints labelConstraints = new ColumnConstraints(); + labelConstraints.setHgrow(Priority.NEVER); + labelConstraints.setPrefWidth(90); + ColumnConstraints posFieldConstraints = new ColumnConstraints(); + posFieldConstraints.setMinWidth(20); + posFieldConstraints.setPrefWidth(90); + + switch (fogVolume.getType()) { + case EXPONENTIAL: { + ExponentialFogVolume exponentialFogVolume = (ExponentialFogVolume) fogVolume; + DoubleAdjuster scaleHeight = new DoubleAdjuster(); + scaleHeight.setName("Height scale"); + scaleHeight.setTooltip("Scales the vertical distribution of the fog"); + scaleHeight.setRange(1, 50); + scaleHeight.set(exponentialFogVolume.getScaleHeight()); + scaleHeight.onValueChange(value -> { + exponentialFogVolume.setScaleHeight(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(exponentialFogVolume.getYOffset()); + yOffset.onValueChange(value -> { + exponentialFogVolume.setYOffset(value); + scene.refresh(); + }); + + volumeSpecificControls.getChildren().addAll(scaleHeight, yOffset); + break; + } + case LAYER: { + LayerFogVolume layerFogVolume = (LayerFogVolume) fogVolume; + DoubleAdjuster layerBreadth = new DoubleAdjuster(); + layerBreadth.setName("Layer thickness"); + layerBreadth.setTooltip("Scales the vertical distribution of the fog"); + layerBreadth.setRange(0.001, 100); + layerBreadth.set(layerFogVolume.getLayerBreadth()); + layerBreadth.clampMin(); + layerBreadth.onValueChange(value -> { + layerFogVolume.setLayerBreadth(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(layerFogVolume.getYOffset()); + yOffset.onValueChange(value -> { + layerFogVolume.setYOffset(value); + scene.refresh(); + }); + + volumeSpecificControls.getChildren().addAll(layerBreadth, yOffset); + break; + } + case SPHERE: { + SphericalFogVolume sphericalFogVolume = (SphericalFogVolume) fogVolume; + + 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)")); + + Vector3 center = sphericalFogVolume.getCenter(); + posX.valueProperty().setValue(center.x); + posY.valueProperty().setValue(center.y); + posZ.valueProperty().setValue(center.z); + + posX.valueProperty().addListener( + (observable, oldValue, newValue) -> { + sphericalFogVolume.setCenterX(newValue.doubleValue()); + scene.refresh(); + }); + posY.valueProperty().addListener( + (observable, oldValue, newValue) -> { + sphericalFogVolume.setCenterY(newValue.doubleValue()); + scene.refresh(); + }); + posZ.valueProperty().addListener( + (observable, oldValue, newValue) -> { + sphericalFogVolume.setCenterZ(newValue.doubleValue()); + scene.refresh(); + }); + + 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(); + posX.valueProperty().setValue(cameraPosition.x); + posY.valueProperty().setValue(cameraPosition.y); + posZ.valueProperty().setValue(cameraPosition.z); + scene.refresh(); + }); + + Button toTarget = new Button(); + toTarget.setText("To target"); + toTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + posX.valueProperty().setValue(targetPosition.x); + posY.valueProperty().setValue(targetPosition.y); + posZ.valueProperty().setValue(targetPosition.z); + scene.refresh(); + } + }); + + 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(sphericalFogVolume.getRadius()); + radius.clampMin(); + radius.onValueChange(value -> { + sphericalFogVolume.setRadius(value); + scene.refresh(); + }); + + volumeSpecificControls.getChildren().addAll(gridPane, hBox, radius); + break; + } + case CUBOID: { + CuboidFogVolume cuboidFogVolume = (CuboidFogVolume) fogVolume; + + 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")); + + AABB bounds = cuboidFogVolume.getBounds(); + x1.valueProperty().setValue(bounds.xmin); + y1.valueProperty().setValue(bounds.ymin); + z1.valueProperty().setValue(bounds.zmin); + x2.valueProperty().setValue(bounds.xmax); + y2.valueProperty().setValue(bounds.ymax); + z2.valueProperty().setValue(bounds.zmax); + + x1.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(newValue.doubleValue(), x2.valueProperty().doubleValue()), + Math.max(newValue.doubleValue(), x2.valueProperty().doubleValue()), + Math.min(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.max(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.min(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()), + Math.max(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()) + ); + scene.refresh(); + }); + y1.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.max(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.min(newValue.doubleValue(), y2.valueProperty().doubleValue()), + Math.max(newValue.doubleValue(), y2.valueProperty().doubleValue()), + Math.min(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()), + Math.max(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()) + ); + scene.refresh(); + }); + z1.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.max(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.min(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.max(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.min(newValue.doubleValue(), z2.valueProperty().doubleValue()), + Math.max(newValue.doubleValue(), z2.valueProperty().doubleValue()) + ); + scene.refresh(); + }); + x2.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(x1.valueProperty().doubleValue(), newValue.doubleValue()), + Math.max(x1.valueProperty().doubleValue(), newValue.doubleValue()), + Math.min(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.max(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.min(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()), + Math.max(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()) + ); + scene.refresh(); + }); + y2.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.max(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.min(y1.valueProperty().doubleValue(), newValue.doubleValue()), + Math.max(y1.valueProperty().doubleValue(), newValue.doubleValue()), + Math.min(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()), + Math.max(z1.valueProperty().doubleValue(), z2.valueProperty().doubleValue()) + ); + scene.refresh(); + }); + z2.valueProperty().addListener( + (observable, oldValue, newValue) -> { + cuboidFogVolume.setBounds( + Math.min(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.max(x1.valueProperty().doubleValue(), x2.valueProperty().doubleValue()), + Math.min(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.max(y1.valueProperty().doubleValue(), y2.valueProperty().doubleValue()), + Math.min(z1.valueProperty().doubleValue(), newValue.doubleValue()), + Math.max(z1.valueProperty().doubleValue(), 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(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(); + x1.valueProperty().setValue(cameraPosition.x); + y1.valueProperty().setValue(cameraPosition.y); + z1.valueProperty().setValue(cameraPosition.z); + scene.refresh(); + }); + + Button pos1ToTarget = new Button(); + pos1ToTarget.setText("To target"); + pos1ToTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + x1.valueProperty().setValue(targetPosition.x); + y1.valueProperty().setValue(targetPosition.y); + z1.valueProperty().setValue(targetPosition.z); + scene.refresh(); + } + }); + + Button pos2ToCamera = new Button(); + pos2ToCamera.setText("To camera"); + pos2ToCamera.setOnAction(event -> { + Vector3 cameraPosition = scene.camera().getPosition(); + x2.valueProperty().setValue(cameraPosition.x); + y2.valueProperty().setValue(cameraPosition.y); + z2.valueProperty().setValue(cameraPosition.z); + scene.refresh(); + }); + + Button pos2ToTarget = new Button(); + pos2ToTarget.setText("To target"); + pos2ToTarget.setOnAction(event -> { + Vector3 targetPosition = scene.getTargetPosition(); + if (targetPosition != null) { + x2.valueProperty().setValue(targetPosition.x); + y2.valueProperty().setValue(targetPosition.y); + z2.valueProperty().setValue(targetPosition.z); + scene.refresh(); + } + }); + + 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); + + volumeSpecificControls.getChildren().addAll(gridPane1, hBox1, gridPane2, hBox2); + break; + } + } + } + } + + @Override public String getTabTitle() { + return "Fog"; + } + + @Override public Node 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 index bedf302107..fad34d5742 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java @@ -55,6 +55,9 @@ public class LightingTab extends ScrollPane implements RenderControlsTab, Initia @FXML private DoubleAdjuster sunIntensity; @FXML private CheckBox drawSun; @FXML private ComboBox sunSamplingStrategy; + @FXML private TitledPane diffuseSamplingDetailsPane; + @FXML private DoubleAdjuster diffuseSampleChance; + @FXML private DoubleAdjuster diffuseSampleRadius; @FXML private DoubleAdjuster sunLuminosity; @FXML private DoubleAdjuster apparentSunBrightness; @FXML private DoubleAdjuster sunRadius; @@ -134,9 +137,33 @@ public LightingTab() throws IOException { sunSamplingStrategy.getItems().addAll(SunSamplingStrategy.values()); sunSamplingStrategy.getSelectionModel().selectedItemProperty().addListener( - (observable, oldValue, newValue) -> scene.setSunSamplingStrategy(newValue)); + (observable, oldValue, newValue) -> { + scene.setSunSamplingStrategy(newValue); + + boolean visible = scene != null && scene.getSunSamplingStrategy().isDiffuseSampling(); + diffuseSamplingDetailsPane.setVisible(visible); + diffuseSamplingDetailsPane.setExpanded(visible); + diffuseSamplingDetailsPane.setManaged(visible); + }); sunSamplingStrategy.setTooltip(new Tooltip("Determines how the sun is sampled at each bounce.")); + boolean visible = scene != null && scene.getSunSamplingStrategy().isDiffuseSampling(); + diffuseSamplingDetailsPane.setVisible(visible); + diffuseSamplingDetailsPane.setExpanded(visible); + diffuseSamplingDetailsPane.setManaged(visible); + + diffuseSampleChance.setName("Diffuse sample chance"); + diffuseSampleChance.setTooltip("Probability of sampling the sun on each diffuse bounce"); + diffuseSampleChance.setRange(Sun.MIN_DIFFUSE_SAMPLE_CHANCE, Sun.MAX_DIFFUSE_SAMPLE_CHANCE); + diffuseSampleChance.clampBoth(); + diffuseSampleChance.onValueChange(value -> scene.sun().setDiffuseSampleChance(value)); + + diffuseSampleRadius.setName("Diffuse sample radius"); + diffuseSampleRadius.setTooltip("Radius of possible sun sampling bounces (relative to the sun's radius)"); + diffuseSampleRadius.setRange(Sun.MIN_DIFFUSE_SAMPLE_RADIUS, Sun.MAX_DIFFUSE_SAMPLE_RADIUS); + diffuseSampleRadius.clampMin(); + diffuseSampleRadius.onValueChange(value -> scene.sun().setDiffuseSampleRadius(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); @@ -203,6 +230,8 @@ public void setController(RenderControlsFxController controller) { sunAltitude.set(QuickMath.radToDeg(scene.sun().getAltitude())); enableEmitters.setSelected(scene.getEmittersEnabled()); sunSamplingStrategy.getSelectionModel().select(scene.getSunSamplingStrategy()); + diffuseSampleChance.set(scene.sun().getDiffuseSampleChance()); + diffuseSampleRadius.set(scene.sun().getDiffuseSampleRadius()); drawSun.setSelected(scene.sun().drawTexture()); sunColor.colorProperty().removeListener(sunColorListener); sunColor.setColor(ColorUtil.toFx(scene.sun().getColor())); 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..10a4e1efd6 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 @@ -87,8 +87,7 @@ public MaterialsTab() { settings.setSpacing(10); settings.getChildren().addAll( new Label("Material Properties"), - emittance, specular, perceptualSmoothness, ior, metalness, - new Label("(set to zero to disable)")); + emittance, specular, perceptualSmoothness, ior, metalness); setPadding(new Insets(10)); setSpacing(15); TextField filterField = new TextField(); diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java index 4bed610878..95b42422ce 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/SkyTab.java @@ -25,6 +25,7 @@ import javafx.fxml.Initializable; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ComboBox; @@ -35,7 +36,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.StringConverter; -import se.llbit.chunky.renderer.scene.FogMode; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.renderer.scene.SimulatedSky; import se.llbit.chunky.renderer.scene.Sky; @@ -43,8 +43,6 @@ 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; @@ -63,14 +61,25 @@ public class SkyTab extends ScrollPane implements RenderControlsTab, Initializab @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 ComboBox cloudLayers; + @FXML private Button addCloudLayer; + @FXML private Button removeCloudLayer; + @FXML private TitledPane cloudDetailsPane; + @FXML private DoubleAdjuster cloudSizeX; + @FXML private DoubleAdjuster cloudSizeY; + @FXML private DoubleAdjuster cloudSizeZ; + @FXML private DoubleAdjuster cloudOffsetX; + @FXML private DoubleAdjuster cloudOffsetY; + @FXML private DoubleAdjuster cloudOffsetZ; + @FXML private LuxColorPicker cloudColor; + @FXML private CheckBox enableVolumetricClouds; + @FXML private DoubleAdjuster cloudDensity; + @FXML private DoubleAdjuster emittance; + @FXML private DoubleAdjuster specular; + @FXML private DoubleAdjuster smoothness; + @FXML private DoubleAdjuster ior; + @FXML private DoubleAdjuster metalness; + @FXML private DoubleAdjuster anisotropy; private final VBox simulatedSettings = new VBox(); private DoubleAdjuster horizonOffset = new DoubleAdjuster(); private ChoiceBox simulatedSky = new ChoiceBox<>(); @@ -79,11 +88,15 @@ public class SkyTab extends ScrollPane implements RenderControlsTab, Initializab 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 = (observable, oldValue, newValue) -> scene.sky().setColor(ColorUtil.fromFx(newValue)); + + private ChangeListener cloudColorListener = + (observable, oldValue, newValue) -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerColor(index, ColorUtil.fromFx(newValue)); + }; private EventHandler simSkyListener = event -> { int selected = simulatedSky.getSelectionModel().getSelectedIndex(); scene.sky().setSimulatedSkyMode(selected); @@ -100,8 +113,6 @@ public SkyTab() throws IOException { scene = controller.getRenderController().getSceneManager().getScene(); skyboxSettings.setRenderController(controller.getRenderController()); skymapSettings.setRenderController(controller.getRenderController()); - uniformFogSettings.setRenderController(controller.getRenderController()); - layeredFogSettings.setRenderController(controller.getRenderController()); } @Override public void initialize(URL location, ResourceBundle resources) { @@ -137,41 +148,150 @@ 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)); - - 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)); - - 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; + cloudLayers.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> + updateControls() + ); + + addCloudLayer.setText("Add cloud layer"); + addCloudLayer.setOnAction(event -> { + scene.sky().addCloudLayer(); + disableControls(); + if (updateLayersList()) { + updateControls(); + } + cloudLayers.getSelectionModel().selectLast(); + }); + + removeCloudLayer.setText("Remove cloud layer"); + removeCloudLayer.setOnAction(event -> { + if (scene.sky().getNumCloudLayers() > 0) { + scene.sky().removeCloudLayer(cloudLayers.getSelectionModel().getSelectedIndex()); + disableControls(); + if (updateLayersList()) { + updateControls(); } } - fogDetailsPane.setExpanded(true); + }); + + cloudSizeX.setName("Cloud X scale"); + cloudSizeX.setTooltip("Scale of the X-dimension of the clouds, measured in blocks per pixel of clouds.png texture"); + cloudSizeX.setRange(0.01, 128); + cloudSizeX.clampMin(); + cloudSizeX.makeLogarithmic(); + cloudSizeX.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerSizeX(index, value); + }); + + cloudSizeY.setName("Cloud Y scale"); + cloudSizeY.setTooltip("Scale of the Y-dimension of the clouds, measured in blocks per pixel of clouds.png texture"); + cloudSizeY.setRange(0.01, 128); + cloudSizeY.clampMin(); + cloudSizeY.makeLogarithmic(); + cloudSizeY.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerSizeY(index, value); + }); + + cloudSizeZ.setName("Cloud Z scale"); + cloudSizeZ.setTooltip("Scale of the Z-dimension of the clouds, measured in blocks per pixel of clouds.png texture"); + cloudSizeZ.setRange(0.01, 128); + cloudSizeZ.clampMin(); + cloudSizeZ.makeLogarithmic(); + cloudSizeZ.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerSizeZ(index, value); + }); + + cloudOffsetX.setName("Cloud X offset"); + cloudOffsetX.setTooltip("Changes the X-offset of the clouds."); + cloudOffsetX.setRange(-256, 256); + cloudOffsetX.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerXOffset(index, value); + }); + + cloudOffsetY.setName("Cloud Y offset"); + cloudOffsetY.setTooltip("Changes the altitude of the clouds."); + cloudOffsetY.setRange(-64, 320); + cloudOffsetY.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerYOffset(index, value); + }); + + cloudOffsetZ.setName("Cloud Z offset"); + cloudOffsetZ.setTooltip("Changes the Z-offset of the clouds."); + cloudOffsetZ.setRange(-256, 256); + cloudOffsetZ.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerZOffset(index, value); + }); + + cloudColor.colorProperty().addListener(cloudColorListener); + + enableVolumetricClouds.setTooltip(new Tooltip("Use a volume scatter for the cloud material.")); + enableVolumetricClouds.selectedProperty().addListener((observable, oldValue, newValue) -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerVolumetricClouds(index, newValue); + disableControls(); + enableControls(newValue); + }); + + cloudDensity.setName("Volumetric cloud density"); + cloudDensity.setRange(0.000001, 1); + cloudDensity.clampMin(); + cloudDensity.setMaximumFractionDigits(6); + cloudDensity.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerDensity(index, value); + }); + + emittance.setName("Emittance"); + emittance.setRange(0, 100); + emittance.clampMin(); + emittance.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerEmittance(index, value.floatValue()); + }); + + specular.setName("Specular"); + specular.setRange(0, 1); + specular.clampBoth(); + specular.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerSpecular(index, value.floatValue()); + }); + + smoothness.setName("Smoothness"); + smoothness.setRange(0, 1); + smoothness.clampBoth(); + smoothness.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerSmoothness(index, value.floatValue()); + }); + + ior.setName("IoR"); + ior.setRange(0, 5); + ior.clampMin(); + ior.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerIor(index, value.floatValue()); + }); + + metalness.setName("Metalness"); + metalness.setRange(0, 1); + metalness.clampBoth(); + metalness.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerMetalness(index, value.floatValue()); + }); + + anisotropy.setName("Anisotropy"); + anisotropy.setRange(-1, 1); + anisotropy.clampBoth(); + anisotropy.onValueChange(value -> { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + scene.sky().setCloudLayerAnisotropy(index, value.floatValue()); }); skyMode.setTooltip(new Tooltip("Set the type of sky to be used in the scene.")); @@ -213,26 +333,96 @@ 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); } + private void disableControls() { + cloudSizeX.setDisable(true); + cloudSizeY.setDisable(true); + cloudSizeZ.setDisable(true); + cloudOffsetX.setDisable(true); + cloudOffsetY.setDisable(true); + cloudOffsetZ.setDisable(true); + cloudColor.setDisable(true); + enableVolumetricClouds.setDisable(true); + cloudDensity.setDisable(true); + emittance.setDisable(true); + specular.setDisable(true); + smoothness.setDisable(true); + ior.setDisable(true); + metalness.setDisable(true); + anisotropy.setDisable(true); + } + + private void enableControls(boolean volumetricClouds) { + cloudSizeX.setDisable(false); + cloudSizeY.setDisable(false); + cloudSizeZ.setDisable(false); + cloudOffsetX.setDisable(false); + cloudOffsetY.setDisable(false); + cloudOffsetZ.setDisable(false); + cloudColor.setDisable(false); + enableVolumetricClouds.setDisable(false); + emittance.setDisable(false); + if (!volumetricClouds) { + specular.setDisable(false); + smoothness.setDisable(false); + ior.setDisable(false); + metalness.setDisable(false); + } else { + cloudDensity.setDisable(false); + anisotropy.setDisable(false); + } + } + + private boolean updateLayersList() { + cloudLayers.getSelectionModel().clearSelection(); + cloudLayers.getItems().clear(); + int numLayers = scene.sky().getNumCloudLayers(); + boolean emptyLayers = !(numLayers > 0); + if (!emptyLayers) { + for (int i = 0; i < numLayers; i++) { + cloudLayers.getItems().add(String.format("Layer %d", i + 1)); + } + } + return emptyLayers; + } + + private void updateControls() { + if (!cloudLayers.getSelectionModel().isEmpty()) { + int index = cloudLayers.getSelectionModel().getSelectedIndex(); + cloudSizeX.set(scene.sky().getCloudLayerSizeX(index)); + cloudSizeY.set(scene.sky().getCloudLayerSizeY(index)); + cloudSizeZ.set(scene.sky().getCloudLayerSizeZ(index)); + cloudOffsetX.set(scene.sky().getCloudLayerXOffset(index)); + cloudOffsetY.set(scene.sky().getCloudLayerYOffset(index)); + cloudOffsetZ.set(scene.sky().getCloudLayerZOffset(index)); + cloudColor.colorProperty().removeListener(cloudColorListener); + cloudColor.setColor(ColorUtil.toFx(scene.sky().getCloudLayerColor(index))); + cloudColor.colorProperty().addListener(cloudColorListener); + boolean volumetricClouds = scene.sky().getCloudLayerVolumetricClouds(index); + enableVolumetricClouds.setSelected(volumetricClouds); + cloudDensity.set(scene.sky().getCloudLayerDensity(index)); + emittance.set(scene.sky().getCloudLayerEmittance(index)); + specular.set(scene.sky().getCloudLayerSpecular(index)); + smoothness.set(scene.sky().getCloudLayerSmoothness(index)); + ior.set(scene.sky().getCloudLayerIor(index)); + metalness.set(scene.sky().getCloudLayerMetalness(index)); + anisotropy.set(scene.sky().getCloudLayerAnisotropy(index)); + enableControls(volumetricClouds); + } + } + @Override public void update(Scene scene) { skyMode.getSelectionModel().select(scene.sky().getSkyMode()); simulatedSky.setOnAction(null); simulatedSky.getSelectionModel().select(scene.sky().getSimulatedSky()); simulatedSky.setOnAction(simSkyListener); - cloudsEnabled.setSelected(scene.sky().cloudsEnabled()); + disableControls(); + if (updateLayersList()) { + updateControls(); + } 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()); @@ -241,12 +431,10 @@ 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 "Sky"; } @Override public Node getTabContent() { diff --git a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java index f34613f649..25b1bf3e05 100644 --- a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java +++ b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java @@ -19,7 +19,6 @@ import se.llbit.chunky.block.minecraft.Candle; import se.llbit.chunky.entity.CalibratedSculkSensorAmethyst; import se.llbit.chunky.entity.Campfire; -import se.llbit.chunky.world.material.CloudMaterial; import java.util.HashMap; import java.util.Map; @@ -30,7 +29,6 @@ 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); @@ -39,8 +37,6 @@ public class ExtraMaterials { } public static void loadDefaultMaterialProperties() { - CloudMaterial.INSTANCE.restoreDefaults(); - Candle.flameMaterial.restoreDefaults(); Candle.flameMaterial.emittance = 1.0f; diff --git a/chunky/src/java/se/llbit/chunky/world/VolumeMaterial.java b/chunky/src/java/se/llbit/chunky/world/VolumeMaterial.java new file mode 100644 index 0000000000..41bd1e4be4 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/VolumeMaterial.java @@ -0,0 +1,36 @@ +package se.llbit.chunky.world; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.resources.Texture; +import se.llbit.math.Ray; +import se.llbit.math.Vector3; + +import java.util.Random; + +public class VolumeMaterial extends Material { + public float anisotropy = 0; + + public VolumeMaterial(String name, Texture texture) { + super(name, texture); + } + + public void setRandomSphericalNormal(Ray ray, Random random) { + // Set a random normal + Vector3 a1 = new Vector3(); + a1.cross(ray.d, new Vector3(0, 1, 0)); + a1.normalize(); + Vector3 a2 = new Vector3(); + a2.cross(ray.d, a1); + // 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; + double t1 = r * FastMath.cos(theta); + double t2 = r * FastMath.sin(theta); + a1.scale(t1); + a1.scaleAdd(t2, a2); + a1.scaleAdd(-Math.sqrt(1 - a1.lengthSquared()), ray.d); + ray.setNormal(a1); + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java index 9040b2dacd..863000bccf 100644 --- a/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java +++ b/chunky/src/java/se/llbit/chunky/world/material/CloudMaterial.java @@ -20,10 +20,7 @@ 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() { + public CloudMaterial() { super("cloud", Texture.air); } } diff --git a/chunky/src/java/se/llbit/chunky/world/material/ParticleFogMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/ParticleFogMaterial.java new file mode 100644 index 0000000000..8fee5a036e --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/material/ParticleFogMaterial.java @@ -0,0 +1,10 @@ +package se.llbit.chunky.world.material; + +import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.world.VolumeMaterial; + +public class ParticleFogMaterial extends VolumeMaterial { + public ParticleFogMaterial() { + super("particle_fog", Texture.air); + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/material/VolumeCloudMaterial.java b/chunky/src/java/se/llbit/chunky/world/material/VolumeCloudMaterial.java new file mode 100644 index 0000000000..5f2e4367cc --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/material/VolumeCloudMaterial.java @@ -0,0 +1,26 @@ +/* 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.VolumeMaterial; + +public class VolumeCloudMaterial extends VolumeMaterial { + public VolumeCloudMaterial() { + super("volume_cloud", Texture.air); + } +} diff --git a/chunky/src/java/se/llbit/math/AABB.java b/chunky/src/java/se/llbit/math/AABB.java index 34797d3514..dad0f918ff 100644 --- a/chunky/src/java/se/llbit/math/AABB.java +++ b/chunky/src/java/se/llbit/math/AABB.java @@ -333,7 +333,12 @@ public boolean hitTest(Ray ray) { } } - return tNear < tFar + Ray.EPSILON && tFar > 0; + if (tNear < tFar + Ray.EPSILON && tFar > 0) { + ray.tNext = tNear; + return true; + } else { + return false; + } } /** diff --git a/chunky/src/java/se/llbit/math/ColorUtil.java b/chunky/src/java/se/llbit/math/ColorUtil.java index 9d1d01ccd3..0b3e82b2cb 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. @@ -406,4 +407,20 @@ 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; + } } diff --git a/chunky/src/java/se/llbit/math/Ray.java b/chunky/src/java/se/llbit/math/Ray.java index 8539bb9ed7..6f98a7261d 100644 --- a/chunky/src/java/se/llbit/math/Ray.java +++ b/chunky/src/java/se/llbit/math/Ray.java @@ -21,6 +21,7 @@ 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.Sun; import se.llbit.chunky.world.Material; import java.util.Random; @@ -68,11 +69,6 @@ public class Ray { */ public Vector4 color = new Vector4(); - /** - * Emittance of previously intersected surface. - */ - public Vector3 emittance = new Vector3(); - /** * Previous material. */ @@ -149,7 +145,6 @@ public void setDefault() { currentMaterial = Air.INSTANCE; depth = 0; color.set(0, 0, 0, 0); - emittance.set(0, 0, 0); specular = true; } @@ -166,7 +161,6 @@ public void set(Ray other) { n.set(other.n); geomN.set(other.geomN); color.set(0, 0, 0, 0); - emittance.set(0, 0, 0); specular = other.specular; } @@ -267,7 +261,8 @@ public float[] getBiomeWaterColor(Scene scene) { /** * Set this ray to a random diffuse reflection of the input ray. */ - public final void diffuseReflection(Ray ray, Random random) { + public final void diffuseReflection(Ray ray, Random random, Scene scene) { + set(ray); // get random point on unit disk @@ -279,7 +274,99 @@ public final void diffuseReflection(Ray ray, Random random) { // 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); + double tz; // to be initialized later, after potentially changing tx and ty + + // diffuse sun sampling (importance sampling) + if(scene.getSunSamplingStrategy().isDiffuseSampling()) { + + // constants + final double sun_az = scene.sun().getAzimuth(); + final double sun_alt = scene.sun().getAltitude(); + 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().getDiffuseSampleRadius(); + double sample_chance = scene.sun().getDiffuseSampleChance(); + 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_DIFFUSE_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 || FastMath.abs(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; diff --git a/chunky/src/res/se/llbit/chunky/ui/render/tabs/FogTab.fxml b/chunky/src/res/se/llbit/chunky/ui/render/tabs/FogTab.fxml new file mode 100644 index 0000000000..7861538990 --- /dev/null +++ b/chunky/src/res/se/llbit/chunky/ui/render/tabs/FogTab.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + +