GrTessellator (AA): improve antialiasing of thin shapes.
authorStephen White <senorblanco@chromium.org>
Mon, 6 Feb 2017 14:50:27 +0000 (09:50 -0500)
committerSkia Commit-Bot <skia-commit-bot@chromium.org>
Mon, 6 Feb 2017 21:40:05 +0000 (21:40 +0000)
(Long description, but actually quite a small change.)

This is the first change inspired by the "straight skeleton" algorithm.
This algorithm gives us a mental model to describe the edge-antialiasing
problem: consider the shape as walls of a house, the outer alpha
geometry is a 45-degree "roof" up to a flat top at a height of 1.0
(the filled interior).  The faces of the sloping roof join at the
bisectors between the inner and outer points.

When the shape being drawn is sufficiently thin, there should be no flat
roof, and the sloping roof meets at an edge (the straight skeleton).

This patch detects cases where an edge inverts on stroking, which
indicates that the flat roof has turned inside out, and should be
reduced to a point instead. The model above describes what to do:
follow down the "roof" along the bisectors to their intersection.
This is the point to which an inverted edge should be collapsed.
Fortunately, the bisector edges are easy to compute: they're the
connector edges joining inner and outer points. Linearly interpolating
the distance from the top to the bottom point gives the alpha we
should use to approximate coverage.

Now that we are correctly handling inversions, bevelling outer edges
is no longer necesary, since pointy outer edges won't cause nasty
opaque artifacts.

A couple of other quality improvements: on intersection, always lerp
the alpha of connector edge, even if the opposite edge is an inner edge
(later, when these edges are collapsed, we need this value to compute
the correct alpha). Fix the case where an intruding outer vertex
intersects exactly with an inner edge by maxing its alpha with the
computed value in check_for_intersection(). Finally, we also no longer
round off the intersections produced by Line::intersect(), since it
introduces a loss of quality with no measurable performance benefit.

Change-Id: I6fd93df3a57fffc0895e8cb68adbdba626ded0f1
Reviewed-on: https://skia-review.googlesource.com/8028
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Stephan White <senorblanco@chromium.org>

gm/thinconcavepaths.cpp
src/gpu/GrTessellator.cpp

index 7e4f15e..bc28d6b 100644 (file)
@@ -35,26 +35,150 @@ void draw_thin_right_angle(SkCanvas* canvas, const SkPaint& paint, SkScalar widt
     canvas->drawPath(path, paint);
 }
 
+// Test thin horizontal line (<1 pixel) which should give lower alpha.
+void draw_golf_club(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkPath path;
+    path.moveTo(20, 10);
+    path.lineTo(80, 10);
+    path.lineTo(80, 10 + width);
+    path.lineTo(30, 10 + width);
+    path.lineTo(30, 20);
+    path.lineTo(20, 20);
+    canvas->drawPath(path, paint);
+}
+
+// Test thin lines between two filled regions. The outer edges overlap, but
+// there are no inverted edges to fix.
+void draw_barbell(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkScalar offset = width * 0.5f;
+    SkPath path;
+    path.moveTo(30,  5);
+    path.lineTo(40 - offset, 15 - offset);
+    path.lineTo(60 + offset, 15 - offset);
+    path.lineTo(70,  5);
+    path.lineTo(70, 25);
+    path.lineTo(60 + offset, 15 + offset);
+    path.lineTo(40 - offset, 15 + offset);
+    path.lineTo(30, 25);
+    canvas->drawPath(path, paint);
+}
+
+// Test a thin rectangle and triangle. The top and bottom inner edges of the
+// rectangle and all inner edges of the triangle invert on stroking.
+void draw_thin_rect_and_triangle(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkPath path;
+    path.moveTo(30,  5);
+    path.lineTo(30 + width,  5);
+    path.lineTo(30 + width,  25);
+    path.lineTo(30,  25);
+    path.moveTo(40,  5);
+    path.lineTo(40 + width,  5);
+    path.lineTo(40,  25);
+    canvas->drawPath(path, paint);
+}
+
+// Two triangles joined by a very thin bridge. The tiny triangle formed
+// by the inner edges at the bridge is inverted.
+// (These are actually now more phat pants than hipster pants.)
+void draw_hipster_pants(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkPath path;
+    path.moveTo(10, 10);
+    path.lineTo(10, 20);
+    path.lineTo(50, 10 + width);
+    path.lineTo(90, 20);
+    path.lineTo(90, 10);
+    canvas->drawPath(path, paint);
+}
+
+// A thin z-shape whose interior inverts on stroking. The top and bottom inner edges invert, and
+// the connector edges at the "elbows" intersect the inner edges.
+void draw_skinny_snake(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkPath path;
+    path.moveTo(20 + width, 10);
+    path.lineTo(20 + width, 20);
+    path.lineTo(10 + width, 30);
+    path.lineTo(10 + width, 40);
+    path.lineTo(10 - width, 40);
+    path.lineTo(10 - width, 30);
+    path.lineTo(20 - width, 20);
+    path.lineTo(20 - width, 10);
+    canvas->drawPath(path, paint);
+}
+
+// Test pointy features whose outer edges extend far to the right on stroking.
+void draw_pointy_golf_club(SkCanvas* canvas, const SkPaint& paint, SkScalar width) {
+    SkPath path;
+    path.moveTo(20, 10);
+    path.lineTo(80, 10 + width * 0.5);
+    path.lineTo(30, 10 + width);
+    path.lineTo(30, 20);
+    path.lineTo(20, 20);
+    canvas->drawPath(path, paint);
+}
+
 };
 
-DEF_SIMPLE_GM(thinconcavepaths, canvas, 400, 400) {
+DEF_SIMPLE_GM(thinconcavepaths, canvas, 550, 400) {
     SkPaint paint;
 
     paint.setAntiAlias(true);
     paint.setStyle(SkPaint::kFill_Style);
 
     canvas->save();
-    for (SkScalar width = 0.5; width < 2.05; width += 0.25) {
+    for (SkScalar width = 0.5f; width < 2.05f; width += 0.25f) {
         draw_thin_stroked_rect(canvas, paint, width);
         canvas->translate(0, 25);
     }
     canvas->restore();
     canvas->translate(50, 0);
     canvas->save();
-    for (SkScalar width = 0.5; width < 2.05; width += 0.25) {
+    for (SkScalar width = 0.5f; width < 2.05f; width += 0.25f) {
         draw_thin_right_angle(canvas, paint, width);
         canvas->translate(0, 25);
     }
     canvas->restore();
+    canvas->translate(40, 0);
+    canvas->save();
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_golf_club(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
+    canvas->translate(70, 0);
+    canvas->save();
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_thin_rect_and_triangle(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
+    canvas->translate(30, 0);
+    canvas->save();
+
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_barbell(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
+    canvas->translate(80, 0);
+    canvas->save();
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_hipster_pants(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
+    canvas->translate(100, 0);
+    canvas->save();
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_skinny_snake(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
+    canvas->translate(30, 0);
+    canvas->save();
+    for (SkScalar width = 0.2f; width < 2.1f; width += 0.2f) {
+        draw_pointy_golf_club(canvas, paint, width);
+        canvas->translate(0, 30);
+    }
+    canvas->restore();
     canvas->translate(100, 0);
 }
index af6fe21..ff050ca 100644 (file)
@@ -217,6 +217,9 @@ inline void* emit_vertex(Vertex* v, const AAParams* aaParams, void* data) {
 }
 
 void* emit_triangle(Vertex* v0, Vertex* v1, Vertex* v2, const AAParams* aaParams, void* data) {
+    LOG("emit_triangle (%g, %g) %d\n", v0->fPoint.fX, v0->fPoint.fY, v0->fAlpha);
+    LOG("              (%g, %g) %d\n", v1->fPoint.fX, v1->fPoint.fY, v1->fAlpha);
+    LOG("              (%g, %g) %d\n", v2->fPoint.fX, v2->fPoint.fY, v2->fAlpha);
 #if TESSELLATOR_WIREFRAME
     data = emit_vertex(v0, aaParams, data);
     data = emit_vertex(v1, aaParams, data);
@@ -287,7 +290,6 @@ struct Line {
         double scale = 1.0f / denom;
         point->fX = SkDoubleToScalar((fB * other.fC - other.fB * fC) * scale);
         point->fY = SkDoubleToScalar((other.fA * fC - fA * other.fC) * scale);
-        round(point);
         return true;
     }
     double fA, fB, fC;
@@ -391,12 +393,15 @@ struct Edge {
         p->fX = SkDoubleToScalar(fTop->fPoint.fX - s * fLine.fB);
         p->fY = SkDoubleToScalar(fTop->fPoint.fY + s * fLine.fA);
         if (alpha) {
-            if (fType == Type::kInner || other.fType == Type::kInner) {
-                *alpha = 255;
+            if (fType == Type::kConnector) {
+                *alpha = (1.0 - s) * fTop->fAlpha + s * fBottom->fAlpha;
+            } else if (other.fType == Type::kConnector) {
+                double t = tNumer / denom;
+                *alpha = (1.0 - t) * other.fTop->fAlpha + t * other.fBottom->fAlpha;
             } else if (fType == Type::kOuter && other.fType == Type::kOuter) {
                 *alpha = 0;
             } else {
-                *alpha = (1.0 - s) * fTop->fAlpha + s * fBottom->fAlpha;
+                *alpha = 255;
             }
         }
         return true;
@@ -1138,6 +1143,7 @@ Vertex* check_for_intersection(Edge* edge, Edge* other, EdgeList* activeEdges, C
             split_edge(edge, v, activeEdges, c, alloc);
             split_edge(other, v, activeEdges, c, alloc);
         }
+        v->fAlpha = SkTMax(v->fAlpha, alpha);
         return v;
     }
     return nullptr;
@@ -1508,6 +1514,20 @@ void simplify_boundary(EdgeList* boundary, Comparator& c, SkChunkAlloc& alloc) {
     }
 }
 
+void fix_inversions(Vertex* prev, Vertex* next, Edge* prevBisector, Edge* nextBisector,
+                    Edge* prevEdge, Comparator& c) {
+    if (!prev || !next) {
+        return;
+    }
+    int winding = c.sweep_lt(prev->fPoint, next->fPoint) ? 1 : -1;
+    SkPoint p;
+    uint8_t alpha;
+    if (winding != prevEdge->fWinding && prevBisector->intersect(*nextBisector, &p, &alpha)) {
+        prev->fPoint = next->fPoint = p;
+        prev->fAlpha = next->fAlpha = alpha;
+    }
+}
+
 // Stage 5d: Displace edges by half a pixel inward and outward along their normals. Intersect to
 // find new vertices, and set zero alpha on the exterior and one alpha on the interior. Build a
 // new antialiased mesh from those vertices.
@@ -1522,8 +1542,7 @@ void boundary_to_aa_mesh(EdgeList* boundary, VertexList* mesh, Comparator& c, Sk
     prevOuter.fC += offset;
     VertexList innerVertices;
     VertexList outerVertices;
-    SkVector prevNormal;
-    get_edge_normal(prevEdge, &prevNormal);
+    Edge* prevBisector = nullptr;
     for (Edge* e = boundary->fHead; e != nullptr; e = e->fRight) {
         double offset = radius * sqrt(e->fLine.magSq()) * e->fWinding;
         Line inner(e->fTop, e->fBottom);
@@ -1535,25 +1554,18 @@ void boundary_to_aa_mesh(EdgeList* boundary, VertexList* mesh, Comparator& c, Sk
         get_edge_normal(e, &normal);
         if (prevInner.intersect(inner, &innerPoint) &&
             prevOuter.intersect(outer, &outerPoint)) {
-            // cos(theta) < -0.999 implies a miter angle of ~2.5 degrees,
-            // below which we'll bevel the outer edges.
-            if (prevNormal.dot(normal) < -0.999) {
-                SkPoint p = e->fWinding > 0 ? e->fTop->fPoint : e->fBottom->fPoint;
-                SkPoint outerPoint1 = p - prevNormal * radius;
-                SkPoint outerPoint2 = p - normal * radius;
-                innerVertices.append(ALLOC_NEW(Vertex, (innerPoint, 255), alloc));
-                innerVertices.append(ALLOC_NEW(Vertex, (innerPoint, 255), alloc));
-                outerVertices.append(ALLOC_NEW(Vertex, (outerPoint1, 0), alloc));
-                outerVertices.append(ALLOC_NEW(Vertex, (outerPoint2, 0), alloc));
-            } else {
-                innerVertices.append(ALLOC_NEW(Vertex, (innerPoint, 255), alloc));
-                outerVertices.append(ALLOC_NEW(Vertex, (outerPoint, 0), alloc));
-            }
+            Vertex* innerVertex = ALLOC_NEW(Vertex, (innerPoint, 255), alloc);
+            Vertex* outerVertex = ALLOC_NEW(Vertex, (outerPoint, 0), alloc);
+            Edge* bisector = new_edge(outerVertex, innerVertex, Edge::Type::kConnector, c, alloc);
+            fix_inversions(innerVertices.fTail, innerVertex, prevBisector, bisector, prevEdge, c);
+            fix_inversions(outerVertices.fTail, outerVertex, prevBisector, bisector, prevEdge, c);
+            innerVertices.append(innerVertex);
+            outerVertices.append(outerVertex);
+            prevBisector = bisector;
         }
         prevInner = inner;
         prevOuter = outer;
         prevEdge = e;
-        prevNormal = normal;
     }
     innerVertices.close();
     outerVertices.close();
@@ -1563,6 +1575,10 @@ void boundary_to_aa_mesh(EdgeList* boundary, VertexList* mesh, Comparator& c, Sk
     if (!innerVertex || !outerVertex) {
         return;
     }
+    Edge* bisector = new_edge(outerVertices.fHead, innerVertices.fHead, Edge::Type::kConnector, c,
+                              alloc);
+    fix_inversions(innerVertices.fTail, innerVertices.fHead, prevBisector, bisector, prevEdge, c);
+    fix_inversions(outerVertices.fTail, outerVertices.fHead, prevBisector, bisector, prevEdge, c);
     do {
         // Connect vertices into a quad mesh. Outer edges get default (1) winding.
         // Inner edges get -2 winding. This ensures that the interior is always filled