diff --git a/filament/include/filament/ColorGrading.h b/filament/include/filament/ColorGrading.h index d0d904d530..9038f9551a 100644 --- a/filament/include/filament/ColorGrading.h +++ b/filament/include/filament/ColorGrading.h @@ -30,17 +30,62 @@ namespace filament { class Engine; class FColorGrading; +/** + * ColorGrading is used to transform (either to modify or correct) the colors of the HDR buffer + * rendered by Filament. Color grading transforms are applied after lighting, and after any lens + * effects (bloom for instance), and include tone mapping. + * + * Creation, usage and destruction + * =============================== + * + * A ColorGrading object is created using the ColorGrading::Builder and destroyed by calling + * Engine::destroy(const ColorGrading*). A ColorGrading object is meant to be set on a View. + * + * ~~~~~~~~~~~{.cpp} + * filament::Engine* engine = filament::Engine::create(); + * + * filament::ColorGrading* colorGrading = filament::ColorGrading::Builder() + * .toneMapping(filament::ColorGrading::ToneMapping::ACES) + * .build(*engine); + * + * myView->setColorGrading(colorGrading); + * + * engine->destroy(colorGrading); + * ~~~~~~~~~~~ + * + * Performance + * =========== + * + * Creating a new ColorGrading object may be more expensive than other Filament objects as a + * 3D LUT may need to be generated. The generation of a 3D LUT, if necessary, may happen on + * the CPU. + * + * Defaults + * ======== + * + * Here are the default color grading options: + * - Tone mapping: ACES + * - White balance: temperature 0, and tint 0 + * - Channel mixer: red {1,0,0}, green {0,1,0}, blue {0,0,1} + * + * @see View + */ class UTILS_PUBLIC ColorGrading : public FilamentAPI { struct BuilderDetails; public: + + /** + * List of available tone-mapping operators. + */ enum class ToneMapping : uint8_t { LINEAR = 0, //!< Linear tone mapping (i.e. no tone mapping) ACES = 1, //!< ACES tone mapping, with a brightness modifier FILMIC = 2, //!< Filmic tone mapping, modelled after ACES but applied in sRGB space REINHARD = 3, //!< Reinhard luma-based tone mapping - DISPLAY_RANGE = 4, //!< Debug tone mapping to validate scene exposure + DISPLAY_RANGE = 4, //!< Tone mapping used to validate/debug scene exposure }; + //! Use Builder to construct a ColorGrading object instance class Builder : public BuilderBase { friend struct BuilderDetails; public: @@ -51,11 +96,82 @@ public: Builder& operator=(Builder const& rhs) noexcept; Builder& operator=(Builder&& rhs) noexcept; + /** + * Selects the tone mapping operator to apply to the HDR color buffer as the last + * operation of the color grading post-processing step. + * + * The default tone mapping operator is ACES. + * + * @param toneMapping The tone mapping operator to apply to the HDR color buffer + * + * @return This Builder, for chaining calls + */ Builder& toneMapping(ToneMapping toneMapping) noexcept; - // TODO: document: temperature from -1 to +1, tint from -1 to +1; clipped otherwise + /** + * Adjusts the while balance of the image. This can be used to remove color casts + * and correct the appearance of the white point in the scene, or to alter the + * overall chromaticity of the image for artistic reasons (to make the image appear + * cooler or warmer for instance). + * + * The while balance adjustment is defined with two values: + * - Temperature, to modify the color temperature. This value will modify the colors + * on a blue/yellow axis. Lower values apply a cool color temperature, and higher + * values apply a warm color temperature. The lowest value, -1.0f, is equivalent to + * a temperature of 2,000K. The highest value, 1.0f, is equivalent to a temperature + * of 50,000K. + * - Tint, to modify the colors on a green/magenta axis. The lowest value, -1.0f, will + * apply a strong green cast, and the highest value, 1.0f, will apply a strong magenta + * cast. + * + * Both values are expected to be in the range [-1.0..+1.0]. Values outside of that + * range will be clipped to that range. + * + * @param temperature Modification on the blue/yellow axis, as a value between -1.0 and +1.0. + * @param tint Modification on the green/magenta axis, as a value between -1.0 and +1.0. + * + * @return This Builder, for chaining calls + */ Builder& whiteBalance(float temperature, float tint) noexcept; + /** + * The channel mixer adjustment modifies each output color channel using the specified + * mix of the source color channels. + * + * By default each output color channel is set to use 100% of the corresponding source + * channel and 0% of the other channels. For instance, the output red channel is set to + * {1.0, 0.0, 1.0} or 100% red, 0% green and 0% blue. + * + * Each output channel can add or subtract data from the source channel by using values + * in the range [-2.0..+2.0]. Values outside of that range will be clipped to that range. + * + * Using the channel mixer adjustment you can for instance create a monochrome output + * by setting all 3 output channels to the same mix. For instance: {0.4, 0.4, 0.2} for + * all 3 output channels(40% red, 40% green and 20% blue). + * + * More complex mixes can be used to create more compelx effects. For instance, here is + * a mix that creates a sepia tone effect: + * - outRed = {0.255, 0.858, 0.087} + * - outGreen = {0.213, 0.715, 0.072} + * - outBlue = {0.170, 0.572, 0.058} + * + * @param outRed The mix of source RGB for the output red channel, between -2.0 and +2.0 + * @param outGreen The mix of source RGB for the output green channel, between -2.0 and +2.0 + * @param outBlue The mix of source RGB for the output blue channel, between -2.0 and +2.0 + * + * @return This Builder, for chaining calls + */ + Builder& channelMixer( + math::float3 outRed, math::float3 outGreen, math::float3 outBlue) noexcept; + + /** + * Creates the ColorGrading object and returns a pointer to it. + * + * @param engine Reference to the filament::Engine to associate this ColorGrading with. + * + * @return pointer to the newly created object or nullptr if exceptions are disabled and + * an error occurred. + */ ColorGrading* build(Engine& engine); private: diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index 6b20b2c753..a37ffb1f16 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -622,7 +622,7 @@ public: * * @param enabled true enables post processing, false disables it. * - * @see setBloomOptions, setToneMapping, setAntiAliasing, setDithering, setSampleCount + * @see setBloomOptions, setColorGrading, setAntiAliasing, setDithering, setSampleCount */ void setPostProcessingEnabled(bool enabled) noexcept; diff --git a/filament/src/ColorGrading.cpp b/filament/src/ColorGrading.cpp index 147a3910bd..cb5f780e30 100644 --- a/filament/src/ColorGrading.cpp +++ b/filament/src/ColorGrading.cpp @@ -18,9 +18,15 @@ #include "details/Engine.h" -#include "ColorSpace.h" #include "FilamentAPI-impl.h" +#include "ColorSpace.h" +// When defined, the ACES tone mapper will match the brightness of the filmic ("ACES sRGB") +// tone mapper. It is *not* correct, but it helps for compatibility +// TODO: Always enable this, expose a float to control this (1.0 == real ACES) +#define TONEMAP_ACES_MATCH_BRIGHTNESS +#include "ToneMapping.h" + #include #include @@ -31,13 +37,6 @@ #include -// When defined, the ACES tone mapper will match the brightness of the filmic ("ACES sRGB") -// tone mapper. It is *not* correct, but it helps for compatibility -// TODO: Always enable this, expose a float to control this (1.0 == real ACES) -#define TONEMAP_ACES_MATCH_BRIGHTNESS - -#include "ToneMapping.h" - namespace filament { using namespace utils; @@ -52,7 +51,10 @@ static constexpr size_t LUT_DIMENSION = 32u; struct ColorGrading::BuilderDetails { ToneMapping toneMapping = ToneMapping::ACES; - float2 whiteBalance = {0.0f, 0.0f}; + float2 whiteBalance = {0.0f, 0.0f}; + float3 outRed = {1.0f, 0.0f, 0.0f}; + float3 outGreen = {0.0f, 1.0f, 0.0f}; + float3 outBlue = {0.0f, 0.0f, 1.0f}; }; using BuilderType = ColorGrading; @@ -76,30 +78,16 @@ ColorGrading::Builder& ColorGrading::Builder::whiteBalance(float temperature, fl return *this; } -ColorGrading* ColorGrading::Builder::build(Engine& engine) { - return upcast(engine).createColorGrading(*this); +ColorGrading::Builder& ColorGrading::Builder::channelMixer( + float3 outRed, float3 outGreen, float3 outBlue) noexcept { + mImpl->outRed = clamp(outRed, -2.0f, 2.0f); + mImpl->outGreen = clamp(outGreen, -2.0f, 2.0f); + mImpl->outBlue = clamp(outBlue, -2.0f, 2.0f); + return *this; } -//------------------------------------------------------------------------------ -// Tone mapping -//------------------------------------------------------------------------------ - -using ToneMapper = float3(*)(float3); - -ToneMapper selectToneMapping(ColorGrading::ToneMapping toneMapping) { - switch(toneMapping) { - case ColorGrading::ToneMapping::LINEAR: - return tonemap::Linear; - case ColorGrading::ToneMapping::ACES: - return tonemap::ACES; - case ColorGrading::ToneMapping::FILMIC: - return tonemap::Filmic; - case ColorGrading::ToneMapping::REINHARD: - return tonemap::Reinhard; - case ColorGrading::ToneMapping::DISPLAY_RANGE: - return tonemap::DisplayRange; - } - return tonemap::ACES; +ColorGrading* ColorGrading::Builder::build(Engine& engine) { + return upcast(engine).createColorGrading(*this); } //------------------------------------------------------------------------------ @@ -132,13 +120,54 @@ inline float3 chromaticAdaptation(float3 v, float2 whiteBalance) { return v; } +//------------------------------------------------------------------------------ +// General color grading +//------------------------------------------------------------------------------ + +mat3f selectColorGradingTransform(ColorGrading::ToneMapping toneMapping) { + switch (toneMapping) { + case ColorGrading::ToneMapping::ACES: + return sRGB_to_AP1; + default: + return mat3f{}; + } + return mat3f{}; +} + +UTILS_ALWAYS_INLINE +inline constexpr float3 channelMixer(float3 v, float3 r, float3 g, float3 b) { + return {dot(v, r), dot(v, g), dot(v, b)}; +} + +//------------------------------------------------------------------------------ +// Tone mapping +//------------------------------------------------------------------------------ + +using ToneMapper = float3(*)(float3); + +ToneMapper selectToneMapping(ColorGrading::ToneMapping toneMapping) { + switch (toneMapping) { + case ColorGrading::ToneMapping::LINEAR: + return tonemap::Linear; + case ColorGrading::ToneMapping::ACES: + return tonemap::ACES; + case ColorGrading::ToneMapping::FILMIC: + return tonemap::Filmic; + case ColorGrading::ToneMapping::REINHARD: + return tonemap::Reinhard; + case ColorGrading::ToneMapping::DISPLAY_RANGE: + return tonemap::DisplayRange; + } + return tonemap::ACES; +} + //------------------------------------------------------------------------------ // Color grading implementation //------------------------------------------------------------------------------ struct Config { + mat3f colorGradingTransform; ToneMapper toneMapper; - float2 whiteBalance; }; FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { @@ -151,8 +180,8 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { void* const data = malloc(lutElementCount * elementSize); Config config{ - .toneMapper = selectToneMapping(builder->toneMapping), - .whiteBalance = builder->whiteBalance + .colorGradingTransform = selectColorGradingTransform(builder->toneMapping), + .toneMapper = selectToneMapping(builder->toneMapping), }; //auto now = std::chrono::steady_clock::now(); @@ -163,7 +192,7 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { JobSystem& js = engine.getJobSystem(); auto slices = js.createJob(); for (size_t b = 0; b < LUT_DIMENSION; b++) { - auto job = js.createJob(slices, [data, b, config](JobSystem&, JobSystem::Job*) { + auto job = js.createJob(slices, [data, b, &config, builder](JobSystem&, JobSystem::Job*) { half4* UTILS_RESTRICT p = (half4*) data + b * LUT_DIMENSION * LUT_DIMENSION; for (size_t g = 0; g < LUT_DIMENSION; g++) { for (size_t r = 0; r < LUT_DIMENSION; r++) { @@ -173,15 +202,26 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) { v = lutToLinear(v); // White balance - v = chromaticAdaptation(v, config.whiteBalance); + v = chromaticAdaptation(v, builder->whiteBalance); + + // Convert to color grading color space + v = config.colorGradingTransform * v; + + // Channel mixer + v = channelMixer(v, builder->outRed, builder->outGreen, builder->outBlue); + + // Kill negative values before tone mapping + v = max(v, 0.0f); // Tone mapping v = config.toneMapper(v); - // TODO: allow to customize the output color space + // Apply OECF + // TODO: allow to customize the output color space, + // here we assume we are in the sRGB gamut already v = image::linearTosRGB(v); - *p++ = half4{ v, 0 }; + *p++ = half4{v, 0.0f}; } } }); diff --git a/filament/src/ColorSpace.h b/filament/src/ColorSpace.h index 7f4f0ef609..44fcc3ae88 100644 --- a/filament/src/ColorSpace.h +++ b/filament/src/ColorSpace.h @@ -54,6 +54,42 @@ constexpr mat3f CIECAT02_to_XYZ{ 0.1827450f, 0.0720978f, 1.0153300f }; +constexpr mat3f AP1_to_XYZ{ + 0.6624541811f, 0.2722287168f, -0.0055746495f, + 0.1340042065f, 0.6740817658f, 0.0040607335f, + 0.1561876870f, 0.0536895174f, 1.0103391003f +}; + +constexpr mat3f XYZ_to_AP1{ + 1.6410233797f, -0.6636628587f, 0.0117218943f, + -0.3248032942f, 1.6153315917f, -0.0082844420f, + -0.2364246952f, 0.0167563477f, 0.9883948585f +}; + +constexpr mat3f AP1_to_AP0{ + 0.6954522414f, 0.0447945634f, -0.0055258826f, + 0.1406786965f, 0.8596711185f, 0.0040252103f, + 0.1638690622f, 0.0955343182f, 1.0015006723f +}; + +constexpr mat3f AP0_to_AP1{ + 1.4514393161f, -0.0765537734f, 0.0083161484f, + -0.2365107469f, 1.1762296998f, -0.0060324498f, + -0.2149285693f, -0.0996759264f, 0.9977163014f +}; + +constexpr mat3f AP1_to_sRGB{ + 1.70505f, -0.13026f, -0.02400f, + -0.62179f, 1.14080f, -0.12897f, + -0.08326f, -0.01055f, 1.15297f +}; + +constexpr mat3f sRGB_to_AP1{ + 0.61319f, 0.07021f, 0.02062f, + 0.33951f, 0.91634f, 0.10957f, + 0.04737f, 0.01345f, 0.86961f +}; + // Standard CIE 1931 2° illuminant D65, in xyY space constexpr float3 ILLUMINANT_D65_xyY{0.31271f, 0.32902f, 1.0f}; @@ -66,7 +102,12 @@ constexpr mat3f sRGB_to_LMS = XYZ_to_CIECAT02 * sRGB_to_XYZ; constexpr mat3f LMS_to_sRGB = XYZ_to_sRGB * CIECAT02_to_XYZ; inline constexpr XYZ xyY_to_XYZ(xyY v) { - return XYZ{v.x / v.y, v.z, (1.0f - v.x - v.y) / v.y}; + const float a = v.z / max(v.y, 1e-5f); + return XYZ{v.x * a, v.z, (1.0f - v.x - v.y) * a}; +} + +inline constexpr xyY XYZ_to_xyY(XYZ v) { + return float3(v.xy / max(v.x + v.y + v.z, 1e-5f), v.y); } // Returns the y chromaticity coordinate in xyY for an illuminant series D, diff --git a/filament/src/ToneMapping.h b/filament/src/ToneMapping.h index 3a6efe53a1..fa75d25a42 100644 --- a/filament/src/ToneMapping.h +++ b/filament/src/ToneMapping.h @@ -17,6 +17,8 @@ #ifndef TNT_FILAMENT_TONE_MAPPING_H #define TNT_FILAMENT_TONE_MAPPING_H +#include "ColorSpace.h" + #include #include @@ -111,68 +113,25 @@ inline float center_hue(float hue, float centerH) { return hueCentered; } -inline float3 XYZ_2_xyY(float3 XYZ) { - float divisor = max(XYZ.x + XYZ.y + XYZ.z, 1e-5f); - return float3(XYZ.xy / divisor, XYZ.y); -} - -inline float3 xyY_2_XYZ(float3 xyY) { - float a = xyY.z / max(xyY.y, 1e-5f); - float3 XYZ = float3(float2{ xyY.x, xyY.z }, (1.0f - xyY.x - xyY.y)); - XYZ.x *= a; - XYZ.z *= a; - return XYZ; -} - inline float3 darkSurround_to_dimSurround(float3 linearCV) { const float DIM_SURROUND_GAMMA = 0.9811f; - const mat3f AP1_2_XYZ{ - 0.6624541811f, 0.2722287168f, -0.0055746495f, - 0.1340042065f, 0.6740817658f, 0.0040607335f, - 0.1561876870f, 0.0536895174f, 1.0103391003f - }; - - const mat3f XYZ_2_AP1{ - 1.6410233797f, -0.6636628587f, 0.0117218943f, - -0.3248032942f, 1.6153315917f, -0.0082844420f, - -0.2364246952f, 0.0167563477f, 0.9883948585f - }; - - float3 XYZ = AP1_2_XYZ * linearCV; - float3 xyY = XYZ_2_xyY(XYZ); + float3 XYZ = AP1_to_XYZ * linearCV; + float3 xyY = XYZ_to_xyY(XYZ); xyY.z = clamp(xyY.z, 0.0f, (float)std::numeric_limits::max()); xyY.z = std::pow(xyY.z, DIM_SURROUND_GAMMA); - XYZ = xyY_2_XYZ(xyY); - return XYZ_2_AP1 * XYZ; + XYZ = xyY_to_XYZ(xyY); + return XYZ_to_AP1 * XYZ; } UTILS_ALWAYS_INLINE inline float3 ACES(float3 color) { // Some bits were removed to adapt to our desired output - // Input: linear sRGB + // Input: ACEScg (AP1) // Output: linear sRGB - const mat3f sRGB_2_AP0{ - 0.439701f, 0.0897923f, 0.017544f, - 0.382978f, 0.8134230f, 0.111544f, - 0.177335f, 0.0967616f, 0.870704f - }; - - const mat3f AP0_2_AP1{ - 1.4514393161f, -0.0765537734f, 0.0083161484f, - -0.2365107469f, 1.1762296998f, -0.0060324498f, - -0.2149285693f, -0.0996759264f, 0.9977163014f - }; - - const mat3f AP1_2_sRGB{ - 1.70505f, -0.13026f, -0.024f, - -0.62179f, 1.1408f, -0.12897f, - -0.08326f, -0.01055f, 1.15297f - }; - // "Glow" module constants const float RRT_GLOW_GAIN = 0.05f; const float RRT_GLOW_MID = 0.08f; @@ -187,8 +146,7 @@ inline float3 ACES(float3 color) { const float RRT_SAT_FACTOR = 0.96f; const float ODT_SAT_FACTOR = 0.93f; - // This assumes our working color space is sRGB - float3 ap0 = sRGB_2_AP0 * color; + float3 ap0 = AP1_to_AP0 * color; // Glow module float saturation = rgb_2_saturation(ap0); @@ -206,7 +164,7 @@ inline float3 ACES(float3 color) { ap0.r += hueWeight * saturation * (RRT_RED_PIVOT - ap0.r) * (1.0f - RRT_RED_SCALE); // ACES to RGB rendering space - float3 ap1 = clamp(AP0_2_AP1 * ap0, 0.0f, (float)std::numeric_limits::max()); + float3 ap1 = clamp(AP0_to_AP1 * ap0, 0.0f, (float)std::numeric_limits::max()); // Global desaturation const float3 AP1_RGB2Y{ 0.272229f, 0.674082f, 0.0536895f }; @@ -233,7 +191,7 @@ inline float3 ACES(float3 color) { linearCV = mix(float3(dot(linearCV, AP1_RGB2Y)), linearCV, ODT_SAT_FACTOR); // Convert to display primary encoding (Rec.709 primaries, D65 white point) - return AP1_2_sRGB * linearCV; + return AP1_to_sRGB * linearCV; } } // namespace aces diff --git a/samples/material_sandbox.cpp b/samples/material_sandbox.cpp index b976b8d98b..2dcddb3ac5 100644 --- a/samples/material_sandbox.cpp +++ b/samples/material_sandbox.cpp @@ -578,6 +578,9 @@ static void gui(filament::Engine* engine, filament::View*) { "Linear\0ACES\0Filmic\0Reinhard\0Display Range\0\0"); ImGui::SliderInt("Temperature", ¶ms.colorGradingOptions.temperature, -100, 100); ImGui::SliderInt("Tint", ¶ms.colorGradingOptions.tint, -100, 100); + ImGui::SliderFloat3("Out Red", ¶ms.colorGradingOptions.outRed.x, -2.0f, 2.0f); + ImGui::SliderFloat3("Out Green", ¶ms.colorGradingOptions.outGreen.x, -2.0f, 2.0f); + ImGui::SliderFloat3("Out Blue", ¶ms.colorGradingOptions.outBlue.x, -2.0f, 2.0f); } if (ImGui::CollapsingHeader("Debug")) { @@ -694,8 +697,9 @@ static void preRender(filament::Engine* engine, filament::View* view, filament:: if (memcmp(&g_params.colorGradingOptions, &g_lastColorGradingOptions, sizeof(ColorGradingOptions))) { ColorGradingOptions& options = g_params.colorGradingOptions; ColorGrading* colorGrading = ColorGrading::Builder() - .toneMapping(options.toneMapping) .whiteBalance(options.temperature / 100.0f, options.tint / 100.0f) + .channelMixer(options.outRed, options.outGreen, options.outBlue) + .toneMapping(options.toneMapping) .build(*engine); view->setColorGrading(colorGrading); diff --git a/samples/material_sandbox.h b/samples/material_sandbox.h index bc27512f36..27806fdbb4 100644 --- a/samples/material_sandbox.h +++ b/samples/material_sandbox.h @@ -64,6 +64,9 @@ struct ColorGradingOptions { ColorGrading::ToneMapping toneMapping = ColorGrading::ToneMapping::ACES; int temperature = 0; int tint = 0; + math::float3 outRed{1.0f, 0.0f, 0.0f}; + math::float3 outGreen{0.0f, 1.0f, 0.0f}; + math::float3 outBlue{0.0f, 0.0f, 1.0f}; }; struct SandboxParameters {