1/* 2 * Copyright 2020 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8#include "modules/skottie/src/effects/Effects.h" 9 10#include "include/core/SkColorFilter.h" 11#include "include/effects/SkRuntimeEffect.h" 12#include "include/private/SkTPin.h" 13#include "modules/skottie/src/Adapter.h" 14#include "modules/skottie/src/SkottieJson.h" 15#include "modules/skottie/src/SkottieValue.h" 16#include "modules/sksg/include/SkSGColorFilter.h" 17 18namespace skottie::internal { 19 20#ifdef SK_ENABLE_SKSL 21 22namespace { 23 24// The contrast effect transfer function can be approximated with the following 25// 3rd degree polynomial: 26// 27// f(x) = -2πC/3 * x³ + πC * x² + (1 - πC/3) * x 28// 29// where C is the normalized contrast value [-1..1]. 30// 31// Derivation: 32// 33// - start off with sampling the AE contrast effect for various contrast/input values [1] 34// 35// - apply cubic polynomial curve fitting to determine best-fit coefficients for given 36// contrast values [2] 37// 38// - observations: 39// * negative contrast appears clamped at -0.5 (-50) 40// * a,b coefficients vary linearly vs. contrast 41// * the b coefficient for max contrast (1.0) looks kinda familiar: 3.14757 - coincidence? 42// probably not. let's run with it: b == πC 43// 44// - additionally, we expect the following to hold: 45// * f(0 ) = 0 \ | d = 0 46// * f(1 ) = 1 | => | a = -2b/3 47// * f(0.5) = 0.5 / | c = 1 - b/3 48// 49// - this yields a pretty decent approximation: [3] 50// 51// 52// Note (courtesy of mtklein, reed): [4] seems to yield a closer approximation, but requires 53// a more expensive sin 54// 55// f(x) = x + a * sin(2πx)/2π 56// 57// [1] https://www.desmos.com/calculator/oksptqpo8z 58// [2] https://www.desmos.com/calculator/oukrf6yahn 59// [3] https://www.desmos.com/calculator/ehem0vy3ft 60// [4] https://www.desmos.com/calculator/5t4xi10q4v 61// 62 63#ifndef SKOTTIE_ACCURATE_CONTRAST_APPROXIMATION 64static sk_sp<SkData> make_contrast_coeffs(float contrast) { 65 struct { float a, b, c; } coeffs; 66 67 coeffs.b = SK_ScalarPI * contrast; 68 coeffs.a = -2 * coeffs.b / 3; 69 coeffs.c = 1 - coeffs.b / 3; 70 71 return SkData::MakeWithCopy(&coeffs, sizeof(coeffs)); 72} 73 74static constexpr char CONTRAST_EFFECT[] = R"( 75 uniform half a; 76 uniform half b; 77 uniform half c; 78 79 half4 main(half4 color) { 80 // C' = a*C^3 + b*C^2 + c*C 81 color.rgb = ((a*color.rgb + b)*color.rgb + c)*color.rgb; 82 return color; 83 } 84)"; 85#else 86// More accurate (but slower) approximation: 87// 88// f(x) = x + a * sin(2πx) 89// 90// a = -contrast/3π 91// 92static sk_sp<SkData> make_contrast_coeffs(float contrast) { 93 const auto coeff_a = -contrast / (3 * SK_ScalarPI); 94 95 return SkData::MakeWithCopy(&coeff_a, sizeof(coeff_a)); 96} 97 98static constexpr char CONTRAST_EFFECT[] = R"( 99 uniform half a; 100 101 half4 main(half4 color) { 102 color.rgb += a * sin(color.rgb * 6.283185); 103 return color; 104 } 105)"; 106 107#endif 108 109// Brightness transfer function approximation: 110// 111// f(x) = 1 - (1 - x)^(2^(1.8*B)) 112// 113// where B is the normalized [-1..1] brightness value 114// 115// Visualization: https://www.desmos.com/calculator/wuyqa2wtol 116// 117static sk_sp<SkData> make_brightness_coeffs(float brightness) { 118 const float coeff_a = std::pow(2.0f, brightness * 1.8f); 119 120 return SkData::MakeWithCopy(&coeff_a, sizeof(coeff_a)); 121} 122 123static constexpr char BRIGHTNESS_EFFECT[] = R"( 124 uniform half a; 125 126 half4 main(half4 color) { 127 color.rgb = 1 - pow(1 - color.rgb, half3(a)); 128 return color; 129 } 130)"; 131 132class BrightnessContrastAdapter final : public DiscardableAdapterBase<BrightnessContrastAdapter, 133 sksg::ExternalColorFilter> { 134public: 135 BrightnessContrastAdapter(const skjson::ArrayValue& jprops, 136 const AnimationBuilder& abuilder, 137 sk_sp<sksg::RenderNode> layer) 138 : INHERITED(sksg::ExternalColorFilter::Make(std::move(layer))) 139 , fBrightnessEffect(SkRuntimeEffect::MakeForColorFilter(SkString(BRIGHTNESS_EFFECT)).effect) 140 , fContrastEffect(SkRuntimeEffect::MakeForColorFilter(SkString(CONTRAST_EFFECT)).effect) { 141 SkASSERT(fBrightnessEffect); 142 SkASSERT(fContrastEffect); 143 144 enum : size_t { 145 kBrightness_Index = 0, 146 kContrast_Index = 1, 147 kUseLegacy_Index = 2, 148 }; 149 150 EffectBinder(jprops, abuilder, this) 151 .bind(kBrightness_Index, fBrightness) 152 .bind( kContrast_Index, fContrast ) 153 .bind( kUseLegacy_Index, fUseLegacy ); 154 } 155 156private: 157 void onSync() override { 158 this->node()->setColorFilter(SkScalarRoundToInt(fUseLegacy) 159 ? this->makeLegacyCF() 160 : this->makeCF()); 161 } 162 163 sk_sp<SkColorFilter> makeLegacyCF() const { 164 // In 'legacy' mode, brightness is 165 // 166 // - in the [-100..100] range 167 // - applied component-wise as a direct offset (255-based) 168 // - (neutral value: 0) 169 // - transfer function: https://www.desmos.com/calculator/zne0oqwwzb 170 // 171 // while contrast is 172 // 173 // - in the [-100..100] range 174 // - applied as a component-wise linear transformation (scale+offset), such that 175 // 176 // -100 always yields mid-gray: contrast(x, -100) == 0.5 177 // 0 is the neutral value: contrast(x, 0) == x 178 // 100 always yields white: contrast(x, 100) == 1 179 // 180 // - transfer function: https://www.desmos.com/calculator/x5rxzhowhs 181 // 182 183 // Normalize to [-1..1] 184 const auto brightness = SkTPin(fBrightness, -100.0f, 100.0f) / 255, // [-100/255 .. 100/255] 185 contrast = SkTPin(fContrast , -100.0f, 100.0f) / 100; // [ -1 .. 1] 186 187 // The component scale is derived from contrast: 188 // 189 // Contrast[-1 .. 0] -> Scale[0 .. 1] 190 // Contrast( 0 .. 1] -> Scale(1 .. +inf) 191 const auto S = contrast > 0 192 ? 1 / std::max(1 - contrast, SK_ScalarNearlyZero) 193 : 1 + contrast; 194 195 // The component offset is derived from both brightness and contrast: 196 // 197 // Brightness[-100/255 .. 100/255] -> Offset[-100/255 .. 100/255] 198 // Contrast [ -1 .. 0] -> Offset[ 0.5 .. 0] 199 // Contrast ( 0 .. 1] -> Offset( 0 .. -inf) 200 // 201 // Why do these pre/post compose depending on contrast scale, you ask? 202 // Because AE - that's why! 203 const auto B = 0.5f * (1 - S) + brightness * std::max(S, 1.0f); 204 205 const float cm[] = { 206 S, 0, 0, 0, B, 207 0, S, 0, 0, B, 208 0, 0, S, 0, B, 209 0, 0, 0, 1, 0, 210 }; 211 212 return SkColorFilters::Matrix(cm); 213 } 214 215 sk_sp<SkColorFilter> makeCF() const { 216 const auto brightness = SkTPin(fBrightness, -150.0f, 150.0f) / 150, // [-1.0 .. 1] 217 contrast = SkTPin(fContrast , -50.0f, 100.0f) / 100; // [-0.5 .. 1] 218 219 auto b_eff = SkScalarNearlyZero(brightness) 220 ? nullptr 221 : fBrightnessEffect->makeColorFilter(make_brightness_coeffs(brightness)), 222 c_eff = SkScalarNearlyZero(fContrast) 223 ? nullptr 224 : fContrastEffect->makeColorFilter(make_contrast_coeffs(contrast)); 225 226 return SkColorFilters::Compose(std::move(c_eff), std::move(b_eff)); 227 } 228 229 const sk_sp<SkRuntimeEffect> fBrightnessEffect, 230 fContrastEffect; 231 232 ScalarValue fBrightness = 0, 233 fContrast = 0, 234 fUseLegacy = 0; 235 236 using INHERITED = DiscardableAdapterBase<BrightnessContrastAdapter, sksg::ExternalColorFilter>; 237}; 238 239} // namespace 240 241#endif // SK_ENABLE_SKSL 242 243sk_sp<sksg::RenderNode> EffectBuilder::attachBrightnessContrastEffect( 244 const skjson::ArrayValue& jprops, sk_sp<sksg::RenderNode> layer) const { 245#ifdef SK_ENABLE_SKSL 246 return fBuilder->attachDiscardableAdapter<BrightnessContrastAdapter>(jprops, 247 *fBuilder, 248 std::move(layer)); 249#else 250 // TODO(skia:12197) 251 return layer; 252#endif 253} 254 255} // namespace skottie::internal 256