[skottie] Improved Hue/Saturation effect
The current HueSaturation effect implementation relies on a simple HSLA color matrix operation and assumes the controls are linear. Turns out AE's saturation is more sophisticated, both in implementation and in control mapping. Updating the effect to use a chain of specialized color filters: - keep HSLAMatrix() for hue adjustments - introduce a custom runtime effect for saturation (following AE's semantics) - use a plain Matrix() CF for lightness adjustments Change-Id: Iba6c9f7fd8c01dc33c1cd00822ea546867c057ac Reviewed-on: https://skia-review.googlesource.com/c/skia/+/452976 Commit-Queue: Florin Malita <fmalita@chromium.org> Commit-Queue: Florin Malita <fmalita@google.com> Reviewed-by: Brian Osman <brianosman@google.com>
This commit is contained in:
parent
de1d7fb07f
commit
6ba939d288
@ -7,17 +7,71 @@
|
|||||||
|
|
||||||
#include "modules/skottie/src/effects/Effects.h"
|
#include "modules/skottie/src/effects/Effects.h"
|
||||||
|
|
||||||
|
#include "include/effects/SkRuntimeEffect.h"
|
||||||
#include "include/private/SkTPin.h"
|
#include "include/private/SkTPin.h"
|
||||||
#include "modules/skottie/src/SkottieJson.h"
|
#include "modules/skottie/src/SkottieJson.h"
|
||||||
#include "modules/skottie/src/SkottieValue.h"
|
#include "modules/skottie/src/SkottieValue.h"
|
||||||
#include "modules/sksg/include/SkSGColorFilter.h"
|
#include "modules/sksg/include/SkSGColorFilter.h"
|
||||||
#include "src/utils/SkJSON.h"
|
#include "src/utils/SkJSON.h"
|
||||||
|
|
||||||
namespace skottie {
|
namespace skottie::internal {
|
||||||
namespace internal {
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
#ifdef SK_ENABLE_SKSL
|
||||||
|
|
||||||
|
// AE Saturation semantics:
|
||||||
|
//
|
||||||
|
// - saturation is applied as a component-wise scale (interpolation/extrapolation)
|
||||||
|
// relative to chroma mid point
|
||||||
|
// - the scale factor is clamped such that none of the components over/under saturates
|
||||||
|
// (e.g. below G/R and B are constrained to low_range and high_range, respectively)
|
||||||
|
// - the scale is also clammped to a maximum value of 126, empirically
|
||||||
|
// - the control is mapped linearly when desaturating, and non-linearly (1/1-S) when saturating
|
||||||
|
//
|
||||||
|
// 0 G R B 1
|
||||||
|
// |---------------+----+------------------+-----------------------------------|
|
||||||
|
// | | |
|
||||||
|
// min mid max
|
||||||
|
// <------- chroma ------>
|
||||||
|
// <------- low_range -------> <---------------- high_range ----------------->
|
||||||
|
//
|
||||||
|
// With some care, we can stay in premul for these calculations.
|
||||||
|
static constexpr char gSaturateSkSL[] =
|
||||||
|
"uniform half u_scale;"
|
||||||
|
|
||||||
|
"half4 main(half4 c) {"
|
||||||
|
// component min/max
|
||||||
|
"half2 rg_srt = (c.r < c.g) ? c.rg : c.gr;"
|
||||||
|
"half c_min = min(rg_srt.x, c.b),"
|
||||||
|
"c_max = max(rg_srt.y, c.b),"
|
||||||
|
|
||||||
|
// chroma and mid-chroma (epsilon to avoid blowing up in the division below)
|
||||||
|
"ch = max(c_max - c_min, 0.0001),"
|
||||||
|
"ch_mid = (c_min + c_max)*0.5,"
|
||||||
|
|
||||||
|
// clamp scale to the maximum value which doesn't over/under saturate individual components
|
||||||
|
"scale_max = min(ch_mid, c.a - ch_mid)/ch*2,"
|
||||||
|
"scale = min(u_scale, scale_max);"
|
||||||
|
|
||||||
|
// lerp
|
||||||
|
"c.rgb = ch_mid + (c.rgb - ch_mid)*scale;"
|
||||||
|
|
||||||
|
"return c;"
|
||||||
|
"}";
|
||||||
|
|
||||||
|
static sk_sp<SkColorFilter> make_saturate(float chroma_scale) {
|
||||||
|
static const auto* effect =
|
||||||
|
SkRuntimeEffect::MakeForColorFilter(SkString(gSaturateSkSL), {}).effect.release();
|
||||||
|
SkASSERT(effect);
|
||||||
|
|
||||||
|
return effect->makeColorFilter(SkData::MakeWithCopy(&chroma_scale, sizeof(chroma_scale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
static sk_sp<SkColorFilter> make_saturate(float) { return nullptr; }
|
||||||
|
#endif // SK_ENABLE_SKSL
|
||||||
|
|
||||||
class HueSaturationEffectAdapter final : public AnimatablePropertyContainer {
|
class HueSaturationEffectAdapter final : public AnimatablePropertyContainer {
|
||||||
public:
|
public:
|
||||||
static sk_sp<HueSaturationEffectAdapter> Make(const skjson::ArrayValue& jprops,
|
static sk_sp<HueSaturationEffectAdapter> Make(const skjson::ArrayValue& jprops,
|
||||||
@ -76,30 +130,57 @@ private:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AE semantics:
|
sk_sp<SkColorFilter> cf;
|
||||||
//
|
|
||||||
// master hue [degrees] => color.H offset
|
|
||||||
// master sat [-100..100] => [-100..0) -> [0 .. color.S)
|
|
||||||
// ( 0..100] -> (color.S .. 1]
|
|
||||||
// master lightness [-100..100] => [-100..0) -> [0 .. color.L]
|
|
||||||
// ( 0..100] -> (color.L .. 1]
|
|
||||||
const auto h = fMasterHue / 360,
|
|
||||||
s = SkTPin(fMasterSat / 100, -1.0f, 1.0f),
|
|
||||||
l = SkTPin(fMasterLight / 100, -1.0f, 1.0f),
|
|
||||||
h_bias = h,
|
|
||||||
s_bias = std::max(s, 0.0f),
|
|
||||||
s_scale = 1 - std::abs(s),
|
|
||||||
l_bias = std::max(l, 0.0f),
|
|
||||||
l_scale = 1 - std::abs(l);
|
|
||||||
|
|
||||||
const float hsl_cm[20] = {
|
if (!SkScalarNearlyZero(fMasterHue)) {
|
||||||
1, 0, 0, 0, h_bias,
|
// Linear control mapping hue(degrees) -> hue offset]
|
||||||
0, s_scale, 0, 0, s_bias,
|
const auto h = fMasterHue/360;
|
||||||
0, 0, l_scale, 0, l_bias,
|
|
||||||
|
const float cm[20] = {
|
||||||
|
1, 0, 0, 0, h,
|
||||||
|
0, 1, 0, 0, 0,
|
||||||
|
0, 0, 1, 0, 0,
|
||||||
0, 0, 0, 1, 0,
|
0, 0, 0, 1, 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return SkColorFilters::HSLAMatrix(hsl_cm);
|
cf = SkColorFilters::HSLAMatrix(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SkScalarNearlyZero(fMasterSat)) {
|
||||||
|
// AE clamps the max chroma scale to this value.
|
||||||
|
static constexpr auto kMaxScale = 126.0f;
|
||||||
|
|
||||||
|
// Control mapping:
|
||||||
|
// * sat [-100 .. 0) -> scale [0 .. 1) , linear
|
||||||
|
// * sat [0 .. 100] -> scale [1 .. max] , nonlinear: 100/(100 - sat)
|
||||||
|
const auto s = SkTPin(fMasterSat/100, -1.0f, 1.0f),
|
||||||
|
chroma_scale = s < 0 ? s + 1 : std::min(1/(1 - s), kMaxScale);
|
||||||
|
|
||||||
|
cf = SkColorFilters::Compose(std::move(cf), make_saturate(chroma_scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SkScalarNearlyZero(fMasterLight)) {
|
||||||
|
// AE implements Lightness as a component-wise interpolation to 0 (for L < 0),
|
||||||
|
// or 1 (for L > 0).
|
||||||
|
//
|
||||||
|
// Control mapping:
|
||||||
|
// * lightness [-100 .. 0) -> lerp[0 .. 1) from 0, linear
|
||||||
|
// * lightness [0 .. 100] -> lerp[1 .. 0] from 1, linear
|
||||||
|
const auto l = SkTPin(fMasterLight/100, -1.0f, 1.0f),
|
||||||
|
ls = 1 - std::abs(l), // scale
|
||||||
|
lo = l < 0 ? 0 : 1 - ls; // offset
|
||||||
|
|
||||||
|
const float cm[20] = {
|
||||||
|
ls, 0, 0, 0, lo,
|
||||||
|
0, ls, 0, 0, lo,
|
||||||
|
0, 0, ls, 0, lo,
|
||||||
|
0, 0, 0, 1, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
cf = SkColorFilters::Compose(std::move(cf), SkColorFilters::Matrix(cm));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cf;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sk_sp<sksg::ExternalColorFilter> fColorFilter;
|
const sk_sp<sksg::ExternalColorFilter> fColorFilter;
|
||||||
@ -119,5 +200,4 @@ sk_sp<sksg::RenderNode> EffectBuilder::attachHueSaturationEffect(
|
|||||||
fBuilder);
|
fBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace internal
|
} // namespace skottie::internal
|
||||||
} // namespace skottie
|
|
||||||
|
1
resources/skottie/skottie-huesaturation-animated.json
Normal file
1
resources/skottie/skottie-huesaturation-animated.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user