Improve coverage AA for thin quads

This brings back the 2px wide picture frame approach described in these
slides: go/thin-line-aa. This has been in place when the per-edge-aa
only needed to support rectangles but was scrapped when the code path
was updated to support arbitrary quadrilaterals.

I opted to have the GrQuadPerEdgeAA logic check for degeneracy and
update what it requests for the outset. This scale factor out to a 2px
wide shape makes sense in the context of anti-aliasing, but not so for
the generalized inset/outset logic defined in GrQuadUtils. It would have
been more efficient to implement it there, but would have locked it in
to being just AA inset/outset.

I also updated SkGpuDevice's drawStrokedLine to construct the quad of
the line directly, and to always turn a line path into a rect, instead
of restricting it based on matrix or stroked width. With this new change
the quality of the fill rect is much higher under rotations and
perspective compared to the hairline.

See rect case: https://drive.google.com/file/d/1xwgG5heADcdXYShsDodgbuv2tbHfuBNt/view?usp=sharing
Hairline case: https://drive.google.com/file/d/1duNLxiYLLJhsJ94Uc01rSxjB4ar6_Ud9/view?usp=sharing

Bug: chromium:820987
Change-Id: Ibd58b89a467ad5a61c5479d11259024259f1bb47
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/329418
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
Michael Ludwig 2020-10-27 09:28:59 -04:00 committed by Skia Commit-Bot
parent 5943feea46
commit 290d6df49b
4 changed files with 78 additions and 34 deletions

View File

@ -20,6 +20,7 @@
#include "src/core/SkCanvasPriv.h"
#include "src/core/SkClipStack.h"
#include "src/core/SkDraw.h"
#include "src/core/SkDrawProcs.h"
#include "src/core/SkImageFilterCache.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkLatticeIter.h"
@ -614,41 +615,38 @@ void SkGpuDevice::drawStrokedLine(const SkPoint points[2],
const SkScalar halfWidth = 0.5f * origPaint.getStrokeWidth();
SkASSERT(halfWidth > 0);
SkVector v = points[1] - points[0];
SkVector parallel = points[1] - points[0];
SkScalar length = SkPoint::Normalize(&v);
if (!length) {
v.fX = 1.0f;
v.fY = 0.0f;
if (!SkPoint::Normalize(&parallel)) {
parallel.fX = 1.0f;
parallel.fY = 0.0f;
}
parallel *= halfWidth;
SkVector ortho = { parallel.fY, -parallel.fX };
if (SkPaint::kButt_Cap == origPaint.getStrokeCap()) {
// No extra extension for butt caps
parallel = {0.f, 0.f};
}
// Order is TL, TR, BR, BL where arbitrarily "down" is p0 to p1 and "right" is positive
SkPoint corners[4] = { points[0] - ortho - parallel,
points[0] + ortho - parallel,
points[1] + ortho + parallel,
points[1] - ortho + parallel };
SkPaint newPaint(origPaint);
newPaint.setStyle(SkPaint::kFill_Style);
SkScalar xtraLength = 0.0f;
if (SkPaint::kButt_Cap != origPaint.getStrokeCap()) {
xtraLength = halfWidth;
}
SkPoint mid = points[0] + points[1];
mid.scale(0.5f);
SkRect rect = SkRect::MakeLTRB(mid.fX-halfWidth, mid.fY - 0.5f*length - xtraLength,
mid.fX+halfWidth, mid.fY + 0.5f*length + xtraLength);
SkMatrix local;
local.setSinCos(v.fX, -v.fY, mid.fX, mid.fY);
SkPreConcatMatrixProvider matrixProvider(this->asMatrixProvider(), local);
GrPaint grPaint;
if (!SkPaintToGrPaint(this->recordingContext(), fRenderTargetContext->colorInfo(), newPaint,
matrixProvider, &grPaint)) {
this->asMatrixProvider(), &grPaint)) {
return;
}
fRenderTargetContext->fillRectWithLocalMatrix(this->clip(), std::move(grPaint),
GrAA(newPaint.isAntiAlias()),
matrixProvider.localToDevice(), rect, local);
GrAA aa = newPaint.isAntiAlias() ? GrAA::kYes : GrAA::kNo;
GrQuadAAFlags edgeAA = newPaint.isAntiAlias() ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone;
fRenderTargetContext->fillQuadWithEdgeAA(this->clip(), std::move(grPaint), aa, edgeAA,
this->localToDevice(), corners, nullptr);
}
void SkGpuDevice::drawPath(const SkPath& origSrcPath, const SkPaint& paint, bool pathIsMutable) {
@ -659,16 +657,21 @@ void SkGpuDevice::drawPath(const SkPath& origSrcPath, const SkPaint& paint, bool
}
#endif
ASSERT_SINGLE_OWNER
if (!origSrcPath.isInverseFillType() && !paint.getPathEffect()) {
if (!origSrcPath.isInverseFillType() && !paint.getPathEffect() && !paint.getMaskFilter() &&
SkPaint::kStroke_Style == paint.getStyle() && paint.getStrokeWidth() > 0.f &&
SkPaint::kRound_Cap != paint.getStrokeCap()) {
SkPoint points[2];
if (SkPaint::kStroke_Style == paint.getStyle() && paint.getStrokeWidth() > 0 &&
!paint.getMaskFilter() && SkPaint::kRound_Cap != paint.getStrokeCap() &&
this->localToDevice().preservesRightAngles() && origSrcPath.isLine(points)) {
// Path-based stroking looks better for thin rects
SkScalar strokeWidth = this->localToDevice().getMaxScale() * paint.getStrokeWidth();
if (strokeWidth >= 1.0f) {
// Round capping support is currently disabled b.c. it would require a RRect
// GrDrawOp that takes a localMatrix.
if (origSrcPath.isLine(points)) {
// The stroked line is an oriented rectangle, which looks the same or better
// (if perspective) compared to path rendering. The exception is subpixel/hairline lines
// that are non-AA or MSAA, in which case the default path renderer achieves higher
// quality.
// FIXME(michaelludwig): If the fill rect op could take an external coverage, or
// checks for and outsets thin non-aa rects to 1px, the path renderer could be skipped.
SkScalar coverage;
if ((paint.isAntiAlias() && fRenderTargetContext->numSamples() == 1) ||
!SkDrawTreatAAStrokeAsHairline(paint.getStrokeWidth(), this->localToDevice(),
&coverage)) {
this->drawStrokedLine(points, paint);
return;
}

View File

@ -1096,6 +1096,22 @@ void TessellationHelper::outset(const skvx::Vec<4, float>& edgeDistances,
outset.asGrQuads(deviceOutset, fDeviceType, localOutset, fLocalType);
}
void TessellationHelper::getEdgeEquations(skvx::Vec<4, float>* a,
skvx::Vec<4, float>* b,
skvx::Vec<4, float>* c) {
SkASSERT(a && b && c);
SkASSERT(fVerticesValid);
const EdgeEquations& eq = this->getEdgeEquations();
*a = eq.fA;
*b = eq.fB;
*c = eq.fC;
}
skvx::Vec<4, float> TessellationHelper::getEdgeLengths() {
SkASSERT(fVerticesValid);
return 1.f / fEdgeVectors.fInvLengths;
}
const TessellationHelper::OutsetRequest& TessellationHelper::getOutsetRequest(
const skvx::Vec<4, float>& edgeDistances) {
// Much of the code assumes that we start from positive distances and apply it unmodified to

View File

@ -78,6 +78,18 @@ namespace GrQuadUtils {
void outset(const skvx::Vec<4, float>& edgeDistances,
GrQuad* deviceOutset, GrQuad* localOutset);
// Compute the edge equations of the original device space quad passed to 'reset()'. The
// coefficients are stored per-edge in 'a', 'b', and 'c', such that ax + by + c = 0, and
// a positive distance indicates the interior of the quad. Edges are ordered L, B, T, R,
// matching edge distances passed to inset() and outset().
void getEdgeEquations(skvx::Vec<4, float>* a,
skvx::Vec<4, float>* b,
skvx::Vec<4, float>* c);
// Compute the edge lengths of the original device space quad passed to 'reset()'. The
// edge lengths are ordered LBTR to match distances passed to inset() and outset().
skvx::Vec<4, float> getEdgeLengths();
private:
// NOTE: This struct is named 'EdgeVectors' because it holds a lot of cached calculations
// pertaining to the edge vectors of the input quad, projected into 2D device coordinates.

View File

@ -362,7 +362,20 @@ void Tessellator::append(GrQuad* deviceQuad, GrQuad* localQuad,
fWriteProc(&fVertexWriter, fVertexSpec, deviceQuad, localQuad, coverage, color,
geomSubset, uvSubset);
// Then outer vertices, which use 0.f for their coverage
// Then outer vertices, which use 0.f for their coverage. If the inset was degenerate
// to a line (had all coverages < 1), tweak the outset distance so the outer frame's
// narrow axis reaches out to 2px, which gives better animation under translation.
if (coverage[0] < 1.f && coverage[1] < 1.f && coverage[2] < 1.f && coverage[3] < 1.f) {
skvx::Vec<4, float> len = fAAHelper.getEdgeLengths();
// Using max guards us against trying to scale a degenerate triangle edge of 0 len
// up to 2px. The shuffles are so that edge 0's adjustment is based on the lengths
// of its connecting edges (1 and 2), and so forth.
skvx::Vec<4, float> maxWH = max(skvx::shuffle<1, 0, 3, 2>(len),
skvx::shuffle<2, 3, 0, 1>(len));
// wh + 2e' = 2, so e' = (2 - wh) / 2 => e' = e * (2 - wh). But if w or h > 1, then
// 2 - wh < 1 and represents the non-narrow axis so clamp to 1.
edgeDistances *= max(1.f, 2.f - maxWH);
}
fAAHelper.outset(edgeDistances, deviceQuad, localQuad);
fWriteProc(&fVertexWriter, fVertexSpec, deviceQuad, localQuad, kZeroCoverage, color,
geomSubset, uvSubset);