diff --git a/android/filament-android/src/main/cpp/ToneMapper.cpp b/android/filament-android/src/main/cpp/ToneMapper.cpp
index 860b95508b..a42a336461 100644
--- a/android/filament-android/src/main/cpp/ToneMapper.cpp
+++ b/android/filament-android/src/main/cpp/ToneMapper.cpp
@@ -52,6 +52,11 @@ Java_com_google_android_filament_ToneMapper_nCreatePBRNeutralToneMapper(JNIEnv*,
return (jlong) new PBRNeutralToneMapper();
}
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_google_android_filament_ToneMapper_nCreateGT7ToneMapper(JNIEnv*, jclass) {
+ return (jlong) new GT7ToneMapper();
+}
+
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_android_filament_ToneMapper_nCreateAgxToneMapper(JNIEnv*, jclass, jint look) {
return (jlong) new AgxToneMapper(AgxToneMapper::AgxLook(look));
diff --git a/android/filament-android/src/main/java/com/google/android/filament/ToneMapper.java b/android/filament-android/src/main/java/com/google/android/filament/ToneMapper.java
index 89b12541c2..3d81b96632 100644
--- a/android/filament-android/src/main/java/com/google/android/filament/ToneMapper.java
+++ b/android/filament-android/src/main/java/com/google/android/filament/ToneMapper.java
@@ -24,6 +24,7 @@ package com.google.android.filament;
*
ACESLegacyToneMapper
* FilmicToneMapper
* PBRNeutralToneMapper
+ * GT7ToneMapper
*
* Debug/validation tone mapping operators
*
@@ -111,6 +112,19 @@ public class ToneMapper {
}
}
+ /**
+ * Gran Turismo 7 tone mapping operator. This tone mapper was designed
+ * to preserve the appearance of materials across lighting conditions while
+ * avoiding artifacts in the highlights in high dynamic range conditions.
+ * This tone mapper targets an SDR paper white value of 250 nits, with a
+ * reference luminance of 100 cd/m^2 (a value of 1.0 in the HDR framebuffer).
+ */
+ public static class GT7ToneMapper extends ToneMapper {
+ public GT7ToneMapper() {
+ super(nCreateGT7ToneMapper());
+ }
+ }
+
/**
* AgX tone mapping operator.
*/
@@ -244,6 +258,7 @@ public class ToneMapper {
private static native long nCreateACESLegacyToneMapper();
private static native long nCreateFilmicToneMapper();
private static native long nCreatePBRNeutralToneMapper();
+ private static native long nCreateGT7ToneMapper();
private static native long nCreateAgxToneMapper(int look);
private static native long nCreateGenericToneMapper(
float contrast, float midGrayIn, float midGrayOut, float hdrMax);
diff --git a/filament/include/filament/ToneMapper.h b/filament/include/filament/ToneMapper.h
index cff0576eef..8762e0eacb 100644
--- a/filament/include/filament/ToneMapper.h
+++ b/filament/include/filament/ToneMapper.h
@@ -45,6 +45,7 @@ namespace filament {
* - ACESLegacyToneMapper
* - FilmicToneMapper
* - PBRNeutralToneMapper
+ * - GT7ToneMapper
* - Debug/validation tone mapping operators
* - LinearToneMapper
* - DisplayRangeToneMapper
@@ -72,15 +73,17 @@ struct UTILS_PUBLIC ToneMapper {
/**
* If true, then this function holds that f(x) = vec3(f(x.r), f(x.g), f(x.b))
*
- * This may be used to indicate that the color grading's LUT only requires a 1D texture instead
- * of a 3D texture, potentially saving a significant amount of memory and generation time.
+ * This may be used to indicate that the color grading's LUT only requires a
+ * 1D texture instead of a 3D texture, potentially saving a significant amount
+ * of memory and generation time.
*/
virtual bool isOneDimensional() const noexcept { return false; }
/**
* True if this tonemapper only works in low-dynamic-range.
*
- * This may be used to indicate that the color grading's LUT doesn't need to be log encoded.
+ * This may be used to indicate that the color grading's LUT doesn't need to be
+ * log encoded.
*/
virtual bool isLDR() const noexcept { return false; }
};
@@ -156,6 +159,26 @@ struct UTILS_PUBLIC PBRNeutralToneMapper final : public ToneMapper {
bool isLDR() const noexcept override { return false; }
};
+/**
+ * Gran Turismo 7 tone mapping operator. This tone mapper was designed
+ * to preserve the appearance of materials across lighting conditions while
+ * avoiding artifacts in the highlights in high dynamic range conditions.
+ * This tone mapper targets an SDR paper white value of 250 nits, with a
+ * reference luminance of 100 cd/m^2 (a value of 1.0 in the HDR framebuffer).
+ */
+struct UTILS_PUBLIC GT7ToneMapper final : public ToneMapper {
+ GT7ToneMapper() noexcept;
+ ~GT7ToneMapper() noexcept final;
+
+ math::float3 operator()(math::float3 x) const noexcept override;
+ bool isOneDimensional() const noexcept override { return false; }
+ bool isLDR() const noexcept override { return false; }
+
+private:
+ struct State;
+ State* mState;
+};
+
/**
* AgX tone mapping operator.
*/
diff --git a/filament/src/ColorSpaceUtils.h b/filament/src/ColorSpaceUtils.h
index 3c10888921..3f5a2ee14c 100644
--- a/filament/src/ColorSpaceUtils.h
+++ b/filament/src/ColorSpaceUtils.h
@@ -218,39 +218,6 @@ inline constexpr float chromaticityCoordinateIlluminantD(float const x) noexcept
return 2.87f * x - 3.0f * x * x - 0.275f;
}
-//------------------------------------------------------------------------------
-// Color space conversions
-//------------------------------------------------------------------------------
-
-inline constexpr XYZ xyY_to_XYZ(xyY const v) noexcept {
- const float a = v.z / max(v.y, 1e-5f);
- return XYZ{v.x * a, v.z, (1.0f - v.x - v.y) * a};
-}
-
-inline constexpr xyY XYZ_to_xyY(XYZ v) noexcept {
- return {v.xy / max(v.x + v.y + v.z, 1e-5f), v.y};
-}
-
-inline constexpr float3 pow3(float3 const x) noexcept {
- return x * x * x;
-}
-
-inline float3 sRGB_to_OkLab(float3 const x) noexcept {
- return OkLab_LMS_to_OkLab * cbrt(sRGB_to_OkLab_LMS * x);
-}
-
-inline float3 Rec2020_to_OkLab(float3 const x) noexcept {
- return OkLab_LMS_to_OkLab * cbrt(Rec2020_to_OkLab_LMS * x);
-}
-
-inline float3 OkLab_to_sRGB(float3 const x) noexcept {
- return OkLab_LMS_to_sRGB * pow3(OkLab_to_OkLab_LMS * x);
-}
-
-inline float3 OkLab_to_Rec2020(float3 const x) noexcept {
- return OkLab_LMS_to_Rec2020 * pow3(OkLab_to_OkLab_LMS * x);
-}
-
//------------------------------------------------------------------------------
// Conversion functions and encoding/decoding
//------------------------------------------------------------------------------
@@ -322,34 +289,95 @@ inline float3 EOTF_sRGB(float3 x) noexcept {
return x;
}
-inline float3 OETF_PQ(float3 x, float maxPqValue) {
+inline float3 OETF_PQ(float3 x) {
constexpr float PQ_constant_N = 2610.0f / 4096.0f / 4.0f;
constexpr float PQ_constant_M = 2523.0f / 4096.0f * 128.0f;
constexpr float PQ_constant_C1 = 3424.0f / 4096.0f;
constexpr float PQ_constant_C2 = 2413.0f / 4096.0f * 32.0f;
constexpr float PQ_constant_C3 = 2392.0f / 4096.0f * 32.0f;
+ constexpr float PQ_C = 10000.0f;
- x /= maxPqValue;
-
- float3 g = pow(x, PQ_constant_N);
+ float3 g = pow(x / PQ_C, PQ_constant_N);
float3 numerator = PQ_constant_C1 + PQ_constant_C2 * g;
float3 denominator = 1.0f + PQ_constant_C3 * g;
return pow(numerator / denominator, PQ_constant_M);
}
-inline float3 EOTF_PQ(float3 x, float maxPqValue) {
+inline float3 EOTF_PQ(float3 x) {
constexpr float PQ_constant_N = 2610.0f / 4096.0f / 4.0f;
constexpr float PQ_constant_M = 2523.0f / 4096.0f * 128.0f;
constexpr float PQ_constant_C1 = 3424.0f / 4096.0f;
constexpr float PQ_constant_C2 = 2413.0f / 4096.0f * 32.0f;
constexpr float PQ_constant_C3 = 2392.0f / 4096.0f * 32.0f;
+ constexpr float PQ_C = 10000.0f;
- float3 g = pow(x, 1.0f / PQ_constant_M);
+ float3 g = pow(saturate(x), 1.0f / PQ_constant_M);
float3 numerator = max(g - PQ_constant_C1, 0.0f);
float3 denominator = PQ_constant_C2 - (PQ_constant_C3 * g);
float3 linearColor = pow(numerator / denominator, 1.0f / PQ_constant_N);
- return linearColor * maxPqValue;
+ return linearColor * PQ_C;
+}
+
+//------------------------------------------------------------------------------
+// Color space conversions
+//------------------------------------------------------------------------------
+
+inline constexpr XYZ xyY_to_XYZ(xyY const v) noexcept {
+ const float a = v.z / max(v.y, 1e-5f);
+ return XYZ{v.x * a, v.z, (1.0f - v.x - v.y) * a};
+}
+
+inline constexpr xyY XYZ_to_xyY(XYZ v) noexcept {
+ return {v.xy / max(v.x + v.y + v.z, 1e-5f), v.y};
+}
+
+inline constexpr float3 pow3(float3 const x) noexcept {
+ return x * x * x;
+}
+
+inline float3 sRGB_to_OkLab(float3 const x) noexcept {
+ return OkLab_LMS_to_OkLab * cbrt(sRGB_to_OkLab_LMS * x);
+}
+
+inline float3 Rec2020_to_OkLab(float3 const x) noexcept {
+ return OkLab_LMS_to_OkLab * cbrt(Rec2020_to_OkLab_LMS * x);
+}
+
+inline float3 OkLab_to_sRGB(float3 const x) noexcept {
+ return OkLab_LMS_to_sRGB * pow3(OkLab_to_OkLab_LMS * x);
+}
+
+inline float3 OkLab_to_Rec2020(float3 const x) noexcept {
+ return OkLab_LMS_to_Rec2020 * pow3(OkLab_to_OkLab_LMS * x);
+}
+
+inline float3 Rec2020_to_ICtCp(float3 const x) noexcept {
+ const float3 lmsPQ = OETF_PQ(float3{
+ (x.r * 1688.0f + x.g * 2146.0f + x.b * 262.0f) / 4096.0f,
+ (x.r * 683.0f + x.g * 2951.0f + x.b * 462.0f) / 4096.0f,
+ (x.r * 99.0f + x.g * 309.0f + x.b * 3688.0f) / 4096.0f
+ });
+
+ return float3{
+ (2048.0f * lmsPQ.x + 2048.0f * lmsPQ.y) / 4096.0f,
+ (6610.0f * lmsPQ.x - 13613.0f * lmsPQ.y + 7003.0f * lmsPQ.z) / 4096.0f,
+ (17933.0f * lmsPQ.x - 17390.0f * lmsPQ.y - 543.0f * lmsPQ.z) / 4096.0f
+ };
+}
+
+inline float3 ICtCp_to_Rec2020(float3 const x) noexcept {
+ const float3 linear = EOTF_PQ(float3{
+ x.r + 0.00860904f * x.g + 0.11103f * x.b,
+ x.r - 0.00860904f * x.g - 0.11103f * x.b,
+ x.r + 0.560031f * x.g - 0.320627f * x.b
+ });
+
+ return float3{
+ std::max(3.43661f * linear.x - 2.50645f * linear.y + 0.0698454f * linear.z, 0.0f),
+ std::max(-0.79133f * linear.x + 1.9836f * linear.y - 0.192271f * linear.z, 0.0f),
+ std::max(-0.0259499f * linear.x - 0.0989137f * linear.y + 1.12486f * linear.z, 0.0f)
+ };
}
//------------------------------------------------------------------------------
diff --git a/filament/src/ToneMapper.cpp b/filament/src/ToneMapper.cpp
index 5c4203dae3..a16acea83d 100644
--- a/filament/src/ToneMapper.cpp
+++ b/filament/src/ToneMapper.cpp
@@ -256,6 +256,188 @@ float3 PBRNeutralToneMapper::operator()(float3 color) const noexcept {
return mix(color, float3(newPeak), g);
}
+//------------------------------------------------------------------------------
+// GT7 tone mapper
+//------------------------------------------------------------------------------
+
+// The following implementation is based on code provided by Polyphony Digital,
+// under the following license:
+//
+// MIT License
+//
+// Copyright (c) 2025 Polyphony Digital Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// See "Driving Toward Reality: Physically Based Tone Mapping and Perceptual
+// Fidelity in Gran Turismo 7", SIGGRAPH 2025, by Yasutomi, Suzuki, and Uchimura.
+
+constexpr float ReferenceLuminance = 100.0f; // 100 cd/m^2 equals a value of 1 in the framebuffer
+constexpr float SdrPaperWhite = 250.0f; // Paper white target of 250 nits
+
+inline float frameBufferValueToPhysicalValue(float v) {
+ // Converts a linear framebuffer value to physical luminance (cd/m^2)
+ // where 1.0 corresponds to the reference luminance.
+ return v * ReferenceLuminance;
+}
+
+inline float3 frameBufferValueToPhysicalValue(float3 v) {
+ return v * ReferenceLuminance;
+}
+
+inline float physicalValueToFrameBufferValue(float v) {
+ // Converts a physical luminance (cd/m^2) to a linear framebuffer value,
+ // where 1.0 corresponds to the reference luminance.
+ return v / ReferenceLuminance;
+}
+
+inline float3 physicalValueToFrameBufferValue(float3 v) {
+ return v / ReferenceLuminance;
+}
+
+class GT7Curve {
+public:
+ float evaluate(const float x) const {
+ if (x < 0.0f) return 0.0f;
+
+ const float weightLinear = smoothstep(0.0f, mMidPoint, x);
+ const float weightToe = 1.0f - weightLinear;
+
+ // Shoulder mapping for highlights.
+ const float shoulder = mKa + mKb * std::expf(x * mKc);
+
+ if (x < mLinearSection * mPeakIntensity) {
+ const float toeMapped = mMidPoint * std::powf(x / mMidPoint, mToeStrength);
+ return weightToe * toeMapped + weightLinear * x;
+ }
+ return shoulder;
+ }
+
+ void initialize(
+ const float displayIntensity,
+ const float alpha,
+ const float grayPoint,
+ const float linearSection,
+ const float toeStrength
+ ) {
+ mPeakIntensity = displayIntensity;
+ mAlpha = alpha;
+ mMidPoint = grayPoint;
+ mLinearSection = linearSection;
+ mToeStrength = toeStrength;
+
+ // Pre-compute constants for the shoulder region.
+ const float k = (mLinearSection - 1.0f) / (mAlpha - 1.0f);
+ mKa = mPeakIntensity * mLinearSection + mPeakIntensity * k;
+ mKb = -mPeakIntensity * k * std::expf(mLinearSection / k);
+ mKc = -1.0f / (k * mPeakIntensity);
+ }
+
+private:
+ float mPeakIntensity{};
+ float mAlpha{};
+ float mMidPoint{};
+ float mLinearSection{};
+ float mToeStrength{};
+ float mKa{};
+ float mKb{};
+ float mKc{};
+};
+
+struct GT7ToneMapper::State {
+ float sdrCorrectionFactor;
+ float framebufferLuminanceTarget;
+ float framebufferLuminanceTargetUcs;
+
+ float blendRatio;
+ float fadeStart;
+ float fadeEnd;
+
+ GT7Curve curve;
+
+ // The display target luminance should be ~250 nits for SDR displays,
+ // and whatever peak luminance an HDR display supports (700, 1000, etc.).
+ void initializeParameters(float sdrCorrection, float displayTargetLuminance) {
+ sdrCorrectionFactor = sdrCorrection;
+ framebufferLuminanceTarget = physicalValueToFrameBufferValue(displayTargetLuminance);
+
+ // TODO: We could expose the curve parameters to users
+ curve.initialize(framebufferLuminanceTarget, 0.25f, 0.538f, 0.444f, 1.280f);
+
+ // TODO: Expose these controls to the user
+ blendRatio = 0.6f;
+ fadeStart = 0.98f;
+ fadeEnd = 1.16f;
+
+ const float3 rgb{framebufferLuminanceTarget};
+ framebufferLuminanceTargetUcs = Rec2020_to_ICtCp(frameBufferValueToPhysicalValue(rgb)).x;
+ }
+};
+
+GT7ToneMapper::GT7ToneMapper() noexcept {
+ mState = new State();
+
+ // Initialize for an SDR target
+ mState->initializeParameters(
+ 1.0f / physicalValueToFrameBufferValue(SdrPaperWhite),
+ SdrPaperWhite
+ );
+
+ // TODO: To initialize for HDR output, pass 1.0 as the SDR correction factor,
+ // and the desired peak display luminance as the second parameter
+}
+
+GT7ToneMapper::~GT7ToneMapper() noexcept {
+ delete mState;
+}
+
+inline float chromaCurve(float a, float b, float x) {
+ return 1.0f - smoothstep(a, b, x);
+}
+
+float3 GT7ToneMapper::operator()(float3 color) const noexcept {
+ const State& state = *mState;
+ const GT7Curve& curve = state.curve;
+
+ float3 ucs = Rec2020_to_ICtCp(frameBufferValueToPhysicalValue(color));
+ float3 skewedRgb{
+ curve.evaluate(color.r),
+ curve.evaluate(color.g),
+ curve.evaluate(color.b)
+ };
+ float3 skewedUcs = Rec2020_to_ICtCp(frameBufferValueToPhysicalValue(skewedRgb));
+
+ float chromaScale = chromaCurve(
+ state.fadeStart, state.fadeEnd, ucs.x / state.framebufferLuminanceTargetUcs);
+ float3 scaledRgb = physicalValueToFrameBufferValue(ICtCp_to_Rec2020(float3{
+ skewedUcs.x,
+ ucs.y * chromaScale,
+ ucs.z * chromaScale
+ }));
+
+ const float blendRatio = state.blendRatio;
+ const float sdrFactor = state.sdrCorrectionFactor;
+
+ float3 luminanceTarget{state.framebufferLuminanceTarget};
+ return sdrFactor * min(mix(skewedRgb, scaledRgb, blendRatio), luminanceTarget);
+}
+
//------------------------------------------------------------------------------
// AgX tone mapper
//------------------------------------------------------------------------------
diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h
index b16ee8aeb0..fe0628727b 100644
--- a/libs/viewer/include/viewer/Settings.h
+++ b/libs/viewer/include/viewer/Settings.h
@@ -61,7 +61,8 @@ enum class ToneMapping : uint8_t {
AGX = 4,
GENERIC = 5,
PBR_NEUTRAL = 6,
- DISPLAY_RANGE = 7,
+ GT7 = 7,
+ DISPLAY_RANGE = 8,
};
using AmbientOcclusionOptions = filament::View::AmbientOcclusionOptions;
diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp
index 48907a8e6f..b1ee65fd3b 100644
--- a/libs/viewer/src/Settings.cpp
+++ b/libs/viewer/src/Settings.cpp
@@ -88,6 +88,7 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp
else if (0 == compare(tokens[i], jsonChunk, "AGX")) { *out = ToneMapping::AGX; }
else if (0 == compare(tokens[i], jsonChunk, "GENERIC")) { *out = ToneMapping::GENERIC; }
else if (0 == compare(tokens[i], jsonChunk, "PBR_NEUTRAL")) { *out = ToneMapping::PBR_NEUTRAL; }
+ else if (0 == compare(tokens[i], jsonChunk, "GT7")) { *out = ToneMapping::GT7; }
else if (0 == compare(tokens[i], jsonChunk, "DISPLAY_RANGE")) { *out = ToneMapping::DISPLAY_RANGE; }
else {
slog.w << "Invalid ToneMapping: '" << STR(tokens[i], jsonChunk) << "'" << io::endl;
@@ -722,6 +723,7 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe
settings.genericToneMapper.hdrMax
);
case ToneMapping::PBR_NEUTRAL: return new PBRNeutralToneMapper;
+ case ToneMapping::GT7: return new GT7ToneMapper;
case ToneMapping::DISPLAY_RANGE: return new DisplayRangeToneMapper;
}
}
@@ -773,6 +775,7 @@ static std::ostream& operator<<(std::ostream& out, ToneMapping in) {
case ToneMapping::AGX: return out << "\"AGX\"";
case ToneMapping::GENERIC: return out << "\"GENERIC\"";
case ToneMapping::PBR_NEUTRAL: return out << "\"PBR_NEUTRAL\"";
+ case ToneMapping::GT7: return out << "\"GT7\"";
case ToneMapping::DISPLAY_RANGE: return out << "\"DISPLAY_RANGE\"";
}
return out << "\"INVALID\"";
diff --git a/libs/viewer/src/ViewerGui.cpp b/libs/viewer/src/ViewerGui.cpp
index a914d841c5..80376b6601 100644
--- a/libs/viewer/src/ViewerGui.cpp
+++ b/libs/viewer/src/ViewerGui.cpp
@@ -178,6 +178,9 @@ static void computeToneMapPlot(ColorGradingSettings& settings, float* plot) {
case ToneMapping::PBR_NEUTRAL:
mapper = new PBRNeutralToneMapper;
break;
+ case ToneMapping::GT7:
+ mapper = new GT7ToneMapper;
+ break;
case ToneMapping::DISPLAY_RANGE:
mapper = new DisplayRangeToneMapper;
break;
@@ -229,7 +232,7 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo
int toneMapping = (int) colorGrading.toneMapping;
ImGui::Combo("Tone-mapping", &toneMapping,
- "Linear\0ACES (legacy)\0ACES\0Filmic\0AgX\0Generic\0PBR Neutral\0Display Range\0\0");
+ "Linear\0ACES (legacy)\0ACES\0Filmic\0AgX\0Generic\0PBR Neutral\0GT7\0Display Range\0\0");
colorGrading.toneMapping = (decltype(colorGrading.toneMapping)) toneMapping;
if (colorGrading.toneMapping == ToneMapping::GENERIC) {
if (ImGui::CollapsingHeader("Tonemap parameters")) {