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

Add UE4 filmic tone mapping and make Hable tone mapping configurable. #1519

Merged
merged 9 commits into from
Oct 28, 2023
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
Loading