diff --git a/android/filament-android/src/main/cpp/ColorGrading.cpp b/android/filament-android/src/main/cpp/ColorGrading.cpp index c2d371197584..d6ee01bef9f2 100644 --- a/android/filament-android/src/main/cpp/ColorGrading.cpp +++ b/android/filament-android/src/main/cpp/ColorGrading.cpp @@ -220,3 +220,23 @@ Java_com_google_android_filament_ColorGrading_nBuilderCurves(JNIEnv* env, jclass env->ReleaseFloatArrayElements(midPoint_, midPoint, JNI_ABORT); env->ReleaseFloatArrayElements(scale_, scale, JNI_ABORT); } + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_ColorGrading_nBuilderCustomLut(JNIEnv *env, jclass, + jlong nativeBuilder, jobject buffer, jint dimension) { + ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder; + if (dimension == 0) { + return; + } + float3* data = (float3*) env->GetDirectBufferAddress(buffer); + size_t count = size_t(dimension) * dimension * dimension; + + utils::FixedCapacityVector lut = + utils::FixedCapacityVector::with_capacity(count); + + for (size_t i = 0; i < count; ++i) { + lut.push_back(data[i]); + } + + builder->customLut(std::move(lut), (uint8_t)dimension); +} 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 e0397b199b97..be7a1054cf2b 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 @@ -556,6 +556,22 @@ public Builder curves( return this; } + /** + * Specifies a custom 3D color grading LUT to map the final sRGB color. + * The LUT is applied after post-processing and in LDR (sRGB space). + * The data must be a 3D array of float3 (RGB) values. + * The data must remain valid until build() is called. + * + * @param buffer Direct ByteBuffer containing the custom LUT data (3D array of float3). + * @param dimension Dimension of the custom LUT (e.g., 16, 32, 64). + * + * @return This Builder, for chaining calls + */ + public Builder customLut(@NonNull java.nio.Buffer buffer, int dimension) { + nBuilderCustomLut(mNativeBuilder, buffer, dimension); + return this; + } + /** * Creates the IndirectLight object and returns a pointer to it. * @@ -620,6 +636,7 @@ void clearNativeObject() { private static native void nBuilderVibrance(long nativeBuilder, float vibrance); private static native void nBuilderSaturation(long nativeBuilder, float saturation); private static native void nBuilderCurves(long nativeBuilder, float[] gamma, float[] midPoint, float[] scale); + private static native void nBuilderCustomLut(long nativeBuilder, java.nio.Buffer buffer, int dim); private static native long nBuilderBuild(long nativeBuilder, long nativeEngine); } diff --git a/filament/include/filament/ColorGrading.h b/filament/include/filament/ColorGrading.h index 3e4916fbb202..b6a80aba1e7f 100644 --- a/filament/include/filament/ColorGrading.h +++ b/filament/include/filament/ColorGrading.h @@ -23,6 +23,7 @@ #include #include +#include #include @@ -461,6 +462,22 @@ class UTILS_PUBLIC ColorGrading : public FilamentAPI { */ Builder& curves(math::float3 shadowGamma, math::float3 midPoint, math::float3 highlightScale) noexcept; + /** + * Specifies a custom 3D color grading LUT to map the final sRGB color. + * The LUT is applied after post-processing and in LDR (sRGB space). + * The data must be a 3D array of float3 (RGB) values. + * The dimension does not need to be a power of two, but must be non-zero. + * The values are always interpolated (trilinear) because the input color from previous steps is continuous. + * The dimension doesn't need to match dimensions(). + * If the dimension is 0 or the data is empty, the custom LUT is skipped (ignored). + * + * @param data FixedCapacityVector containing the custom LUT data (3D array of float3). + * @param dimension Dimension of the custom LUT. + * + * @return This Builder, for chaining calls + */ + Builder& customLut(utils::FixedCapacityVector data, uint8_t dimension) noexcept; + /** * Sets the output color space for this ColorGrading object. After all color grading steps * have been applied, the final color will be converted in the desired color space. diff --git a/filament/src/details/ColorGrading.cpp b/filament/src/details/ColorGrading.cpp index 267e30ac5291..64da1e3bcc45 100644 --- a/filament/src/details/ColorGrading.cpp +++ b/filament/src/details/ColorGrading.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -101,6 +102,10 @@ struct ColorGrading::BuilderDetails { // Output color space ColorSpace outputColorSpace = Rec709-sRGB-D65; + // Custom LUT + utils::FixedCapacityVector customLutData; + uint8_t customLutDimension = 0; + bool operator!=(const BuilderDetails &rhs) const { return !(rhs == *this); } @@ -130,7 +135,9 @@ struct ColorGrading::BuilderDetails { shadowGamma == rhs.shadowGamma && midPoint == rhs.midPoint && highlightScale == rhs.highlightScale && - outputColorSpace == rhs.outputColorSpace; + outputColorSpace == rhs.outputColorSpace && + customLutData == rhs.customLutData && + customLutDimension == rhs.customLutDimension; } }; @@ -272,11 +279,27 @@ ColorGrading::Builder& ColorGrading::Builder::outputColorSpace( return *this; } +ColorGrading::Builder& ColorGrading::Builder::customLut( + utils::FixedCapacityVector data, uint8_t dimension) noexcept { + mImpl->customLutData = std::move(data); + mImpl->customLutDimension = dimension; + return *this; +} + #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif ColorGrading* ColorGrading::Builder::build(Engine& engine) { + if (mImpl->customLutDimension == 0 || mImpl->customLutData.empty()) { + mImpl->customLutData.clear(); + mImpl->customLutDimension = 0; + } else { + FILAMENT_CHECK_PRECONDITION(mImpl->customLutData.size() == + size_t(mImpl->customLutDimension) * mImpl->customLutDimension * mImpl->customLutDimension) + << "Custom LUT data size does not match dimension^3"; + } + // We want to see if any of the default adjustment values have been modified // We skip the tonemapping operator on purpose since we always want to apply it BuilderDetails defaults; @@ -544,6 +567,39 @@ inline float3 curves(float3 v, float3 shadowGamma, float3 midPoint, float3 highl }; } +static float3 applyCustomLut(float3 v, const math::float3* lut, uint8_t dim) noexcept { + float3 pos = v * float(dim - 1); + float3 pos_floor = floor(pos); + float3 pos_ceil = min(pos_floor + 1.0f, float(dim - 1)); + float3 d = pos - pos_floor; + + int3 i0 = int3(pos_floor); + int3 i1 = int3(pos_ceil); + + auto fetch = [&](int r, int g, int b) { + return lut[r + g * dim + b * dim * dim]; + }; + + float3 c000 = fetch(i0.x, i0.y, i0.z); + float3 c100 = fetch(i1.x, i0.y, i0.z); + float3 c010 = fetch(i0.x, i1.y, i0.z); + float3 c110 = fetch(i1.x, i1.y, i0.z); + float3 c001 = fetch(i0.x, i0.y, i1.z); + float3 c101 = fetch(i1.x, i0.y, i1.z); + float3 c011 = fetch(i0.x, i1.y, i1.z); + float3 c111 = fetch(i1.x, i1.y, i1.z); + + float3 c00 = c000 * (1.0f - d.x) + c100 * d.x; + float3 c10 = c010 * (1.0f - d.x) + c110 * d.x; + float3 c01 = c001 * (1.0f - d.x) + c101 * d.x; + float3 c11 = c011 * (1.0f - d.x) + c111 * d.x; + + float3 c0 = c00 * (1.0f - d.y) + c10 * d.y; + float3 c1 = c01 * (1.0f - d.y) + c11 * d.y; + + return c0 * (1.0f - d.z) + c1 * d.z; +} + //------------------------------------------------------------------------------ // Luminance scaling //------------------------------------------------------------------------------ @@ -665,6 +721,7 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { // spaces are the same, but we currently don't check that. We must revise these conditions if we // ever handle this case. mIsOneDimensional = !builder->hasAdjustments && !builder->luminanceScaling + && builder->customLutData.empty() && builder->toneMapper->isOneDimensional() && engine.features.engine.color_grading.use_1d_lut; mIsLDR = mIsOneDimensional && builder->toneMapper->isLDR(); @@ -795,6 +852,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { // Apply OETF v = config.oetf(v); + // Apply custom LUT if provided + if (!builder->customLutData.empty()) { + v = applyCustomLut(v, builder->customLutData.data(), builder->customLutDimension); + } + return v; }; diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h index 52862953ed89..4d4af5dd3e6c 100644 --- a/libs/viewer/include/viewer/Settings.h +++ b/libs/viewer/include/viewer/Settings.h @@ -146,6 +146,14 @@ struct AgxToneMapperSettings { bool operator==(const AgxToneMapperSettings& rhs) const; }; +enum class CustomLut : uint8_t { + NONE = 0, + NEGATIVE = 1, + GRAYSCALE = 2, + SEPIA = 3, + TEAL_AND_ORANGE = 4, +}; + struct ColorGradingSettings { // fields are ordered to avoid padding bool enabled = true; @@ -154,7 +162,7 @@ struct ColorGradingSettings { bool gamutMapping = false; filament::ColorGrading::QualityLevel quality = filament::ColorGrading::QualityLevel::MEDIUM; ToneMapping toneMapping = ToneMapping::ACES_LEGACY; - bool padding0{}; + CustomLut customLut = CustomLut::NONE; AgxToneMapperSettings agxToneMapper; color::ColorSpace colorspace = Rec709-sRGB-D65; GenericToneMapperSettings genericToneMapper; diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index 2c42fa8e5bcd..aae67ea63e29 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -98,6 +98,18 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp return i + 1; } +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, CustomLut* out) { + if (0 == compare(tokens[i], jsonChunk, "NONE")) { *out = CustomLut::NONE; } + else if (0 == compare(tokens[i], jsonChunk, "NEGATIVE")) { *out = CustomLut::NEGATIVE; } + else if (0 == compare(tokens[i], jsonChunk, "GRAYSCALE")) { *out = CustomLut::GRAYSCALE; } + else if (0 == compare(tokens[i], jsonChunk, "SEPIA")) { *out = CustomLut::SEPIA; } + else if (0 == compare(tokens[i], jsonChunk, "TEAL_AND_ORANGE")) { *out = CustomLut::TEAL_AND_ORANGE; } + else { + slog.w << "Invalid CustomLut: '" << STR(tokens[i], jsonChunk) << "'" << io::endl; + } + return i + 1; +} + static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, color::ColorSpace* out) { using namespace filament::color; if (0 == compare(tokens[i], jsonChunk, "Rec709-Linear-D65")) { *out = Rec709-Linear-D65; } @@ -207,6 +219,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGra i = parse(tokens, i + 1, jsonChunk, &out->quality); } else if (compare(tok, jsonChunk, "toneMapping") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->toneMapping); + } else if (compare(tok, jsonChunk, "customLut") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->customLut); } else if (compare(tok, jsonChunk, "genericToneMapper") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper); } else if (compare(tok, jsonChunk, "agxToneMapper") == 0) { @@ -984,10 +998,57 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe } } +static utils::FixedCapacityVector generateCustomLut(CustomLut type, uint8_t dim) { + using namespace filament::math; + size_t count = size_t(dim) * dim * dim; + auto lut = utils::FixedCapacityVector::with_capacity(count); + + for (size_t b = 0; b < dim; ++b) { + for (size_t g = 0; g < dim; ++g) { + for (size_t r = 0; r < dim; ++r) { + float3 v = float3{r, g, b} * (1.0f / (dim - 1)); + switch (type) { + case CustomLut::NONE: + break; + case CustomLut::NEGATIVE: + v = 1.0f - v; + break; + case CustomLut::GRAYSCALE: + { + float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f}); + v = float3{luma}; + } + break; + case CustomLut::SEPIA: + { + float luma = dot(v, float3{0.299f, 0.587f, 0.114f}); + v = float3{ + clamp(luma * 1.2f, 0.0f, 1.0f), + clamp(luma * 1.0f, 0.0f, 1.0f), + clamp(luma * 0.8f, 0.0f, 1.0f) + }; + } + break; + case CustomLut::TEAL_AND_ORANGE: + { + float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f}); + float3 teal{0.0f, 0.5f, 0.5f}; + float3 orange{1.0f, 0.5f, 0.0f}; + v = teal * (1.0f - luma) + orange * luma; + } + break; + } + lut.push_back(v); + } + } + } + return lut; +} + ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) { ToneMapper* toneMapper = createToneMapper(settings); - ColorGrading *colorGrading = ColorGrading::Builder() - .quality(settings.quality) + ColorGrading::Builder builder; + builder.quality(settings.quality) .exposure(settings.exposure) .nightAdaptation(settings.nightAdaptation) .whiteBalance(settings.temperature, settings.tint) @@ -1006,8 +1067,14 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e .toneMapper(toneMapper) .luminanceScaling(settings.luminanceScaling) .gamutMapping(settings.gamutMapping) - .outputColorSpace(settings.colorspace) - .build(*engine); + .outputColorSpace(settings.colorspace); + + if (settings.customLut != CustomLut::NONE) { + uint8_t dim = 16; + builder.customLut(generateCustomLut(settings.customLut, dim), dim); + } + + ColorGrading *colorGrading = builder.build(*engine); delete toneMapper; return colorGrading; } @@ -1080,12 +1147,24 @@ static std::ostream& operator<<(std::ostream& out, const AgxToneMapperSettings& << "}"; } +static std::ostream& operator<<(std::ostream& out, CustomLut in) { + switch (in) { + case CustomLut::NONE: return out << "\"NONE\""; + case CustomLut::NEGATIVE: return out << "\"NEGATIVE\""; + case CustomLut::GRAYSCALE: return out << "\"GRAYSCALE\""; + case CustomLut::SEPIA: return out << "\"SEPIA\""; + case CustomLut::TEAL_AND_ORANGE: return out << "\"TEAL_AND_ORANGE\""; + } + return out << "\"INVALID\""; +} + static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& in) { return out << "{\n" << "\"enabled\": " << to_string(in.enabled) << ",\n" << "\"colorspace\": " << to_string(in.colorspace) << ",\n" << "\"quality\": " << (in.quality) << ",\n" << "\"toneMapping\": " << (in.toneMapping) << ",\n" + << "\"customLut\": " << (in.customLut) << ",\n" << "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n" << "\"agxToneMapper\": " << (in.agxToneMapper) << ",\n" << "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n" @@ -1346,6 +1425,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings& rhs) const { colorspace == rhs.colorspace && quality == rhs.quality && toneMapping == rhs.toneMapping && + customLut == rhs.customLut && genericToneMapper == rhs.genericToneMapper && agxToneMapper == rhs.agxToneMapper && luminanceScaling == rhs.luminanceScaling && diff --git a/libs/viewer/src/ViewerGui.cpp b/libs/viewer/src/ViewerGui.cpp index b8f041c562f7..7a0b4b9bc3b0 100644 --- a/libs/viewer/src/ViewerGui.cpp +++ b/libs/viewer/src/ViewerGui.cpp @@ -226,6 +226,11 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo ImGui::Combo("Quality##colorGradingQuality", &quality, "Low\0Medium\0High\0Ultra\0\0"); colorGrading.quality = (decltype(colorGrading.quality)) quality; + int customLut = (int) colorGrading.customLut; + if (ImGui::Combo("Custom LUT##colorGradingCustomLut", &customLut, "None\0Negative\0Grayscale\0Sepia\0Teal and Orange\0\0")) { + colorGrading.customLut = (CustomLut) customLut; + } + int colorspace = (colorGrading.colorspace == Rec709-Linear-D65) ? 0 : 1; ImGui::Combo("Output color space", &colorspace, "Rec709-Linear-D65\0Rec709-sRGB-D65\0\0"); colorGrading.colorspace = (colorspace == 0) ? Rec709-Linear-D65 : Rec709-sRGB-D65;