diff --git a/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java b/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java index e98ba16b5a..619b7e52ef 100644 --- a/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java +++ b/chunky/src/java/se/llbit/chunky/block/MinecraftBlockProvider.java @@ -1938,9 +1938,7 @@ public Block getBlockByTag(String namespacedName, Tag tag) { return stairs( tag, Texture.redSandstoneSide, Texture.redSandstoneTop, Texture.redSandstoneBottom); case "magma_block": { - Block block = new MinecraftBlock(name, Texture.magma); - block.emittance = 0.6f; - return block; + return new MinecraftBlock(name, Texture.magma); } case "nether_wart_block": return new MinecraftBlock(name, Texture.netherWartBlock); diff --git a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java index 3a39859f5d..b98fcd9195 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java +++ b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java @@ -20,7 +20,10 @@ import se.llbit.chunky.block.minecraft.*; import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.resources.Texture; +import se.llbit.chunky.renderer.EmitterMappingType; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.math.Octree; +import se.llbit.math.Vector3; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.IntTag; import se.llbit.nbt.StringTag; @@ -245,7 +248,7 @@ public static Map> getDefaultMaterialProperties() { materialProperties.put( "minecraft:lava", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); Consumer glassConfig = block -> { @@ -337,24 +340,38 @@ public static Map> getDefaultMaterialProperties() { block.metalness = 1.0f; block.setPerceptualSmoothness(0.9); }); - materialProperties.put("minecraft:redstone_torch", block -> { + Consumer redstoneTorchConfig = block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(255, 255, 210, 0.35f); + block.addRefColorGammaCorrected(255, 185, 0, 0.25f); + block.addRefColorGammaCorrected(221, 0, 0, 0.3f); if (block instanceof RedstoneTorch && ((RedstoneTorch) block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(7); } - }); - materialProperties.put("minecraft:redstone_wall_torch", block -> { - if (block instanceof RedstoneWallTorch && ((RedstoneWallTorch) block).isLit()) { - block.emittance = 1.0f; - } - }); - materialProperties.put("minecraft:torch", block -> { - block.emittance = 1.0f; - }); - materialProperties.put("minecraft:wall_torch", block -> { - block.emittance = 1.0f; - }); + }; + materialProperties.put("minecraft:redstone_torch", redstoneTorchConfig); + materialProperties.put("minecraft:redstone_wall_torch", redstoneTorchConfig); + materialProperties.put("minecraft:redstone_ore", block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(254, 118, 118, 0.2f); + block.addRefColorGammaCorrected(210, 3, 3, 0.2f); + }); + materialProperties.put("minecraft:deepslate_redstone_ore", block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(254, 118, 118, 0.2f); + block.addRefColorGammaCorrected(210, 3, 3, 0.35f); + }); + Consumer torchConfig = block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(255, 255, 210, 0.35f); + block.addRefColorGammaCorrected(255, 185, 0, 0.25f); + block.setLightLevel(14); + }; + materialProperties.put("minecraft:torch", torchConfig); + materialProperties.put("minecraft:wall_torch", torchConfig); materialProperties.put("minecraft:fire", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = -0.5f; }); materialProperties.put("minecraft:ice", block -> { block.ior = 1.31f; @@ -365,37 +382,40 @@ public static Map> getDefaultMaterialProperties() { block.refractive = true; }); materialProperties.put("minecraft:glowstone", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); materialProperties.put("minecraft:portal", block -> { // MC <1.13 - block.emittance = 0.4f; + block.setLightLevel(11); }); materialProperties.put("minecraft:nether_portal", block -> { // MC >=1.13 - block.emittance = 0.4f; + block.setLightLevel(11); }); materialProperties.put("minecraft:jack_o_lantern", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 0.5f; }); materialProperties.put("minecraft:beacon", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); block.ior = 1.52f; }); materialProperties.put("minecraft:redstone_lamp", block -> { if (block instanceof RedstoneLamp && ((RedstoneLamp) block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(15); } }); materialProperties.put("minecraft:emerald_block", block -> { block.specular = 0.04f; }); materialProperties.put("minecraft:sea_lantern", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); - materialProperties.put("minecraft:magma", block -> { - block.emittance = 0.6f; + materialProperties.put("minecraft:magma_block", block -> { + block.setLightLevel(3); }); materialProperties.put("minecraft:end_rod", block -> { - block.emittance = 1.0f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(248, 236, 219, 0.3f); + block.setLightLevel(14); }); materialProperties.put("minecraft:kelp", block -> { block.waterlogged = true; @@ -412,68 +432,84 @@ public static Map> getDefaultMaterialProperties() { materialProperties.put("minecraft:sea_pickle", block -> { if (block instanceof SeaPickle) { if (((SeaPickle) block).live) { - block.emittance = 1.0f / 15f * (3 * ((SeaPickle) block).pickles + 1); + block.setLightLevel(3 * ((SeaPickle) block).pickles + 1); } } }); materialProperties.put("minecraft:campfire", block -> { if (block instanceof Campfire && ((Campfire)block).isLit) { - block.emittance = 1.0f; + block.setLightLevel(15); } }); materialProperties.put("minecraft:furnace", block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(255, 255, 215, 0.38f); + block.addRefColorGammaCorrected(230, 171, 16, 0.38f); if(block instanceof Furnace && ((Furnace)block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(13); } }); materialProperties.put("minecraft:smoker", block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(228, 169, 17, 0.32f); if(block instanceof Smoker && ((Smoker)block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(13); } }); materialProperties.put("minecraft:blast_furnace", block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(224, 128, 46, 0.25f); if(block instanceof BlastFurnace && ((BlastFurnace)block).isLit()) { - block.emittance = 1.0f; + block.setLightLevel(13); } }); materialProperties.put("minecraft:lantern", block -> { - block.emittance = 1.0f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(254, 254, 179, 0.25f); + block.addRefColorGammaCorrected(253, 158, 76, 0.45f); + block.addRefColorGammaCorrected(134, 73, 42, 0.05f); + block.setLightLevel(15); }); materialProperties.put("minecraft:shroomlight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); }); materialProperties.put("minecraft:soul_fire_lantern", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(220, 252, 255, 0.5f); + block.addRefColorGammaCorrected(76, 198, 202, 0.3f); + block.setLightLevel(10); }); materialProperties.put("minecraft:soul_lantern", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_fire_torch", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_torch", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_fire_wall_torch", block -> { // MC 20w06a-20w16a - block.emittance = 0.6f; - }); - materialProperties.put("minecraft:soul_wall_torch", block -> { // MC >= 20w17a - block.emittance = 0.6f; - }); + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(220, 252, 255, 0.5f); + block.addRefColorGammaCorrected(76, 198, 202, 0.3f); + block.setLightLevel(10); + }); + Consumer soulTorchConfig = block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(199, 252, 254, 0.45f); + block.addRefColorGammaCorrected(35, 204, 209, 0.25f); + block.setLightLevel(10); + }; + materialProperties.put("minecraft:soul_fire_torch", soulTorchConfig); // MC 20w06a-20w16a + materialProperties.put("minecraft:soul_torch", soulTorchConfig); // MC >= 20w17a + materialProperties.put("minecraft:soul_fire_wall_torch", soulTorchConfig); // MC 20w06a-20w16a + materialProperties.put("minecraft:soul_wall_torch", soulTorchConfig); // MC >= 20w17a materialProperties.put("minecraft:soul_fire", block -> { - block.emittance = 0.6f; + block.setLightLevel(10); + block.emitterMappingOffset = -0.5f; }); materialProperties.put("minecraft:crying_obsidian", block -> { - block.emittance = 0.6f; + block.setLightLevel(10); }); materialProperties.put("minecraft:enchanting_table", block -> { - block.emittance = 0.5f; + block.setLightLevel(7); }); materialProperties.put("minecraft:respawn_anchor", block -> { if (block instanceof RespawnAnchor) { int charges = ((RespawnAnchor)block).charges; if (charges > 0) { - block.emittance = 1.0f / 15 * (charges * 4 - 2); + block.setLightLevel(charges * 4 - 2); } } }); @@ -528,77 +564,105 @@ public static Map> getDefaultMaterialProperties() { } }); materialProperties.put("minecraft:small_amethyst_bud", block -> { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); }); materialProperties.put("minecraft:medium_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 2; + block.setLightLevel(2); }); materialProperties.put("minecraft:large_amethyst_bud", block -> { - block.emittance = 1.0f / 15f * 4; + block.setLightLevel(4); }); materialProperties.put("minecraft:amethyst_cluster", block -> { - block.emittance = 1.0f / 15f * 5; + block.setLightLevel(5); }); materialProperties.put("minecraft:tinted_glass", glassConfig); materialProperties.put("minecraft:sculk_sensor", block -> { if (block instanceof SculkSensor && ((SculkSensor) block).isActive()) { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); } }); materialProperties.put("minecraft:calibrated_sculk_sensor", block -> { if (block instanceof CalibratedSculkSensor && ((CalibratedSculkSensor) block).isActive()) { - block.emittance = 1.0f / 15f; + block.setLightLevel(1); } }); materialProperties.put("minecraft:glow_lichen", block -> { - block.emittance = 1.0f / 15f * 7; - }); - materialProperties.put("minecraft:cave_vines_plant", block -> { - if (block instanceof CaveVines && ((CaveVines) block).hasBerries()) { - block.emittance = 1.0f / 15f * 14; - } + block.setLightLevel(7); }); - materialProperties.put("minecraft:cave_vines", block -> { + Consumer caveVinesConfig = block -> { + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(241, 189, 85, 0.3f); + block.addRefColorGammaCorrected(164, 100, 34, 0.05f); if (block instanceof CaveVines && ((CaveVines) block).hasBerries()) { - block.emittance = 1.0f / 15f * 14; + block.setLightLevel(14); } - }); + }; + materialProperties.put("minecraft:cave_vines_plant", caveVinesConfig); + materialProperties.put("minecraft:cave_vines", caveVinesConfig); materialProperties.put("minecraft:light", block -> { if (block instanceof LightBlock) { - block.emittance = 1.0f / 15f * 4 * ((LightBlock) block).getLevel(); + block.setLightLevel(4 * ((LightBlock) block).getLevel()); } }); materialProperties.put("minecraft:ochre_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:verdant_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:pearlescent_froglight", block -> { - block.emittance = 1.0f; + block.setLightLevel(15); + block.emitterMappingOffset = 1.0f; }); materialProperties.put("minecraft:sculk_catalyst", block -> { - block.emittance = 1.0f / 15f * 6; + block.setLightLevel(6); }); + Consumer copperBulbRedLight = block -> { + block.addRefColorGammaCorrected(217, 35, 35, 0.05f); + block.addRefColorGammaCorrected(176, 23, 23, 0.05f); + block.addRefColorGammaCorrected(163, 24, 24, 0.05f); + block.addRefColorGammaCorrected(138, 24, 24, 0.05f); + }; for(String s : new String[]{"minecraft:", "minecraft:waxed_"}) { materialProperties.put(s + "copper_bulb", block -> { - if(block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 1.0f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(255, 235, 186, 0.25f); + block.addRefColorGammaCorrected(251, 184, 96, 0.25f); + copperBulbRedLight.accept(block); + copperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(15); } }); materialProperties.put(s + "exposed_copper_bulb", block -> { - if(block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 12 / 15f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(253, 202, 138, 0.25f); + block.addRefColorGammaCorrected(223, 139, 41, 0.2f); + copperBulbRedLight.accept(block); + exposedCopperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(12); } }); materialProperties.put(s + "weathered_copper_bulb", block -> { - if(block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 8 / 15f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(234, 184, 91, 0.25f); + block.addRefColorGammaCorrected(224, 151, 53, 0.25f); + copperBulbRedLight.accept(block); + weatheredCopperConfig.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(8); } }); materialProperties.put(s + "oxidized_copper_bulb", block -> { - if(block instanceof CopperBulb && ((CopperBulb) block).isLit()) { - block.emittance = 4 / 15f; + block.emitterMappingType = EmitterMappingType.REFERENCE_COLORS; + block.addRefColorGammaCorrected(212, 153, 67, 0.25f); + block.addRefColorGammaCorrected(191, 113, 65, 0.25f); + copperBulbRedLight.accept(block); + if(block instanceof CopperBulb && (((CopperBulb) block).isLit() || ((CopperBulb) block).isPowered())) { + block.setLightLevel(4); } }); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/EmitterMappingType.java b/chunky/src/java/se/llbit/chunky/renderer/EmitterMappingType.java new file mode 100644 index 0000000000..2fde1f0653 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/EmitterMappingType.java @@ -0,0 +1,30 @@ +package se.llbit.chunky.renderer; + +import se.llbit.util.Registerable; + +public enum EmitterMappingType implements Registerable { + NONE("None", "Fallback to default option - should only be used for materials (not global default)"), + BRIGHTEST_CHANNEL("Brightest Channel", "Emitted light (R', G', B') = (R*M^P, G*M^P, B*M^P) where M = max(R, G, B) and P is the specified power. Emitted light will always match pixel color."), + REFERENCE_COLORS("Reference Colors", "Like BRIGHTEST_CHANNEL, but only for colors near enough to a reference color; rest of pixels won't emit at all."), + INDEPENDENT_CHANNELS("Independent Channels", "Emitted light (R', G', B') = (R^P, G^P, B^P) where P is the specified power. Saturation of emitted light increases with P - possibly less realistic in some situations."); + private final String displayName; + private final String description; + EmitterMappingType(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + @Override + public String getName() { + return this.displayName; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String getId() { + return this.name(); + } +} 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 5dd836524f..6cb9472812 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -20,6 +20,7 @@ import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.minecraft.Water; +import se.llbit.chunky.renderer.EmitterMappingType; import se.llbit.chunky.renderer.EmitterSamplingStrategy; import se.llbit.chunky.renderer.WorkerState; import se.llbit.chunky.world.Material; @@ -140,7 +141,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, for (int i = 0; i < count; i++) { boolean doMetal = pMetal > Ray.EPSILON && random.nextFloat() < pMetal; if (doMetal || (pSpecular > Ray.EPSILON && random.nextFloat() < pSpecular)) { - hit |= doSpecularReflection(ray, next, cumulativeColor, doMetal, random, state, scene); + hit |= doSpecularReflection(ray, next, currentMat, cumulativeColor, doMetal, random, state, scene); } else if(random.nextFloat() < pDiffuse) { hit |= doDiffuseReflection(ray, next, currentMat, cumulativeColor, random, state, scene); } else if (n1 != n2) { @@ -202,20 +203,23 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, return hit; } - private static boolean doSpecularReflection(Ray ray, Ray next, Vector4 cumulativeColor, boolean doMetal, Random random, WorkerState state, Scene scene) { + private static boolean doSpecularReflection(Ray ray, Ray next, Material currentMat, Vector4 cumulativeColor, boolean doMetal, Random random, WorkerState state, Scene scene) { boolean hit = false; + Vector3 emittance = new Vector3(); + if(scene.emittersEnabled && currentMat.emittance > Ray.EPSILON) { + doEmittanceMapping(emittance, ray.color, scene, currentMat); + } next.specularReflection(ray, random); if (pathTrace(scene, next, state, false)) { - if (doMetal) { // use the albedo color as specular color - cumulativeColor.x += ray.color.x * next.color.x; - cumulativeColor.y += ray.color.y * next.color.y; - cumulativeColor.z += ray.color.z * next.color.z; + 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; } else { - cumulativeColor.x += next.color.x; - cumulativeColor.y += next.color.y; - cumulativeColor.z += next.color.z; + cumulativeColor.x += emittance.x + next.color.x; + cumulativeColor.y += emittance.y + next.color.y; + cumulativeColor.z += emittance.z + next.color.z; } hit = true; } @@ -229,12 +233,9 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && 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); - + doEmittanceMapping(emittance, ray.color, scene, currentMat); hit = true; + } else if (scene.emittersEnabled && scene.emitterSamplingStrategy != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() != null) { // Sample emitter switch (scene.emitterSamplingStrategy) { @@ -470,6 +471,30 @@ private static void translucentRayColor(Scene scene, Ray ray, Ray next, Vector4 cumulativeColor.add(outputColor); } + private static void doEmittanceMapping(Vector3 emittance, Vector4 color, Scene scene, Material material) { + double exp = Math.max(scene.getEmitterMappingExponent() + material.emitterMappingOffset, 0); + EmitterMappingType mt = material.emitterMappingType == EmitterMappingType.NONE ? scene.getEmitterMappingType() : material.emitterMappingType; + double val; + switch(mt) { + case BRIGHTEST_CHANNEL: + val = FastMath.pow(Math.max(color.x, Math.max(color.y, color.z)), exp); + emittance.set(color.x * val, color.y * val, color.z * val); + break; + case REFERENCE_COLORS: + boolean emit = false; + for(Vector4 refcolor : material.emitterMappingReferenceColors) { + emit = emit || (Math.max(Math.abs(color.x - refcolor.x), Math.max(Math.abs(color.y - refcolor.y), Math.abs(color.z - refcolor.z))) <= refcolor.w); + } + val = emit ? FastMath.pow(Math.max(color.x, Math.max(color.y, color.z)), exp) : 0; + emittance.set(color.x * val, color.y * val, color.z * val); + break; + case INDEPENDENT_CHANNELS: + emittance.set(FastMath.pow(color.x, exp), FastMath.pow(color.y, exp), FastMath.pow(color.z, exp)); + break; + } + emittance.scale(material.emittance * scene.emitterIntensity); + } + private static double reassignTransmissivity(double from, double to, double other, double trans, double cap) { // Formula here derived algebraically from this system: // (cap - to_new)/(cap - other_new) = (from - to)/(from - other), (cap + to_new + other_new)/3 = trans @@ -507,11 +532,12 @@ private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition double e = Math.abs(emitterRay.d.dot(emitterRay.getNormal())); e /= Math.max(distance * distance, 1); e *= pos.block.surfaceArea(face); - e *= emitterRay.getCurrentMaterial().emittance; - e *= scene.emitterIntensity; e *= scaler; + Vector3 emittance = new Vector3(); + doEmittanceMapping(emittance, emitterRay.color, scene, emitterRay.getCurrentMaterial()); + emittance.scale(e); - result.scaleAdd(e, emitterRay.color); + result.add(new Vector4(emittance, 0)); } } } 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 64cafa4aec..5f9ced5153 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -118,6 +118,15 @@ public class Scene implements JsonSerializable, Refreshable { * Default emitter intensity. */ public static final double DEFAULT_EMITTER_INTENSITY = 13; + /** + * Default exponent for emitter mapping. + */ + public static final double DEFAULT_EMITTER_MAPPING_EXPONENT = 1.5; + + /** + * Default method for emitter mapping. + */ + public static final EmitterMappingType DEFAULT_EMITTER_MAPPING_TYPE = EmitterMappingType.BRIGHTEST_CHANNEL; /** * Minimum emitter intensity. @@ -129,6 +138,16 @@ public class Scene implements JsonSerializable, Refreshable { */ public static final double MAX_EMITTER_INTENSITY = 1000; + /** + * Minimum emitter mapping exponent. + */ + public static final double MIN_EMITTER_MAPPING_EXPONENT = 0; + + /** + * Maximum emitter mapping exponent. + */ + public static final double MAX_EMITTER_MAPPING_EXPONENT = 5; + /** * Default transmissivity cap. */ @@ -213,6 +232,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 emitterMappingExponent = DEFAULT_EMITTER_MAPPING_EXPONENT; + protected EmitterMappingType emitterMappingType = DEFAULT_EMITTER_MAPPING_TYPE; protected EmitterSamplingStrategy emitterSamplingStrategy = EmitterSamplingStrategy.NONE; protected boolean fancierTranslucency = true; protected double transmissivityCap = DEFAULT_TRANSMISSIVITY_CAP; @@ -458,6 +479,8 @@ public synchronized void copyState(Scene other, boolean copyChunks) { sunSamplingStrategy = other.sunSamplingStrategy; emittersEnabled = other.emittersEnabled; emitterIntensity = other.emitterIntensity; + emitterMappingExponent = other.emitterMappingExponent; + emitterMappingType = other.emitterMappingType; emitterSamplingStrategy = other.emitterSamplingStrategy; preventNormalEmitterWithSampling = other.preventNormalEmitterWithSampling; fancierTranslucency = other.fancierTranslucency; @@ -1767,6 +1790,36 @@ public void setEmitterIntensity(double value) { refresh(); } + /** + * @return The current emitter mapping exponent + */ + public double getEmitterMappingExponent() { + return emitterMappingExponent; + } + + /** + * Set the emitter mapping exponent. + */ + public void setEmitterMappingExponent(double value) { + emitterMappingExponent = value; + refresh(); + } + + /** + * @return The current emitter mapping type. + */ + public EmitterMappingType getEmitterMappingType() { + return emitterMappingType; + } + + /** + * Set the emitter mapping type. + */ + public void setEmitterMappingType(EmitterMappingType value) { + emitterMappingType = value; + refresh(); + } + /** * Set the transparent sky option. */ @@ -2601,6 +2654,8 @@ public void setUseCustomWaterColor(boolean value) { json.add("saveSnapshots", saveSnapshots); json.add("emittersEnabled", emittersEnabled); json.add("emitterIntensity", emitterIntensity); + json.add("emitterMappingExponent", emitterMappingExponent); + json.add("emitterMappingType", emitterMappingType.getId()); json.add("fancierTranslucency", fancierTranslucency); json.add("transmissivityCap", transmissivityCap); json.add("sunSamplingStrategy", sunSamplingStrategy.getId()); @@ -2869,6 +2924,8 @@ public synchronized void importFromJson(JsonObject json) { saveSnapshots = json.get("saveSnapshots").boolValue(saveSnapshots); emittersEnabled = json.get("emittersEnabled").boolValue(emittersEnabled); emitterIntensity = json.get("emitterIntensity").doubleValue(emitterIntensity); + emitterMappingExponent = json.get("emitterMappingExponent").doubleValue(emitterMappingExponent); + emitterMappingType = EmitterMappingType.valueOf(json.get("emitterMappingType").asString(DEFAULT_EMITTER_MAPPING_TYPE.getId())); fancierTranslucency = json.get("fancierTranslucency").boolValue(fancierTranslucency); transmissivityCap = json.get("transmissivityCap").doubleValue(transmissivityCap); @@ -3183,6 +3240,26 @@ public void setEmittance(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the emittance property for the given material. + */ + public void setEmitterMappingOffset(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("emitterMappingOffset", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + + /** + * Modifies the emittance property for the given material. + */ + public void setEmitterMappingTypeOverride(String materialName, EmitterMappingType value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("emitterMappingType", Json.of(value.toString())); + 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 9e43ecfa6b..c2e8df0bf9 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 @@ -25,7 +25,9 @@ import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import se.llbit.chunky.renderer.EmitterMappingType; import se.llbit.chunky.renderer.EmitterSamplingStrategy; import se.llbit.chunky.renderer.SunSamplingStrategy; import se.llbit.chunky.renderer.scene.Scene; @@ -52,6 +54,9 @@ public class LightingTab extends ScrollPane implements RenderControlsTab, Initia @FXML private DoubleAdjuster skyIntensity; @FXML private DoubleAdjuster apparentSkyBrightness; @FXML private DoubleAdjuster emitterIntensity; + @FXML private VBox emitterSettings; + @FXML private ComboBox emitterMappingType; + @FXML private DoubleAdjuster emitterMappingExponent; @FXML private DoubleAdjuster sunIntensity; @FXML private CheckBox drawSun; @FXML private ComboBox sunSamplingStrategy; @@ -101,7 +106,21 @@ public LightingTab() throws IOException { enableEmitters.setTooltip(new Tooltip("Allow blocks to emit light based on their material settings.")); enableEmitters.selectedProperty().addListener( - (observable, oldValue, newValue) -> scene.setEmittersEnabled(newValue)); + (observable, oldValue, newValue) -> { + scene.setEmittersEnabled(newValue); + emitterSettings.setVisible(newValue); + emitterSettings.setManaged(newValue); + }); + boolean showEmitterSettings = scene != null && scene.getEmittersEnabled(); + emitterSettings.setVisible(showEmitterSettings); + emitterSettings.setManaged(showEmitterSettings); + +// fancierTranslucency.selectedProperty() +// .addListener((observable, oldValue, newValue) -> { +// scene.setFancierTranslucency(newValue); +// transmissivityCap.setVisible(newValue); +// transmissivityCap.setManaged(newValue); +// }); emitterIntensity.setName("Emitter intensity"); emitterIntensity.setTooltip("Modifies the intensity of emitter light."); @@ -110,6 +129,18 @@ public LightingTab() throws IOException { emitterIntensity.clampMin(); emitterIntensity.onValueChange(value -> scene.setEmitterIntensity(value)); + emitterMappingType.getItems().addAll(EmitterMappingType.values()); + emitterMappingType.getItems().remove(EmitterMappingType.NONE); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> scene.setEmitterMappingType(newValue)); + emitterMappingType.setTooltip(new Tooltip("Determines how per-pixel light emission is computed.")); + + emitterMappingExponent.setName("Emitter mapping exponent"); + emitterMappingExponent.setTooltip("Determines how much light is emitted from darker or lighter pixels.\nHigher values will result in darker pixels emitting less light."); + emitterMappingExponent.setRange(Scene.MIN_EMITTER_MAPPING_EXPONENT, Scene.MAX_EMITTER_MAPPING_EXPONENT); + emitterMappingExponent.clampMin(); + emitterMappingExponent.onValueChange(value -> scene.setEmitterMappingExponent(value)); + emitterSamplingStrategy.getItems().addAll(EmitterSamplingStrategy.values()); emitterSamplingStrategy.getSelectionModel().selectedItemProperty() .addListener((observable, oldvalue, newvalue) -> { @@ -194,6 +225,8 @@ public void setController(RenderControlsFxController controller) { skyIntensity.set(scene.sky().getSkyLight()); apparentSkyBrightness.set(scene.sky().getApparentSkyLight()); emitterIntensity.set(scene.getEmitterIntensity()); + emitterMappingExponent.set(scene.getEmitterMappingExponent()); + emitterMappingType.getSelectionModel().select(scene.getEmitterMappingType()); 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..99e09955db 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java @@ -17,6 +17,7 @@ */ package se.llbit.chunky.ui.render.tabs; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -25,12 +26,11 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListView; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import se.llbit.chunky.block.*; +import se.llbit.chunky.renderer.EmitterMappingType; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.ui.DoubleAdjuster; @@ -50,16 +50,26 @@ public class MaterialsTab extends HBox implements RenderControlsTab, Initializab private Scene scene; private final DoubleAdjuster emittance = new DoubleAdjuster(); + private final DoubleAdjuster emitterMappingOffset = new DoubleAdjuster(); + private final ComboBox emitterMappingType = new ComboBox<>(); + private javafx.beans.value.ChangeListener emtListener = null; private final DoubleAdjuster specular = new DoubleAdjuster(); private final DoubleAdjuster ior = new DoubleAdjuster(); private final DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); private final DoubleAdjuster metalness = new DoubleAdjuster(); + private final CheckBox advanced = new CheckBox(); + private final VBox advancedSettings = new VBox(); private final ListView listView; public MaterialsTab() { emittance.setName("Emittance"); emittance.setRange(0, 100); emittance.setTooltip("Intensity of the light emitted from the selected material."); + emitterMappingOffset.setName("Emitter mapping offset"); + emitterMappingOffset.setRange(-5, 5); + emitterMappingOffset.setTooltip("Offset applied to the global emitter mapping exponent."); + emitterMappingType.getItems().addAll(EmitterMappingType.values()); + emitterMappingType.setTooltip(new Tooltip("Overrides the global setting for emitter mapping type.")); specular.setName("Specular"); specular.setRange(0, 1); specular.setTooltip("Reflectivity of the selected material."); @@ -72,6 +82,20 @@ public MaterialsTab() { metalness.setName("Metalness"); metalness.setRange(0, 1); metalness.setTooltip("Metalness (texture-tinted reflectivity) of the selected material."); + advanced.setText("Advanced settings"); + advanced.setTooltip(new Tooltip("Show advanced settings")); + advanced.selectedProperty().addListener((observable, oldValue, newValue) -> { + advancedSettings.setVisible(newValue); + advancedSettings.setManaged(newValue); + }); + HBox emt = new HBox(); + emt.setSpacing(10); + emt.setAlignment(Pos.CENTER_LEFT); + emt.getChildren().addAll(new Label("Emitter mapping type:"), emitterMappingType); + advancedSettings.setSpacing(10); + advancedSettings.getChildren().addAll(emitterMappingOffset, emt); + advancedSettings.setVisible(false); + advancedSettings.setManaged(false); ObservableList blockIds = FXCollections.observableArrayList(); blockIds.addAll(MaterialStore.collections.keySet()); blockIds.addAll(ExtraMaterials.idMap.keySet()); @@ -88,7 +112,9 @@ public MaterialsTab() { settings.getChildren().addAll( new Label("Material Properties"), emittance, specular, perceptualSmoothness, ior, metalness, - new Label("(set to zero to disable)")); + new Label("(set to zero to disable)"), + advanced, advancedSettings + ); setPadding(new Insets(10)); setSpacing(15); TextField filterField = new TextField(); @@ -111,8 +137,13 @@ public MaterialsTab() { private void updateSelectedMaterial(String materialName) { boolean materialExists = false; + if(emtListener != null) { + emitterMappingType.getSelectionModel().selectedItemProperty().removeListener(emtListener); + emtListener = null; + } if (MaterialStore.collections.containsKey(materialName)) { double emAcc = 0; + double emoAcc = 0; double specAcc = 0; double iorAcc = 0; double perceptualSmoothnessAcc = 0; @@ -120,12 +151,15 @@ private void updateSelectedMaterial(String materialName) { Collection blocks = MaterialStore.collections.get(materialName); for (Block block : blocks) { emAcc += block.emittance; + emoAcc += block.emitterMappingOffset; specAcc += block.specular; iorAcc += block.ior; perceptualSmoothnessAcc += block.getPerceptualSmoothness(); metalnessAcc += block.metalness; } emittance.set(emAcc / blocks.size()); + emitterMappingOffset.set(emoAcc / blocks.size()); + emitterMappingType.getSelectionModel().select(EmitterMappingType.NONE); specular.set(specAcc / blocks.size()); ior.set(iorAcc / blocks.size()); perceptualSmoothness.set(perceptualSmoothnessAcc / blocks.size()); @@ -135,6 +169,8 @@ private void updateSelectedMaterial(String materialName) { Material material = ExtraMaterials.idMap.get(materialName); if (material != null) { emittance.set(material.emittance); + emitterMappingOffset.set(material.emitterMappingOffset); + emitterMappingType.getSelectionModel().select(material.emitterMappingType); specular.set(material.specular); ior.set(material.ior); perceptualSmoothness.set(material.getPerceptualSmoothness()); @@ -145,6 +181,8 @@ private void updateSelectedMaterial(String materialName) { Block block = new MinecraftBlock(materialName.substring(10), Texture.air); scene.getPalette().applyMaterial(block); emittance.set(block.emittance); + emitterMappingOffset.set(block.emitterMappingOffset); + emitterMappingType.getSelectionModel().select(block.emitterMappingType); specular.set(block.specular); ior.set(block.ior); perceptualSmoothness.set(block.getPerceptualSmoothness()); @@ -152,13 +190,19 @@ private void updateSelectedMaterial(String materialName) { materialExists = true; } if (materialExists) { - emittance.onValueChange(value -> scene.setEmittance(materialName, value.floatValue())); + emittance.onValueChange(value -> { + scene.setEmittance(materialName, value.floatValue()); + }); + emitterMappingOffset.onValueChange(value -> scene.setEmitterMappingOffset(materialName, value.floatValue())); + emtListener = (observable, oldValue, newValue) -> scene.setEmitterMappingTypeOverride(materialName, newValue); + emitterMappingType.getSelectionModel().selectedItemProperty().addListener(emtListener); 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 -> {}); + emitterMappingOffset.onValueChange(value -> {}); specular.onValueChange(value -> {}); ior.onValueChange(value -> {}); perceptualSmoothness.onValueChange(value -> {}); diff --git a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java index f34613f649..bd19c72d6c 100644 --- a/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java +++ b/chunky/src/java/se/llbit/chunky/world/ExtraMaterials.java @@ -42,16 +42,19 @@ public static void loadDefaultMaterialProperties() { CloudMaterial.INSTANCE.restoreDefaults(); Candle.flameMaterial.restoreDefaults(); - Candle.flameMaterial.emittance = 1.0f; + Candle.flameMaterial.setLightLevel(12); + Candle.flameMaterial.emitterMappingOffset = -0.5f; Campfire.flameMaterial.restoreDefaults(); - Campfire.flameMaterial.emittance = 1.0f; + Campfire.flameMaterial.setLightLevel(15); + Campfire.flameMaterial.emitterMappingOffset = -0.5f; Campfire.soulFlameMaterial.restoreDefaults(); - Campfire.soulFlameMaterial.emittance = 0.6f; + Campfire.soulFlameMaterial.setLightLevel(10); + Campfire.soulFlameMaterial.emitterMappingOffset = -0.5f; CalibratedSculkSensorAmethyst.activeMaterial.restoreDefaults(); - CalibratedSculkSensorAmethyst.activeMaterial.emittance = 1.0f / 15; + CalibratedSculkSensorAmethyst.activeMaterial.setLightLevel(1); CalibratedSculkSensorAmethyst.inactiveMaterial.restoreDefaults(); } diff --git a/chunky/src/java/se/llbit/chunky/world/Material.java b/chunky/src/java/se/llbit/chunky/world/Material.java index 572af56774..ca3eec0150 100644 --- a/chunky/src/java/se/llbit/chunky/world/Material.java +++ b/chunky/src/java/se/llbit/chunky/world/Material.java @@ -16,11 +16,17 @@ */ package se.llbit.chunky.world; +import se.llbit.chunky.renderer.EmitterMappingType; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.Texture; +import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonString; import se.llbit.json.JsonValue; import se.llbit.math.Ray; +import se.llbit.math.Vector4; + +import java.util.ArrayList; public abstract class Material { @@ -60,6 +66,22 @@ public abstract class Material { */ public float emittance = 0; + /** + * Offset to apply to the global emitter mapping exponent (the resulting value will be constrained to be >= 0). + */ + public float emitterMappingOffset = 0; + + /** + * Overrides the global emitter mapping type unless set to NONE. + */ + public EmitterMappingType emitterMappingType = EmitterMappingType.NONE; + + /** + * (x, y, z): The color to use for the REFERENCE_COLORS emitter mapping type. + * w: The range surrounding the specified color to apply full brightness. + */ + public ArrayList emitterMappingReferenceColors = new ArrayList<>(); + /** * 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 +125,9 @@ public void restoreDefaults() { solid = true; specular = 0; emittance = 0; + emitterMappingOffset = 0; + emitterMappingType = EmitterMappingType.NONE; + emitterMappingReferenceColors = new ArrayList<>(); roughness = 0; subSurfaceScattering = false; } @@ -122,9 +147,24 @@ public JsonValue toJson() { public void loadMaterialProperties(JsonObject json) { ior = json.get("ior").floatValue(ior); specular = json.get("specular").floatValue(specular); - emittance = json.get("emittance").floatValue(emittance); roughness = json.get("roughness").floatValue(roughness); metalness = json.get("metalness").floatValue(metalness); + emittance = json.get("emittance").floatValue(emittance); + emitterMappingOffset = json.get("emitterMappingOffset").floatValue(emitterMappingOffset); + emitterMappingType = EmitterMappingType.valueOf(json.get("emitterMappingType").asString(emitterMappingType.toString())); + JsonArray referenceColors = json.get("emitterMappingReferenceColors").array(); + // Overwrite existing reference colors, but only if any are specified + if(referenceColors.size() > 0) { + emitterMappingReferenceColors = new ArrayList<>(); + } + for(JsonValue refColorJson : referenceColors.elements) { + Vector4 refColor = new Vector4(); + refColor.x = refColorJson.object().get("red").floatValue(0); + refColor.y = refColorJson.object().get("green").floatValue(0); + refColor.z = refColorJson.object().get("blue").floatValue(0); + refColor.w = refColorJson.object().get("range").floatValue(0); + emitterMappingReferenceColors.add(refColor); + } } public boolean isWater() { @@ -146,4 +186,16 @@ public double getPerceptualSmoothness() { public void setPerceptualSmoothness(double perceptualSmoothness) { roughness = (float) Math.pow(1 - perceptualSmoothness, 2); } + + /** + * Set the emittance based on a Minecraft light level + * @param level The light level from 0 to 15 + */ + public void setLightLevel(float level) { + emittance = level / 15; + } + + public void addRefColorGammaCorrected(float r, float g, float b, float delta) { + emitterMappingReferenceColors.add(new Vector4(Math.pow(r/255, Scene.DEFAULT_GAMMA), Math.pow(g/255, Scene.DEFAULT_GAMMA), Math.pow(b/255, Scene.DEFAULT_GAMMA), delta)); + } } 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..047cacf8de 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 @@ -20,15 +20,22 @@ - - - + + + + + + + + -