From: Stephen White Date: Mon, 6 Feb 2017 14:50:27 +0000 (-0500) Subject: GrTessellator (AA): improve antialiasing of thin shapes. X-Git-Tag: accepted/tizen/5.0/unified/20181102.025319~55^2~484 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=92eba8a5efc0860d4e95ba7f25052474a0bfca0c;p=platform%2Fupstream%2FlibSkiaSharp.git GrTessellator (AA): improve antialiasing of thin shapes. (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 Commit-Queue: Stephan White --- diff --git a/gm/thinconcavepaths.cpp b/gm/thinconcavepaths.cpp index 7e4f15e..bc28d6b 100644 --- a/gm/thinconcavepaths.cpp +++ b/gm/thinconcavepaths.cpp @@ -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); } diff --git a/src/gpu/GrTessellator.cpp b/src/gpu/GrTessellator.cpp index af6fe21..ff050ca 100644 --- a/src/gpu/GrTessellator.cpp +++ b/src/gpu/GrTessellator.cpp @@ -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