Add perspective shadows

Bug: skia:
Change-Id: I1972f85f593828c982ea08143e1ed7eb70345eaa
Reviewed-on: https://skia-review.googlesource.com/10296
Commit-Queue: Jim Van Verth <jvanverth@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
diff --git a/include/utils/SkCamera.h b/include/utils/SkCamera.h
index 4b77ec6..3cb13fc 100644
--- a/include/utils/SkCamera.h
+++ b/include/utils/SkCamera.h
@@ -103,10 +103,10 @@
     void update();
     void patchToMatrix(const SkPatch3D&, SkMatrix* matrix) const;
 
-    SkPoint3D   fLocation;
-    SkPoint3D   fAxis;
-    SkPoint3D   fZenith;
-    SkPoint3D   fObserver;
+    SkPoint3D   fLocation;   // origin of the camera's space
+    SkPoint3D   fAxis;       // view direction
+    SkPoint3D   fZenith;     // up direction
+    SkPoint3D   fObserver;   // eye position (may not be the same as the origin)
 
 private:
     mutable SkMatrix    fOrientation;
diff --git a/samplecode/SampleAndroidShadows.cpp b/samplecode/SampleAndroidShadows.cpp
index bb56b7e..0c0baab 100644
--- a/samplecode/SampleAndroidShadows.cpp
+++ b/samplecode/SampleAndroidShadows.cpp
@@ -37,6 +37,7 @@
     SkPoint3  fLightPos;
     SkScalar  fZDelta;
     SkScalar  fAnimTranslate;
+    SkScalar  fAnimAngle;
 
     bool      fShowAmbient;
     bool      fShowSpot;
@@ -48,6 +49,7 @@
     ShadowsView()
         : fZDelta(0)
         , fAnimTranslate(0)
+        , fAnimAngle(0)
         , fShowAmbient(true)
         , fShowSpot(true)
         , fUseAlt(true)
@@ -403,10 +405,12 @@
         canvas->drawRRect(shadowRRect, paint);
     }
 
-    void drawShadowedPath(SkCanvas* canvas, const SkPath& path, SkScalar zValue,
+    void drawShadowedPath(SkCanvas* canvas, const SkPath& path,
+                          std::function<SkScalar(SkScalar, SkScalar)> zFunc,
                           const SkPaint& paint, SkScalar ambientAlpha,
                           const SkPoint3& lightPos, SkScalar lightWidth, SkScalar spotAlpha) {
 #ifdef USE_SHADOW_UTILS
+        SkScalar zValue = zFunc(0, 0);
         if (fUseAlt) {
             if (fShowAmbient) {
                 this->drawAmbientShadowAlt(canvas, path, zValue, ambientAlpha);
@@ -422,14 +426,13 @@
                 spotAlpha = 0;
             }
 
-            SkShadowUtils::DrawShadow(canvas, path,
-                                      zValue,
-                                      lightPos, lightWidth,
-                                      ambientAlpha, spotAlpha, SK_ColorBLACK);
-            //SkShadowUtils::DrawUncachedShadow(canvas, path,
-            //                                  [zValue](SkScalar, SkScalar) { return zValue; },
-            //                                  lightPos, lightWidth,
-            //                                  ambientAlpha, spotAlpha, SK_ColorBLACK);
+            //SkShadowUtils::DrawShadow(canvas, path,
+            //                          zValue,
+            //                          lightPos, lightWidth,
+            //                          ambientAlpha, spotAlpha, SK_ColorBLACK);
+            SkShadowUtils::DrawUncachedShadow(canvas, path, zFunc,
+                                              lightPos, lightWidth,
+                                              ambientAlpha, spotAlpha, SK_ColorBLACK);
         }
 #else
         if (fShowAmbient) {
@@ -475,39 +478,52 @@
         canvas->translate(200, 90);
         lightPos.fX += 200;
         lightPos.fY += 90;
-        this->drawShadowedPath(canvas, fRRPath, SkTMax(1.0f, 2+fZDelta), paint, kAmbientAlpha,
+        SkScalar zValue = SkTMax(1.0f, 2 + fZDelta);
+        std::function<SkScalar(SkScalar, SkScalar)> zFunc = 
+            [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fRRPath, zFunc, paint, kAmbientAlpha,
                                lightPos, kLightWidth, kSpotAlpha);
 
         paint.setColor(SK_ColorRED);
         canvas->translate(250, 0);
         lightPos.fX += 250;
-        this->drawShadowedPath(canvas, fRectPath, SkTMax(1.0f, 4+fZDelta), paint, kAmbientAlpha,
+        zValue = SkTMax(1.0f, 4 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fRectPath, zFunc, paint, kAmbientAlpha,
                                lightPos, kLightWidth, kSpotAlpha);
 
         paint.setColor(SK_ColorBLUE);
         canvas->translate(-250, 110);
         lightPos.fX -= 250;
         lightPos.fY += 110;
-        this->drawShadowedPath(canvas, fCirclePath, SkTMax(1.0f, 8+fZDelta), paint, kAmbientAlpha,
+        zValue = SkTMax(1.0f, 8 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fCirclePath, zFunc, paint, kAmbientAlpha,
                                lightPos, kLightWidth, 0.5f);
 
         paint.setColor(SK_ColorGREEN);
         canvas->translate(250, 0);
         lightPos.fX += 250;
-        this->drawShadowedPath(canvas, fRRPath, SkTMax(1.0f, 64+fZDelta), paint, kAmbientAlpha,
+        zValue = SkTMax(1.0f, 64 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fRRPath, zFunc, paint, kAmbientAlpha,
                                lightPos, kLightWidth, kSpotAlpha);
 
         paint.setColor(SK_ColorYELLOW);
         canvas->translate(-250, 110);
         lightPos.fX -= 250;
         lightPos.fY += 110;
-        this->drawShadowedPath(canvas, fFunkyRRPath, SkTMax(1.0f, 8+fZDelta), paint, kAmbientAlpha,
+        zValue = SkTMax(1.0f, 8 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fFunkyRRPath, zFunc, paint, kAmbientAlpha,
                                lightPos, kLightWidth, kSpotAlpha);
 
         paint.setColor(SK_ColorCYAN);
         canvas->translate(250, 0);
         lightPos.fX += 250;
-        this->drawShadowedPath(canvas, fCubicPath, SkTMax(1.0f, 16 + fZDelta), paint,
+        zValue = SkTMax(1.0f, 16 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, fCubicPath, zFunc, paint,
                                kAmbientAlpha, lightPos, kLightWidth, kSpotAlpha);
 
         // circular reveal
@@ -520,17 +536,19 @@
         canvas->translate(-125, 60);
         lightPos.fX -= 125;
         lightPos.fY += 60;
-        this->drawShadowedPath(canvas, tmpPath, SkTMax(1.0f, 32 + fZDelta), paint, .1f,
+        zValue = SkTMax(1.0f, 32 + fZDelta);
+        zFunc = [zValue](SkScalar, SkScalar) { return zValue; };
+        this->drawShadowedPath(canvas, tmpPath, zFunc, paint, .1f,
                                lightPos, kLightWidth, .5f);
 
         // perspective paths
         SkPoint pivot = SkPoint::Make(fWideRectPath.getBounds().width()/2,
                                       fWideRectPath.getBounds().height()/2);
-        SkPoint translate = SkPoint::Make(50, 450);
+        SkPoint translate = SkPoint::Make(100, 450);
         paint.setColor(SK_ColorWHITE);
         Sk3DView view;
         view.save();
-        view.rotateX(10);
+        view.rotateX(fAnimAngle);
         SkMatrix persp;
         view.getMatrix(&persp);
         persp.preTranslate(-pivot.fX, -pivot.fY);
@@ -539,14 +557,20 @@
         lightPos = fLightPos;
         lightPos.fX += pivot.fX + translate.fX;
         lightPos.fY += pivot.fY + translate.fY;
-        this->drawShadowedPath(canvas, fWideRectPath, SkTMax(1.0f, 16 + fZDelta), paint, .1f,
+        zValue = SkTMax(1.0f, 16 + fZDelta);
+        SkScalar radians = SkDegreesToRadians(fAnimAngle);
+        zFunc = [zValue, pivot, radians](SkScalar x, SkScalar y) {
+            return SkScalarSin(-radians)*y +
+                   zValue - SkScalarSin(-radians)*pivot.fY;
+        };
+        this->drawShadowedPath(canvas, fWideRectPath, zFunc, paint, .1f,
                                lightPos, kLightWidth, .5f);
 
         pivot = SkPoint::Make(fWideOvalPath.getBounds().width() / 2,
                               fWideOvalPath.getBounds().height() / 2);
-        translate = SkPoint::Make(50, 600);
+        translate = SkPoint::Make(100, 600);
         view.restore();
-        view.rotateY(10);
+        view.rotateY(fAnimAngle);
         view.getMatrix(&persp);
         persp.preTranslate(-pivot.fX, -pivot.fY);
         persp.postTranslate(pivot.fX + translate.fX, pivot.fY + translate.fY);
@@ -554,12 +578,18 @@
         lightPos = fLightPos;
         lightPos.fX += pivot.fX + translate.fX;
         lightPos.fY += pivot.fY + translate.fY;
-        this->drawShadowedPath(canvas, fWideOvalPath, SkTMax(1.0f, 32 + fZDelta), paint, .1f,
+        zValue = SkTMax(1.0f, 32 + fZDelta);
+        zFunc = [zValue, pivot, radians](SkScalar x, SkScalar y) {
+            return -SkScalarSin(radians)*x +
+                zValue + SkScalarSin(radians)*pivot.fX;
+        };
+        this->drawShadowedPath(canvas, fWideOvalPath, zFunc, paint, .1f,
                                lightPos, kLightWidth, .5f);
     }
 
     bool onAnimate(const SkAnimTimer& timer) override {
         fAnimTranslate = timer.pingPong(30, 0, 200, -200);
+        fAnimAngle = timer.pingPong(15, 0, 0, 20);
 
         return true;
     }
diff --git a/src/utils/SkCamera.cpp b/src/utils/SkCamera.cpp
index 23ab396..cb364a5 100644
--- a/src/utils/SkCamera.cpp
+++ b/src/utils/SkCamera.cpp
@@ -214,6 +214,7 @@
 void SkCamera3D::doUpdate() const {
     SkUnit3D    axis, zenith, cross;
 
+    // construct a orthonormal basis of cross (x), zenith (y), and axis (z)
     fAxis.normalize(&axis);
 
     {
@@ -234,6 +235,20 @@
         SkScalar y = fObserver.fY;
         SkScalar z = fObserver.fZ;
 
+        // Looking along the view axis we have:
+        //
+        //   /|\ zenith
+        //    |
+        //    |
+        //    |  * observer (projected on XY plane)
+        //    |
+        //    |____________\ cross
+        //                 /
+        //
+        // So this does a z-shear along the view axis based on the observer's x and y values,
+        // and scales in x and y relative to the negative of the observer's z value
+        // (the observer is in the negative z direction).
+
         orien->set(SkMatrix::kMScaleX, x * axis.fX - z * cross.fX);
         orien->set(SkMatrix::kMSkewX,  x * axis.fY - z * cross.fY);
         orien->set(SkMatrix::kMTransX, x * axis.fZ - z * cross.fZ);
@@ -264,6 +279,15 @@
     dot = SkUnit3D::Dot(*SkTCast<const SkUnit3D*>(&diff),
                         *SkTCast<const SkUnit3D*>(SkTCast<const SkScalar*>(&fOrientation) + 6));
 
+    // This multiplies fOrientation by the matrix [quilt.fU quilt.fV diff] -- U, V, and diff are
+    // column vectors in the matrix -- then divides by the length of the projection of diff onto
+    // the view axis (which is 'dot'). This transforms the patch (which transforms from local path
+    // space to world space) into view space (since fOrientation transforms from world space to
+    // view space).
+    //
+    // The divide by 'dot' isn't strictly necessary as the homogeneous divide would do much the
+    // same thing (it's just scaling the entire matrix by 1/dot). It looks like it's normalizing
+    // the matrix into some canonical space.
     patchPtr = (const SkScalar*)&quilt;
     matrix->set(SkMatrix::kMScaleX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot));
     matrix->set(SkMatrix::kMSkewY,  SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot));
diff --git a/src/utils/SkInsetConvexPolygon.cpp b/src/utils/SkInsetConvexPolygon.cpp
index 93bbae9..bb46942 100755
--- a/src/utils/SkInsetConvexPolygon.cpp
+++ b/src/utils/SkInsetConvexPolygon.cpp
@@ -49,17 +49,46 @@
     return 0;
 }
 
-// Perpendicularly offset line segment p0-p1 'distance' units in the direction specified by 'dir'
-static void inset_edge(const SkPoint& p0, const SkPoint& p1, SkScalar distance, int dir,
-                       InsetSegment* inset) {
-    SkASSERT(dir == -1 || dir == 1);
-    // compute perpendicular
-    SkVector perp;
-    perp.fX = p0.fY - p1.fY;
-    perp.fY = p1.fX - p0.fX;
-    perp.setLength(distance*dir);
-    inset->fP0 = p0 + perp;
-    inset->fP1 = p1 + perp;
+// Offset line segment p0-p1 'd0' and 'd1' units in the direction specified by 'side'
+bool SkOffsetSegment(const SkPoint& p0, const SkPoint& p1, SkScalar d0, SkScalar d1,
+                     int side, SkPoint* offset0, SkPoint* offset1) {
+    SkASSERT(side == -1 || side == 1);
+    SkVector perp = SkVector::Make(p0.fY - p1.fY, p1.fX - p0.fX);
+    if (SkScalarNearlyEqual(d0, d1)) {
+        // if distances are equal, can just outset by the perpendicular
+        perp.setLength(d0*side);
+        *offset0 = p0 + perp;
+        *offset1 = p1 + perp;
+    } else {
+        // Otherwise we need to compute the outer tangent.
+        // See: http://www.ambrsoft.com/TrigoCalc/Circles2/Circles2Tangent_.htm
+        if (d0 < d1) {
+            side = -side;
+        }
+        SkScalar dD = d0 - d1;
+        // if one circle is inside another, we can't compute an offset
+        if (dD*dD >= p0.distanceToSqd(p1)) {
+            return false;
+        }
+        SkPoint outerTangentIntersect = SkPoint::Make((p1.fX*d0 - p0.fX*d1) / dD,
+                                                      (p1.fY*d0 - p0.fY*d1) / dD);
+
+        SkScalar d0sq = d0*d0;
+        SkVector dP = outerTangentIntersect - p0;
+        SkScalar dPlenSq = dP.lengthSqd();
+        SkScalar discrim = SkScalarSqrt(dPlenSq - d0sq);
+        offset0->fX = p0.fX + (d0sq*dP.fX - side*d0*dP.fY*discrim) / dPlenSq;
+        offset0->fY = p0.fY + (d0sq*dP.fY + side*d0*dP.fX*discrim) / dPlenSq;
+
+        SkScalar d1sq = d1*d1;
+        dP = outerTangentIntersect - p1;
+        dPlenSq = dP.lengthSqd();
+        discrim = SkScalarSqrt(dPlenSq - d1sq);
+        offset1->fX = p1.fX + (d1sq*dP.fX - side*d1*dP.fY*discrim) / dPlenSq;
+        offset1->fY = p1.fY + (d1sq*dP.fY + side*d1*dP.fX*discrim) / dPlenSq;
+    }
+
+    return true;
 }
 
 // Compute the intersection 'p' between segments s0 and s1, if any.
@@ -147,7 +176,8 @@
 // Note: the assumption is that inputPolygon is convex and has no coincident points.
 //
 bool SkInsetConvexPolygon(const SkPoint* inputPolygonVerts, int inputPolygonSize,
-                          SkScalar insetDistance, SkTDArray<SkPoint>* insetPolygon) {
+                          std::function<SkScalar(int index)> insetDistanceFunc,
+                          SkTDArray<SkPoint>* insetPolygon) {
     if (inputPolygonSize < 3) {
         return false;
     }
@@ -168,8 +198,10 @@
     SkAutoSTMalloc<64, EdgeData> edgeData(inputPolygonSize);
     for (int i = 0; i < inputPolygonSize; ++i) {
         int j = (i + 1) % inputPolygonSize;
-        inset_edge(inputPolygonVerts[i], inputPolygonVerts[j], insetDistance, winding,
-                   &edgeData[i].fInset);
+        SkOffsetSegment(inputPolygonVerts[i], inputPolygonVerts[j],
+                        insetDistanceFunc(i), insetDistanceFunc(j),
+                        winding,
+                        &edgeData[i].fInset.fP0, &edgeData[i].fInset.fP1);
         edgeData[i].fIntersection = edgeData[i].fInset.fP0;
         edgeData[i].fTValue = SK_ScalarMin;
         edgeData[i].fValid = true;
@@ -231,14 +263,27 @@
         }
     }
 
-    // store all the valid intersections
+    // store all the valid intersections that aren't nearly coincident
+    // TODO: look at the main algorithm and see if we can detect these better
+    static constexpr SkScalar kCleanupTolerance = 0.01f;
+
     insetPolygon->reset();
     insetPolygon->setReserve(insetVertexCount);
+    currIndex = -1;
     for (int i = 0; i < inputPolygonSize; ++i) {
-        if (edgeData[i].fValid) {
+        if (edgeData[i].fValid && (currIndex == -1 ||
+            !edgeData[i].fIntersection.equalsWithinTolerance((*insetPolygon)[currIndex],
+                                                             kCleanupTolerance))) {
             *insetPolygon->push() = edgeData[i].fIntersection;
+            currIndex++;
         }
     }
+    // make sure the first and last points aren't coincident
+    if (currIndex >= 1 &&
+        (*insetPolygon)[0].equalsWithinTolerance((*insetPolygon)[currIndex],
+                                                 kCleanupTolerance)) {
+        insetPolygon->pop();
+    }
     SkASSERT(is_convex(*insetPolygon));
 
     return (insetPolygon->count() >= 3);
diff --git a/src/utils/SkInsetConvexPolygon.h b/src/utils/SkInsetConvexPolygon.h
index 3ab7558..1d5a19c 100755
--- a/src/utils/SkInsetConvexPolygon.h
+++ b/src/utils/SkInsetConvexPolygon.h
@@ -8,20 +8,49 @@
 #ifndef SkInsetConvexPolygon_DEFINED
 #define SkInsetConvexPolygon_DEFINED
 
+#include <functional>
+
 #include "SkTDArray.h"
 #include "SkPoint.h"
 
- /**
+/**
  * Generates a polygon that is inset a given distance from the boundary of a given convex polygon.
  *
  * @param inputPolygonVerts  Array of points representing the vertices of the original polygon.
  *  It should be convex and have no coincident points.
  * @param inputPolygonSize  Number of vertices in the original polygon.
- * @param insetDistance  How far we wish to inset the polygon. This should be a positive value.
+ * @param insetDistanceFunc  How far we wish to inset the polygon for a given index in the array.
+ *  This should return a positive value.
  * @param insetPolygon  The resulting inset polygon, if any.
  * @return true if an inset polygon exists, false otherwise.
  */
 bool SkInsetConvexPolygon(const SkPoint* inputPolygonVerts, int inputPolygonSize,
-                          SkScalar insetDistance, SkTDArray<SkPoint>* insetPolygon);
+                          std::function<SkScalar(int index)> insetDistanceFunc,
+                          SkTDArray<SkPoint>* insetPolygon);
+
+inline bool SkInsetConvexPolygon(const SkPoint* inputPolygonVerts, int inputPolygonSize,
+                                 SkScalar inset,
+                                 SkTDArray<SkPoint>* insetPolygon) {
+    return SkInsetConvexPolygon(inputPolygonVerts, inputPolygonSize,
+                                [inset](int) { return inset; },
+                                insetPolygon);
+}
+
+/**
+ * Offset a segment by the given distance at each point.
+ * Uses the outer tangents of two circles centered on each endpoint.
+ * See: https://en.wikipedia.org/wiki/Tangent_lines_to_circles
+ *
+ * @param p0  First endpoint.
+ * @param p1  Second endpoint.
+ * @param d0  Offset distance from first endpoint.
+ * @param d1  Offset distance from second endpoint.
+ * @param side  Indicates whether we want to offset to the left (1) or right (-1) side of segment.
+ * @param offset0  First endpoint of offset segment.
+ * @param offset1  Second endpoint of offset segment.
+ * @return true if an offset segment exists, false otherwise.
+ */
+bool SkOffsetSegment(const SkPoint& p0, const SkPoint& p1, SkScalar d0, SkScalar d1,
+                     int side, SkPoint* offset0, SkPoint* offset1);
 
 #endif
diff --git a/src/utils/SkShadowTessellator.cpp b/src/utils/SkShadowTessellator.cpp
old mode 100644
new mode 100755
index fa3f3f4..7d29b06
--- a/src/utils/SkShadowTessellator.cpp
+++ b/src/utils/SkShadowTessellator.cpp
@@ -35,9 +35,13 @@
     }
 
 protected:
+    static constexpr auto kMinHeight = 0.1f;
+
     int vertexCount() const { return fPositions.count(); }
     int indexCount() const { return fIndices.count(); }
 
+    bool setZOffset(const SkRect& bounds, bool perspective);
+
     virtual void handleLine(const SkPoint& p) = 0;
     void handleLine(const SkMatrix& m, SkPoint* p);
 
@@ -48,13 +52,18 @@
 
     void handleConic(const SkMatrix& m, SkPoint pts[3], SkScalar w);
 
-    void addArc(const SkVector& nextNormal);
-    void finishArcAndAddEdge(const SkVector& nextPoint, const SkVector& nextNormal);
-    virtual void addEdge(const SkVector& nextPoint, const SkVector& nextNormal) = 0;
+    bool setTransformedHeightFunc(const SkMatrix& ctm);
 
-    SkShadowTessellator::HeightFunc fHeightFunc;
+    void addArc(const SkVector& nextNormal, bool finishArc);
 
-    // first three points
+    SkShadowTessellator::HeightFunc         fHeightFunc;
+    std::function<SkScalar(const SkPoint&)> fTransformedHeightFunc;
+    SkScalar                                fZOffset;
+    // members for perspective height function
+    SkScalar                                fZParams[3];
+    SkScalar                                fPartialDeterminants[3];
+
+    // first two points
     SkTDArray<SkPoint>  fInitPoints;
     // temporary buffer
     SkTDArray<SkPoint>  fPointBuffer;
@@ -80,16 +89,16 @@
     SkPoint             fPrevPoint;
 };
 
-static bool compute_normal(const SkPoint& p0, const SkPoint& p1, SkScalar radius, SkScalar dir,
+static bool compute_normal(const SkPoint& p0, const SkPoint& p1, SkScalar dir,
                            SkVector* newNormal) {
     SkVector normal;
     // compute perpendicular
     normal.fX = p0.fY - p1.fY;
     normal.fY = p1.fX - p0.fX;
+    normal *= dir;
     if (!normal.normalize()) {
         return false;
     }
-    normal *= radius*dir;
     *newNormal = normal;
     return true;
 }
@@ -112,6 +121,7 @@
 SkBaseShadowTessellator::SkBaseShadowTessellator(SkShadowTessellator::HeightFunc heightFunc,
                                                  bool transparent)
         : fHeightFunc(heightFunc)
+        , fZOffset(0)
         , fFirstVertex(-1)
         , fSucceeded(false)
         , fTransparent(transparent)
@@ -122,6 +132,31 @@
     // child classes will set reserve for positions, colors and indices
 }
 
+bool SkBaseShadowTessellator::setZOffset(const SkRect& bounds, bool perspective) {
+    SkScalar minZ = fHeightFunc(bounds.fLeft, bounds.fTop);
+    if (perspective) {
+        SkScalar z = fHeightFunc(bounds.fLeft, bounds.fBottom);
+        if (z < minZ) {
+            minZ = z;
+        }
+        z = fHeightFunc(bounds.fRight, bounds.fTop);
+        if (z < minZ) {
+            minZ = z;
+        }
+        z = fHeightFunc(bounds.fRight, bounds.fBottom);
+        if (z < minZ) {
+            minZ = z;
+        }
+    }
+
+    if (minZ < kMinHeight) {
+        fZOffset = -minZ + kMinHeight;
+        return true;
+    }
+
+    return false;
+}
+
 // tesselation tolerance values, in device space pixels
 #if SK_SUPPORT_GPU
 static const SkScalar kQuadTolerance = 0.2f;
@@ -180,6 +215,9 @@
 }
 
 void SkBaseShadowTessellator::handleConic(const SkMatrix& m, SkPoint pts[3], SkScalar w) {
+    if (m.hasPerspective()) {
+        w = SkConic::TransformW(pts, w, m);
+    }
     m.mapPoints(pts, 3);
     SkAutoConicToQuads quadder;
     const SkPoint* quads = quadder.computeQuads(pts, w, kConicTolerance);
@@ -196,36 +234,89 @@
     }
 }
 
-void SkBaseShadowTessellator::addArc(const SkVector& nextNormal) {
+void SkBaseShadowTessellator::addArc(const SkVector& nextNormal, bool finishArc) {
     // fill in fan from previous quad
     SkScalar rotSin, rotCos;
     int numSteps;
     compute_radial_steps(fPrevNormal, nextNormal, fRadius, &rotSin, &rotCos, &numSteps);
     SkVector prevNormal = fPrevNormal;
     for (int i = 0; i < numSteps; ++i) {
-        SkVector nextNormal;
-        nextNormal.fX = prevNormal.fX*rotCos - prevNormal.fY*rotSin;
-        nextNormal.fY = prevNormal.fY*rotCos + prevNormal.fX*rotSin;
-        *fPositions.push() = fPrevPoint + nextNormal;
+        SkVector currNormal;
+        currNormal.fX = prevNormal.fX*rotCos - prevNormal.fY*rotSin;
+        currNormal.fY = prevNormal.fY*rotCos + prevNormal.fX*rotSin;
+        *fPositions.push() = fPrevPoint + currNormal;
         *fColors.push() = fPenumbraColor;
         *fIndices.push() = fPrevUmbraIndex;
         *fIndices.push() = fPositions.count() - 2;
         *fIndices.push() = fPositions.count() - 1;
 
-        prevNormal = nextNormal;
+        prevNormal = currNormal;
     }
+    if (finishArc) {
+        *fPositions.push() = fPrevPoint + nextNormal;
+        *fColors.push() = fPenumbraColor;
+        *fIndices.push() = fPrevUmbraIndex;
+        *fIndices.push() = fPositions.count() - 2;
+        *fIndices.push() = fPositions.count() - 1;
+    }
+    fPrevNormal = nextNormal;
 }
 
-void SkBaseShadowTessellator::finishArcAndAddEdge(const SkPoint& nextPoint,
-                                                  const SkVector& nextNormal) {
-    // close out previous arc
-    *fPositions.push() = fPrevPoint + nextNormal;
-    *fColors.push() = fPenumbraColor;
-    *fIndices.push() = fPrevUmbraIndex;
-    *fIndices.push() = fPositions.count() - 2;
-    *fIndices.push() = fPositions.count() - 1;
+bool SkBaseShadowTessellator::setTransformedHeightFunc(const SkMatrix& ctm) {
+    if (!ctm.hasPerspective()) {
+        fTransformedHeightFunc = [this](const SkPoint& p) {
+            return this->fHeightFunc(0, 0);
+        };
+    } else {
+        SkMatrix ctmInverse;
+        if (!ctm.invert(&ctmInverse)) {
+            return false;
+        }
+        SkScalar C = fHeightFunc(0, 0);
+        SkScalar A = fHeightFunc(1, 0) - C;
+        SkScalar B = fHeightFunc(0, 1) - C;
 
-    this->addEdge(nextPoint, nextNormal);
+        // multiply by transpose
+        fZParams[0] = ctmInverse[SkMatrix::kMScaleX] * A +
+                      ctmInverse[SkMatrix::kMSkewY] * B +
+                      ctmInverse[SkMatrix::kMPersp0] * C;
+        fZParams[1] = ctmInverse[SkMatrix::kMSkewX] * A +
+                      ctmInverse[SkMatrix::kMScaleY] * B +
+                      ctmInverse[SkMatrix::kMPersp1] * C;
+        fZParams[2] = ctmInverse[SkMatrix::kMTransX] * A +
+                      ctmInverse[SkMatrix::kMTransY] * B +
+                      ctmInverse[SkMatrix::kMPersp2] * C;
+
+        // We use Cramer's rule to solve for the W value for a given post-divide X and Y,
+        // so pre-compute those values that are independent of X and Y.
+        // W is det(ctmInverse)/(PD[0]*X + PD[1]*Y + PD[2])
+        fPartialDeterminants[0] = ctm[SkMatrix::kMSkewY] * ctm[SkMatrix::kMPersp1] -
+                                  ctm[SkMatrix::kMScaleY] * ctm[SkMatrix::kMPersp0];
+        fPartialDeterminants[1] = ctm[SkMatrix::kMPersp0] * ctm[SkMatrix::kMSkewX] -
+                                  ctm[SkMatrix::kMPersp1] * ctm[SkMatrix::kMScaleX];
+        fPartialDeterminants[2] = ctm[SkMatrix::kMScaleX] * ctm[SkMatrix::kMScaleY] -
+                                  ctm[SkMatrix::kMSkewX] * ctm[SkMatrix::kMSkewY];
+        SkScalar ctmDeterminant = ctm[SkMatrix::kMTransX] * fPartialDeterminants[0] +
+                                  ctm[SkMatrix::kMTransY] * fPartialDeterminants[1] +
+                                  ctm[SkMatrix::kMPersp2] * fPartialDeterminants[2];
+
+        // Pre-bake the numerator of Cramer's rule into the zParams to avoid another multiply.
+        // TODO: this may introduce numerical instability, but I haven't seen any issues yet.
+        fZParams[0] *= ctmDeterminant;
+        fZParams[1] *= ctmDeterminant;
+        fZParams[2] *= ctmDeterminant;
+
+        fTransformedHeightFunc = [this](const SkPoint& p) {
+            SkScalar denom = p.fX * this->fPartialDeterminants[0] +
+                             p.fY * this->fPartialDeterminants[1] +
+                             this->fPartialDeterminants[2];
+            SkScalar w = SkScalarFastInvert(denom);
+            return (this->fZParams[0] * p.fX + this->fZParams[1] * p.fY + this->fZParams[2])*w +
+                   this->fZOffset;
+        };
+    }
+
+    return true;
 }
 
 
@@ -239,26 +330,35 @@
 
 private:
     void handleLine(const SkPoint& p) override;
-    void addEdge(const SkVector& nextPoint, const SkVector& nextNormal) override;
+    void addEdge(const SkVector& nextPoint, const SkVector& nextNormal);
 
+    static constexpr auto kHeightFactor = 1.0f / 128.0f;
+    static constexpr auto kGeomFactor = 64.0f;
+    static constexpr auto kMaxEdgeLenSqr = 20 * 20;
+
+    SkScalar offset(SkScalar z) {
+        return z * kHeightFactor * kGeomFactor;
+    }
+    SkColor umbraColor(SkScalar z) {
+        SkScalar umbraAlpha = SkScalarInvert((1.0f + SkTMax(z*kHeightFactor, 0.0f)));
+        return SkColorSetARGB(255, 0, fAmbientAlpha * 255.9999f, umbraAlpha * 255.9999f);
+    }
+
+    SkScalar            fAmbientAlpha;
     int                 fCentroidCount;
 
     typedef SkBaseShadowTessellator INHERITED;
 };
 
-static const float kHeightFactor = 1.0f / 128.0f;
-static const float kGeomFactor = 64.0f;
-
 SkAmbientShadowTessellator::SkAmbientShadowTessellator(const SkPath& path,
                                                        const SkMatrix& ctm,
                                                        SkShadowTessellator::HeightFunc heightFunc,
                                                        SkScalar ambientAlpha,
                                                        bool transparent)
-        : INHERITED(heightFunc, transparent) {
-    // Set radius and colors
-    // TODO: vary colors and radius based on heightFunc
+        : INHERITED(heightFunc, transparent)
+        , fAmbientAlpha(ambientAlpha) {
+    // Set base colors
     SkScalar occluderHeight = heightFunc(0, 0);
-    fRadius = occluderHeight * kHeightFactor * kGeomFactor;
     SkScalar umbraAlpha = SkScalarInvert((1.0f + SkTMax(occluderHeight*kHeightFactor, 0.0f)));
     // umbraColor is the interior value, penumbraColor the exterior value.
     // umbraAlpha is the factor that is linearly interpolated from outside to inside, and
@@ -267,6 +367,11 @@
     fUmbraColor = SkColorSetARGB(255, 0, ambientAlpha * 255.9999f, umbraAlpha * 255.9999f);
     fPenumbraColor = SkColorSetARGB(255, 0, ambientAlpha * 255.9999f, 0);
 
+    // make sure we're not below the canvas plane
+    this->setZOffset(path.getBounds(), ctm.hasPerspective());
+
+    this->setTransformedHeightFunc(ctm);
+
     // Outer ring: 3*numPts
     // Middle ring: numPts
     fPositions.setReserve(4 * path.countPoints());
@@ -311,28 +416,62 @@
     }
 
     SkVector normal;
-    if (compute_normal(fPrevPoint, fFirstPoint, fRadius, fDirection,
-                       &normal)) {
-        this->addArc(normal);
+    if (compute_normal(fPrevPoint, fFirstPoint, fDirection, &normal)) {
+        SkScalar z = fTransformedHeightFunc(fPrevPoint);
+        fRadius = this->offset(z);
+        SkVector scaledNormal(normal);
+        scaledNormal *= fRadius;
+        this->addArc(scaledNormal, true);
 
-        // close out previous arc
-        *fPositions.push() = fPrevPoint + normal;
-        *fColors.push() = fPenumbraColor;
-        *fIndices.push() = fPrevUmbraIndex;
-        *fIndices.push() = fPositions.count() - 2;
-        *fIndices.push() = fPositions.count() - 1;
+        // set up for final edge
+        z = fTransformedHeightFunc(fFirstPoint);
+        normal *= this->offset(z);
 
-        // add final edge
+        // make sure we don't end up with a sharp alpha edge along the quad diagonal
+        if (fColors[fPrevUmbraIndex] != fColors[fFirstVertex] &&
+            fFirstPoint.distanceToSqd(fPositions[fPrevUmbraIndex]) > kMaxEdgeLenSqr) {
+            SkPoint centerPoint = fPositions[fPrevUmbraIndex] + fFirstPoint;
+            centerPoint *= 0.5f;
+            *fPositions.push() = centerPoint;
+            *fColors.push() = SkPMLerp(fColors[fFirstVertex], fColors[fPrevUmbraIndex], 128);
+            SkVector midNormal = fPrevNormal + normal;
+            midNormal *= 0.5f;
+            *fPositions.push() = centerPoint + midNormal;
+            *fColors.push() = fPenumbraColor;
+
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 3;
+            *fIndices.push() = fPositions.count() - 2;
+
+            *fIndices.push() = fPositions.count() - 3;
+            *fIndices.push() = fPositions.count() - 1;
+            *fIndices.push() = fPositions.count() - 2;
+
+            fPrevUmbraIndex = fPositions.count() - 2;
+        }
+
+        // final edge
         *fPositions.push() = fFirstPoint + normal;
         *fColors.push() = fPenumbraColor;
 
-        *fIndices.push() = fPrevUmbraIndex;
-        *fIndices.push() = fPositions.count() - 2;
-        *fIndices.push() = fFirstVertex;
+        if (fColors[fPrevUmbraIndex] > fColors[fFirstVertex]) {
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 2;
+            *fIndices.push() = fFirstVertex;
 
-        *fIndices.push() = fPositions.count() - 2;
-        *fIndices.push() = fPositions.count() - 1;
-        *fIndices.push() = fFirstVertex;
+            *fIndices.push() = fPositions.count() - 2;
+            *fIndices.push() = fPositions.count() - 1;
+            *fIndices.push() = fFirstVertex;
+        } else {
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 2;
+            *fIndices.push() = fPositions.count() - 1;
+
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 1;
+            *fIndices.push() = fFirstVertex;
+        }
+        fPrevNormal = normal;
     }
 
     // finalize centroid
@@ -347,9 +486,9 @@
     // final fan
     if (fPositions.count() >= 3) {
         fPrevUmbraIndex = fFirstVertex;
-        fPrevNormal = normal;
         fPrevPoint = fFirstPoint;
-        this->addArc(fFirstNormal);
+        fRadius = this->offset(fTransformedHeightFunc(fPrevPoint));
+        this->addArc(fFirstNormal, false);
 
         *fIndices.push() = fFirstVertex;
         *fIndices.push() = fPositions.count() - 1;
@@ -379,8 +518,8 @@
         fDirection = (perpDot > 0) ? -1 : 1;
 
         // add first quad
-        if (!compute_normal(fInitPoints[0], fInitPoints[1], fRadius, fDirection,
-                            &fFirstNormal)) {
+        SkVector normal;
+        if (!compute_normal(fInitPoints[0], fInitPoints[1], fDirection, &normal)) {
             // first two points are incident, make the third point the second and continue
             fInitPoints[1] = p;
             return;
@@ -388,45 +527,106 @@
 
         fFirstPoint = fInitPoints[0];
         fFirstVertex = fPositions.count();
+        SkScalar z = fTransformedHeightFunc(fFirstPoint);
+        fFirstNormal = normal;
+        fFirstNormal *= this->offset(z);
+
         fPrevNormal = fFirstNormal;
         fPrevPoint = fFirstPoint;
         fPrevUmbraIndex = fFirstVertex;
 
-        *fPositions.push() = fInitPoints[0];
-        *fColors.push() = fUmbraColor;
-        *fPositions.push() = fInitPoints[0] + fFirstNormal;
+        *fPositions.push() = fFirstPoint;
+        *fColors.push() = this->umbraColor(z);
+        *fPositions.push() = fFirstPoint + fFirstNormal;
         *fColors.push() = fPenumbraColor;
         if (fTransparent) {
-            fPositions[0] += fInitPoints[0];
+            fPositions[0] += fFirstPoint;
             fCentroidCount = 1;
         }
-        this->addEdge(fInitPoints[1], fFirstNormal);
+
+        // add the first quad
+        z = fTransformedHeightFunc(fInitPoints[1]);
+        fRadius = this->offset(z);
+        fUmbraColor = this->umbraColor(z);
+        normal *= fRadius;
+        this->addEdge(fInitPoints[1], normal);
 
         // to ensure we skip this block next time
         *fInitPoints.push() = p;
     }
 
     SkVector normal;
-    if (compute_normal(fPositions[fPrevUmbraIndex], p, fRadius, fDirection, &normal)) {
-        this->addArc(normal);
-        this->finishArcAndAddEdge(p, normal);
+    if (compute_normal(fPositions[fPrevUmbraIndex], p, fDirection, &normal)) {
+        SkVector scaledNormal = normal;
+        scaledNormal *= fRadius;
+        this->addArc(scaledNormal, true);
+        SkScalar z = fTransformedHeightFunc(p);
+        fRadius = this->offset(z);
+        fUmbraColor = this->umbraColor(z);
+        normal *= fRadius;
+        this->addEdge(p, normal);
     }
 }
 
 void SkAmbientShadowTessellator::addEdge(const SkPoint& nextPoint, const SkVector& nextNormal) {
+    // make sure we don't end up with a sharp alpha edge along the quad diagonal
+    if (fColors[fPrevUmbraIndex] != fUmbraColor &&
+        nextPoint.distanceToSqd(fPositions[fPrevUmbraIndex]) > kMaxEdgeLenSqr) {
+        SkPoint centerPoint = fPositions[fPrevUmbraIndex] + nextPoint;
+        centerPoint *= 0.5f;
+        *fPositions.push() = centerPoint;
+        *fColors.push() = SkPMLerp(fUmbraColor, fColors[fPrevUmbraIndex], 128);
+        SkVector midNormal = fPrevNormal + nextNormal;
+        midNormal *= 0.5f;
+        *fPositions.push() = centerPoint + midNormal;
+        *fColors.push() = fPenumbraColor;
+
+        // set triangularization to get best interpolation of color
+        if (fColors[fPrevUmbraIndex] > fColors[fPositions.count() - 2]) {
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 3;
+            *fIndices.push() = fPositions.count() - 2;
+
+            *fIndices.push() = fPositions.count() - 3;
+            *fIndices.push() = fPositions.count() - 1;
+            *fIndices.push() = fPositions.count() - 2;
+        } else {
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 2;
+            *fIndices.push() = fPositions.count() - 1;
+
+            *fIndices.push() = fPrevUmbraIndex;
+            *fIndices.push() = fPositions.count() - 1;
+            *fIndices.push() = fPositions.count() - 3;
+        }
+
+        fPrevUmbraIndex = fPositions.count() - 2;
+    }
+
     // add next quad
     *fPositions.push() = nextPoint;
     *fColors.push() = fUmbraColor;
     *fPositions.push() = nextPoint + nextNormal;
     *fColors.push() = fPenumbraColor;
 
-    *fIndices.push() = fPrevUmbraIndex;
-    *fIndices.push() = fPositions.count() - 3;
-    *fIndices.push() = fPositions.count() - 2;
+    // set triangularization to get best interpolation of color
+    if (fColors[fPrevUmbraIndex] > fColors[fPositions.count() - 2]) {
+        *fIndices.push() = fPrevUmbraIndex;
+        *fIndices.push() = fPositions.count() - 3;
+        *fIndices.push() = fPositions.count() - 2;
 
-    *fIndices.push() = fPositions.count() - 3;
-    *fIndices.push() = fPositions.count() - 1;
-    *fIndices.push() = fPositions.count() - 2;
+        *fIndices.push() = fPositions.count() - 3;
+        *fIndices.push() = fPositions.count() - 1;
+        *fIndices.push() = fPositions.count() - 2;
+    } else {
+        *fIndices.push() = fPrevUmbraIndex;
+        *fIndices.push() = fPositions.count() - 2;
+        *fIndices.push() = fPositions.count() - 1;
+
+        *fIndices.push() = fPrevUmbraIndex;
+        *fIndices.push() = fPositions.count() - 1;
+        *fIndices.push() = fPositions.count() - 3;
+    }
 
     // if transparent, add point to first one in array and add to center fan
     if (fTransparent) {
@@ -454,7 +654,7 @@
 
 private:
     void computeClipAndPathPolygons(const SkPath& path, const SkMatrix& ctm,
-                                    SkScalar scale, const SkVector& xlate);
+                                    const SkMatrix& shadowTransform);
     void computeClipVectorsAndTestCentroid();
     bool clipUmbraPoint(const SkPoint& umbraPoint, const SkPoint& centroid, SkPoint* clipPoint);
     int getClosestUmbraPoint(const SkPoint& point);
@@ -464,7 +664,16 @@
 
     void mapPoints(SkScalar scale, const SkVector& xlate, SkPoint* pts, int count);
     bool addInnerPoint(const SkPoint& pathPoint);
-    void addEdge(const SkVector& nextPoint, const SkVector& nextNormal) override;
+    void addEdge(const SkVector& nextPoint, const SkVector& nextNormal);
+
+    SkScalar offset(SkScalar z) {
+        float zRatio = SkTPin(z / (fLightZ - z), 0.0f, 0.95f);
+        return fLightRadius*zRatio;
+    }
+
+    SkScalar            fLightZ;
+    SkScalar            fLightRadius;
+    SkScalar            fOffsetAdjust;
 
     SkTDArray<SkPoint>  fClipPolygon;
     SkTDArray<SkVector> fClipVectors;
@@ -486,27 +695,49 @@
                                                  SkShadowTessellator::HeightFunc heightFunc,
                                                  const SkPoint3& lightPos, SkScalar lightRadius,
                                                  SkScalar spotAlpha, bool transparent)
-        : INHERITED(heightFunc, transparent)
-        , fCurrClipPoint(0)
-        , fPrevUmbraOutside(false)
-        , fFirstUmbraOutside(false)
-        , fValidUmbra(true) {
+    : INHERITED(heightFunc, transparent)
+    , fLightZ(lightPos.fZ)
+    , fLightRadius(lightRadius)
+    , fOffsetAdjust(0)
+    , fCurrClipPoint(0)
+    , fPrevUmbraOutside(false)
+    , fFirstUmbraOutside(false)
+    , fValidUmbra(true) {
+
+    // make sure we're not below the canvas plane
+    if (this->setZOffset(path.getBounds(), ctm.hasPerspective())) {
+        // Adjust light height and radius
+        fLightRadius *= (fLightZ + fZOffset) / fLightZ;
+        fLightZ += fZOffset;
+    }
 
     // Set radius and colors
-    // TODO: vary colors and radius based on heightFunc
     SkPoint center = SkPoint::Make(path.getBounds().centerX(), path.getBounds().centerY());
-    SkScalar occluderHeight = heightFunc(center.fX, center.fY);
-    float zRatio = SkTPin(occluderHeight / (lightPos.fZ - occluderHeight), 0.0f, 0.95f);
+    SkScalar occluderHeight = heightFunc(center.fX, center.fY) + fZOffset;
+    float zRatio = SkTPin(occluderHeight / (fLightZ - occluderHeight), 0.0f, 0.95f);
     SkScalar radius = lightRadius * zRatio;
     fRadius = radius;
     fUmbraColor = SkColorSetARGB(255, 0, spotAlpha * 255.9999f, 255);
     fPenumbraColor = SkColorSetARGB(255, 0, spotAlpha * 255.9999f, 0);
 
     // Compute the scale and translation for the spot shadow.
-    SkScalar scale = lightPos.fZ / (lightPos.fZ - occluderHeight);
-    ctm.mapPoints(&center, 1);
-    SkVector translate = SkVector::Make(zRatio * (center.fX - lightPos.fX),
-                                        zRatio * (center.fY - lightPos.fY));
+    SkMatrix shadowTransform;
+    if (!ctm.hasPerspective()) {
+        SkScalar scale = fLightZ / (fLightZ - occluderHeight);
+        SkVector translate = SkVector::Make(-zRatio * lightPos.fX, -zRatio * lightPos.fY);
+        shadowTransform.setScaleTranslate(scale, scale, translate.fX, translate.fY);
+    } else {
+        // For perspective, we have a scale, a z-shear, and another projective divide --
+        // this varies at each point so we can't use an affine transform.
+        // We'll just apply this to each generated point in turn.
+        shadowTransform.reset();
+        // Also can't cull the center (for now).
+        fTransparent = true;
+    }
+    SkMatrix fullTransform = SkMatrix::Concat(shadowTransform, ctm);
+
+    // Set up our reverse mapping
+    this->setTransformedHeightFunc(fullTransform);
 
     // TODO: calculate these reserves better
     // Penumbra ring: 3*numPts
@@ -520,7 +751,7 @@
     fClipPolygon.setReserve(path.countPoints());
 
     // compute rough clip bounds for umbra, plus offset polygon, plus centroid
-    this->computeClipAndPathPolygons(path, ctm, scale, translate);
+    this->computeClipAndPathPolygons(path, ctm, shadowTransform);
     if (fClipPolygon.count() < 3 || fPathPolygon.count() < 3) {
         return;
     }
@@ -528,6 +759,8 @@
     // check to see if umbra collapses
     SkScalar minDistSq = fCentroid.distanceToLineSegmentBetweenSqd(fPathPolygon[0],
                                                                    fPathPolygon[1]);
+    SkRect bounds;
+    bounds.setBounds(&fPathPolygon[0], fPathPolygon.count());
     for (int i = 1; i < fPathPolygon.count(); ++i) {
         int j = i + 1;
         if (i == fPathPolygon.count() - 1) {
@@ -544,6 +777,7 @@
     if (minDistSq < (radius + kTolerance)*(radius + kTolerance)) {
         // if the umbra would collapse, we back off a bit on inner blur and adjust the alpha
         SkScalar newRadius = SkScalarSqrt(minDistSq) - kTolerance;
+        fOffsetAdjust = newRadius - radius;
         SkScalar ratio = 256 * newRadius / radius;
         // they aren't PMColors, but the interpolation algorithm is the same
         fUmbraColor = SkPMLerp(fUmbraColor, fPenumbraColor, (unsigned)ratio);
@@ -554,7 +788,8 @@
     this->computeClipVectorsAndTestCentroid();
 
     // generate inner ring
-    if (!SkInsetConvexPolygon(&fPathPolygon[0], fPathPolygon.count(), radius, &fUmbraPolygon)) {
+    if (!SkInsetConvexPolygon(&fPathPolygon[0], fPathPolygon.count(), radius,
+                              &fUmbraPolygon)) {
         // this shouldn't happen, but just in case we'll inset using the centroid
         fValidUmbra = false;
     }
@@ -575,15 +810,9 @@
 
     // finish up the final verts
     SkVector normal;
-    if (compute_normal(fPrevPoint, fFirstPoint, fRadius, fDirection, &normal)) {
-        this->addArc(normal);
-
-        // close out previous arc
-        *fPositions.push() = fPrevPoint + normal;
-        *fColors.push() = fPenumbraColor;
-        *fIndices.push() = fPrevUmbraIndex;
-        *fIndices.push() = fPositions.count() - 2;
-        *fIndices.push() = fPositions.count() - 1;
+    if (compute_normal(fPrevPoint, fFirstPoint, fDirection, &normal)) {
+        normal *= fRadius;
+        this->addArc(normal, true);
 
         // add to center fan
         if (fTransparent) {
@@ -621,14 +850,15 @@
         *fIndices.push() = fPositions.count() - 2;
         *fIndices.push() = fPositions.count() - 1;
         *fIndices.push() = fFirstVertex;
+
+        fPrevNormal = normal;
     }
 
     // final fan
     if (fPositions.count() >= 3) {
         fPrevUmbraIndex = fFirstVertex;
         fPrevPoint = fFirstPoint;
-        fPrevNormal = normal;
-        this->addArc(fFirstNormal);
+        this->addArc(fFirstNormal, false);
 
         *fIndices.push() = fFirstVertex;
         *fIndices.push() = fPositions.count() - 1;
@@ -638,19 +868,45 @@
             *fIndices.push() = fFirstVertex + 1;
         }
     }
+
+    if (ctm.hasPerspective()) {
+        for (int i = 0; i < fPositions.count(); ++i) {
+            SkScalar pathZ = fTransformedHeightFunc(fPositions[i]);
+            SkScalar factor = SkScalarInvert(fLightZ - pathZ);
+            fPositions[i].fX = (fPositions[i].fX*fLightZ - lightPos.fX*pathZ)*factor;
+            fPositions[i].fY = (fPositions[i].fY*fLightZ - lightPos.fY*pathZ)*factor;
+        }
+#ifdef DRAW_CENTROID
+        SkScalar pathZ = fTransformedHeightFunc(fCentroid);
+        SkScalar factor = SkScalarInvert(fLightZ - pathZ);
+        fCentroid.fX = (fCentroid.fX*fLightZ - lightPos.fX*pathZ)*factor;
+        fCentroid.fY = (fCentroid.fY*fLightZ - lightPos.fY*pathZ)*factor;
+#endif
+    }
+#ifdef DRAW_CENTROID
+    *fPositions.push() = fCentroid + SkVector::Make(-2, -2);
+    *fColors.push() = SkColorSetARGB(255, 0, 255, 255);
+    *fPositions.push() = fCentroid + SkVector::Make(2, -2);
+    *fColors.push() = SkColorSetARGB(255, 0, 255, 255);
+    *fPositions.push() = fCentroid + SkVector::Make(-2, 2);
+    *fColors.push() = SkColorSetARGB(255, 0, 255, 255);
+    *fPositions.push() = fCentroid + SkVector::Make(2, 2);
+    *fColors.push() = SkColorSetARGB(255, 0, 255, 255);
+
+    *fIndices.push() = fPositions.count() - 4;
+    *fIndices.push() = fPositions.count() - 2;
+    *fIndices.push() = fPositions.count() - 1;
+
+    *fIndices.push() = fPositions.count() - 4;
+    *fIndices.push() = fPositions.count() - 1;
+    *fIndices.push() = fPositions.count() - 3;
+#endif
+
     fSucceeded = true;
 }
 
 void SkSpotShadowTessellator::computeClipAndPathPolygons(const SkPath& path, const SkMatrix& ctm,
-                                                         SkScalar scale, const SkVector& xlate) {
-    // For the path polygon we are going to apply 'scale' and 'xlate' (in that order) to each
-    // computed path point. We want the effect to be to scale the points relative to the path
-    // bounds center and then translate them by the 'xlate' param we were passed.
-    SkPoint center = SkPoint::Make(path.getBounds().centerX(), path.getBounds().centerY());
-    ctm.mapPoints(&center, 1);
-    SkVector translate = center * (1.f - scale) + xlate;
-    SkMatrix shadowTransform;
-    shadowTransform.setScaleTranslate(scale, scale, translate.fX, translate.fY);
+                                                         const SkMatrix& shadowTransform) {
 
     fPathPolygon.setReserve(path.countPoints());
 
@@ -785,9 +1041,7 @@
         } else if (t_num >= 0 && t_num <= denom) {
             SkScalar s_num = dp.cross(fClipVectors[fCurrClipPoint]);
             // if umbra point is inside the clip polygon
-            if (s_num < 0) {
-                return false;
-            } else {
+            if (s_num >= 0 && s_num <= denom) {
                 segmentVector *= s_num/denom;
                 *clipPoint = umbraPoint + segmentVector;
                 return true;
@@ -895,13 +1149,13 @@
         fDirection = (perpDot > 0) ? -1 : 1;
 
         // add first quad
-        if (!compute_normal(fInitPoints[0], fInitPoints[1], fRadius, fDirection,
-                            &fFirstNormal)) {
+        if (!compute_normal(fInitPoints[0], fInitPoints[1], fDirection, &fFirstNormal)) {
             // first two points are incident, make the third point the second and continue
             fInitPoints[1] = p;
             return;
         }
 
+        fFirstNormal *= fRadius;
         fFirstPoint = fInitPoints[0];
         fFirstVertex = fPositions.count();
         fPrevNormal = fFirstNormal;
@@ -931,9 +1185,10 @@
     }
 
     SkVector normal;
-    if (compute_normal(fPrevPoint, p, fRadius, fDirection, &normal)) {
-        this->addArc(normal);
-        this->finishArcAndAddEdge(p, normal);
+    if (compute_normal(fPrevPoint, p, fDirection, &normal)) {
+        normal *= fRadius;
+        this->addArc(normal, true);
+        this->addEdge(p, normal);
     }
 }
 
diff --git a/tests/InsetConvexPolyTest.cpp b/tests/InsetConvexPolyTest.cpp
index 5376789..9c1349c 100644
--- a/tests/InsetConvexPolyTest.cpp
+++ b/tests/InsetConvexPolyTest.cpp
@@ -76,22 +76,20 @@
     }
 
     // just to full inset
-    // for shadows having a flat poly here is fine
-    // may want to revisit for strokes
+    // fails, but outputs a line segment
     result = SkInsetConvexPolygon(&rrectPoly[0], rrectPoly.count(), 55, &insetPoly);
-    REPORTER_ASSERT(reporter, result);
-    REPORTER_ASSERT(reporter, is_convex(insetPoly));
-    REPORTER_ASSERT(reporter, insetPoly.count() == 4);
-    if (insetPoly.count() == 4) {
+    REPORTER_ASSERT(reporter, !result);
+    REPORTER_ASSERT(reporter, !is_convex(insetPoly));
+    REPORTER_ASSERT(reporter, insetPoly.count() == 2);
+    if (insetPoly.count() == 2) {
         REPORTER_ASSERT(reporter, insetPoly[0].equals(-50, 0));
         REPORTER_ASSERT(reporter, insetPoly[1].equals(50, 0));
-        REPORTER_ASSERT(reporter, insetPoly[2].equals(50, 0));
-        REPORTER_ASSERT(reporter, insetPoly[3].equals(-50, 0));
     }
 
     // past full inset
     result = SkInsetConvexPolygon(&rrectPoly[0], rrectPoly.count(), 75, &insetPoly);
     REPORTER_ASSERT(reporter, !result);
+    REPORTER_ASSERT(reporter, insetPoly.count() == 0);
 
     // troublesome case
     SkTDArray<SkPoint> clippedRRectPoly;