/* * 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 "include/core/SkBlendMode.h" #include "include/core/SkCanvas.h" #include "include/core/SkColor.h" #include "include/core/SkM44.h" #include "include/core/SkMatrix.h" #include "include/core/SkPaint.h" #include "include/core/SkPicture.h" #include "include/core/SkPictureRecorder.h" #include "include/core/SkPoint.h" #include "include/core/SkRect.h" #include "include/core/SkRefCnt.h" #include "include/core/SkSamplingOptions.h" #include "include/core/SkScalar.h" #include "include/core/SkShader.h" #include "include/core/SkSize.h" #include "include/core/SkString.h" #include "include/core/SkTileMode.h" #include "include/effects/SkRuntimeEffect.h" #include "include/private/base/SkAssert.h" #include "include/private/base/SkTPin.h" #include "modules/skottie/src/Adapter.h" #include "modules/skottie/src/SkottiePriv.h" #include "modules/skottie/src/SkottieValue.h" #include "modules/skottie/src/effects/Effects.h" #include "modules/sksg/include/SkSGNode.h" #include "modules/sksg/include/SkSGRenderNode.h" #include #include #include #include #include namespace skjson { class ArrayValue; } namespace sksg { class InvalidationController; } namespace skottie::internal { namespace { // This shader maps its child shader onto a sphere. To simplify things, we set it up such that: // // - the sphere is centered at origin and has r == 1 // - the eye is positioned at (0,0,eye_z), where eye_z is chosen to visually match AE // - the POI for a given pixel is on the z = 0 plane (x,y,0) // - we're only rendering inside the projected circle, which guarantees a quadratic solution // // Effect stages: // // 1) ray-cast to find the sphere intersection (selectable front/back solution); // given the sphere geometry, this is also the normal // 2) rotate the normal // 3) UV-map the sphere // 4) scale uv to source size and sample // 5) apply lighting model // // Note: the current implementation uses two passes for two-side ("full") rendering, on the // assumption that in practice most textures are opaque and two-side mode is infrequent; // if this proves to be problematic, we could expand the implementation to blend both sides // in one pass. // static constexpr char gSphereSkSL[] = "uniform shader child;" "uniform half3x3 rot_matrix;" "uniform half2 child_scale;" "uniform half side_select;" // apply_light() "%s" "half3 to_sphere(half3 EYE) {" "half eye_z2 = EYE.z*EYE.z;" "half a = dot(EYE, EYE)," "b = -2*eye_z2," "c = eye_z2 - 1," "t = (-b + side_select*sqrt(b*b - 4*a*c))/(2*a);" "return half3(0, 0, -EYE.z) + EYE*t;" "}" "half4 main(float2 xy) {" "half3 EYE = half3(xy, -5.5)," "N = to_sphere(EYE)," "RN = rot_matrix*N;" "half kRPI = 1/3.1415927;" "half2 UV = half2(" "0.5 + kRPI * 0.5 * atan(RN.x, RN.z)," "0.5 + kRPI * asin(RN.y)" ");" "return apply_light(EYE, N, child.eval(UV*child_scale));" "}" ; // CC Sphere uses a Phong-like lighting model: // // - "ambient" controls the intensity of the texture color // - "diffuse" controls a multiplicative mix of texture and light color // - "specular" controls a light color specular component // - "roughness" is the specular exponent reciprocal // - "light intensity" modulates the diffuse and specular components (but not ambient) // - "light height" and "light direction" specify the light source position in spherical coords // // Implementation-wise, light intensity/height/direction are all combined into l_vec. // For efficiency, we fall back to a stripped-down shader (ambient-only) when the diffuse & specular // components are not used. // // TODO: "metal" and "reflective" parameters are ignored. static constexpr char gBasicLightSkSL[] = "uniform half l_coeff_ambient;" "half4 apply_light(half3 EYE, half3 N, half4 c) {" "c.rgb *= l_coeff_ambient;" "return c;" "}" ; static constexpr char gFancyLightSkSL[] = "uniform half3 l_vec;" "uniform half3 l_color;" "uniform half l_coeff_ambient;" "uniform half l_coeff_diffuse;" "uniform half l_coeff_specular;" "uniform half l_specular_exp;" "half4 apply_light(half3 EYE, half3 N, half4 c) {" "half3 LR = reflect(-l_vec*side_select, N);" "half s_base = max(dot(normalize(EYE), LR), 0)," "a = l_coeff_ambient," "d = l_coeff_diffuse * max(dot(l_vec, N), 0)," "s = l_coeff_specular * saturate(pow(s_base, l_specular_exp));" "c.rgb = (a + d*l_color)*c.rgb + s*l_color*c.a;" "return c;" "}" ; static sk_sp sphere_fancylight_effect() { static const SkRuntimeEffect* effect = SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gFancyLightSkSL), {}) .effect.release(); if (0 && !effect) { printf("!!! %s\n", SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gFancyLightSkSL), {}) .errorText.c_str()); } SkASSERT(effect); return sk_ref_sp(effect); } static sk_sp sphere_basiclight_effect() { static const SkRuntimeEffect* effect = SkRuntimeEffect::MakeForShader(SkStringPrintf(gSphereSkSL, gBasicLightSkSL), {}) .effect.release(); SkASSERT(effect); return sk_ref_sp(effect); } class SphereNode final : public sksg::CustomRenderNode { public: SphereNode(sk_sp child, const SkSize& child_size) : INHERITED({std::move(child)}) , fChildSize(child_size) {} enum class RenderSide { kFull, kOutside, kInside, }; SG_ATTRIBUTE(Center , SkPoint , fCenter) SG_ATTRIBUTE(Radius , float , fRadius) SG_ATTRIBUTE(Rotation, SkM44 , fRot ) SG_ATTRIBUTE(Side , RenderSide, fSide ) SG_ATTRIBUTE(LightVec , SkV3 , fLightVec ) SG_ATTRIBUTE(LightColor , SkV3 , fLightColor ) SG_ATTRIBUTE(AmbientLight , float, fAmbientLight ) SG_ATTRIBUTE(DiffuseLight , float, fDiffuseLight ) SG_ATTRIBUTE(SpecularLight, float, fSpecularLight) SG_ATTRIBUTE(SpecularExp , float, fSpecularExp ) private: sk_sp contentShader() { if (!fContentShader || this->hasChildrenInval()) { const auto& child = this->children()[0]; child->revalidate(nullptr, SkMatrix::I()); SkPictureRecorder recorder; child->render(recorder.beginRecording(SkRect::MakeSize(fChildSize))); fContentShader = recorder.finishRecordingAsPicture() ->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkFilterMode::kLinear, nullptr, nullptr); } return fContentShader; } sk_sp buildEffectShader(float selector) { const auto has_fancy_light = fLightVec.length() > 0 && (fDiffuseLight > 0 || fSpecularLight > 0); SkRuntimeShaderBuilder builder(has_fancy_light ? sphere_fancylight_effect() : sphere_basiclight_effect()); builder.child ("child") = this->contentShader(); builder.uniform("child_scale") = fChildSize; builder.uniform("side_select") = selector; builder.uniform("rot_matrix") = std::array{ fRot.rc(0,0), fRot.rc(0,1), fRot.rc(0,2), fRot.rc(1,0), fRot.rc(1,1), fRot.rc(1,2), fRot.rc(2,0), fRot.rc(2,1), fRot.rc(2,2), }; builder.uniform("l_coeff_ambient") = fAmbientLight; if (has_fancy_light) { builder.uniform("l_vec") = fLightVec * -selector; builder.uniform("l_color") = fLightColor; builder.uniform("l_coeff_diffuse") = fDiffuseLight; builder.uniform("l_coeff_specular") = fSpecularLight; builder.uniform("l_specular_exp") = fSpecularExp; } const auto lm = SkMatrix::Translate(fCenter.fX, fCenter.fY) * SkMatrix::Scale(fRadius, fRadius); return builder.makeShader(&lm); } SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override { fSphereShader.reset(); if (fSide != RenderSide::kOutside) { fSphereShader = this->buildEffectShader(1); } if (fSide != RenderSide::kInside) { auto outside = this->buildEffectShader(-1); fSphereShader = fSphereShader ? SkShaders::Blend(SkBlendMode::kSrcOver, std::move(fSphereShader), std::move(outside)) : std::move(outside); } SkASSERT(fSphereShader); return SkRect::MakeLTRB(fCenter.fX - fRadius, fCenter.fY - fRadius, fCenter.fX + fRadius, fCenter.fY + fRadius); } void onRender(SkCanvas* canvas, const RenderContext* ctx) const override { if (fRadius <= 0) { return; } SkPaint sphere_paint; sphere_paint.setAntiAlias(true); sphere_paint.setShader(fSphereShader); canvas->drawCircle(fCenter, fRadius, sphere_paint); } const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; } // no hit-testing const SkSize fChildSize; // Cached shaders sk_sp fSphereShader; sk_sp fContentShader; // Effect controls. SkM44 fRot; SkPoint fCenter = {0,0}; float fRadius = 0; RenderSide fSide = RenderSide::kFull; SkV3 fLightVec = {0,0,1}, fLightColor = {1,1,1}; float fAmbientLight = 1, fDiffuseLight = 0, fSpecularLight = 0, fSpecularExp = 0; using INHERITED = sksg::CustomRenderNode; }; class SphereAdapter final : public DiscardableAdapterBase { public: SphereAdapter(const skjson::ArrayValue& jprops, const AnimationBuilder* abuilder, sk_sp node) : INHERITED(std::move(node)) { enum : size_t { // kRotGrp_Index = 0, kRotX_Index = 1, kRotY_Index = 2, kRotZ_Index = 3, kRotOrder_Index = 4, // ??? = 5, kRadius_Index = 6, kOffset_Index = 7, kRender_Index = 8, // kLight_Index = 9, kLightIntensity_Index = 10, kLightColor_Index = 11, kLightHeight_Index = 12, kLightDirection_Index = 13, // ??? = 14, // kShading_Index = 15, kAmbient_Index = 16, kDiffuse_Index = 17, kSpecular_Index = 18, kRoughness_Index = 19, }; EffectBinder(jprops, *abuilder, this) .bind( kOffset_Index, fOffset ) .bind( kRadius_Index, fRadius ) .bind( kRotX_Index, fRotX ) .bind( kRotY_Index, fRotY ) .bind( kRotZ_Index, fRotZ ) .bind(kRotOrder_Index, fRotOrder) .bind( kRender_Index, fRender ) .bind(kLightIntensity_Index, fLightIntensity) .bind( kLightColor_Index, fLightColor ) .bind( kLightHeight_Index, fLightHeight ) .bind(kLightDirection_Index, fLightDirection) .bind( kAmbient_Index, fAmbient ) .bind( kDiffuse_Index, fDiffuse ) .bind( kSpecular_Index, fSpecular ) .bind( kRoughness_Index, fRoughness ); } private: void onSync() override { const auto side = [](ScalarValue s) { switch (SkScalarRoundToInt(s)) { case 1: return SphereNode::RenderSide::kFull; case 2: return SphereNode::RenderSide::kOutside; case 3: default: return SphereNode::RenderSide::kInside; } SkUNREACHABLE; }; const auto rotation = [](ScalarValue order, ScalarValue x, ScalarValue y, ScalarValue z) { const SkM44 rx = SkM44::Rotate({1,0,0}, SkDegreesToRadians( x)), ry = SkM44::Rotate({0,1,0}, SkDegreesToRadians( y)), rz = SkM44::Rotate({0,0,1}, SkDegreesToRadians(-z)); switch (SkScalarRoundToInt(order)) { case 1: return rx * ry * rz; case 2: return rx * rz * ry; case 3: return ry * rx * rz; case 4: return ry * rz * rx; case 5: return rz * rx * ry; case 6: default: return rz * ry * rx; } SkUNREACHABLE; }; const auto light_vec = [](float height, float direction) { float z = std::sin(height * SK_ScalarPI / 2), r = std::sqrt(1 - z*z), x = std::cos(direction) * r, y = std::sin(direction) * r; return SkV3{x,y,z}; }; const auto& sph = this->node(); sph->setCenter({fOffset.x, fOffset.y}); sph->setRadius(fRadius); sph->setSide(side(fRender)); sph->setRotation(rotation(fRotOrder, fRotX, fRotY, fRotZ)); sph->setAmbientLight (SkTPin(fAmbient * 0.01f, 0.0f, 2.0f)); const auto intensity = SkTPin(fLightIntensity * 0.01f, 0.0f, 10.0f); sph->setDiffuseLight (SkTPin(fDiffuse * 0.01f, 0.0f, 1.0f) * intensity); sph->setSpecularLight(SkTPin(fSpecular* 0.01f, 0.0f, 1.0f) * intensity); sph->setLightVec(light_vec( SkTPin(fLightHeight * 0.01f, -1.0f, 1.0f), SkDegreesToRadians(fLightDirection - 90) )); const auto lc = static_cast(fLightColor); sph->setLightColor({lc.fR, lc.fG, lc.fB}); sph->setSpecularExp(1/SkTPin(fRoughness, 0.001f, 0.5f)); } Vec2Value fOffset = {0,0}; ScalarValue fRadius = 0, fRotX = 0, fRotY = 0, fRotZ = 0, fRotOrder = 1, fRender = 1; ColorValue fLightColor; ScalarValue fLightIntensity = 0, fLightHeight = 0, fLightDirection = 0, fAmbient = 100, fDiffuse = 0, fSpecular = 0, fRoughness = 0.5f; using INHERITED = DiscardableAdapterBase; }; } // namespace sk_sp EffectBuilder::attachSphereEffect( const skjson::ArrayValue& jprops, sk_sp layer) const { auto sphere = sk_make_sp(std::move(layer), fLayerSize); return fBuilder->attachDiscardableAdapter(jprops, fBuilder, std::move(sphere)); } } // namespace skottie::internal