Add custom color grading LUT support and test UI in gltf_viewer.

Implemented the custom 3D LUT feature in the ColorGrading API, allowing
users to specify a custom LUT for final color mapping.

To test this feature, added a "Custom LUT" option to the color grading
settings in the gltf_viewer sample. This includes several procedurally
generated LUTs for testing purposes:
- Negative
- Grayscale
- Sepia
- Teal and Orange

Updated settings serialization (Settings.cpp) and the viewer UI
(ViewerGui.cpp) to support these options.

FIXES=[362596936]
This commit is contained in:
Mathias Agopian
2026-04-03 16:57:42 -07:00
committed by Mathias Agopian
parent 115c2cf8fe
commit 84dc1b1c43
7 changed files with 215 additions and 6 deletions

View File

@@ -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<math::float3> lut =
utils::FixedCapacityVector<math::float3>::with_capacity(count);
for (size_t i = 0; i < count; ++i) {
lut.push_back(data[i]);
}
builder->customLut(std::move(lut), (uint8_t)dimension);
}

View File

@@ -556,6 +556,22 @@ public class ColorGrading {
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 @@ public class ColorGrading {
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);
}

View File

@@ -23,6 +23,7 @@
#include <filament/ToneMapper.h>
#include <utils/compiler.h>
#include <utils/FixedCapacityVector.h>
#include <math/mathfwd.h>
@@ -461,6 +462,22 @@ public:
*/
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<math::float3> 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.

View File

@@ -33,6 +33,7 @@
#include <utils/JobSystem.h>
#include <utils/Mutex.h>
#include <utils/Panic.h>
#include <cmath>
#include <cstdlib>
@@ -101,6 +102,10 @@ struct ColorGrading::BuilderDetails {
// Output color space
ColorSpace outputColorSpace = Rec709-sRGB-D65;
// Custom LUT
utils::FixedCapacityVector<math::float3> 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<math::float3> 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;
};

View File

@@ -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;

View File

@@ -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<math::float3> generateCustomLut(CustomLut type, uint8_t dim) {
using namespace filament::math;
size_t count = size_t(dim) * dim * dim;
auto lut = utils::FixedCapacityVector<float3>::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 &&

View File

@@ -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;