diff --git a/android/filament-android/src/main/cpp/ColorGrading.cpp b/android/filament-android/src/main/cpp/ColorGrading.cpp
index 91c3e3ad76..d690216bb5 100644
--- a/android/filament-android/src/main/cpp/ColorGrading.cpp
+++ b/android/filament-android/src/main/cpp/ColorGrading.cpp
@@ -76,6 +76,13 @@ Java_com_google_android_filament_ColorGrading_nBuilderLuminanceScaling(JNIEnv*,
builder->luminanceScaling(luminanceScaling);
}
+extern "C" JNIEXPORT void JNICALL
+Java_com_google_android_filament_ColorGrading_nBuilderGamutMapping(JNIEnv*, jclass,
+ jlong nativeBuilder, jboolean gamutMapping) {
+ ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder;
+ builder->gamutMapping(gamutMapping);
+}
+
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_ColorGrading_nBuilderExposure(JNIEnv*, jclass,
jlong nativeBuilder, jfloat exposure) {
diff --git a/android/filament-android/src/main/java/com/google/android/filament/ColorGrading.java b/android/filament-android/src/main/java/com/google/android/filament/ColorGrading.java
index 01401a1eaa..ca45d37019 100644
--- a/android/filament-android/src/main/java/com/google/android/filament/ColorGrading.java
+++ b/android/filament-android/src/main/java/com/google/android/filament/ColorGrading.java
@@ -65,6 +65,7 @@ import static com.google.android.filament.Asserts.assertFloat4In;
*
Curves
* Tone mapping
* Luminance scaling
+ * Gamut mapping
*
*
* Defaults
@@ -83,6 +84,7 @@ import static com.google.android.filament.Asserts.assertFloat4In;
* Curves: gamma {1,1,1}, midPoint {1,1,1}, and scale {1,1,1}
* Tone mapping: {@link ToneMapper.ACESLegacy}
* Luminance scaling: false
+ * Gamut mapping: false
*
*
* @see View
@@ -213,6 +215,22 @@ public class ColorGrading {
return this;
}
+ /**
+ * Enables or disables gamut mapping to the destination color space's gamut. When gamut
+ * mapping is turned off, out-of-gamut colors are clipped to the destination's gamut,
+ * which may produce hue skews (blue skewing to purple, green to yellow, etc.). When
+ * gamut mapping is enabled, out-of-gamut colors are brought back in gamut by trying to
+ * preserve the perceived chroma and lightness of the original values.
+ *
+ * @param gamutMapping Enables or disables gamut mapping
+ *
+ * @return This Builder, for chaining calls
+ */
+ public Builder gamutMapping(boolean gamutMapping) {
+ nBuilderGamutMapping(mNativeBuilder, gamutMapping);
+ return this;
+ }
+
/**
* Adjusts the exposure of this image. The exposure is specified in stops:
* each stop brightens (positive values) or darkens (negative values) the image by
@@ -524,6 +542,7 @@ public class ColorGrading {
private static native void nBuilderToneMapper(long nativeBuilder, long toneMapper);
private static native void nBuilderToneMapping(long nativeBuilder, int toneMapping);
private static native void nBuilderLuminanceScaling(long nativeBuilder, boolean luminanceScaling);
+ private static native void nBuilderGamutMapping(long nativeBuilder, boolean gamutMapping);
private static native void nBuilderExposure(long nativeBuilder, float exposure);
private static native void nBuilderWhiteBalance(long nativeBuilder, float temperature, float tint);
private static native void nBuilderChannelMixer(long nativeBuilder, float[] outRed, float[] outGreen, float[] outBlue);
diff --git a/filament/include/filament/ColorGrading.h b/filament/include/filament/ColorGrading.h
index 9b583a5e54..1f54741537 100644
--- a/filament/include/filament/ColorGrading.h
+++ b/filament/include/filament/ColorGrading.h
@@ -76,6 +76,7 @@ class FColorGrading;
* - Curves
* - Tone mapping
* - Luminance scaling
+ * - Gamut mapping
*
* Defaults
* ========
@@ -93,6 +94,7 @@ class FColorGrading;
* - Curves: gamma {1,1,1}, midPoint {1,1,1}, and scale {1,1,1}
* - Tone mapping: ACESLegacyToneMapper
* - Luminance scaling: false
+ * - Gamut mapping: false
*
* @see View
*/
@@ -186,12 +188,25 @@ public:
* When luminance scaling is enabled, tone mapping is performed on the luminance of each
* pixel instead of per-channel.
*
- * @param luminanceScaling Enables or disables EVILS post-tone mapping
+ * @param luminanceScaling Enables or disables luminance scaling post-tone mapping
*
* @return This Builder, for chaining calls
*/
Builder& luminanceScaling(bool luminanceScaling) noexcept;
+ /**
+ * Enables or disables gamut mapping to the destination color space's gamut. When gamut
+ * mapping is turned off, out-of-gamut colors are clipped to the destination's gamut,
+ * which may produce hue skews (blue skewing to purple, green to yellow, etc.). When
+ * gamut mapping is enabled, out-of-gamut colors are brought back in gamut by trying to
+ * preserve the perceived chroma and lightness of the original values.
+ *
+ * @param gamutMapping Enables or disables gamut mapping
+ *
+ * @return This Builder, for chaining calls
+ */
+ Builder& gamutMapping(bool gamutMapping) noexcept;
+
/**
* Adjusts the exposure of this image. The exposure is specified in stops:
* each stop brightens (positive values) or darkens (negative values) the image by
diff --git a/filament/src/ColorGrading.cpp b/filament/src/ColorGrading.cpp
index 8fc5732bae..804cb77c06 100644
--- a/filament/src/ColorGrading.cpp
+++ b/filament/src/ColorGrading.cpp
@@ -56,8 +56,9 @@ struct ColorGrading::BuilderDetails {
// Everything below must be part of the == comparison operator
ColorGrading::QualityLevel quality = QualityLevel::MEDIUM;
- // Luminance scaling
+ // Out-of-gamut color handling
bool luminanceScaling = false;
+ bool gamutMapping = false;
// Exposure
float exposure = 0.0f;
// White balance
@@ -92,6 +93,7 @@ struct ColorGrading::BuilderDetails {
// Note: Do NOT compare hasAdjustments and toneMapper
return quality == rhs.quality &&
luminanceScaling == rhs.luminanceScaling &&
+ gamutMapping == rhs.gamutMapping &&
exposure == rhs.exposure &&
whiteBalance == rhs.whiteBalance &&
outRed == rhs.outRed &&
@@ -141,6 +143,11 @@ ColorGrading::Builder& ColorGrading::Builder::luminanceScaling(bool luminanceSca
return *this;
}
+ColorGrading::Builder& ColorGrading::Builder::gamutMapping(bool gamutMapping) noexcept {
+ mImpl->gamutMapping = gamutMapping;
+ return *this;
+}
+
ColorGrading::Builder& ColorGrading::Builder::exposure(float exposure) noexcept {
mImpl->exposure = exposure;
return *this;
@@ -268,22 +275,21 @@ inline float3 adjustExposure(float3 v, float exposure) {
// the von Kries method, using the CIECAT16 transform.
// See https://en.wikipedia.org/wiki/Chromatic_adaptation
// See https://en.wikipedia.org/wiki/CIECAM02#Chromatic_adaptation
-UTILS_ALWAYS_INLINE
-inline float3 adaptationTransform(float2 whiteBalance) {
+constexpr mat3f adaptationTransform(float2 whiteBalance) noexcept {
// See Mathematica notebook in docs/math/White Balance.nb
float k = whiteBalance.x; // temperature
float t = whiteBalance.y; // tint
- float x = ILLUMINANT_D65_xyY.x - k * (k < 0.0f ? 0.0214f : 0.066f);
+ float x = ILLUMINANT_D65_xyY[0] - k * (k < 0.0f ? 0.0214f : 0.066f);
float y = chromaticityCoordinateIlluminantD(x) + t * 0.066f;
float3 lms = XYZ_to_CIECAT16 * xyY_to_XYZ({x, y, 1.0f});
- return ILLUMINANT_D65_LMS_CAT16 / lms;
+ return LMS_CAT16_to_REC2020 * mat3f{ILLUMINANT_D65_LMS_CAT16 / lms} * REC2020_to_LMS_CAT16;
}
UTILS_ALWAYS_INLINE
-inline float3 chromaticAdaptation(float3 v, float3 adaptationTransform) {
- return LMS_CAT16_to_sRGB * (adaptationTransform * (sRGB_to_LMS_CAT16 * v));
+inline float3 chromaticAdaptation(float3 v, mat3f adaptationTransform) {
+ return adaptationTransform * v;
}
//------------------------------------------------------------------------------
@@ -405,7 +411,7 @@ static float3 luminanceScaling(float3 x,
// Quality
//------------------------------------------------------------------------------
-size_t selectLutDimension(ColorGrading::QualityLevel quality) {
+static size_t selectLutDimension(ColorGrading::QualityLevel quality) noexcept {
switch (quality) {
case ColorGrading::QualityLevel::LOW: return 16u;
case ColorGrading::QualityLevel::MEDIUM: return 32u;
@@ -414,8 +420,8 @@ size_t selectLutDimension(ColorGrading::QualityLevel quality) {
}
}
-void selectLutTextureParams(ColorGrading::QualityLevel quality,
- TextureFormat& internalFormat, PixelDataFormat& format, PixelDataType& type) {
+static void selectLutTextureParams(ColorGrading::QualityLevel quality,
+ TextureFormat& internalFormat, PixelDataFormat& format, PixelDataType& type) noexcept {
// We use RGBA16F for high quality modes instead of RGB16F because RGB16F
// is not supported everywhere
switch (quality) {
@@ -442,13 +448,45 @@ void selectLutTextureParams(ColorGrading::QualityLevel quality,
}
}
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+// The following functions exist to preserve backward compatibility with the
+// `FILMIC` set via the deprecated `ToneMapping` API. Selecting `ToneMapping::FILMIC`
+// forces post-processing to be performed in sRGB to guarantee that the inverse tone
+// mapping function in the shaders will match the forward tone mapping step exactly.
+
+static mat3f selectColorGradingTransformIn(ColorGrading::ToneMapping toneMapping) noexcept {
+ if (toneMapping == ColorGrading::ToneMapping::FILMIC) {
+ return mat3f{};
+ }
+ return sRGB_to_REC2020;
+}
+
+static mat3f selectColorGradingTransformOut(ColorGrading::ToneMapping toneMapping) noexcept {
+ if (toneMapping == ColorGrading::ToneMapping::FILMIC) {
+ return mat3f{};
+ }
+ return REC2020_to_sRGB;
+}
+
+static float3 selectColorGradingLuminance(ColorGrading::ToneMapping toneMapping) noexcept {
+ if (toneMapping == ColorGrading::ToneMapping::FILMIC) {
+ return LUMINANCE_REC709;
+ }
+ return LUMINANCE_REC2020;
+}
+#pragma clang diagnostic pop
+
//------------------------------------------------------------------------------
// Color grading implementation
//------------------------------------------------------------------------------
struct Config {
size_t lutDimension;
- float3 adaptationTransform;
+ mat3f adaptationTransform;
+ mat3f colorGradingIn;
+ mat3f colorGradingOut;
+ float3 colorGradingLuminance;
};
// Inside the FColorGrading constructor, TSAN sporadically detects a data race on the config struct;
@@ -466,8 +504,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
utils::SpinLock configLock;
{
std::lock_guard lock(configLock);
- c.lutDimension = selectLutDimension(builder->quality);
- c.adaptationTransform = adaptationTransform(builder->whiteBalance);
+ c.lutDimension = selectLutDimension(builder->quality);
+ c.adaptationTransform = adaptationTransform(builder->whiteBalance);
+ c.colorGradingIn = selectColorGradingTransformIn(builder->toneMapping);
+ c.colorGradingOut = selectColorGradingTransformOut(builder->toneMapping);
+ c.colorGradingLuminance = selectColorGradingLuminance(builder->toneMapping);
}
mDimension = c.lutDimension;
@@ -511,18 +552,16 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
// LogC encoding
v = LogC_to_linear(v);
- // TODO: Performed in sRGB, should be in Rec.2020 or AP1
+ // Move to color grading color space
+ v = c.colorGradingIn * v;
if (builder->hasAdjustments) {
// Exposure
v = adjustExposure(v, builder->exposure);
+
// White balance
v = chromaticAdaptation(v, config.adaptationTransform);
- }
- // TODO: We should convert to a color grading color space here
-
- if (builder->hasAdjustments) {
// Kill negative values before the next transforms
v = max(v, 0.0f);
@@ -530,12 +569,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
v = channelMixer(v, builder->outRed, builder->outGreen, builder->outBlue);
// Shadows/mid-tones/highlights
- v = tonalRanges(v, LUMA_REC709,
+ v = tonalRanges(v, c.colorGradingLuminance,
builder->shadows, builder->midtones, builder->highlights,
builder->tonalRanges);
- // The adjustments below behave better in log space using the ACEScct
- // color space.
+ // The adjustments below behave better in log space
v = linear_to_LogC(v);
// ASC CDL
@@ -548,12 +586,12 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
v = LogC_to_linear(v);
// Vibrance in linear space
- v = vibrance(v, LUMA_REC709, builder->vibrance);
+ v = vibrance(v, c.colorGradingLuminance, builder->vibrance);
// Saturation in linear space
- v = saturation(v, LUMA_REC709, builder->saturation);
+ v = saturation(v, c.colorGradingLuminance, builder->saturation);
- // Kill negative values before tone mapping
+ // Kill negative values before curves
v = max(v, 0.0f);
// RGB curves
@@ -563,15 +601,23 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
// Tone mapping
if (builder->luminanceScaling) {
- v = luminanceScaling(v, *builder->toneMapper, LUMA_HK_REC709);
+ v = luminanceScaling(v, *builder->toneMapper, c.colorGradingLuminance);
} else {
v = (*builder->toneMapper)(v);
}
+ // Go back to display color space
+ v = c.colorGradingOut * v;
+
+ // Apply gamut mapping
+ if (builder->gamutMapping) {
+ // TODO: This should depend on the output color space
+ v = gamutMapping_sRGB(v);
+ }
+
// TODO: We should convert to the output color space if we use a working
// color space that's not sRGB
// TODO: Allow the user to customize the output color space
- // TODO: Gamut mapping
// We need to clamp for the output transfer function
v = saturate(v);
diff --git a/filament/src/ColorSpace.cpp b/filament/src/ColorSpace.cpp
index 93d31c662c..9b64f5b95f 100644
--- a/filament/src/ColorSpace.cpp
+++ b/filament/src/ColorSpace.cpp
@@ -45,10 +45,14 @@ using namespace math;
* SOFTWARE.
*/
+// TODO: Replace compute_max_saturation() with solution from
+// https://simonstechblog.blogspot.com/2021/06/implementing-gamut-mapping.html to
+// target arbitrary output gamuts. The solution below works only for sRGB/Rec.709
+
// Finds the maximum saturation possible for a given hue that fits in sRGB
// Saturation here is defined as S = C/L
// a and b must be normalized so a^2 + b^2 == 1
-float compute_max_saturation(float a, float b) noexcept {
+static float compute_max_saturation(float a, float b) noexcept {
// Max saturation will be when one of r, g or b goes below zero.
// Select different coefficients depending on which component goes below zero first
@@ -127,13 +131,13 @@ float compute_max_saturation(float a, float b) noexcept {
// finds L_cusp and C_cusp for a given hue
// a and b must be normalized so a^2 + b^2 == 1
-float2 find_cusp(float a, float b) noexcept {
+static float2 find_cusp(float a, float b) noexcept {
// First, find the maximum saturation (saturation S = C/L)
float S_cusp = compute_max_saturation(a, b);
// Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
float3 rgb_at_max = OkLab_to_sRGB({1.0f, S_cusp * a, S_cusp * b});
- float L_cusp = std::cbrt(1.f / max(max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b));
+ float L_cusp = std::cbrt(1.0f / max(rgb_at_max));
float C_cusp = L_cusp * S_cusp;
return {L_cusp, C_cusp};
@@ -143,11 +147,11 @@ float2 find_cusp(float a, float b) noexcept {
// L = L0 * (1 - t) + t * L1;
// C = t * C1;
// a and b must be normalized so a^2 + b^2 == 1
-float find_gamut_intersection(float a, float b, float L1, float C1, float L0) noexcept {
+static float find_gamut_intersection(float a, float b, float L1, float C1, float L0) noexcept {
// Find the cusp of the gamut triangle
float2 cusp = find_cusp(a, b);
- // Find the intersection for upper and lower half seprately
+ // Find the intersection for upper and lower half separately
float t;
if (((L1 - L0) * cusp.y - (cusp.x - L0) * C1) <= 0.f) {
// Lower half
@@ -231,8 +235,15 @@ constexpr float sgn(float x) noexcept {
return (float) (0.f < x) - (float) (x < 0.f);
}
-inline float3 gamut_clip_adaptive_L0_0_5(float3 rgb, float alpha = 0.05f) noexcept {
- if (all(lessThanEqual(rgb, float3{1.0f})) && all(greaterThanEqual(rgb, float3{0.0f}))) {
+// Adaptive L0 = 0.5, with alpha set to 0.05 by default
+// The threshold parameters defines a flexible range above 1.0 and below 0.0 where out-of-gamut
+// values are still considered in-gamut. This helps control for inaccuracies in the previous
+// color grading steps that may slightly deviate from in-gamut values when they shouldn't.
+inline float3 gamut_clip_adaptive_L0_0_5(float3 rgb,
+ float alpha = 0.05f, float threshold = 0.03f) noexcept {
+
+ if (all(lessThanEqual(rgb, float3{1.0f + threshold})) &&
+ all(greaterThanEqual(rgb, float3{-threshold}))) {
return rgb;
}
@@ -246,7 +257,7 @@ inline float3 gamut_clip_adaptive_L0_0_5(float3 rgb, float alpha = 0.05f) noexce
float Ld = L - 0.5f;
float e1 = 0.5f + std::abs(Ld) + alpha * C;
- float L0 = 0.5f * (1.f + sgn(Ld) * (e1 - std::sqrt(e1 * e1 - 2.f * std::abs(Ld))));
+ float L0 = 0.5f * (1.0f + sgn(Ld) * (e1 - std::sqrt(e1 * e1 - 2.0f * std::abs(Ld))));
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1.f - t) + t * L;
@@ -256,7 +267,7 @@ inline float3 gamut_clip_adaptive_L0_0_5(float3 rgb, float alpha = 0.05f) noexce
}
float3 gamutMapping_sRGB(float3 rgb) noexcept {
- return gamut_clip_adaptive_L0_0_5(rgb, 0.5f);
+ return gamut_clip_adaptive_L0_0_5(rgb);
}
/*
diff --git a/filament/src/ColorSpace.h b/filament/src/ColorSpace.h
index 28194cd300..9622ef337b 100644
--- a/filament/src/ColorSpace.h
+++ b/filament/src/ColorSpace.h
@@ -53,15 +53,15 @@ constexpr mat3f sRGB_to_XYZ{
};
constexpr mat3f REC2020_to_XYZ{
- 0.6369580f, 0.2627002f, 0.0000000f,
- 0.1446169f, 0.6779981f, 0.0280727f,
- 0.1688810f, 0.0593017f, 1.0609851f
+ 0.6369530f, 0.2626983f, 0.0000000f,
+ 0.1446169f, 0.6780088f, 0.0280731f,
+ 0.1688558f, 0.0592929f, 1.0608272f
};
constexpr mat3f XYZ_to_REC2020{
- 1.7166512f, -0.6666844f, 0.0176399f,
- -0.3556708f, 1.6164812f, -0.0427706f,
- -0.2533663f, 0.0157685f, 0.9421031f
+ 1.7166634f, -0.6666738f, 0.0176425f,
+ -0.3556733f, 1.6164557f, -0.0427770f,
+ -0.2533681f, 0.0157683f, 0.9422433f
};
constexpr mat3f XYZ_to_CIECAT16{
@@ -156,6 +156,10 @@ constexpr mat3f sRGB_to_LMS_CAT16 = XYZ_to_CIECAT16 * sRGB_to_XYZ;
constexpr mat3f LMS_CAT16_to_sRGB = XYZ_to_sRGB * CIECAT16_to_XYZ;
+constexpr mat3f REC2020_to_LMS_CAT16 = XYZ_to_CIECAT16 * REC2020_to_XYZ;
+
+constexpr mat3f LMS_CAT16_to_REC2020 = XYZ_to_REC2020 * CIECAT16_to_XYZ;
+
constexpr mat3f REC2020_to_AP0 = AP1_to_AP0 * XYZ_to_AP1 * REC2020_to_XYZ;
constexpr mat3f AP1_to_REC2020 = XYZ_to_REC2020 * AP1_to_XYZ;
@@ -172,16 +176,16 @@ constexpr float3 ILLUMINANT_D65_xyY{0.31271f, 0.32902f, 1.0f};
constexpr float3 ILLUMINANT_D65_LMS_CAT16{0.975533f, 1.016483f, 1.084837f};
// RGB to luminance coefficients for Rec.2020, from REC2020_to_XYZ
-constexpr float3 LUMA_REC2020{0.2627002f, 0.6779981f, 0.0593017f};
+constexpr float3 LUMINANCE_REC2020{0.2627002f, 0.6779981f, 0.0593017f};
// RGB to luminance coefficients for ACEScg (AP1), from AP1_to_XYZ
-constexpr float3 LUMA_AP1{0.272229f, 0.674082f, 0.0536895f};
+constexpr float3 LUMINANCE_AP1{0.272229f, 0.674082f, 0.0536895f};
// RGB to luminance coefficients for Rec.709, from sRGB_to_XYZ
-constexpr float3 LUMA_REC709{0.2126730f, 0.7151520f, 0.0721750f};
+constexpr float3 LUMINANCE_REC709{0.2126730f, 0.7151520f, 0.0721750f};
// RGB to luminance coefficients for Rec.709 with HK-like weighting
-constexpr float3 LUMA_HK_REC709{0.13913043f, 0.73043478f, 0.13043478f};
+constexpr float3 LUMINANCE_HK_REC709{0.13913043f, 0.73043478f, 0.13043478f};
constexpr float MIDDLE_GRAY_ACEScg = 0.18f;
diff --git a/filament/src/ToneMapper.cpp b/filament/src/ToneMapper.cpp
index 7fdf4eb189..29d84327ef 100644
--- a/filament/src/ToneMapper.cpp
+++ b/filament/src/ToneMapper.cpp
@@ -135,7 +135,7 @@ float3 ACES(float3 color, float brightness) noexcept {
constexpr float RRT_SAT_FACTOR = 0.96f;
constexpr float ODT_SAT_FACTOR = 0.93f;
- float3 ap0 = sRGB_to_AP0 * color;
+ float3 ap0 = REC2020_to_AP0 * color;
// Glow module
float saturation = rgb_2_saturation(ap0);
@@ -156,7 +156,7 @@ float3 ACES(float3 color, float brightness) noexcept {
float3 ap1 = clamp(AP0_to_AP1 * ap0, 0.0f, (float) std::numeric_limits::max());
// Global desaturation
- ap1 = mix(float3(dot(ap1, LUMA_AP1)), ap1, RRT_SAT_FACTOR);
+ ap1 = mix(float3(dot(ap1, LUMINANCE_AP1)), ap1, RRT_SAT_FACTOR);
// NOTE: This is specific to Filament and added only to match ACES to our legacy tone mapper
// which was a fit of ACES in Rec.709 but with a brightness boost.
@@ -175,9 +175,9 @@ float3 ACES(float3 color, float brightness) noexcept {
float3 linearCV = darkSurround_to_dimSurround(rgbPost);
// Apply desaturation to compensate for luminance difference
- linearCV = mix(float3(dot(linearCV, LUMA_AP1)), linearCV, ODT_SAT_FACTOR);
+ linearCV = mix(float3(dot(linearCV, LUMINANCE_AP1)), linearCV, ODT_SAT_FACTOR);
- return AP1_to_sRGB * linearCV;
+ return AP1_to_REC2020 * linearCV;
}
} // namespace aces
@@ -260,7 +260,8 @@ float3 DisplayRangeToneMapper::operator()(math::float3 c) const noexcept {
// The 5th color in the array (cyan) represents middle gray (18%)
// Every stop above or below middle gray causes a color shift
- float v = log2(dot(c, LUMA_REC709) / 0.18f);
+ // TODO: This should depend on the working color grading color space
+ float v = log2(dot(c, LUMINANCE_REC2020) / 0.18f);
v = clamp(v + 5.0f, 0.0f, 15.0f);
size_t index = size_t(v);
diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h
index 59413898a1..8daad7244f 100644
--- a/libs/viewer/include/viewer/Settings.h
+++ b/libs/viewer/include/viewer/Settings.h
@@ -118,6 +118,7 @@ struct ColorGradingSettings {
ToneMapping toneMapping = ToneMapping::ACES_LEGACY;
GenericToneMapperSettings genericToneMapper;
bool luminanceScaling = false;
+ bool gamutMapping = false;
float exposure = 0.0f;
float temperature = 0.0f;
float tint = 0.0f;
diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp
index a255f5b6e4..954db89c9d 100644
--- a/libs/viewer/src/Settings.cpp
+++ b/libs/viewer/src/Settings.cpp
@@ -310,6 +310,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGra
i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper);
} else if (compare(tok, jsonChunk, "luminanceScaling") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->luminanceScaling);
+ } else if (compare(tok, jsonChunk, "gamutMapping") == 0) {
+ i = parse(tokens, i + 1, jsonChunk, &out->gamutMapping);
} else if (compare(tok, jsonChunk, "exposure") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->exposure);
} else if (compare(tok, jsonChunk, "temperature") == 0) {
@@ -1034,6 +1036,7 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e
.curves(settings.gamma, settings.midPoint, settings.scale)
.toneMapper(toneMapper)
.luminanceScaling(settings.luminanceScaling)
+ .gamutMapping(settings.gamutMapping)
.build(*engine);
delete toneMapper;
return colorGrading;
@@ -1156,6 +1159,7 @@ static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& i
<< "\"toneMapping\": " << (in.toneMapping) << ",\n"
<< "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n"
<< "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n"
+ << "\"gamutMapping\": " << to_string(in.gamutMapping) << ",\n"
<< "\"exposure\": " << (in.exposure) << ",\n"
<< "\"temperature\": " << (in.temperature) << ",\n"
<< "\"tint\": " << (in.tint) << ",\n"
@@ -1427,6 +1431,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings &rhs) const {
toneMapping == rhs.toneMapping &&
genericToneMapper == rhs.genericToneMapper &&
luminanceScaling == rhs.luminanceScaling &&
+ gamutMapping == rhs.gamutMapping &&
exposure == rhs.exposure &&
temperature == rhs.temperature &&
tint == rhs.tint &&
diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp
index 18112bdb00..6a6afb03c9 100644
--- a/libs/viewer/src/SimpleViewer.cpp
+++ b/libs/viewer/src/SimpleViewer.cpp
@@ -211,6 +211,7 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo
ImGui::PopStyleColor();
ImGui::Checkbox("Luminance scaling", &colorGrading.luminanceScaling);
+ ImGui::Checkbox("Gamut mapping", &colorGrading.gamutMapping);
ImGui::SliderFloat("Exposure", &colorGrading.exposure, -10.0f, 10.0f);
diff --git a/libs/viewer/tests/test_settings.cpp b/libs/viewer/tests/test_settings.cpp
index 2f4f6f4a43..75f3e84c90 100644
--- a/libs/viewer/tests/test_settings.cpp
+++ b/libs/viewer/tests/test_settings.cpp
@@ -45,6 +45,7 @@ static const char* JSON_TEST_DEFAULTS = R"TXT(
"hdrMax": 16.0
},
"luminanceScaling": false,
+ "gamutMapping": false,
"exposure": 0,
"temperature": 0,
"tint": 0,