Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions android/filament-android/src/main/cpp/ColorGrading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
17 changes: 17 additions & 0 deletions filament/include/filament/ColorGrading.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <filament/ToneMapper.h>

#include <utils/compiler.h>
#include <utils/FixedCapacityVector.h>

#include <math/mathfwd.h>

Expand Down Expand Up @@ -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<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.
Expand Down
64 changes: 63 additions & 1 deletion filament/src/details/ColorGrading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#include <utils/JobSystem.h>
#include <utils/Mutex.h>
#include <utils/Panic.h>

#include <cmath>
#include <cstdlib>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
};

Expand Down
10 changes: 9 additions & 1 deletion libs/viewer/include/viewer/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
88 changes: 84 additions & 4 deletions libs/viewer/src/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funnily enough, all these examples can be achieved without LUTs using the existing ColorGrading APIs :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it's just an example.

{
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)
Expand All @@ -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;
}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 &&
Expand Down
5 changes: 5 additions & 0 deletions libs/viewer/src/ViewerGui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading