[skottie] Initial camera support

Camera layers introduce a top-level 3d camera/view matrix based on their
transform properties:

  * position - camera location
  * point of interest (stored as anchor point by BM) - camera direction
  * rotation - camera orientation

The perspective degree is controlled by a "zoom" camera property (which
corresponds to the view distance), and the composition dimensions.

Current limitations:

  * single camera track/layer
  * affects all layers (not just 3d-tagged layers)
  * parent layer transforms are likely not applied correctly

Bug: skia:
Change-Id: Ifc1b8b699ff09fa13b4804d18546b444d02e81c2
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/201651
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index ebdd7ed..d615611 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -117,12 +117,15 @@
 
 sk_sp<sksg::Transform> AnimationBuilder::attachMatrix3D(const skjson::ObjectValue& t,
                                                         AnimatorScope* ascope,
-                                                        sk_sp<sksg::Transform> parent) const {
+                                                        sk_sp<sksg::Transform> parent,
+                                                        sk_sp<TransformAdapter3D> adapter) const {
     static const VectorValue g_default_vec_0   = {  0,   0,   0},
                              g_default_vec_100 = {100, 100, 100};
 
-    auto matrix = sksg::Matrix<SkMatrix44>::Make(SkMatrix::I());
-    auto adapter = sk_make_sp<TransformAdapter3D>(matrix);
+    if (!adapter) {
+        // Default to TransformAdapter3D (we only use external adapters for cameras).
+        adapter = sk_make_sp<TransformAdapter3D>();
+    }
 
     auto bound = this->bindProperty<VectorValue>(t["a"], ascope,
             [adapter](const VectorValue& a) {
@@ -165,7 +168,7 @@
     // TODO: dispatch 3D transform properties
 
     return (bound)
-        ? sksg::Transform::MakeConcat(std::move(parent), std::move(matrix))
+        ? sksg::Transform::MakeConcat(std::move(parent), adapter->refTransform())
         : parent;
 }
 
@@ -266,13 +269,14 @@
                                    sk_sp<PropertyObserver> pobserver, sk_sp<Logger> logger,
                                    sk_sp<MarkerObserver> mobserver,
                                    Animation::Builder::Stats* stats,
-                                   float duration, float framerate)
+                                   const SkSize& size, float duration, float framerate)
     : fResourceProvider(std::move(rp))
     , fLazyFontMgr(std::move(fontmgr))
     , fPropertyObserver(std::move(pobserver))
     , fLogger(std::move(logger))
     , fMarkerObserver(std::move(mobserver))
     , fStats(stats)
+    , fSize(size)
     , fDuration(duration)
     , fFrameRate(framerate)
     , fHasNontrivialBlending(false) {}
@@ -499,7 +503,7 @@
                                        std::move(fPropertyObserver),
                                        std::move(fLogger),
                                        std::move(fMarkerObserver),
-                                       &fStats, duration, fps);
+                                       &fStats, size, duration, fps);
     auto scene = builder.parse(json);
 
     const auto t2 = std::chrono::steady_clock::now();
diff --git a/modules/skottie/src/SkottieAdapter.cpp b/modules/skottie/src/SkottieAdapter.cpp
index 1abfebc..ea69f44 100644
--- a/modules/skottie/src/SkottieAdapter.cpp
+++ b/modules/skottie/src/SkottieAdapter.cpp
@@ -7,6 +7,7 @@
 
 #include "SkottieAdapter.h"
 
+#include "Sk3D.h"
 #include "SkFont.h"
 #include "SkMatrix.h"
 #include "SkMatrix44.h"
@@ -75,11 +76,15 @@
     fZ = v.size() > 2 ? v[2] : 0;
 }
 
-TransformAdapter3D::TransformAdapter3D(sk_sp<sksg::Matrix<SkMatrix44>> matrix)
-    : fMatrixNode(std::move(matrix)) {}
+TransformAdapter3D::TransformAdapter3D()
+    : fMatrixNode(sksg::Matrix<SkMatrix44>::Make(SkMatrix::I())) {}
 
 TransformAdapter3D::~TransformAdapter3D() = default;
 
+sk_sp<sksg::Transform> TransformAdapter3D::refTransform() const {
+    return fMatrixNode;
+}
+
 SkMatrix44 TransformAdapter3D::totalMatrix() const {
     SkMatrix44 t;
 
@@ -104,6 +109,62 @@
     fMatrixNode->setMatrix(this->totalMatrix());
 }
 
+CameraAdapter:: CameraAdapter(const SkSize& viewport_size)
+    : fViewportSize(viewport_size) {}
+
+CameraAdapter::~CameraAdapter() = default;
+
+SkMatrix44 CameraAdapter::totalMatrix() const {
+    // Camera parameters:
+    //
+    //   * location          -> position attribute
+    //   * point of interest -> anchor point attribute
+    //   * orientation       -> rotation attribute
+    //
+    // Note: the orientation is specified post position/POI adjustment.
+    //
+    SkPoint3 pos = { this->getPosition().fX,
+                     this->getPosition().fY,
+                    -this->getPosition().fZ },
+             poi = { this->getAnchorPoint().fX,
+                     this->getAnchorPoint().fY,
+                    -this->getAnchorPoint().fZ },
+              up = { 0, 1, 0 };
+
+    SkMatrix44 cam_t;
+    Sk3LookAt(&cam_t, pos, poi, up);
+
+    {
+        SkMatrix44 rot;
+        rot.setRotateDegreesAbout(1, 0, 0, this->getRotation().fX);
+        cam_t.postConcat(rot);
+        rot.setRotateDegreesAbout(0, 1, 0, this->getRotation().fY);
+        cam_t.postConcat(rot);
+        rot.setRotateDegreesAbout(0, 0, 1, this->getRotation().fZ);
+        cam_t.postConcat(rot);
+    }
+
+    // View parameters:
+    //
+    //   * size     -> composition size (TODO: AE seems to base it on width only?)
+    //   * distance -> "zoom" camera attribute
+    //
+    const auto view_size     = SkTMax(fViewportSize.width(), fViewportSize.height()),
+               view_distance = this->getZoom(),
+               view_angle    = std::atan(view_size * 0.5f / view_distance);
+
+    SkMatrix44 view_t;
+    Sk3Perspective(&view_t, 0, view_distance, 2 * view_angle);
+    view_t.postScale(view_size * 0.5f, view_size * 0.5f, 1);
+
+    SkMatrix44 t;
+    t.setTranslate(fViewportSize.width() * 0.5f, fViewportSize.height() * 0.5f, 0);
+    t.preConcat(view_t);
+    t.preConcat(cam_t);
+
+    return t;
+}
+
 RepeaterAdapter::RepeaterAdapter(sk_sp<sksg::RenderNode> repeater_node, Composite composite)
     : fRepeaterNode(repeater_node)
     , fComposite(composite)
diff --git a/modules/skottie/src/SkottieAdapter.h b/modules/skottie/src/SkottieAdapter.h
index 1138eb6..6452d8d 100644
--- a/modules/skottie/src/SkottieAdapter.h
+++ b/modules/skottie/src/SkottieAdapter.h
@@ -29,11 +29,16 @@
 class RenderNode;
 class RRect;
 class TextBlob;
+class Transform;
 class TransformEffect;
 class TrimEffect;
 
 };
 
+namespace skjson {
+    class ObjectValue;
+}
+
 namespace skottie {
 
 #define ADAPTER_PROPERTY(p_name, p_type, p_default) \
@@ -108,10 +113,10 @@
     sk_sp<sksg::Matrix<SkMatrix>> fMatrixNode;
 };
 
-class TransformAdapter3D final : public SkNVRefCnt<TransformAdapter3D> {
+class TransformAdapter3D : public SkRefCnt {
 public:
-    explicit TransformAdapter3D(sk_sp<sksg::Matrix<SkMatrix44>>);
-    ~TransformAdapter3D();
+    TransformAdapter3D();
+    ~TransformAdapter3D() override;
 
     struct Vec3 {
         float fX, fY, fZ;
@@ -129,12 +134,32 @@
     ADAPTER_PROPERTY(Rotation   , Vec3, Vec3({  0,   0,   0}))
     ADAPTER_PROPERTY(Scale      , Vec3, Vec3({100, 100, 100}))
 
-    SkMatrix44 totalMatrix() const;
+    sk_sp<sksg::Transform> refTransform() const;
 
-private:
+protected:
     void apply();
 
+private:
+    virtual SkMatrix44 totalMatrix() const;
+
     sk_sp<sksg::Matrix<SkMatrix44>> fMatrixNode;
+
+    using INHERITED = SkRefCnt;
+};
+
+class CameraAdapter final : public TransformAdapter3D {
+public:
+    explicit CameraAdapter(const SkSize& viewport_size);
+    ~CameraAdapter() override;
+
+    ADAPTER_PROPERTY(Zoom, SkScalar, 0)
+
+private:
+    SkMatrix44 totalMatrix() const override;
+
+    const SkSize fViewportSize;
+
+    using INHERITED = TransformAdapter3D;
 };
 
 class RepeaterAdapter final : public SkNVRefCnt<RepeaterAdapter> {
diff --git a/modules/skottie/src/SkottieLayer.cpp b/modules/skottie/src/SkottieLayer.cpp
index 5cf304b..399f396 100644
--- a/modules/skottie/src/SkottieLayer.cpp
+++ b/modules/skottie/src/SkottieLayer.cpp
@@ -12,6 +12,7 @@
 #include "SkImage.h"
 #include "SkJSON.h"
 #include "SkMakeUnique.h"
+#include "SkottieAdapter.h"
 #include "SkottieJson.h"
 #include "SkottieValue.h"
 #include "SkParse.h"
@@ -171,6 +172,8 @@
     return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode));
 }
 
+static constexpr int kCameraLayerType = 13;
+
 } // namespace
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachNestedAnimation(const char* name,
@@ -406,9 +409,13 @@
     AnimatorScope*                          fScope;
     SkTHashMap<int, sk_sp<sksg::Transform>> fLayerMatrixMap;
     sk_sp<sksg::RenderNode>                 fCurrentMatte;
+    sk_sp<sksg::Transform>                  fCameraTransform;
+
+    enum class TransformType { kLayer, kCamera };
 
     sk_sp<sksg::Transform> attachLayerTransform(const skjson::ObjectValue& jlayer,
-                                                const AnimationBuilder* abuilder) {
+                                                const AnimationBuilder* abuilder,
+                                                TransformType type = TransformType::kLayer) {
         const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
         if (layer_index < 0)
             return nullptr;
@@ -416,7 +423,7 @@
         if (auto* m = fLayerMatrixMap.find(layer_index))
             return *m;
 
-        return this->attachLayerTransformImpl(jlayer, abuilder, layer_index);
+        return this->attachLayerTransformImpl(jlayer, abuilder, type, layer_index);
     }
 
 private:
@@ -434,16 +441,47 @@
             if (!l) continue;
 
             if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
-                return this->attachLayerTransformImpl(*l, abuilder, parent_index);
+                const auto parent_type = ParseDefault<int>((*l)["ty"], -1) == kCameraLayerType
+                        ? TransformType::kCamera
+                        : TransformType::kLayer;
+                return this->attachLayerTransformImpl(*l, abuilder, parent_type, parent_index);
             }
         }
 
         return nullptr;
     }
 
+    sk_sp<sksg::Transform> attachTransformNode(const skjson::ObjectValue& jlayer,
+                                               const AnimationBuilder* abuilder,
+                                               sk_sp<sksg::Transform> parent_transform,
+                                               TransformType type) const {
+        const skjson::ObjectValue* jtransform = jlayer["ks"];
+        if (!jtransform) {
+            return nullptr;
+        }
+
+        if (type == TransformType::kCamera) {
+            auto camera_adapter = sk_make_sp<CameraAdapter>(abuilder->fSize);
+
+            abuilder->bindProperty<ScalarValue>(jlayer["pe"], fScope,
+                [camera_adapter] (const ScalarValue& pe) {
+                    // 'pe' (perspective?) corresponds to AE's "zoom" camera property.
+                    camera_adapter->setZoom(pe);
+                });
+
+            return abuilder->attachMatrix3D(*jtransform, fScope,
+                                            std::move(parent_transform),
+                                            std::move(camera_adapter));
+        }
+
+        return (ParseDefault<int>(jlayer["ddd"], 0) == 0)
+                ? abuilder->attachMatrix2D(*jtransform, fScope, std::move(parent_transform))
+                : abuilder->attachMatrix3D(*jtransform, fScope, std::move(parent_transform));
+    }
+
     sk_sp<sksg::Transform> attachLayerTransformImpl(const skjson::ObjectValue& jlayer,
                                                     const AnimationBuilder* abuilder,
-                                                    int layer_index) {
+                                                    TransformType type, int layer_index) {
         SkASSERT(!fLayerMatrixMap.find(layer_index));
 
         // Add a stub entry to break recursion cycles.
@@ -451,14 +489,10 @@
 
         auto parent_matrix = this->attachParentLayerTransform(jlayer, abuilder, layer_index);
 
-        if (const skjson::ObjectValue* jtransform = jlayer["ks"]) {
-            auto transform_node = (ParseDefault<int>(jlayer["ddd"], 0) == 0)
-                ? abuilder->attachMatrix2D(*jtransform, fScope, std::move(parent_matrix))
-                : abuilder->attachMatrix3D(*jtransform, fScope, std::move(parent_matrix));
-
-            return *fLayerMatrixMap.set(layer_index, std::move(transform_node));
-        }
-        return nullptr;
+        return *fLayerMatrixMap.set(layer_index, this->attachTransformNode(jlayer,
+                                                                           abuilder,
+                                                                           std::move(parent_matrix),
+                                                                           type));
     }
 };
 
@@ -490,7 +524,21 @@
         &AnimationBuilder::attachTextLayer,     // 'ty': 5
     };
 
-    int type = ParseDefault<int>((*jlayer)["ty"], -1);
+    const auto type = ParseDefault<int>((*jlayer)["ty"], -1);
+
+    if (type == kCameraLayerType) {
+        // Camera layers are special: they don't build normal SG fragments, but drive a root-level
+        // transform.
+        if (layerCtx->fCameraTransform) {
+            this->log(Logger::Level::kWarning, jlayer, "Ignoring duplicate camera layer.");
+        } else {
+            layerCtx->fCameraTransform =
+                    layerCtx->attachLayerTransform(*jlayer, this,
+                                                   AttachLayerContext::TransformType::kCamera);
+        }
+        return nullptr;
+    }
+
     if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerAttachers))) {
         return nullptr;
     }
@@ -593,9 +641,9 @@
     return std::move(controller_node);
 }
 
-sk_sp<sksg::RenderNode> AnimationBuilder::attachComposition(const skjson::ObjectValue& comp,
+sk_sp<sksg::RenderNode> AnimationBuilder::attachComposition(const skjson::ObjectValue& jcomp,
                                                             AnimatorScope* scope) const {
-    const skjson::ArrayValue* jlayers = comp["layers"];
+    const skjson::ArrayValue* jlayers = jcomp["layers"];
     if (!jlayers) return nullptr;
 
     std::vector<sk_sp<sksg::RenderNode>> layers;
@@ -612,11 +660,22 @@
         return nullptr;
     }
 
-    // Layers are painted in bottom->top order.
-    std::reverse(layers.begin(), layers.end());
-    layers.shrink_to_fit();
+    sk_sp<sksg::RenderNode> comp;
+    if (layers.size() == 1) {
+        comp = std::move(layers[0]);
+    } else {
+        // Layers are painted in bottom->top order.
+        std::reverse(layers.begin(), layers.end());
+        layers.shrink_to_fit();
+        comp = sksg::Group::Make(std::move(layers));
+    }
 
-    return sksg::Group::Make(std::move(layers));
+    // Optional camera.
+    if (layerCtx.fCameraTransform) {
+        comp = sksg::TransformEffect::Make(std::move(comp), std::move(layerCtx.fCameraTransform));
+    }
+
+    return comp;
 }
 
 } // namespace internal
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 7736912..979f694 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -37,6 +37,9 @@
 
 namespace skottie {
 
+class TransformAdapter2D;
+class TransformAdapter3D;
+
 namespace internal {
 
 using AnimatorScope = sksg::AnimatorList;
@@ -45,7 +48,8 @@
 public:
     AnimationBuilder(sk_sp<ResourceProvider>, sk_sp<SkFontMgr>, sk_sp<PropertyObserver>,
                      sk_sp<Logger>, sk_sp<MarkerObserver>,
-                     Animation::Builder::Stats*, float duration, float framerate);
+                     Animation::Builder::Stats*, const SkSize& size,
+                     float duration, float framerate);
 
     std::unique_ptr<sksg::Scene> parse(const skjson::ObjectValue&);
 
@@ -74,7 +78,8 @@
     sk_sp<sksg::Transform> attachMatrix2D(const skjson::ObjectValue&, AnimatorScope*,
                                           sk_sp<sksg::Transform>) const;
     sk_sp<sksg::Transform> attachMatrix3D(const skjson::ObjectValue&, AnimatorScope*,
-                                          sk_sp<sksg::Transform>) const;
+                                          sk_sp<sksg::Transform>,
+                                          sk_sp<TransformAdapter3D> = nullptr) const;
     sk_sp<sksg::RenderNode> attachOpacity(const skjson::ObjectValue&, AnimatorScope*,
                                       sk_sp<sksg::RenderNode>) const;
     sk_sp<sksg::Path> attachPath(const skjson::Value&, AnimatorScope*) const;
@@ -174,6 +179,7 @@
     sk_sp<Logger>              fLogger;
     sk_sp<MarkerObserver>      fMarkerObserver;
     Animation::Builder::Stats* fStats;
+    const SkSize               fSize;
     const float                fDuration,
                                fFrameRate;
     mutable const char*        fPropertyObserverContext;