Add channel mixer and docs to ColorGrading (#2618)

This commit is contained in:
Romain Guy
2020-06-01 09:07:34 -07:00
committed by GitHub
parent 50adc1f661
commit c49ffcc354
7 changed files with 257 additions and 95 deletions

View File

@@ -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<BuilderDetails> {
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:

View File

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

View File

@@ -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 <math/vec2.h>
#include <math/vec3.h>
@@ -31,13 +37,6 @@
#include <functional>
// 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};
}
}
});

View File

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

View File

@@ -17,6 +17,8 @@
#ifndef TNT_FILAMENT_TONE_MAPPING_H
#define TNT_FILAMENT_TONE_MAPPING_H
#include "ColorSpace.h"
#include <utils/compiler.h>
#include <math/mat3.h>
@@ -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<math::half>::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<math::half>::max());
float3 ap1 = clamp(AP0_to_AP1 * ap0, 0.0f, (float)std::numeric_limits<math::half>::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

View File

@@ -578,6 +578,9 @@ static void gui(filament::Engine* engine, filament::View*) {
"Linear\0ACES\0Filmic\0Reinhard\0Display Range\0\0");
ImGui::SliderInt("Temperature", &params.colorGradingOptions.temperature, -100, 100);
ImGui::SliderInt("Tint", &params.colorGradingOptions.tint, -100, 100);
ImGui::SliderFloat3("Out Red", &params.colorGradingOptions.outRed.x, -2.0f, 2.0f);
ImGui::SliderFloat3("Out Green", &params.colorGradingOptions.outGreen.x, -2.0f, 2.0f);
ImGui::SliderFloat3("Out Blue", &params.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);

View File

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