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:
parent
5943feea46
commit
290d6df49b
@ -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(¶llel)) {
|
||||
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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user