From 5c91b71d6d12797108376c8e459ad5c159cbf7bf Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Sat, 28 Oct 2023 22:58:13 +0200 Subject: [PATCH] Add UE4 filmic tone mapping and make Hable tone mapping configurable. (#1519) * Add the filmic tone mapper from Unreal Engine 4. * Fix hable tone mapping. * Make PostProcessingFilter extend the Registerable interface. * Prepare hable tone mapping for configuration. * Add an interface for configurable objects. * Make the hable and ue4 tone mapping configurable. * Start implementing a UI for hable and ue4 post processor configuration. * Implement UI logic to customize hable and ue4 post processors. * Replace casts with .floatValue() --- .../HableToneMappingFilter.java | 171 +++++++++++++++-- .../postprocessing/PostProcessingFilter.java | 16 +- .../postprocessing/PostProcessingFilters.java | 1 + .../postprocessing/UE4ToneMappingFilter.java | 176 ++++++++++++++++++ .../se/llbit/chunky/renderer/scene/Scene.java | 27 ++- .../ui/render/tabs/PostprocessingTab.java | 136 +++++++++++++- .../src/java/se/llbit/util/Configurable.java | 29 +++ .../ui/render/tabs/PostprocessingTab.fxml | 80 ++++++-- 8 files changed, 580 insertions(+), 56 deletions(-) create mode 100644 chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java create mode 100644 chunky/src/java/se/llbit/util/Configurable.java diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java index da2e8321c1..09a51f0dc8 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java @@ -1,27 +1,144 @@ package se.llbit.chunky.renderer.postprocessing; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.json.JsonObject; +import se.llbit.util.Configurable; + /** - * Implementation of Hable tone mapping + * Implementation of Hable (i.e. Uncharted 2) tone mapping + * * @link http://filmicworlds.com/blog/filmic-tonemapping-operators/ + * @link https://www.gdcvault.com/play/1012351/Uncharted-2-HDR */ -public class HableToneMappingFilter extends SimplePixelPostProcessingFilter { - private static final float hA = 0.15f; - private static final float hB = 0.50f; - private static final float hC = 0.10f; - private static final float hD = 0.20f; - private static final float hE = 0.02f; - private static final float hF = 0.30f; - private static final float hW = 11.2f; - private static final float whiteScale = 1.0f / (((hW * (hA * hW + hC * hB) + hD * hE) / (hW * (hA * hW + hB) + hD * hF)) - hE / hF); - +public class HableToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { + public enum Preset { + /** + * Parameters from John Hable's blog post + */ + FILMIC_WORLDS, + + /** + * Parameters from John Hable's GDC talk + */ + GDC + } + + private float hA; + private float hB; + private float hC; + private float hD; + private float hE; + private float hF; + private float hW; + private float whiteScale; + + public HableToneMappingFilter() { + reset(); + } + + private void recalculateWhiteScale() { + whiteScale = 1.0f / (((hW * (hA * hW + hC * hB) + hD * hE) / (hW * (hA * hW + hB) + hD * hF)) - hE / hF); + } + + public float getShoulderStrength() { + return hA; + } + + public void setShoulderStrength(float hA) { + this.hA = hA; + recalculateWhiteScale(); + } + + public float getLinearStrength() { + return hB; + } + + public void setLinearStrength(float hB) { + this.hB = hB; + recalculateWhiteScale(); + } + + public float getLinearAngle() { + return hC; + } + + public void setLinearAngle(float hC) { + this.hC = hC; + recalculateWhiteScale(); + } + + public float getToeStrength() { + return hD; + } + + public void setToeStrength(float hD) { + this.hD = hD; + recalculateWhiteScale(); + } + + public float getToeNumerator() { + return hE; + } + + public void setToeNumerator(float hE) { + this.hE = hE; + recalculateWhiteScale(); + } + + public float getToeDenominator() { + return hF; + } + + public void setToeDenominator(float hF) { + this.hF = hF; + recalculateWhiteScale(); + } + + public float getLinearWhitePointValue() { + return hW; + } + + public void setLinearWhitePointValue(float hW) { + this.hW = hW; + recalculateWhiteScale(); + } + + public void reset() { + applyPreset(Preset.FILMIC_WORLDS); + } + + public void applyPreset(Preset preset) { + switch (preset) { + case FILMIC_WORLDS: + hA = 0.15f; + hB = 0.50f; + hC = 0.10f; + hD = 0.20f; + hE = 0.02f; + hF = 0.30f; + hW = 11.2f; + break; + case GDC: + hA = 0.22f; + hB = 0.30f; + hC = 0.10f; + hD = 0.20f; + hE = 0.01f; + hF = 0.30f; + hW = 11.2f; + break; + } + recalculateWhiteScale(); + } + @Override public void processPixel(double[] pixel) { - // This adjusts the exposure by a factor of 16 so that the resulting exposure approximately matches the other - // post-processing methods. Without this, the image would be very dark. - for(int i = 0; i < 3; ++i) { - pixel[i] *= 16; + for (int i = 0; i < 3; ++i) { + pixel[i] *= 2; // exposure bias pixel[i] = ((pixel[i] * (hA * pixel[i] + hC * hB) + hD * hE) / (pixel[i] * (hA * pixel[i] + hB) + hD * hF)) - hE / hF; pixel[i] *= whiteScale; + pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); } } @@ -34,4 +151,28 @@ public String getName() { public String getId() { return "TONEMAP3"; } + + @Override + public void loadConfiguration(JsonObject json) { + reset(); + hA = json.get("shoulderStrength").floatValue(hA); + hB = json.get("linearStrength").floatValue(hB); + hC = json.get("linearAngle").floatValue(hC); + hD = json.get("toeStrength").floatValue(hD); + hE = json.get("toeNumerator").floatValue(hE); + hF = json.get("toeDenominator").floatValue(hF); + hW = json.get("linearWhitePointValue").floatValue(hW); + recalculateWhiteScale(); + } + + @Override + public void storeConfiguration(JsonObject json) { + json.add("shoulderStrength", hA); + json.add("linearStrength", hB); + json.add("linearAngle", hC); + json.add("toeStrength", hD); + json.add("toeNumerator", hE); + json.add("toeDenominator", hF); + json.add("linearWhitePointValue", hW); + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java index 2de0c0ad5c..40afc3ddde 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilter.java @@ -2,6 +2,7 @@ import se.llbit.chunky.plugin.PluginApi; import se.llbit.chunky.resources.BitmapImage; +import se.llbit.util.Registerable; import se.llbit.util.TaskTracker; /** @@ -14,7 +15,7 @@ * PixelPostProcessingFilter} instead. */ @PluginApi -public interface PostProcessingFilter { +public interface PostProcessingFilter extends Registerable { /** * Post process the entire frame * @param width The width of the image @@ -26,23 +27,12 @@ public interface PostProcessingFilter { */ void processFrame(int width, int height, double[] input, BitmapImage output, double exposure, TaskTracker.Task task); - /** - * Get name of the post processing filter - * @return The name of the post processing filter - */ - String getName(); - /** * Get description of the post processing filter * @return The description of the post processing filter */ + @Override default String getDescription() { return null; } - - /** - * Get id of the post processing filter - * @return The id of the post processing filter - */ - String getId(); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java index 4c54162d91..7aadb64d07 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/PostProcessingFilters.java @@ -18,6 +18,7 @@ public abstract class PostProcessingFilters { addPostProcessingFilter(new Tonemap1Filter()); addPostProcessingFilter(new ACESFilmicFilter()); addPostProcessingFilter(new HableToneMappingFilter()); + addPostProcessingFilter(new UE4ToneMappingFilter()); } public static Optional getPostProcessingFilterFromId(String id) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java new file mode 100644 index 0000000000..921a631604 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java @@ -0,0 +1,176 @@ +package se.llbit.chunky.renderer.postprocessing; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.json.JsonObject; +import se.llbit.math.QuickMath; +import se.llbit.util.Configurable; + +/** + * Implementation of the Unreal Engine 4 Filmic Tone Mapper. + * + * @link https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/ColorGrading/ + * @link https://www.desmos.com/calculator/h8rbdpawxj?lang=de + */ +public class UE4ToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { + public enum Preset { + /** + * ACES curve parameters + **/ + ACES, + /** + * UE4 legacy tone mapping style + **/ + LEGACY_UE4 + } + + private float saturation; + private float slope; // ga + private float toe; // t0 + private float shoulder; // s0 + private float blackClip; // t1 + private float whiteClip; // s1 + + private float ta; + private float sa; + + public UE4ToneMappingFilter() { + reset(); + } + + private void recalculateConstants() { + ta = (1f - toe - 0.18f) / slope - 0.733f; + sa = (shoulder - 0.18f) / slope - 0.733f; + } + + public float getSaturation() { + return saturation; + } + + public void setSaturation(float saturation) { + this.saturation = saturation; + } + + public float getSlope() { + return slope; + } + + public void setSlope(float slope) { + this.slope = slope; + this.recalculateConstants(); + } + + public float getToe() { + return toe; + } + + public void setToe(float toe) { + this.toe = toe; + recalculateConstants(); + } + + public float getShoulder() { + return shoulder; + } + + public void setShoulder(float shoulder) { + this.shoulder = shoulder; + recalculateConstants(); + } + + public float getBlackClip() { + return blackClip; + } + + public void setBlackClip(float blackClip) { + this.blackClip = blackClip; + } + + public float getWhiteClip() { + return whiteClip; + } + + public void setWhiteClip(float whiteClip) { + this.whiteClip = whiteClip; + } + + public void applyPreset(Preset preset) { + switch (preset) { + case ACES: + saturation = 1f; + slope = 0.88f; + toe = 0.55f; + shoulder = 0.26f; + blackClip = 0.0f; + whiteClip = 0.04f; + break; + case LEGACY_UE4: + saturation = 1f; + slope = 0.98f; + toe = 0.3f; + shoulder = 0.22f; + blackClip = 0.0f; + whiteClip = 0.025f; + break; + } + recalculateConstants(); + } + + public void reset() { + applyPreset(Preset.ACES); + } + + private float processComponent(float c) { + float logc = (float) Math.log10(c); + + if (logc >= ta && logc <= sa) { + return (float) (saturation * (slope * (logc + 0.733) + 0.18)); + } + if (logc > sa) { + return (float) (saturation * (1 + whiteClip - (2 * (1 + whiteClip - shoulder)) / (1 + Math.exp(((2 * slope) / (1 + whiteClip - shoulder)) * (logc - sa))))); + } + // if (logc < ta) { + return (float) (saturation * ((2 * (1 + blackClip - toe)) / (1 + Math.exp(-((2 * slope) / (1 + blackClip - toe)) * (logc - ta))) - blackClip)); + // } + } + + @Override + public void processPixel(double[] pixel) { + for (int i = 0; i < 3; ++i) { + pixel[i] = QuickMath.max(QuickMath.min(processComponent((float) pixel[i] * 1.25f), 1), 0); + pixel[i] = FastMath.pow(pixel[i], 1 / Scene.DEFAULT_GAMMA); + } + } + + @Override + public String getName() { + return "Unreal Engine 4 Filmic tone mapping"; + } + + @Override + public String getId() { + return "UE4_FILMIC"; + } + + @Override + public void loadConfiguration(JsonObject json) { + reset(); + saturation = json.get("saturation").floatValue(saturation); + slope = json.get("slope").floatValue(slope); + toe = json.get("toe").floatValue(toe); + shoulder = json.get("shoulder").floatValue(shoulder); + blackClip = json.get("blackClip").floatValue(blackClip); + whiteClip = json.get("whiteClip").floatValue(whiteClip); + recalculateConstants(); + } + + @Override + public void storeConfiguration(JsonObject json) { + json.add("saturation", saturation); + json.add("slope", slope); + json.add("toe", toe); + json.add("shoulder", shoulder); + json.add("blackClip", blackClip); + json.add("whiteClip", whiteClip); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 6bac9a009e..645c353b0f 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -1731,6 +1731,9 @@ public PostProcessingFilter getPostProcessingFilter() { */ public synchronized void setPostprocess(PostProcessingFilter p) { postProcessingFilter = p; + if (postProcessingFilter instanceof Configurable) { + ((Configurable) postProcessingFilter).reset(); + } if (mode == RenderMode.PREVIEW) { // Don't interrupt the render if we are currently rendering. refresh(); @@ -2623,6 +2626,11 @@ public void setUseCustomWaterColor(boolean value) { json.add("yMax", yMax); json.add("exposure", exposure); json.add("postprocess", postProcessingFilter.getId()); + if (postProcessingFilter instanceof Configurable) { + JsonObject postprocessJson = new JsonObject(); + ((Configurable) postProcessingFilter).storeConfiguration(postprocessJson); + json.add("postprocessSettings", postprocessJson); + } json.add("outputMode", outputMode.getName()); json.add("renderTime", renderTime); json.add("spp", spp); @@ -2873,14 +2881,17 @@ public synchronized void importFromJson(JsonObject json) { exposure = json.get("exposure").doubleValue(exposure); postProcessingFilter = PostProcessingFilters - .getPostProcessingFilterFromId(json.get("postprocess").stringValue(postProcessingFilter.getId())) - .orElseGet(() -> { - if (json.get("postprocess").stringValue(null) != null) { - Log.warn("The post processing filter " + json + - " is unknown. Maybe you're missing a plugin that was used to create this scene?"); - } - return DEFAULT_POSTPROCESSING_FILTER; - }); + .getPostProcessingFilterFromId(json.get("postprocess").stringValue(postProcessingFilter.getId())) + .orElseGet(() -> { + if (json.get("postprocess").stringValue(null) != null) { + Log.warn("The post processing filter " + json + + " is unknown. Maybe you're missing a plugin that was used to create this scene?"); + } + return DEFAULT_POSTPROCESSING_FILTER; + }); + if (postProcessingFilter instanceof Configurable) { + ((Configurable) postProcessingFilter).loadConfiguration(json.get("postprocessSettings").asObject()); + } outputMode = PictureExportFormats .getFormat(json.get("outputMode").stringValue(outputMode.getName())) .orElse(PictureExportFormats.PNG); diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java index b83f900e77..2381d704c5 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/PostprocessingTab.java @@ -16,29 +16,34 @@ */ package se.llbit.chunky.ui.render.tabs; +import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Node; -import javafx.scene.control.ChoiceBox; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Separator; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.VBox; import javafx.util.StringConverter; +import se.llbit.chunky.renderer.RenderMode; +import se.llbit.chunky.renderer.postprocessing.HableToneMappingFilter; import se.llbit.chunky.renderer.postprocessing.PostProcessingFilter; import se.llbit.chunky.renderer.postprocessing.PostProcessingFilters; +import se.llbit.chunky.renderer.postprocessing.UE4ToneMappingFilter; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.resources.BitmapImage; import se.llbit.chunky.ui.DoubleAdjuster; +import se.llbit.chunky.ui.DoubleTextField; import se.llbit.chunky.ui.controller.RenderControlsFxController; import se.llbit.chunky.ui.render.RenderControlsTab; import se.llbit.util.ProgressListener; import se.llbit.util.TaskTracker; +import se.llbit.util.TaskTracker.Task; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; -import se.llbit.util.TaskTracker.Task; public class PostprocessingTab extends ScrollPane implements RenderControlsTab, Initializable { private Scene scene; @@ -47,6 +52,27 @@ public class PostprocessingTab extends ScrollPane implements RenderControlsTab, @FXML private DoubleAdjuster exposure; @FXML private ChoiceBox postprocessingFilter; + @FXML private VBox hableCurveSettings; + @FXML private DoubleTextField hableShoulderStrength; + @FXML private DoubleTextField hableLinearStrength; + @FXML private DoubleTextField hableLinearAngle; + @FXML private DoubleTextField hableToeStrength; + @FXML private DoubleTextField hableToeNumerator; + @FXML private DoubleTextField hableToeDenominator; + @FXML private DoubleTextField hableLinearWhitePointValue; + @FXML private Button gdcPreset; + @FXML private Button fwPreset; + + @FXML private VBox ue4CurveSettings; + @FXML private DoubleTextField ue4Saturation; + @FXML private DoubleTextField ue4Slope; + @FXML private DoubleTextField ue4Toe; + @FXML private DoubleTextField ue4Shoulder; + @FXML private DoubleTextField ue4BlackClip; + @FXML private DoubleTextField ue4WhiteClip; + @FXML private Button acesPreset; + @FXML private Button ue4LegacyPreset; + public PostprocessingTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("PostprocessingTab.fxml")); loader.setRoot(this); @@ -61,7 +87,28 @@ public PostprocessingTab() throws IOException { @Override public void update(Scene scene) { postprocessingFilter.getSelectionModel().select(scene.getPostProcessingFilter()); + hableCurveSettings.setVisible(scene.getPostProcessingFilter() instanceof HableToneMappingFilter); + ue4CurveSettings.setVisible(scene.getPostProcessingFilter() instanceof UE4ToneMappingFilter); exposure.set(scene.getExposure()); + + if (scene.getPostProcessingFilter() instanceof HableToneMappingFilter) { + HableToneMappingFilter filter = (HableToneMappingFilter) scene.getPostProcessingFilter(); + hableShoulderStrength.valueProperty().set(filter.getShoulderStrength()); + hableLinearStrength.valueProperty().set(filter.getLinearStrength()); + hableLinearAngle.valueProperty().set(filter.getLinearAngle()); + hableToeStrength.valueProperty().set(filter.getToeStrength()); + hableToeNumerator.valueProperty().set(filter.getToeNumerator()); + hableToeDenominator.valueProperty().set(filter.getToeDenominator()); + hableLinearWhitePointValue.valueProperty().set(filter.getLinearWhitePointValue()); + } else if (scene.getPostProcessingFilter() instanceof UE4ToneMappingFilter) { + UE4ToneMappingFilter filter = (UE4ToneMappingFilter) scene.getPostProcessingFilter(); + ue4Saturation.valueProperty().set(filter.getSaturation()); + ue4Slope.valueProperty().set(filter.getSlope()); + ue4Toe.valueProperty().set(filter.getToe()); + ue4Shoulder.valueProperty().set(filter.getShoulder()); + ue4BlackClip.valueProperty().set(filter.getBlackClip()); + ue4WhiteClip.valueProperty().set(filter.getWhiteClip()); + } } @Override public String getTabTitle() { @@ -85,8 +132,7 @@ public PostprocessingTab() throws IOException { postprocessingFilter.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> { scene.setPostprocess(newValue); - scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); - controller.getCanvas().forceRepaint(); + applyChangedSettings(false); }); postprocessingFilter.setConverter(new StringConverter() { @Override @@ -106,9 +152,81 @@ public PostProcessingFilter fromString(String string) { exposure.clampMin(); exposure.onValueChange(value -> { scene.setExposure(value); - scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); - controller.getCanvas().forceRepaint(); + applyChangedSettings(false); + }); + hableCurveSettings.managedProperty().bind(hableCurveSettings.visibleProperty()); + gdcPreset.setOnAction((e) -> { + if (scene.postProcessingFilter instanceof HableToneMappingFilter) { + ((HableToneMappingFilter) scene.postProcessingFilter).applyPreset(HableToneMappingFilter.Preset.GDC); + applyChangedSettings(true); + } + }); + fwPreset.setOnAction((e) -> { + if (scene.postProcessingFilter instanceof HableToneMappingFilter) { + ((HableToneMappingFilter) scene.postProcessingFilter).applyPreset(HableToneMappingFilter.Preset.FILMIC_WORLDS); + applyChangedSettings(true); + } }); + ue4CurveSettings.managedProperty().bind(ue4CurveSettings.visibleProperty()); + acesPreset.setOnAction((e) -> { + if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { + ((UE4ToneMappingFilter) scene.postProcessingFilter).applyPreset(UE4ToneMappingFilter.Preset.ACES); + applyChangedSettings(true); + } + }); + ue4LegacyPreset.setOnAction((e) -> { + if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { + ((UE4ToneMappingFilter) scene.postProcessingFilter).applyPreset(UE4ToneMappingFilter.Preset.LEGACY_UE4); + applyChangedSettings(true); + } + }); + + EventHandler postprocessingSettingsHandler = e -> { + if (e.getCode() == KeyCode.ENTER) { + if (scene.postProcessingFilter instanceof HableToneMappingFilter) { + HableToneMappingFilter filter = (HableToneMappingFilter) scene.postProcessingFilter; + filter.setShoulderStrength(hableShoulderStrength.valueProperty().floatValue()); + filter.setLinearStrength(hableLinearStrength.valueProperty().floatValue()); + filter.setLinearAngle(hableLinearAngle.valueProperty().floatValue()); + filter.setToeStrength(hableToeStrength.valueProperty().floatValue()); + filter.setToeNumerator(hableToeNumerator.valueProperty().floatValue()); + filter.setToeDenominator(hableToeDenominator.valueProperty().floatValue()); + filter.setLinearWhitePointValue(hableLinearWhitePointValue.valueProperty().floatValue()); + } else if (scene.postProcessingFilter instanceof UE4ToneMappingFilter) { + UE4ToneMappingFilter filter = (UE4ToneMappingFilter) scene.postProcessingFilter; + filter.setSaturation(ue4Saturation.valueProperty().floatValue()); + filter.setSlope(ue4Slope.valueProperty().floatValue()); + filter.setToe(ue4Toe.valueProperty().floatValue()); + filter.setShoulder(ue4Shoulder.valueProperty().floatValue()); + filter.setBlackClip(ue4BlackClip.valueProperty().floatValue()); + filter.setWhiteClip(ue4WhiteClip.valueProperty().floatValue()); + } + applyChangedSettings(true); + } + }; + hableShoulderStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableLinearStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableLinearAngle.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableToeStrength.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableToeNumerator.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableToeDenominator.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + hableLinearWhitePointValue.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4Saturation.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4Slope.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4Toe.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4Shoulder.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4BlackClip.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + ue4WhiteClip.addEventFilter(KeyEvent.KEY_PRESSED, postprocessingSettingsHandler); + } + + private void applyChangedSettings(boolean refreshScene) { + if (refreshScene && scene.getMode() == RenderMode.PREVIEW) { + // Don't interrupt the render if we are currently rendering. + scene.refresh(); + } + update(scene); + scene.postProcessFrame(new TaskTracker(ProgressListener.NONE)); + controller.getCanvas().forceRepaint(); } /** diff --git a/chunky/src/java/se/llbit/util/Configurable.java b/chunky/src/java/se/llbit/util/Configurable.java new file mode 100644 index 0000000000..fe180d155a --- /dev/null +++ b/chunky/src/java/se/llbit/util/Configurable.java @@ -0,0 +1,29 @@ +package se.llbit.util; + +import se.llbit.json.JsonObject; + +/** + * This interface specifies an object that can be configured by the user. + * This would be, for example, a post processing method. + */ +public interface Configurable { + /** + * Load the configuration from the given JSON object that may have been created by {@link #storeConfiguration(JsonObject)} + * but may as well have been created by external tools. + * + * @param json Source object + */ + void loadConfiguration(JsonObject json); + + /** + * Store the configuration in the given JSON object such that it can be loaded later with {@link #loadConfiguration(JsonObject)}. + * + * @param json Destination object + */ + void storeConfiguration(JsonObject json); + + /** + * Restore the default configuration. + */ + void reset(); +} diff --git a/chunky/src/res/se/llbit/chunky/ui/render/tabs/PostprocessingTab.fxml b/chunky/src/res/se/llbit/chunky/ui/render/tabs/PostprocessingTab.fxml index af17ccfcad..1306dfd18c 100644 --- a/chunky/src/res/se/llbit/chunky/ui/render/tabs/PostprocessingTab.fxml +++ b/chunky/src/res/se/llbit/chunky/ui/render/tabs/PostprocessingTab.fxml @@ -1,23 +1,81 @@ - - - - - - - - + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + +