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 e908b82471..fc123e317b 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -160,6 +160,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add if (random.nextFloat() < pDiffuse) { // Diffuse reflection. + boolean wasFirstReflection = firstReflection; firstReflection = false; if (!scene.kill(ray.depth + 1, random)) { @@ -169,12 +170,20 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && currentMat.emittance > Ray.EPSILON) { - 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; + if (wasFirstReflection) { + ray.apparentBrightness.x = ray.color.x * ray.color.x * currentMat.apparentBrightness * scene.apparentEmitterBrightness * scene.emitterIntensity; + ray.apparentBrightness.y = ray.color.y * ray.color.y * currentMat.apparentBrightness * scene.apparentEmitterBrightness * scene.emitterIntensity; + ray.apparentBrightness.z = ray.color.z * ray.color.z * currentMat.apparentBrightness * scene.apparentEmitterBrightness * scene.emitterIntensity; + + } else { + ray.emittance.x = ray.color.x * ray.color.x * + currentMat.emittance * scene.emitterLightIntensity * scene.emitterIntensity; + ray.emittance.y = ray.color.y * ray.color.y * + currentMat.emittance * scene.emitterLightIntensity * scene.emitterIntensity; + ray.emittance.z = ray.color.z * ray.color.z * + currentMat.emittance * scene.emitterLightIntensity * scene.emitterIntensity; + } + hit = true; } else if(scene.emittersEnabled && scene.emitterSamplingStrategy != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() != null) { // Sample emitter @@ -232,11 +241,11 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add reflected.diffuseReflection(ray, random); hit = pathTrace(scene, reflected, state, 0, false) || hit; if (hit) { - ray.color.x = (addEmitted * ray.emittance.x) + ray.color.x * (directLightR * scene.sun.emittance.x + ( + ray.color.x = (addEmitted * ray.apparentBrightness.x) + (addEmitted * ray.emittance.x) + ray.color.x * (directLightR * scene.sun.emittance.x + ( reflected.color.x + reflected.emittance.x) + (indirectEmitterColor.x)); - ray.color.y = (addEmitted * ray.emittance.y) + ray.color.y * (directLightG * scene.sun.emittance.y + ( + ray.color.y = (addEmitted * ray.apparentBrightness.y) + (addEmitted * ray.emittance.y) + ray.color.y * (directLightG * scene.sun.emittance.y + ( reflected.color.y + reflected.emittance.y) + (indirectEmitterColor.y)); - ray.color.z = (addEmitted * ray.emittance.z) + ray.color.z * (directLightB * scene.sun.emittance.z + ( + ray.color.z = (addEmitted * ray.apparentBrightness.z) + (addEmitted * ray.emittance.z) + ray.color.z * (directLightB * scene.sun.emittance.z + ( reflected.color.z + reflected.emittance.z) + (indirectEmitterColor.z)); } else if(indirectEmitterColor.x > Ray.EPSILON || indirectEmitterColor.y > Ray.EPSILON || indirectEmitterColor.z > Ray.EPSILON) { hit = true; @@ -251,11 +260,11 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add hit = pathTrace(scene, reflected, state, 0, false) || hit; if (hit) { ray.color.x = - (addEmitted * ray.emittance.x) + ray.color.x * ((reflected.color.x + reflected.emittance.x) + (indirectEmitterColor.x)); + (addEmitted * ray.apparentBrightness.x) + (addEmitted * ray.emittance.x) + ray.color.x * ((reflected.color.x + reflected.emittance.x) + (indirectEmitterColor.x)); ray.color.y = - (addEmitted * ray.emittance.y) + ray.color.y * ((reflected.color.y + reflected.emittance.y) + (indirectEmitterColor.y)); + (addEmitted * ray.apparentBrightness.y) + (addEmitted * ray.emittance.y) + ray.color.y * ((reflected.color.y + reflected.emittance.y) + (indirectEmitterColor.y)); ray.color.z = - (addEmitted * ray.emittance.z) + ray.color.z * ((reflected.color.z + reflected.emittance.z) + (indirectEmitterColor.z)); + (addEmitted * ray.apparentBrightness.z) + (addEmitted * ray.emittance.z) + ray.color.z * ((reflected.color.z + reflected.emittance.z) + (indirectEmitterColor.z)); } else if(indirectEmitterColor.x > Ray.EPSILON || indirectEmitterColor.y > Ray.EPSILON || indirectEmitterColor.z > Ray.EPSILON) { hit = true; ray.color.x *= indirectEmitterColor.x; @@ -468,6 +477,7 @@ private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition e *= pos.block.surfaceArea(face); e *= emitterRay.getCurrentMaterial().emittance; e *= scene.emitterIntensity; + e *= scene.emitterLightIntensity; e *= scaler; result.scaleAdd(e, emitterRay.color); 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 20ddac8d95..6fd38fc7ae 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -118,7 +118,7 @@ public class Scene implements JsonSerializable, Refreshable { /** * Default emitter intensity. */ - public static final double DEFAULT_EMITTER_INTENSITY = 13; + public static final double DEFAULT_EMITTER_INTENSITY = 1; /** * Minimum emitter intensity. @@ -130,6 +130,36 @@ public class Scene implements JsonSerializable, Refreshable { */ public static final double MAX_EMITTER_INTENSITY = 1000; + /** + * Default emitter light intensity. + */ + public static final double DEFAULT_EMITTER_LIGHT_INTENSITY = 13; + + /** + * Minimum emitter light intensity. + */ + public static final double MIN_EMITTER_LIGHT_INTENSITY = 0.01; + + /** + * Maximum emitter light intensity. + */ + public static final double MAX_EMITTER_LIGHT_INTENSITY = 50; + + /** + * Default apparent emitter brightness. + */ + public static final double DEFAULT_APPARENT_EMITTER_BRIGHTNESS = 1; + + /** + * Minimum apparent emitter brightness. + */ + public static final double MIN_APPARENT_EMITTER_BRIGHTNESS = 0; + + /** + * Maximum apparent emitter brightness. + */ + public static final double MAX_APPARENT_EMITTER_BRIGHTNESS = 50; + /** * Default exposure. */ @@ -195,6 +225,8 @@ public class Scene implements JsonSerializable, Refreshable { protected boolean saveSnapshots = false; protected boolean emittersEnabled = DEFAULT_EMITTERS_ENABLED; protected double emitterIntensity = DEFAULT_EMITTER_INTENSITY; + protected double emitterLightIntensity = DEFAULT_EMITTER_LIGHT_INTENSITY; + protected double apparentEmitterBrightness = DEFAULT_APPARENT_EMITTER_BRIGHTNESS; protected EmitterSamplingStrategy emitterSamplingStrategy = EmitterSamplingStrategy.NONE; protected SunSamplingStrategy sunSamplingStrategy = SunSamplingStrategy.FAST; @@ -459,6 +491,8 @@ public synchronized void copyState(Scene other, boolean copyChunks) { sunSamplingStrategy = other.sunSamplingStrategy; emittersEnabled = other.emittersEnabled; emitterIntensity = other.emitterIntensity; + emitterLightIntensity = other.emitterLightIntensity; + apparentEmitterBrightness = other.apparentEmitterBrightness; emitterSamplingStrategy = other.emitterSamplingStrategy; preventNormalEmitterWithSampling = other.preventNormalEmitterWithSampling; transparentSky = other.transparentSky; @@ -1882,6 +1916,36 @@ public void setEmitterIntensity(double value) { refresh(); } + /** + * @return The current emitter light intensity + */ + public double getEmitterLightIntensity() { + return emitterLightIntensity; + } + + /** + * Set the emitter light intensity. + */ + public void setEmitterLightIntensity(double value) { + emitterLightIntensity = value; + refresh(); + } + + /** + * @return The current apparent emitter brightness + */ + public double getApparentEmitterBrightness() { + return apparentEmitterBrightness; + } + + /** + * Set the apparent emitter brightness. + */ + public void setApparentEmitterBrightness(double value) { + apparentEmitterBrightness = value; + refresh(); + } + /** * Set the transparent sky option. */ @@ -2729,6 +2793,8 @@ public void setUseCustomWaterColor(boolean value) { json.add("saveSnapshots", saveSnapshots); json.add("emittersEnabled", emittersEnabled); json.add("emitterIntensity", emitterIntensity); + json.add("emitterLightIntensity", emitterLightIntensity); + json.add("apparentEmitterBrightness", apparentEmitterBrightness); json.add("sunSamplingStrategy", sunSamplingStrategy.getId()); json.add("stillWater", stillWater); json.add("waterOpacity", waterOpacity); @@ -3011,7 +3077,15 @@ public synchronized void importFromJson(JsonObject json) { dumpFrequency = json.get("dumpFrequency").intValue(dumpFrequency); saveSnapshots = json.get("saveSnapshots").boolValue(saveSnapshots); emittersEnabled = json.get("emittersEnabled").boolValue(emittersEnabled); - emitterIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity); + if (json.get("emitterLightIntensity").isUnknown()) { + // load equivalent values in scenes saved in older versions. + emitterIntensity = 1; + emitterLightIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity); + } else { + emitterIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity); + emitterLightIntensity = json.get("emitterLightIntensity").doubleValue(emitterLightIntensity); + } + apparentEmitterBrightness = json.get("apparentEmitterBrightness").doubleValue(apparentEmitterBrightness); if (json.get("sunSamplingStrategy").isUnknown()) { boolean sunSampling = json.get("sunEnabled").boolValue(false); @@ -3321,6 +3395,16 @@ public void setEmittance(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the apparent brightness property for the given material. + */ + public void setApparentBrightness(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("apparentBrightness", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + /** * Modifies the specular coefficient property for the given material. */ 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..353bc181ad 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 @@ -52,6 +52,8 @@ public class LightingTab extends ScrollPane implements RenderControlsTab, Initia @FXML private DoubleAdjuster skyIntensity; @FXML private DoubleAdjuster apparentSkyBrightness; @FXML private DoubleAdjuster emitterIntensity; + @FXML private DoubleAdjuster emitterLightIntensity; + @FXML private DoubleAdjuster apparentEmitterBrightness; @FXML private DoubleAdjuster sunIntensity; @FXML private CheckBox drawSun; @FXML private ComboBox sunSamplingStrategy; @@ -104,12 +106,26 @@ public LightingTab() throws IOException { (observable, oldValue, newValue) -> scene.setEmittersEnabled(newValue)); emitterIntensity.setName("Emitter intensity"); - emitterIntensity.setTooltip("Modifies the intensity of emitter light."); + emitterIntensity.setTooltip("Changes the brightness of emitters."); emitterIntensity.setRange(Scene.MIN_EMITTER_INTENSITY, Scene.MAX_EMITTER_INTENSITY); emitterIntensity.makeLogarithmic(); emitterIntensity.clampMin(); emitterIntensity.onValueChange(value -> scene.setEmitterIntensity(value)); + emitterLightIntensity.setName("Emitter light intensity"); + emitterLightIntensity.setTooltip("Modifies the intensity of emitter light."); + emitterLightIntensity.setRange(Scene.MIN_EMITTER_LIGHT_INTENSITY, Scene.MAX_EMITTER_LIGHT_INTENSITY); + emitterLightIntensity.makeLogarithmic(); + emitterLightIntensity.clampMin(); + emitterLightIntensity.onValueChange(value -> scene.setEmitterLightIntensity(value)); + + apparentEmitterBrightness.setName("Apparent emitter brightness"); + apparentEmitterBrightness.setTooltip("Modifies the apparent brightness of emitters."); + apparentEmitterBrightness.setRange(Scene.MIN_APPARENT_EMITTER_BRIGHTNESS, Scene.MAX_APPARENT_EMITTER_BRIGHTNESS); + apparentEmitterBrightness.makeLogarithmic(); + apparentEmitterBrightness.clampMin(); + apparentEmitterBrightness.onValueChange(value -> scene.setApparentEmitterBrightness(value)); + emitterSamplingStrategy.getItems().addAll(EmitterSamplingStrategy.values()); emitterSamplingStrategy.getSelectionModel().selectedItemProperty() .addListener((observable, oldvalue, newvalue) -> { @@ -194,6 +210,8 @@ public void setController(RenderControlsFxController controller) { skyIntensity.set(scene.sky().getSkyLight()); apparentSkyBrightness.set(scene.sky().getApparentSkyLight()); emitterIntensity.set(scene.getEmitterIntensity()); + emitterLightIntensity.set(scene.getEmitterLightIntensity()); + apparentEmitterBrightness.set(scene.getApparentEmitterBrightness()); sunIntensity.set(scene.sun().getIntensity()); sunLuminosity.set(scene.sun().getLuminosity()); apparentSunBrightness.set(scene.sun().getApparentBrightness()); 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..090b5ec8f9 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 @@ -50,6 +50,7 @@ public class MaterialsTab extends HBox implements RenderControlsTab, Initializab private Scene scene; private final DoubleAdjuster emittance = new DoubleAdjuster(); + private final DoubleAdjuster apparentBrightness = new DoubleAdjuster(); private final DoubleAdjuster specular = new DoubleAdjuster(); private final DoubleAdjuster ior = new DoubleAdjuster(); private final DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); @@ -60,9 +61,15 @@ public MaterialsTab() { emittance.setName("Emittance"); emittance.setRange(0, 100); emittance.setTooltip("Intensity of the light emitted from the selected material."); + emittance.clampMin(); + apparentBrightness.setName("Apparent Brightness"); + apparentBrightness.setRange(0, 100); + apparentBrightness.setTooltip("Apparent brightness of the texture of the selected material."); + apparentBrightness.clampMin(); specular.setName("Specular"); specular.setRange(0, 1); specular.setTooltip("Reflectivity of the selected material."); + specular.clampBoth(); ior.setName("IoR"); ior.setRange(0, 5); ior.setTooltip("Index of Refraction of the selected material."); @@ -72,6 +79,7 @@ public MaterialsTab() { metalness.setName("Metalness"); metalness.setRange(0, 1); metalness.setTooltip("Metalness (texture-tinted reflectivity) of the selected material."); + metalness.clampBoth(); ObservableList blockIds = FXCollections.observableArrayList(); blockIds.addAll(MaterialStore.collections.keySet()); blockIds.addAll(ExtraMaterials.idMap.keySet()); @@ -87,7 +95,7 @@ public MaterialsTab() { settings.setSpacing(10); settings.getChildren().addAll( new Label("Material Properties"), - emittance, specular, perceptualSmoothness, ior, metalness, + emittance, apparentBrightness, specular, perceptualSmoothness, ior, metalness, new Label("(set to zero to disable)")); setPadding(new Insets(10)); setSpacing(15); @@ -113,6 +121,7 @@ private void updateSelectedMaterial(String materialName) { boolean materialExists = false; if (MaterialStore.collections.containsKey(materialName)) { double emAcc = 0; + double apparentBrightnessAcc = 0; double specAcc = 0; double iorAcc = 0; double perceptualSmoothnessAcc = 0; @@ -120,12 +129,14 @@ private void updateSelectedMaterial(String materialName) { Collection blocks = MaterialStore.collections.get(materialName); for (Block block : blocks) { emAcc += block.emittance; + apparentBrightnessAcc += block.apparentBrightness; specAcc += block.specular; iorAcc += block.ior; perceptualSmoothnessAcc += block.getPerceptualSmoothness(); metalnessAcc += block.metalness; } emittance.set(emAcc / blocks.size()); + apparentBrightness.set(apparentBrightnessAcc / blocks.size()); specular.set(specAcc / blocks.size()); ior.set(iorAcc / blocks.size()); perceptualSmoothness.set(perceptualSmoothnessAcc / blocks.size()); @@ -135,6 +146,7 @@ private void updateSelectedMaterial(String materialName) { Material material = ExtraMaterials.idMap.get(materialName); if (material != null) { emittance.set(material.emittance); + apparentBrightness.set(material.apparentBrightness); specular.set(material.specular); ior.set(material.ior); perceptualSmoothness.set(material.getPerceptualSmoothness()); @@ -145,6 +157,7 @@ private void updateSelectedMaterial(String materialName) { Block block = new MinecraftBlock(materialName.substring(10), Texture.air); scene.getPalette().applyMaterial(block); emittance.set(block.emittance); + apparentBrightness.set(block.apparentBrightness); specular.set(block.specular); ior.set(block.ior); perceptualSmoothness.set(block.getPerceptualSmoothness()); @@ -153,12 +166,14 @@ private void updateSelectedMaterial(String materialName) { } if (materialExists) { emittance.onValueChange(value -> scene.setEmittance(materialName, value.floatValue())); + apparentBrightness.onValueChange(value -> scene.setApparentBrightness(materialName, value.floatValue())); specular.onValueChange(value -> scene.setSpecular(materialName, value.floatValue())); ior.onValueChange(value -> scene.setIor(materialName, value.floatValue())); perceptualSmoothness.onValueChange(value -> scene.setPerceptualSmoothness(materialName, value.floatValue())); metalness.onValueChange(value -> scene.setMetalness(materialName, value.floatValue())); } else { emittance.onValueChange(value -> {}); + apparentBrightness.onValueChange(value -> {}); specular.onValueChange(value -> {}); ior.onValueChange(value -> {}); perceptualSmoothness.onValueChange(value -> {}); diff --git a/chunky/src/java/se/llbit/chunky/world/Material.java b/chunky/src/java/se/llbit/chunky/world/Material.java index 572af56774..8e65c4b6ec 100644 --- a/chunky/src/java/se/llbit/chunky/world/Material.java +++ b/chunky/src/java/se/llbit/chunky/world/Material.java @@ -60,6 +60,11 @@ public abstract class Material { */ public float emittance = 0; + /** + * The apparent brightness of the material. + */ + public float apparentBrightness = 1; + /** * The (linear) roughness controlling how rough a shiny block appears. A value of 0 makes the * surface perfectly specular, a value of 1 makes it diffuse. @@ -103,6 +108,7 @@ public void restoreDefaults() { solid = true; specular = 0; emittance = 0; + apparentBrightness = 1; roughness = 0; subSurfaceScattering = false; } @@ -123,6 +129,7 @@ public void loadMaterialProperties(JsonObject json) { ior = json.get("ior").floatValue(ior); specular = json.get("specular").floatValue(specular); emittance = json.get("emittance").floatValue(emittance); + apparentBrightness = json.get("apparentBrightness").floatValue(apparentBrightness); roughness = json.get("roughness").floatValue(roughness); metalness = json.get("metalness").floatValue(metalness); } diff --git a/chunky/src/java/se/llbit/math/Ray.java b/chunky/src/java/se/llbit/math/Ray.java index 064efb6ed2..37689b69c7 100644 --- a/chunky/src/java/se/llbit/math/Ray.java +++ b/chunky/src/java/se/llbit/math/Ray.java @@ -74,6 +74,11 @@ public class Ray { */ public Vector3 emittance = new Vector3(); + /** + * Apparent brightness of previously intersected surface. + */ + public Vector3 apparentBrightness = new Vector3(); + /** * Previous material. */ @@ -151,6 +156,7 @@ public void setDefault() { depth = 0; color.set(0, 0, 0, 0); emittance.set(0, 0, 0); + apparentBrightness.set(0, 0, 0); specular = true; } @@ -168,6 +174,7 @@ public void set(Ray other) { geomN.set(other.geomN); color.set(0, 0, 0, 0); emittance.set(0, 0, 0); + apparentBrightness.set(0, 0, 0); specular = other.specular; } diff --git a/chunky/src/res/se/llbit/chunky/ui/render/tabs/LightingTab.fxml b/chunky/src/res/se/llbit/chunky/ui/render/tabs/LightingTab.fxml index 63708d2812..c269cc225f 100644 --- a/chunky/src/res/se/llbit/chunky/ui/render/tabs/LightingTab.fxml +++ b/chunky/src/res/se/llbit/chunky/ui/render/tabs/LightingTab.fxml @@ -21,6 +21,8 @@ + +