Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color-based, per-material emittance mapping #1627

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
224 changes: 144 additions & 80 deletions chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions chunky/src/java/se/llbit/chunky/renderer/EmitterMappingType.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
60 changes: 43 additions & 17 deletions chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Comment on lines +536 to +540
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this without allocating Vector3 and Vector4?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Vector3 emiitance is definitely useful because otherwise we'd need a doEmittanceMappingR etc. but the Vector4 could be removed

}
}
}
Expand Down
77 changes: 77 additions & 0 deletions chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
*/
Expand Down
35 changes: 34 additions & 1 deletion chunky/src/java/se/llbit/chunky/ui/render/tabs/LightingTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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> emitterMappingType;
@FXML private DoubleAdjuster emitterMappingExponent;
@FXML private DoubleAdjuster sunIntensity;
@FXML private CheckBox drawSun;
@FXML private ComboBox<SunSamplingStrategy> sunSamplingStrategy;
Expand Down Expand Up @@ -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);
// });
Comment on lines +118 to +123
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unrelated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh I wonder how that got there, I'll remove it


emitterIntensity.setName("Emitter intensity");
emitterIntensity.setTooltip("Modifies the intensity of emitter light.");
Expand All @@ -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) -> {
Expand Down Expand Up @@ -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());
Expand Down
Loading
Loading