diff --git a/NEW_RELEASE_NOTES.md b/NEW_RELEASE_NOTES.md index 2261ab10d78..87484203dfa 100644 --- a/NEW_RELEASE_NOTES.md +++ b/NEW_RELEASE_NOTES.md @@ -10,4 +10,5 @@ appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md). - engine: Added parameter for configuring JobSystem thread count - engine: In Java, introduce Engine.Builder -- matinfo: Add support for viewing ESSL 1.0 shaders +- engine: New tone mapper: `AgXTonemapper`. +- matinfo: Add support for viewing ESSL 1.0 shaders \ No newline at end of file diff --git a/android/filament-android/src/main/cpp/ToneMapper.cpp b/android/filament-android/src/main/cpp/ToneMapper.cpp index ea040538cd7..21c7e7ab24d 100644 --- a/android/filament-android/src/main/cpp/ToneMapper.cpp +++ b/android/filament-android/src/main/cpp/ToneMapper.cpp @@ -47,6 +47,11 @@ Java_com_google_android_filament_ToneMapper_nCreateFilmicToneMapper(JNIEnv*, jcl return (jlong) new FilmicToneMapper(); } +extern "C" JNIEXPORT jlong JNICALL +Java_com_google_android_filament_ToneMapper_nCreateAgxToneMapper(JNIEnv*, jclass, jint look) { + return (jlong) new AgxToneMapper(AgxToneMapper::AgxLook(look)); +} + extern "C" JNIEXPORT jlong JNICALL Java_com_google_android_filament_ToneMapper_nCreateGenericToneMapper(JNIEnv*, jclass, jfloat contrast, jfloat midGrayIn, jfloat midGrayOut, jfloat hdrMax) { 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 15800562e3e..58ffc501ec1 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 @@ -100,6 +100,45 @@ public Filmic() { } } + /** + * AgX tone mapping operator. + */ + public static class Agx extends ToneMapper { + + public enum AgxLook { + /** + * Base contrast with no look applied + */ + NONE, + + /** + * A punchy and more chroma laden look for sRGB displays + */ + PUNCHY, + + /** + * A golden tinted, slightly washed look for BT.1886 displays + */ + GOLDEN + } + + /** + * Builds a new AgX tone mapper with no look applied. + */ + public Agx() { + this(AgxLook.NONE); + } + + /** + * Builds a new AgX tone mapper. + * + * @param look: an optional creative adjustment to contrast and saturation + */ + public Agx(AgxLook look) { + super(nCreateAgxToneMapper(look.ordinal())); + } + } + /** * Generic tone mapping operator that gives control over the tone mapping * curve. This operator can be used to control the aesthetics of the final @@ -194,6 +233,7 @@ public void setHdrMax(float hdrMax) { private static native long nCreateACESToneMapper(); private static native long nCreateACESLegacyToneMapper(); private static native long nCreateFilmicToneMapper(); + 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 8d19c59756b..d220f47b662 100644 --- a/filament/include/filament/ToneMapper.h +++ b/filament/include/filament/ToneMapper.h @@ -115,6 +115,30 @@ struct UTILS_PUBLIC FilmicToneMapper final : public ToneMapper { math::float3 operator()(math::float3 x) const noexcept override; }; +/** + * AgX tone mapping operator. + */ +struct UTILS_PUBLIC AgxToneMapper final : public ToneMapper { + + enum class AgxLook : uint8_t { + NONE = 0, //!< Base contrast with no look applied + PUNCHY, //!< A punchy and more chroma laden look for sRGB displays + GOLDEN //!< A golden tinted, slightly washed look for BT.1886 displays + }; + + /** + * Builds a new AgX tone mapper. + * + * @param look an optional creative adjustment to contrast and saturation + */ + explicit AgxToneMapper(AgxLook look = AgxLook::NONE) noexcept; + ~AgxToneMapper() noexcept final; + + math::float3 operator()(math::float3 x) const noexcept override; + + AgxLook look; +}; + /** * Generic tone mapping operator that gives control over the tone mapping * curve. This operator can be used to control the aesthetics of the final diff --git a/filament/src/ToneMapper.cpp b/filament/src/ToneMapper.cpp index d3dcdd698af..b24f253e0a1 100644 --- a/filament/src/ToneMapper.cpp +++ b/filament/src/ToneMapper.cpp @@ -230,6 +230,106 @@ float3 FilmicToneMapper::operator()(math::float3 x) const noexcept { return (x * (a * x + b)) / (x * (c * x + d) + e); } +//------------------------------------------------------------------------------ +// AgX tone mapper +//------------------------------------------------------------------------------ + +AgxToneMapper::AgxToneMapper(AgxToneMapper::AgxLook look) noexcept : look(look) {} +AgxToneMapper::~AgxToneMapper() noexcept = default; + +// These matrices taken from Blender's implementation of AgX, which works with Rec.2020 primaries. +// https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBaseRec2020.py +constexpr mat3f AgXInsetMatrix { + 0.856627153315983, 0.137318972929847, 0.11189821299995, + 0.0951212405381588, 0.761241990602591, 0.0767994186031903, + 0.0482516061458583, 0.101439036467562, 0.811302368396859 +}; +constexpr mat3f AgXOutsetMatrixInv { + 0.899796955911611, 0.11142098895748, 0.11142098895748, + 0.0871996192028351, 0.875575586156966, 0.0871996192028349, + 0.013003424885555, 0.0130034248855548, 0.801379391839686 +}; +constexpr mat3f AgXOutsetMatrix { inverse(AgXOutsetMatrixInv) }; + +// LOG2_MIN = -10.0 +// LOG2_MAX = +6.5 +// MIDDLE_GRAY = 0.18 +const float AgxMinEv = -12.47393f; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) +const float AgxMaxEv = 4.026069f; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) + +// Adapted from https://iolite-engine.com/blog_posts/minimal_agx_implementation +float3 agxDefaultContrastApprox(float3 x) { + float3 x2 = x * x; + float3 x4 = x2 * x2; + float3 x6 = x4 * x2; + return - 17.86 * x6 * x + + 78.01 * x6 + - 126.7 * x4 * x + + 92.06 * x4 + - 28.72 * x2 * x + + 4.361 * x2 + - 0.1718 * x + + 0.002857; +} + +// Adapted from https://iolite-engine.com/blog_posts/minimal_agx_implementation +float3 agxLook(float3 val, AgxToneMapper::AgxLook look) { + if (look == AgxToneMapper::AgxLook::NONE) { + return val; + } + + const float3 lw = float3(0.2126, 0.7152, 0.0722); + float luma = dot(val, lw); + + // Default + float3 offset = float3(0.0); + float3 slope = float3(1.0); + float3 power = float3(1.0); + float sat = 1.0; + + if (look == AgxToneMapper::AgxLook::GOLDEN) { + slope = float3(1.0, 0.9, 0.5); + power = float3(0.8); + sat = 1.3; + } + if (look == AgxToneMapper::AgxLook::PUNCHY) { + slope = float3(1.0); + power = float3(1.35, 1.35, 1.35); + sat = 1.4; + } + + // ASC CDL + val = pow(val * slope + offset, power); + return luma + sat * (val - luma); +} + +float3 AgxToneMapper::operator()(float3 v) const noexcept { + // Ensure no negative values + v = max(float3(0.0), v); + + v = AgXInsetMatrix * v; + + // Log2 encoding + v = max(v, 1E-10); // avoid 0 or negative numbers for log2 + v = log2(v); + v = (v - AgxMinEv) / (AgxMaxEv - AgxMinEv); + + v = clamp(v, 0, 1); + + // Apply sigmoid + v = agxDefaultContrastApprox(v); + + // Apply AgX look + v = agxLook(v, look); + + v = AgXOutsetMatrix * v; + + // Linearize + v = pow(max(float3(0.0), v), 2.2); + + return v; +} + //------------------------------------------------------------------------------ // Display range tone mapper //------------------------------------------------------------------------------ diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h index eeb62e2a784..fc46d2887ed 100644 --- a/libs/viewer/include/viewer/Settings.h +++ b/libs/viewer/include/viewer/Settings.h @@ -57,8 +57,9 @@ enum class ToneMapping : uint8_t { ACES_LEGACY = 1, ACES = 2, FILMIC = 3, - GENERIC = 4, - DISPLAY_RANGE = 5, + AGX = 4, + GENERIC = 5, + DISPLAY_RANGE = 6, }; using AmbientOcclusionOptions = filament::View::AmbientOcclusionOptions; @@ -114,8 +115,14 @@ struct GenericToneMapperSettings { float midGrayIn = 0.18f; float midGrayOut = 0.215f; float hdrMax = 10.0f; - bool operator!=(const GenericToneMapperSettings &rhs) const { return !(rhs == *this); } - bool operator==(const GenericToneMapperSettings &rhs) const; + bool operator!=(const GenericToneMapperSettings& rhs) const { return !(rhs == *this); } + bool operator==(const GenericToneMapperSettings& rhs) const; +}; + +struct AgxToneMapperSettings { + AgxToneMapper::AgxLook look = AgxToneMapper::AgxLook::NONE; + bool operator!=(const AgxToneMapperSettings& rhs) const { return !(rhs == *this); } + bool operator==(const AgxToneMapperSettings& rhs) const; }; struct ColorGradingSettings { @@ -127,7 +134,7 @@ struct ColorGradingSettings { filament::ColorGrading::QualityLevel quality = filament::ColorGrading::QualityLevel::MEDIUM; ToneMapping toneMapping = ToneMapping::ACES_LEGACY; bool padding0{}; - bool padding1{}; + AgxToneMapperSettings agxToneMapper; color::ColorSpace colorspace = Rec709-sRGB-D65; GenericToneMapperSettings genericToneMapper; math::float4 shadows{1.0f, 1.0f, 1.0f, 0.0f}; diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index f416fdd43a9..22b64588b09 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -86,6 +86,7 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp else if (0 == compare(tokens[i], jsonChunk, "ACES_LEGACY")) { *out = ToneMapping::ACES_LEGACY; } else if (0 == compare(tokens[i], jsonChunk, "ACES")) { *out = ToneMapping::ACES; } else if (0 == compare(tokens[i], jsonChunk, "FILMIC")) { *out = ToneMapping::FILMIC; } + 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, "DISPLAY_RANGE")) { *out = ToneMapping::DISPLAY_RANGE; } else { @@ -130,6 +131,36 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, GenericT return i; } +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, AgxToneMapper::AgxLook* out) { + if (0 == compare(tokens[i], jsonChunk, "NONE")) { *out = AgxToneMapper::AgxLook::NONE; } + else if (0 == compare(tokens[i], jsonChunk, "PUNCHY")) { *out = AgxToneMapper::AgxLook::PUNCHY; } + else if (0 == compare(tokens[i], jsonChunk, "GOLDEN")) { *out = AgxToneMapper::AgxLook::GOLDEN; } + else { + slog.w << "Invalid AgxLook: '" << STR(tokens[i], jsonChunk) << "'" << io::endl; + } + return i + 1; +} + +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, AgxToneMapperSettings* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + for (int j = 0; j < size; ++j) { + const jsmntok_t tok = tokens[i]; + CHECK_KEY(tok); + if (compare(tok, jsonChunk, "look") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->look); + } else { + slog.w << "Invalid AgX tone mapper key: '" << STR(tok, jsonChunk) << "'" << io::endl; + i = parse(tokens, i + 1); + } + if (i < 0) { + slog.e << "Invalid AgX tone mapper value: '" << STR(tok, jsonChunk) << "'" << io::endl; + return i; + } + } + return i; +} + static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGradingSettings* out) { CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); int size = tokens[i++].size; @@ -146,6 +177,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGra i = parse(tokens, i + 1, jsonChunk, &out->toneMapping); } else if (compare(tok, jsonChunk, "genericToneMapper") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper); + } else if (compare(tok, jsonChunk, "agxToneMapper") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->agxToneMapper); } else if (compare(tok, jsonChunk, "luminanceScaling") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->luminanceScaling); } else if (compare(tok, jsonChunk, "gamutMapping") == 0) { @@ -619,6 +652,7 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe case ToneMapping::ACES_LEGACY: return new ACESLegacyToneMapper; case ToneMapping::ACES: return new ACESToneMapper; case ToneMapping::FILMIC: return new FilmicToneMapper; + case ToneMapping::AGX: return new AgxToneMapper(settings.agxToneMapper.look); case ToneMapping::GENERIC: return new GenericToneMapper( settings.genericToneMapper.contrast, settings.genericToneMapper.midGrayIn, @@ -673,6 +707,7 @@ static std::ostream& operator<<(std::ostream& out, ToneMapping in) { case ToneMapping::ACES_LEGACY: return out << "\"ACES_LEGACY\""; case ToneMapping::ACES: return out << "\"ACES\""; case ToneMapping::FILMIC: return out << "\"FILMIC\""; + case ToneMapping::AGX: return out << "\"AGX\""; case ToneMapping::GENERIC: return out << "\"GENERIC\""; case ToneMapping::DISPLAY_RANGE: return out << "\"DISPLAY_RANGE\""; } @@ -688,6 +723,21 @@ static std::ostream& operator<<(std::ostream& out, const GenericToneMapperSettin << "}"; } +static std::ostream& operator<<(std::ostream& out, AgxToneMapper::AgxLook in) { + switch (in) { + case AgxToneMapper::AgxLook::NONE: return out << "\"NONE\""; + case AgxToneMapper::AgxLook::PUNCHY: return out << "\"PUNCHY\""; + case AgxToneMapper::AgxLook::GOLDEN: return out << "\"GOLDEN\""; + } + return out << "\"INVALID\""; +} + +static std::ostream& operator<<(std::ostream& out, const AgxToneMapperSettings& in) { + return out << "{\n" + << "\"look\": " << (in.look) << ",\n" + << "}"; +} + static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& in) { return out << "{\n" << "\"enabled\": " << to_string(in.enabled) << ",\n" @@ -695,6 +745,7 @@ static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& i << "\"quality\": " << (in.quality) << ",\n" << "\"toneMapping\": " << (in.toneMapping) << ",\n" << "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n" + << "\"agxToneMapper\": " << (in.agxToneMapper) << ",\n" << "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n" << "\"gamutMapping\": " << to_string(in.gamutMapping) << ",\n" << "\"exposure\": " << (in.exposure) << ",\n" @@ -854,7 +905,7 @@ static std::ostream& operator<<(std::ostream& out, const Settings& in) { << "}"; } -bool GenericToneMapperSettings::operator==(const GenericToneMapperSettings &rhs) const { +bool GenericToneMapperSettings::operator==(const GenericToneMapperSettings& rhs) const { static_assert(sizeof(GenericToneMapperSettings) == 16, "Please update Settings.cpp"); return contrast == rhs.contrast && midGrayIn == rhs.midGrayIn && @@ -862,7 +913,12 @@ bool GenericToneMapperSettings::operator==(const GenericToneMapperSettings &rhs) hdrMax == rhs.hdrMax; } -bool ColorGradingSettings::operator==(const ColorGradingSettings &rhs) const { +bool AgxToneMapperSettings::operator==(const AgxToneMapperSettings& rhs) const { + static_assert(sizeof(AgxToneMapperSettings) == 1, "Please update Settings.cpp"); + return look == rhs.look; +} + +bool ColorGradingSettings::operator==(const ColorGradingSettings& rhs) const { // If you had to fix the following codeline, then you likely also need to update the // implementation of operator==. static_assert(sizeof(ColorGradingSettings) == 312, "Please update Settings.cpp"); @@ -871,6 +927,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings &rhs) const { quality == rhs.quality && toneMapping == rhs.toneMapping && genericToneMapper == rhs.genericToneMapper && + agxToneMapper == rhs.agxToneMapper && luminanceScaling == rhs.luminanceScaling && gamutMapping == rhs.gamutMapping && exposure == rhs.exposure && diff --git a/libs/viewer/src/ViewerGui.cpp b/libs/viewer/src/ViewerGui.cpp index 4a94c4afac8..11443516209 100644 --- a/libs/viewer/src/ViewerGui.cpp +++ b/libs/viewer/src/ViewerGui.cpp @@ -133,6 +133,9 @@ static void computeToneMapPlot(ColorGradingSettings& settings, float* plot) { case ToneMapping::FILMIC: mapper = new FilmicToneMapper; break; + case ToneMapping::AGX: + mapper = new AgxToneMapper(settings.agxToneMapper.look); + break; case ToneMapping::GENERIC: mapper = new GenericToneMapper( settings.genericToneMapper.contrast, @@ -193,7 +196,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\0Generic\0Display Range\0\0"); + "Linear\0ACES (legacy)\0ACES\0Filmic\0AgX\0Generic\0Display Range\0\0"); colorGrading.toneMapping = (decltype(colorGrading.toneMapping)) toneMapping; if (colorGrading.toneMapping == ToneMapping::GENERIC) { if (ImGui::CollapsingHeader("Tonemap parameters")) { @@ -204,6 +207,11 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo ImGui::SliderFloat("HDR max", &generic.hdrMax, 1.0f, 64.0f); } } + if (colorGrading.toneMapping == ToneMapping::AGX) { + int agxLook = (int) colorGrading.agxToneMapper.look; + ImGui::Combo("AgX Look", &agxLook, "None\0Punchy\0Golden\0\0"); + colorGrading.agxToneMapper.look = (decltype(colorGrading.agxToneMapper.look)) agxLook; + } computeToneMapPlot(colorGrading, toneMapPlot);