/* * Copyright 2021 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "modules/skottie/src/effects/Effects.h" #include "include/effects/SkRuntimeEffect.h" #include "include/utils/SkRandom.h" #include "modules/skottie/src/Adapter.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottieValue.h" #include "modules/sksg/include/SkSGRenderNode.h" #include namespace skottie::internal { #ifdef SK_ENABLE_SKSL namespace { // An implementation of the ADBE Fractal Noise effect: // // - multiple noise sublayers (octaves) are combined using a weighted average // - each layer is subject to a (cumulative) transform, filter and post-sampling options // // Parameters: // // * Noise Type -- controls noise layer post-sampling filtering // (Block, Linear, Soft Linear, Spline) // * Fractal Type -- determines a noise layer post-filtering transformation // (Basic, Turbulent Smooth, Turbulent Basic, etc) // * Transform -- offset/scale/rotate the noise effect (local matrix) // // * Complexity -- number of sublayers; // can be fractional, where the fractional part modulates the last layer // * Evolution -- controls noise topology in a gradual manner (can be animated for smooth // noise transitions) // * Sub Influence -- relative amplitude weight for sublayers (cumulative) // // * Sub Scaling/Rotation/Offset -- relative scale for sublayers (cumulative) // // * Invert -- invert noise values // // * Contrast -- apply a contrast to the noise result // // * Brightness -- apply a brightness effect to the noise result // // // TODO: // - Invert // - Contrast/Brightness static constexpr char gNoiseEffectSkSL[] = "uniform float3x3 u_submatrix;" // sublayer transform "uniform float2 u_noise_planes;" // noise planes computed based on evolution params "uniform float u_noise_weight," // noise planes lerp weight "u_octaves," // number of octaves (can be fractional) "u_persistence;" // relative octave weight // Hash based on hash13 (https://www.shadertoy.com/view/4djSRW). "float hash(float3 v) {" "v = fract(v*0.1031);" "v += dot(v, v.zxy + 31.32);" "return fract((v.x + v.y)*v.z);" "}" // The general idea is to compute a coherent hash for two planes in discretized (x,y,e) space, // and interpolate between them. This yields gradual changes when animating |e| - which is the // desired outcome. "float sample_noise(float2 xy) {" "xy = floor(xy);" "float n0 = hash(float3(xy, u_noise_planes.x))," "n1 = hash(float3(xy, u_noise_planes.y));" // Note: Ideally we would use 4 samples (-1, 0, 1, 2) and cubic interpolation for // better results -- but that's significantly more expensive than lerp. "return mix(n0, n1, u_noise_weight);" "}" // filter() placeholder "%s" // fractal() placeholder "%s" // Generate ceil(u_octaves) noise layers and combine based on persistentce and sublayer xform. "float4 main(vec2 xy) {" "float oct = u_octaves," // initial octave count (this is the effective loop counter) "amp = 1," // initial layer amplitude "wacc = 0," // weight accumulator "n = 0;" // noise accumulator // Constant loop counter chosen to be >= ceil(u_octaves). // The logical counter is actually 'oct'. "for (float i = 0; i < %u; ++i) {" // effective layer weight computed to accommodate fixed loop counters // // -- for full octaves: layer amplitude // -- for fractional octave: layer amplitude modulated by fractional part // -- for octaves > ceil(u_octaves): 0 // // e.g. for 6 loops and u_octaves = 2.3, this generates the sequence [1,1,.3,0,0] "float w = amp*saturate(oct);" "n += w*fractal(filter(xy));" "wacc += w;" "amp *= u_persistence;" "oct -= 1;" "xy = (u_submatrix*float3(xy,1)).xy;" "}" "n /= wacc;" // TODO: fractal functions "return float4(n,n,n,1);" "}"; static constexpr char gFilterNearestSkSL[] = "float filter(float2 xy) {" "return sample_noise(xy);" "}"; static constexpr char gFilterLinearSkSL[] = "float filter(float2 xy) {" "xy -= 0.5;" "float n00 = sample_noise(xy + float2(0,0))," "n10 = sample_noise(xy + float2(1,0))," "n01 = sample_noise(xy + float2(0,1))," "n11 = sample_noise(xy + float2(1,1));" "float2 t = fract(xy);" "return mix(mix(n00, n10, t.x), mix(n01, n11, t.x), t.y);" "}"; static constexpr char gFilterSoftLinearSkSL[] = "float filter(float2 xy) {" "xy -= 0.5;" "float n00 = sample_noise(xy + float2(0,0))," "n10 = sample_noise(xy + float2(1,0))," "n01 = sample_noise(xy + float2(0,1))," "n11 = sample_noise(xy + float2(1,1));" "float2 t = smoothstep(0, 1, fract(xy));" "return mix(mix(n00, n10, t.x), mix(n01, n11, t.x), t.y);" "}"; static constexpr char gFractalBasicSkSL[] = "float fractal(float n) {" "return n;" "}"; static constexpr char gFractalTurbulentBasicSkSL[] = "float fractal(float n) {" "return 2*abs(0.5 - n);" "}"; static constexpr char gFractalTurbulentSmoothSkSL[] = "float fractal(float n) {" "n = 2*abs(0.5 - n);" "return n*n;" "}"; static constexpr char gFractalTurbulentSharpSkSL[] = "float fractal(float n) {" "return sqrt(2*abs(0.5 - n));" "}"; enum class NoiseFilter { kNearest, kLinear, kSoftLinear, // TODO: kSpline? }; enum class NoiseFractal { kBasic, kTurbulentBasic, kTurbulentSmooth, kTurbulentSharp, }; sk_sp make_noise_effect(unsigned loops, const char* filter, const char* fractal) { auto result = SkRuntimeEffect::MakeForShader( SkStringPrintf(gNoiseEffectSkSL, filter, fractal, loops), {}); if (0 && !result.effect) { printf("!!! %s\n", result.errorText.c_str()); } return std::move(result.effect); } template sk_sp noise_effect() { static constexpr char const* gFilters[] = { gFilterNearestSkSL, gFilterLinearSkSL, gFilterSoftLinearSkSL }; static constexpr char const* gFractals[] = { gFractalBasicSkSL, gFractalTurbulentBasicSkSL, gFractalTurbulentSmoothSkSL, gFractalTurbulentSharpSkSL }; static_assert(static_cast(FILTER) < SK_ARRAY_COUNT(gFilters)); static_assert(static_cast(FRACTAL) < SK_ARRAY_COUNT(gFractals)); static const SkRuntimeEffect* effect = make_noise_effect(LOOPS, gFilters[static_cast(FILTER)], gFractals[static_cast(FRACTAL)]) .release(); SkASSERT(effect); return sk_ref_sp(effect); } class FractalNoiseNode final : public sksg::CustomRenderNode { public: explicit FractalNoiseNode(sk_sp child) : INHERITED({std::move(child)}) {} SG_ATTRIBUTE(Matrix , SkMatrix , fMatrix ) SG_ATTRIBUTE(SubMatrix , SkMatrix , fSubMatrix ) SG_ATTRIBUTE(NoiseFilter , NoiseFilter , fFilter ) SG_ATTRIBUTE(NoiseFractal , NoiseFractal, fFractal ) SG_ATTRIBUTE(NoisePlanes , SkV2 , fNoisePlanes ) SG_ATTRIBUTE(NoiseWeight , float , fNoiseWeight ) SG_ATTRIBUTE(Octaves , float , fOctaves ) SG_ATTRIBUTE(Persistence , float , fPersistence ) private: template sk_sp getEffect() const { // Bin the loop counter based on the number of octaves (range: [1..20]). // Low complexities are common, so we maximize resolution for the low end. if (fOctaves > 8) return noise_effect<20, FI, FR>(); if (fOctaves > 4) return noise_effect< 8, FI, FR>(); if (fOctaves > 3) return noise_effect< 4, FI, FR>(); if (fOctaves > 2) return noise_effect< 3, FI, FR>(); if (fOctaves > 1) return noise_effect< 2, FI, FR>(); return noise_effect<1, FI, FR>(); } template sk_sp getEffect() const { switch (fFractal) { case NoiseFractal::kBasic: return this->getEffect(); case NoiseFractal::kTurbulentBasic: return this->getEffect(); case NoiseFractal::kTurbulentSmooth: return this->getEffect(); case NoiseFractal::kTurbulentSharp: return this->getEffect(); } SkUNREACHABLE; } sk_sp getEffect() const { switch (fFilter) { case NoiseFilter::kNearest : return this->getEffect(); case NoiseFilter::kLinear : return this->getEffect(); case NoiseFilter::kSoftLinear: return this->getEffect(); } SkUNREACHABLE; } sk_sp buildEffectShader() const { SkRuntimeShaderBuilder builder(this->getEffect()); builder.uniform("u_noise_planes") = fNoisePlanes; builder.uniform("u_noise_weight") = fNoiseWeight; builder.uniform("u_octaves" ) = fOctaves; builder.uniform("u_persistence" ) = fPersistence; builder.uniform("u_submatrix" ) = std::array{ fSubMatrix.rc(0,0), fSubMatrix.rc(1,0), fSubMatrix.rc(2,0), fSubMatrix.rc(0,1), fSubMatrix.rc(1,1), fSubMatrix.rc(2,1), fSubMatrix.rc(0,2), fSubMatrix.rc(1,2), fSubMatrix.rc(2,2), }; return builder.makeShader(&fMatrix, false); } SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override { const auto& child = this->children()[0]; const auto bounds = child->revalidate(ic, ctm); fEffectShader = this->buildEffectShader(); return bounds; } void onRender(SkCanvas* canvas, const RenderContext* ctx) const override { const auto& bounds = this->bounds(); const auto local_ctx = ScopedRenderContext(canvas, ctx) .setIsolation(bounds, canvas->getTotalMatrix(), true); canvas->saveLayer(&bounds, nullptr); this->children()[0]->render(canvas, local_ctx); SkPaint effect_paint; effect_paint.setShader(fEffectShader); effect_paint.setBlendMode(SkBlendMode::kSrcIn); canvas->drawPaint(effect_paint); } const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; } // no hit-testing sk_sp fEffectShader; SkMatrix fMatrix, fSubMatrix; NoiseFilter fFilter = NoiseFilter::kNearest; NoiseFractal fFractal = NoiseFractal::kBasic; SkV2 fNoisePlanes = {0,0}; float fNoiseWeight = 0, fOctaves = 1, fPersistence = 1; using INHERITED = sksg::CustomRenderNode; }; class FractalNoiseAdapter final : public DiscardableAdapterBase { public: FractalNoiseAdapter(const skjson::ArrayValue& jprops, const AnimationBuilder* abuilder, sk_sp node) : INHERITED(std::move(node)) { EffectBinder(jprops, *abuilder, this) .bind( 0, fFractalType ) .bind( 1, fNoiseType ) .bind( 2, fInvert ) .bind( 3, fContrast ) .bind( 4, fBrightness ) // 5 -- overflow // 6 -- transform begin-group .bind( 7, fRotation ) .bind( 8, fUniformScaling ) .bind( 9, fScale ) .bind(10, fScaleWidth ) .bind(11, fScaleHeight ) .bind(12, fOffset ) // 13 -- TODO: perspective offset // 14 -- transform end-group .bind(15, fComplexity ) // 16 -- sub settings begin-group .bind(17, fSubInfluence ) .bind(18, fSubScale ) .bind(19, fSubRotation ) .bind(20, fSubOffset ) // 21 -- center subscale // 22 -- sub settings end-group .bind(23, fEvolution ) // 24 -- evolution options begin-group .bind(25, fCycleEvolution ) .bind(26, fCycleRevolutions) .bind(27, fRandomSeed ) // 28 -- evolution options end-group .bind(29, fOpacity ); // 30 -- TODO: blending mode } private: std::tuple noise() const { // Constant chosen to visually match AE's evolution rate. static constexpr auto kEvolutionScale = 0.25f; // Evolution inputs: // // * evolution - main evolution control (degrees) // * cycle evolution - flag controlling whether evolution cycles // * cycle revolutions - number of revolutions after which evolution cycles (period) // * random seed - determines an arbitrary starting plane (evolution offset) // // The shader uses evolution floor/ceil to select two noise planes, and the fractional part // to interpolate between the two -> in order to wrap around smoothly, the cycle/period // must be integral. const float evo_rad = SkDegreesToRadians(fEvolution), rev_rad = std::max(fCycleRevolutions, 1.0f)*SK_FloatPI*2, cycle = fCycleEvolution ? SkScalarRoundToScalar(rev_rad*kEvolutionScale) : SK_ScalarMax, // Adjust scale when cycling to ensure an integral period (post scaling). scale = fCycleEvolution ? cycle/rev_rad : kEvolutionScale, offset = SkRandom(static_cast(fRandomSeed)).nextRangeU(0, 100), evo = evo_rad*scale, evo_ = std::floor(evo), weight = evo - evo_; // We want the GLSL mod() flavor. auto glsl_mod = [](float x, float y) { return x - y*std::floor(x/y); }; const SkV2 noise_planes = { glsl_mod(evo_ + 0, cycle) + offset, glsl_mod(evo_ + 1, cycle) + offset, }; return std::make_tuple(noise_planes, weight); } SkMatrix shaderMatrix() const { static constexpr float kGridSize = 64; const auto scale = (SkScalarRoundToInt(fUniformScaling) == 1) ? SkV2{fScale, fScale} : SkV2{fScaleWidth, fScaleHeight}; return SkMatrix::Translate(fOffset.x, fOffset.y) * SkMatrix::Scale(SkTPin(scale.x, 1.0f, 10000.0f) * 0.01f, SkTPin(scale.y, 1.0f, 10000.0f) * 0.01f) * SkMatrix::RotateDeg(fRotation) * SkMatrix::Scale(kGridSize, kGridSize); } SkMatrix subMatrix() const { const auto scale = 100 / SkTPin(fSubScale, 10.0f, 10000.0f); return SkMatrix::Translate(-fSubOffset.x * 0.01f, -fSubOffset.y * 0.01f) * SkMatrix::RotateDeg(-fSubRotation) * SkMatrix::Scale(scale, scale); } NoiseFilter noiseFilter() const { switch (SkScalarRoundToInt(fNoiseType)) { case 1: return NoiseFilter::kNearest; case 2: return NoiseFilter::kLinear; default: return NoiseFilter::kSoftLinear; } SkUNREACHABLE; } NoiseFractal noiseFractal() const { switch (SkScalarRoundToInt(fFractalType)) { case 1: return NoiseFractal::kBasic; case 3: return NoiseFractal::kTurbulentSmooth; case 4: return NoiseFractal::kTurbulentBasic; default: return NoiseFractal::kTurbulentSharp; } SkUNREACHABLE; } void onSync() override { const auto& n = this->node(); const auto [noise_planes, noise_weight] = this->noise(); n->setOctaves(SkTPin(fComplexity, 1.0f, 20.0f)); n->setPersistence(SkTPin(fSubInfluence * 0.01f, 0.0f, 100.0f)); n->setNoisePlanes(noise_planes); n->setNoiseWeight(noise_weight); n->setNoiseFilter(this->noiseFilter()); n->setNoiseFractal(this->noiseFractal()); n->setMatrix(this->shaderMatrix()); n->setSubMatrix(this->subMatrix()); } Vec2Value fOffset = {0,0}, fSubOffset = {0,0}; ScalarValue fFractalType = 0, fNoiseType = 0, fRotation = 0, fUniformScaling = 0, fScale = 100, // used when uniform scaling is selected fScaleWidth = 100, // used when uniform scaling is not selected fScaleHeight = 100, // ^ fComplexity = 1, fSubInfluence = 100, fSubScale = 50, fSubRotation = 0, fEvolution = 0, fCycleEvolution = 0, fCycleRevolutions = 0, fRandomSeed = 0, fOpacity = 100, // TODO fInvert = 0, // TODO fContrast = 100, // TODO fBrightness = 0; // TODO using INHERITED = DiscardableAdapterBase; }; } // namespace #endif // SK_ENABLE_SKSL sk_sp EffectBuilder::attachFractalNoiseEffect( const skjson::ArrayValue& jprops, sk_sp layer) const { #ifdef SK_ENABLE_SKSL auto fractal_noise = sk_make_sp(std::move(layer)); return fBuilder->attachDiscardableAdapter(jprops, fBuilder, std::move(fractal_noise)); #else // TODO(skia:12197) return layer; #endif } } // namespace skottie::internal