From ef6fb93a6aee456e78514aff4a0bfbb566df817a Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 21:41:56 +0100 Subject: [PATCH 1/9] Add the filmic tone mapper from Unreal Engine 4. --- .../postprocessing/PostProcessingFilters.java | 1 + .../postprocessing/UE4ToneMappingFilter.java | 152 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java 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..34e2f0b9c5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java @@ -0,0 +1,152 @@ +package se.llbit.chunky.renderer.postprocessing; + +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.math.QuickMath; + +/** + * 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 { + 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"; + } +} From 1acc7ceaca81a84cc4e253fea4abeaf1c1a9c840 Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 21:54:52 +0100 Subject: [PATCH 2/9] Fix hable tone mapping. --- .../postprocessing/HableToneMappingFilter.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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..e4ac5531e4 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java @@ -1,8 +1,13 @@ package se.llbit.chunky.renderer.postprocessing; +import org.apache.commons.math3.util.FastMath; +import se.llbit.chunky.renderer.scene.Scene; + /** * Implementation of Hable 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; @@ -16,12 +21,11 @@ public class HableToneMappingFilter extends SimplePixelPostProcessingFilter { @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); } } From 34338acf8322f882776ad6a52ea9cab56e936d2b Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 21:56:10 +0100 Subject: [PATCH 3/9] Make PostProcessingFilter extend the Registerable interface. --- .../postprocessing/PostProcessingFilter.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) 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(); } From bd1d224fe3f2620457a03a7e243718ff86bef190 Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 22:10:20 +0100 Subject: [PATCH 4/9] Prepare hable tone mapping for configuration. --- .../HableToneMappingFilter.java | 131 ++++++++++++++++-- 1 file changed, 121 insertions(+), 10 deletions(-) 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 e4ac5531e4..375ba5be5c 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java @@ -4,21 +4,132 @@ import se.llbit.chunky.renderer.scene.Scene; /** - * 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 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) { for (int i = 0; i < 3; ++i) { From 569838b9ec7ae46156ee3ab282f699dd63bc7018 Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 22:56:48 +0100 Subject: [PATCH 5/9] Add an interface for configurable objects. --- .../src/java/se/llbit/util/Configurable.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 chunky/src/java/se/llbit/util/Configurable.java 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(); +} From 0a3d0ea4df6c3e96c50ed8941529c0ebcbdab368 Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Wed, 28 Dec 2022 22:58:13 +0100 Subject: [PATCH 6/9] Make the hable and ue4 tone mapping configurable. --- .../HableToneMappingFilter.java | 28 ++++++++++++++++++- .../postprocessing/UE4ToneMappingFilter.java | 26 ++++++++++++++++- .../se/llbit/chunky/renderer/scene/Scene.java | 27 ++++++++++++------ 3 files changed, 71 insertions(+), 10 deletions(-) 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 375ba5be5c..09a51f0dc8 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/HableToneMappingFilter.java @@ -2,6 +2,8 @@ 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 (i.e. Uncharted 2) tone mapping @@ -9,7 +11,7 @@ * @link http://filmicworlds.com/blog/filmic-tonemapping-operators/ * @link https://www.gdcvault.com/play/1012351/Uncharted-2-HDR */ -public class HableToneMappingFilter extends SimplePixelPostProcessingFilter { +public class HableToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { public enum Preset { /** * Parameters from John Hable's blog post @@ -149,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/UE4ToneMappingFilter.java b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java index 34e2f0b9c5..921a631604 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java +++ b/chunky/src/java/se/llbit/chunky/renderer/postprocessing/UE4ToneMappingFilter.java @@ -2,7 +2,9 @@ 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. @@ -10,7 +12,7 @@ * @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 { +public class UE4ToneMappingFilter extends SimplePixelPostProcessingFilter implements Configurable { public enum Preset { /** * ACES curve parameters @@ -149,4 +151,26 @@ public String getName() { 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); From 7eb4ab47c860ffa09bc875b70a69a489dde9c6ff Mon Sep 17 00:00:00 2001 From: Maik Marschner Date: Sat, 22 Jul 2023 23:49:03 +0200 Subject: [PATCH 7/9] Start implementing a UI for hable and ue4 post processor configuration. --- .../ui/render/tabs/PostprocessingTab.java | 16 ++++ .../ui/render/tabs/PostprocessingTab.fxml | 81 ++++++++++++++++--- 2 files changed, 86 insertions(+), 11 deletions(-) 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..aff67ac1a9 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 @@ -30,6 +30,7 @@ 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; @@ -47,6 +48,21 @@ public class PostprocessingTab extends ScrollPane implements RenderControlsTab, @FXML private DoubleAdjuster exposure; @FXML private ChoiceBox postprocessingFilter; + @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 DoubleTextField ue4Saturation; + @FXML private DoubleTextField ue4Slope; + @FXML private DoubleTextField ue4Toe; + @FXML private DoubleTextField ue4Shoulder; + @FXML private DoubleTextField ue4BlackClip; + @FXML private DoubleTextField ue4WhiteClip; + public PostprocessingTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("PostprocessingTab.fxml")); loader.setRoot(this); 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..afdd3850ee 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,82 @@ - - - - - - - - + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + +