Skip to content

Commit

Permalink
Add UE4 filmic tone mapping and make Hable tone mapping configurable. (
Browse files Browse the repository at this point in the history
…#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()
  • Loading branch information
leMaik authored Oct 28, 2023
1 parent c89be36 commit 5c91b71
Show file tree
Hide file tree
Showing 8 changed files with 580 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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 <a href="http://filmicworlds.com/blog/filmic-tonemapping-operators/">John Hable's blog post</a>
*/
FILMIC_WORLDS,

/**
* Parameters from <a href="https://www.gdcvault.com/play/1012351/Uncharted-2-HDR">John Hable's GDC talk</a>
*/
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);
}
}

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public abstract class PostProcessingFilters {
addPostProcessingFilter(new Tonemap1Filter());
addPostProcessingFilter(new ACESFilmicFilter());
addPostProcessingFilter(new HableToneMappingFilter());
addPostProcessingFilter(new UE4ToneMappingFilter());
}

public static Optional<PostProcessingFilter> getPostProcessingFilterFromId(String id) {
Expand Down
Loading

0 comments on commit 5c91b71

Please sign in to comment.