Skip to content

Commit 2fbf6eb

Browse files
committed
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]
1 parent e8fe85e commit 2fbf6eb

File tree

7 files changed

+215
-6
lines changed

7 files changed

+215
-6
lines changed

android/filament-android/src/main/cpp/ColorGrading.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,23 @@ Java_com_google_android_filament_ColorGrading_nBuilderCurves(JNIEnv* env, jclass
220220
env->ReleaseFloatArrayElements(midPoint_, midPoint, JNI_ABORT);
221221
env->ReleaseFloatArrayElements(scale_, scale, JNI_ABORT);
222222
}
223+
224+
extern "C" JNIEXPORT void JNICALL
225+
Java_com_google_android_filament_ColorGrading_nBuilderCustomLut(JNIEnv *env, jclass,
226+
jlong nativeBuilder, jobject buffer, jint dimension) {
227+
ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder;
228+
if (dimension == 0) {
229+
return;
230+
}
231+
float3* data = (float3*) env->GetDirectBufferAddress(buffer);
232+
size_t count = size_t(dimension) * dimension * dimension;
233+
234+
utils::FixedCapacityVector<math::float3> lut =
235+
utils::FixedCapacityVector<math::float3>::with_capacity(count);
236+
237+
for (size_t i = 0; i < count; ++i) {
238+
lut.push_back(data[i]);
239+
}
240+
241+
builder->customLut(std::move(lut), (uint8_t)dimension);
242+
}

android/filament-android/src/main/java/com/google/android/filament/ColorGrading.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,22 @@ public Builder curves(
556556
return this;
557557
}
558558

559+
/**
560+
* Specifies a custom 3D color grading LUT to map the final sRGB color.
561+
* The LUT is applied after post-processing and in LDR (sRGB space).
562+
* The data must be a 3D array of float3 (RGB) values.
563+
* The data must remain valid until build() is called.
564+
*
565+
* @param buffer Direct ByteBuffer containing the custom LUT data (3D array of float3).
566+
* @param dimension Dimension of the custom LUT (e.g., 16, 32, 64).
567+
*
568+
* @return This Builder, for chaining calls
569+
*/
570+
public Builder customLut(@NonNull java.nio.Buffer buffer, int dimension) {
571+
nBuilderCustomLut(mNativeBuilder, buffer, dimension);
572+
return this;
573+
}
574+
559575
/**
560576
* Creates the IndirectLight object and returns a pointer to it.
561577
*
@@ -620,6 +636,7 @@ void clearNativeObject() {
620636
private static native void nBuilderVibrance(long nativeBuilder, float vibrance);
621637
private static native void nBuilderSaturation(long nativeBuilder, float saturation);
622638
private static native void nBuilderCurves(long nativeBuilder, float[] gamma, float[] midPoint, float[] scale);
639+
private static native void nBuilderCustomLut(long nativeBuilder, java.nio.Buffer buffer, int dim);
623640

624641
private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
625642
}

filament/include/filament/ColorGrading.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <filament/ToneMapper.h>
2424

2525
#include <utils/compiler.h>
26+
#include <utils/FixedCapacityVector.h>
2627

2728
#include <math/mathfwd.h>
2829

@@ -461,6 +462,22 @@ class UTILS_PUBLIC ColorGrading : public FilamentAPI {
461462
*/
462463
Builder& curves(math::float3 shadowGamma, math::float3 midPoint, math::float3 highlightScale) noexcept;
463464

465+
/**
466+
* Specifies a custom 3D color grading LUT to map the final sRGB color.
467+
* The LUT is applied after post-processing and in LDR (sRGB space).
468+
* The data must be a 3D array of float3 (RGB) values.
469+
* The dimension does not need to be a power of two, but must be non-zero.
470+
* The values are always interpolated (trilinear) because the input color from previous steps is continuous.
471+
* The dimension doesn't need to match dimensions().
472+
* If the dimension is 0 or the data is empty, the custom LUT is skipped (ignored).
473+
*
474+
* @param data FixedCapacityVector containing the custom LUT data (3D array of float3).
475+
* @param dimension Dimension of the custom LUT.
476+
*
477+
* @return This Builder, for chaining calls
478+
*/
479+
Builder& customLut(utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept;
480+
464481
/**
465482
* Sets the output color space for this ColorGrading object. After all color grading steps
466483
* have been applied, the final color will be converted in the desired color space.

filament/src/details/ColorGrading.cpp

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include <utils/JobSystem.h>
3535
#include <utils/Mutex.h>
36+
#include <utils/Panic.h>
3637

3738
#include <cmath>
3839
#include <cstdlib>
@@ -101,6 +102,10 @@ struct ColorGrading::BuilderDetails {
101102
// Output color space
102103
ColorSpace outputColorSpace = Rec709-sRGB-D65;
103104

105+
// Custom LUT
106+
utils::FixedCapacityVector<math::float3> customLutData;
107+
uint8_t customLutDimension = 0;
108+
104109
bool operator!=(const BuilderDetails &rhs) const {
105110
return !(rhs == *this);
106111
}
@@ -130,7 +135,9 @@ struct ColorGrading::BuilderDetails {
130135
shadowGamma == rhs.shadowGamma &&
131136
midPoint == rhs.midPoint &&
132137
highlightScale == rhs.highlightScale &&
133-
outputColorSpace == rhs.outputColorSpace;
138+
outputColorSpace == rhs.outputColorSpace &&
139+
customLutData == rhs.customLutData &&
140+
customLutDimension == rhs.customLutDimension;
134141
}
135142
};
136143

@@ -272,11 +279,27 @@ ColorGrading::Builder& ColorGrading::Builder::outputColorSpace(
272279
return *this;
273280
}
274281

282+
ColorGrading::Builder& ColorGrading::Builder::customLut(
283+
utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept {
284+
mImpl->customLutData = std::move(data);
285+
mImpl->customLutDimension = dimension;
286+
return *this;
287+
}
288+
275289
#if defined(__clang__)
276290
#pragma clang diagnostic push
277291
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
278292
#endif
279293
ColorGrading* ColorGrading::Builder::build(Engine& engine) {
294+
if (mImpl->customLutDimension == 0 || mImpl->customLutData.empty()) {
295+
mImpl->customLutData.clear();
296+
mImpl->customLutDimension = 0;
297+
} else {
298+
FILAMENT_CHECK_PRECONDITION(mImpl->customLutData.size() ==
299+
size_t(mImpl->customLutDimension) * mImpl->customLutDimension * mImpl->customLutDimension)
300+
<< "Custom LUT data size does not match dimension^3";
301+
}
302+
280303
// We want to see if any of the default adjustment values have been modified
281304
// We skip the tonemapping operator on purpose since we always want to apply it
282305
BuilderDetails defaults;
@@ -544,6 +567,39 @@ inline float3 curves(float3 v, float3 shadowGamma, float3 midPoint, float3 highl
544567
};
545568
}
546569

570+
static float3 applyCustomLut(float3 v, const math::float3* lut, uint8_t dim) noexcept {
571+
float3 pos = v * float(dim - 1);
572+
float3 pos_floor = floor(pos);
573+
float3 pos_ceil = min(pos_floor + 1.0f, float(dim - 1));
574+
float3 d = pos - pos_floor;
575+
576+
int3 i0 = int3(pos_floor);
577+
int3 i1 = int3(pos_ceil);
578+
579+
auto fetch = [&](int r, int g, int b) {
580+
return lut[r + g * dim + b * dim * dim];
581+
};
582+
583+
float3 c000 = fetch(i0.x, i0.y, i0.z);
584+
float3 c100 = fetch(i1.x, i0.y, i0.z);
585+
float3 c010 = fetch(i0.x, i1.y, i0.z);
586+
float3 c110 = fetch(i1.x, i1.y, i0.z);
587+
float3 c001 = fetch(i0.x, i0.y, i1.z);
588+
float3 c101 = fetch(i1.x, i0.y, i1.z);
589+
float3 c011 = fetch(i0.x, i1.y, i1.z);
590+
float3 c111 = fetch(i1.x, i1.y, i1.z);
591+
592+
float3 c00 = c000 * (1.0f - d.x) + c100 * d.x;
593+
float3 c10 = c010 * (1.0f - d.x) + c110 * d.x;
594+
float3 c01 = c001 * (1.0f - d.x) + c101 * d.x;
595+
float3 c11 = c011 * (1.0f - d.x) + c111 * d.x;
596+
597+
float3 c0 = c00 * (1.0f - d.y) + c10 * d.y;
598+
float3 c1 = c01 * (1.0f - d.y) + c11 * d.y;
599+
600+
return c0 * (1.0f - d.z) + c1 * d.z;
601+
}
602+
547603
//------------------------------------------------------------------------------
548604
// Luminance scaling
549605
//------------------------------------------------------------------------------
@@ -665,6 +721,7 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
665721
// spaces are the same, but we currently don't check that. We must revise these conditions if we
666722
// ever handle this case.
667723
mIsOneDimensional = !builder->hasAdjustments && !builder->luminanceScaling
724+
&& builder->customLutData.empty()
668725
&& builder->toneMapper->isOneDimensional()
669726
&& engine.features.engine.color_grading.use_1d_lut;
670727
mIsLDR = mIsOneDimensional && builder->toneMapper->isLDR();
@@ -795,6 +852,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
795852
// Apply OETF
796853
v = config.oetf(v);
797854

855+
// Apply custom LUT if provided
856+
if (!builder->customLutData.empty()) {
857+
v = applyCustomLut(v, builder->customLutData.data(), builder->customLutDimension);
858+
}
859+
798860
return v;
799861
};
800862

libs/viewer/include/viewer/Settings.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ struct AgxToneMapperSettings {
146146
bool operator==(const AgxToneMapperSettings& rhs) const;
147147
};
148148

149+
enum class CustomLut : uint8_t {
150+
NONE = 0,
151+
NEGATIVE = 1,
152+
GRAYSCALE = 2,
153+
SEPIA = 3,
154+
TEAL_AND_ORANGE = 4,
155+
};
156+
149157
struct ColorGradingSettings {
150158
// fields are ordered to avoid padding
151159
bool enabled = true;
@@ -154,7 +162,7 @@ struct ColorGradingSettings {
154162
bool gamutMapping = false;
155163
filament::ColorGrading::QualityLevel quality = filament::ColorGrading::QualityLevel::MEDIUM;
156164
ToneMapping toneMapping = ToneMapping::ACES_LEGACY;
157-
bool padding0{};
165+
CustomLut customLut = CustomLut::NONE;
158166
AgxToneMapperSettings agxToneMapper;
159167
color::ColorSpace colorspace = Rec709-sRGB-D65;
160168
GenericToneMapperSettings genericToneMapper;

libs/viewer/src/Settings.cpp

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp
9898
return i + 1;
9999
}
100100

101+
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, CustomLut* out) {
102+
if (0 == compare(tokens[i], jsonChunk, "NONE")) { *out = CustomLut::NONE; }
103+
else if (0 == compare(tokens[i], jsonChunk, "NEGATIVE")) { *out = CustomLut::NEGATIVE; }
104+
else if (0 == compare(tokens[i], jsonChunk, "GRAYSCALE")) { *out = CustomLut::GRAYSCALE; }
105+
else if (0 == compare(tokens[i], jsonChunk, "SEPIA")) { *out = CustomLut::SEPIA; }
106+
else if (0 == compare(tokens[i], jsonChunk, "TEAL_AND_ORANGE")) { *out = CustomLut::TEAL_AND_ORANGE; }
107+
else {
108+
slog.w << "Invalid CustomLut: '" << STR(tokens[i], jsonChunk) << "'" << io::endl;
109+
}
110+
return i + 1;
111+
}
112+
101113
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, color::ColorSpace* out) {
102114
using namespace filament::color;
103115
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
207219
i = parse(tokens, i + 1, jsonChunk, &out->quality);
208220
} else if (compare(tok, jsonChunk, "toneMapping") == 0) {
209221
i = parse(tokens, i + 1, jsonChunk, &out->toneMapping);
222+
} else if (compare(tok, jsonChunk, "customLut") == 0) {
223+
i = parse(tokens, i + 1, jsonChunk, &out->customLut);
210224
} else if (compare(tok, jsonChunk, "genericToneMapper") == 0) {
211225
i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper);
212226
} else if (compare(tok, jsonChunk, "agxToneMapper") == 0) {
@@ -984,10 +998,57 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe
984998
}
985999
}
9861000

1001+
static utils::FixedCapacityVector<math::float3> generateCustomLut(CustomLut type, uint8_t dim) {
1002+
using namespace filament::math;
1003+
size_t count = size_t(dim) * dim * dim;
1004+
auto lut = utils::FixedCapacityVector<float3>::with_capacity(count);
1005+
1006+
for (size_t b = 0; b < dim; ++b) {
1007+
for (size_t g = 0; g < dim; ++g) {
1008+
for (size_t r = 0; r < dim; ++r) {
1009+
float3 v = float3{r, g, b} * (1.0f / (dim - 1));
1010+
switch (type) {
1011+
case CustomLut::NONE:
1012+
break;
1013+
case CustomLut::NEGATIVE:
1014+
v = 1.0f - v;
1015+
break;
1016+
case CustomLut::GRAYSCALE:
1017+
{
1018+
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
1019+
v = float3{luma};
1020+
}
1021+
break;
1022+
case CustomLut::SEPIA:
1023+
{
1024+
float luma = dot(v, float3{0.299f, 0.587f, 0.114f});
1025+
v = float3{
1026+
clamp(luma * 1.2f, 0.0f, 1.0f),
1027+
clamp(luma * 1.0f, 0.0f, 1.0f),
1028+
clamp(luma * 0.8f, 0.0f, 1.0f)
1029+
};
1030+
}
1031+
break;
1032+
case CustomLut::TEAL_AND_ORANGE:
1033+
{
1034+
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
1035+
float3 teal{0.0f, 0.5f, 0.5f};
1036+
float3 orange{1.0f, 0.5f, 0.0f};
1037+
v = teal * (1.0f - luma) + orange * luma;
1038+
}
1039+
break;
1040+
}
1041+
lut.push_back(v);
1042+
}
1043+
}
1044+
}
1045+
return lut;
1046+
}
1047+
9871048
ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) {
9881049
ToneMapper* toneMapper = createToneMapper(settings);
989-
ColorGrading *colorGrading = ColorGrading::Builder()
990-
.quality(settings.quality)
1050+
ColorGrading::Builder builder;
1051+
builder.quality(settings.quality)
9911052
.exposure(settings.exposure)
9921053
.nightAdaptation(settings.nightAdaptation)
9931054
.whiteBalance(settings.temperature, settings.tint)
@@ -1006,8 +1067,14 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e
10061067
.toneMapper(toneMapper)
10071068
.luminanceScaling(settings.luminanceScaling)
10081069
.gamutMapping(settings.gamutMapping)
1009-
.outputColorSpace(settings.colorspace)
1010-
.build(*engine);
1070+
.outputColorSpace(settings.colorspace);
1071+
1072+
if (settings.customLut != CustomLut::NONE) {
1073+
uint8_t dim = 16;
1074+
builder.customLut(generateCustomLut(settings.customLut, dim), dim);
1075+
}
1076+
1077+
ColorGrading *colorGrading = builder.build(*engine);
10111078
delete toneMapper;
10121079
return colorGrading;
10131080
}
@@ -1080,12 +1147,24 @@ static std::ostream& operator<<(std::ostream& out, const AgxToneMapperSettings&
10801147
<< "}";
10811148
}
10821149

1150+
static std::ostream& operator<<(std::ostream& out, CustomLut in) {
1151+
switch (in) {
1152+
case CustomLut::NONE: return out << "\"NONE\"";
1153+
case CustomLut::NEGATIVE: return out << "\"NEGATIVE\"";
1154+
case CustomLut::GRAYSCALE: return out << "\"GRAYSCALE\"";
1155+
case CustomLut::SEPIA: return out << "\"SEPIA\"";
1156+
case CustomLut::TEAL_AND_ORANGE: return out << "\"TEAL_AND_ORANGE\"";
1157+
}
1158+
return out << "\"INVALID\"";
1159+
}
1160+
10831161
static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& in) {
10841162
return out << "{\n"
10851163
<< "\"enabled\": " << to_string(in.enabled) << ",\n"
10861164
<< "\"colorspace\": " << to_string(in.colorspace) << ",\n"
10871165
<< "\"quality\": " << (in.quality) << ",\n"
10881166
<< "\"toneMapping\": " << (in.toneMapping) << ",\n"
1167+
<< "\"customLut\": " << (in.customLut) << ",\n"
10891168
<< "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n"
10901169
<< "\"agxToneMapper\": " << (in.agxToneMapper) << ",\n"
10911170
<< "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n"
@@ -1346,6 +1425,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings& rhs) const {
13461425
colorspace == rhs.colorspace &&
13471426
quality == rhs.quality &&
13481427
toneMapping == rhs.toneMapping &&
1428+
customLut == rhs.customLut &&
13491429
genericToneMapper == rhs.genericToneMapper &&
13501430
agxToneMapper == rhs.agxToneMapper &&
13511431
luminanceScaling == rhs.luminanceScaling &&

libs/viewer/src/ViewerGui.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo
226226
ImGui::Combo("Quality##colorGradingQuality", &quality, "Low\0Medium\0High\0Ultra\0\0");
227227
colorGrading.quality = (decltype(colorGrading.quality)) quality;
228228

229+
int customLut = (int) colorGrading.customLut;
230+
if (ImGui::Combo("Custom LUT##colorGradingCustomLut", &customLut, "None\0Negative\0Grayscale\0Sepia\0Teal and Orange\0\0")) {
231+
colorGrading.customLut = (CustomLut) customLut;
232+
}
233+
229234
int colorspace = (colorGrading.colorspace == Rec709-Linear-D65) ? 0 : 1;
230235
ImGui::Combo("Output color space", &colorspace, "Rec709-Linear-D65\0Rec709-sRGB-D65\0\0");
231236
colorGrading.colorspace = (colorspace == 0) ? Rec709-Linear-D65 : Rec709-sRGB-D65;

0 commit comments

Comments
 (0)