Add "radial edges" to tessellated stroking

Tessellated stroking works by creating stroke-width edges orthogonal
to the curve at set locations and then connecting them with a
quad strip. In the past these edges have been spaced evenly in the
parametric sense, but this does not work for areas of tight curvature
like cusps. To work around this we would do expensive CPU work to chop
curves around points of strong curvature and insert combinations of
lines and round joins.

This CL introduces a second set of orthogonal edges called "radial
edges". While parametric edges are spaced evenly in the parametric
sense, radial edges divide the curve's _rotation_ into even steps. The
tessellation shader evaluates both sets of edges (parametric and
radial) and sorts them into a single quad strip. With this new
combined set of edges we can draw any curve and the CPU never has to
worry about curvature.

Bug: skia:10419
Change-Id: I49291cc2efdf0d26835abbe9a9d21643bce0b4da
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/312601
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
This commit is contained in:
Chris Dalton 2020-08-25 11:33:59 -06:00 committed by Skia Commit-Bot
parent f621e234fc
commit a316aa65fc
4 changed files with 592 additions and 514 deletions

View File

@ -8,6 +8,7 @@
#include "src/gpu/tessellate/GrStrokePatchBuilder.h"
#include "include/core/SkStrokeRec.h"
#include "include/private/SkFloatingPoint.h"
#include "include/private/SkNx.h"
#include "src/core/SkGeometry.h"
#include "src/core/SkMathPriv.h"
@ -16,32 +17,17 @@
#include "src/gpu/tessellate/GrVectorXform.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
// This is the maximum distance in pixels that we can stray from the edge of a stroke when
// converting it to flat line segments.
static constexpr float kLinearizationIntolerance = 8; // 1/8 pixel.
constexpr static float kLinearizationIntolerance =
GrTessellationPathRenderer::kLinearizationIntolerance;
constexpr static float kInternalRoundJoinType = GrStrokeTessellateShader::kInternalRoundJoinType;
constexpr static float kStandardCubicType = GrStrokeTessellateShader::kStandardCubicType;
constexpr static float kDoubleSidedRoundJoinType = -GrStrokeTessellateShader::kRoundJoinType;
static Sk2f lerp(const Sk2f& a, const Sk2f& b, float T) {
static SkPoint lerp(const SkPoint& a, const SkPoint& b, float T) {
SkASSERT(1 != T); // The below does not guarantee lerp(a, b, 1) === b.
return (b - a) * T + a;
}
static inline void transpose(const Sk2f& a, const Sk2f& b, Sk2f* X, Sk2f* Y) {
float transpose[4];
a.store(transpose);
b.store(transpose+2);
Sk2f::Load2(transpose, X, Y);
}
static inline float calc_curvature_costheta(const Sk2f& leftTan, const Sk2f& rightTan) {
Sk2f X, Y;
transpose(leftTan, rightTan, &X, &Y);
Sk2f invlength = (X*X + Y*Y).rsqrt();
Sk2f dotprod = leftTan * rightTan;
return (dotprod[0] + dotprod[1]) * invlength[0] * invlength[1];
}
void GrStrokePatchBuilder::allocVertexChunk(int minVertexAllocCount) {
VertexChunk* chunk = &fVertexChunkArray->push_back();
fCurrChunkVertexData = (SkPoint*)fTarget->makeVertexSpaceAtLeast(
@ -68,13 +54,12 @@ SkPoint* GrStrokePatchBuilder::reservePatch() {
return patch;
}
void GrStrokePatchBuilder::writeCubicSegment(float leftJoinType, const SkPoint pts[4],
float overrideNumSegments) {
void GrStrokePatchBuilder::writeCubicSegment(float prevJoinType, const SkPoint pts[4]) {
SkPoint c1 = (pts[1] == pts[0]) ? pts[2] : pts[1];
SkPoint c2 = (pts[2] == pts[3]) ? pts[1] : pts[2];
if (fHasPreviousSegment) {
this->writeJoin(leftJoinType, pts[0], fLastControlPoint, c1);
this->writeJoin(prevJoinType, fLastControlPoint, pts[0], c1);
} else {
fCurrContourFirstControlPoint = c1;
fHasPreviousSegment = true;
@ -82,21 +67,20 @@ void GrStrokePatchBuilder::writeCubicSegment(float leftJoinType, const SkPoint p
if (SkPoint* patch = this->reservePatch()) {
memcpy(patch, pts, sizeof(SkPoint) * 4);
patch[4].set(overrideNumSegments, fCurrStrokeRadius);
patch[4].set(kStandardCubicType, fCurrStrokeRadius);
}
fLastControlPoint = c2;
fCurrentPoint = pts[3];
}
void GrStrokePatchBuilder::writeJoin(float joinType, const SkPoint& anchorPoint,
const SkPoint& prevControlPoint,
const SkPoint& nextControlPoint) {
void GrStrokePatchBuilder::writeJoin(float joinType, const SkPoint& prevControlPoint,
const SkPoint& anchorPoint, const SkPoint& nextControlPoint) {
if (SkPoint* joinPatch = this->reservePatch()) {
joinPatch[0] = anchorPoint;
joinPatch[1] = prevControlPoint;
joinPatch[2] = nextControlPoint;
joinPatch[3] = anchorPoint;
joinPatch[0] = prevControlPoint;
joinPatch[1] = anchorPoint;
joinPatch[2] = anchorPoint;
joinPatch[3] = nextControlPoint;
joinPatch[4].set(joinType, fCurrStrokeRadius);
}
}
@ -105,21 +89,19 @@ void GrStrokePatchBuilder::writeSquareCap(const SkPoint& endPoint, const SkPoint
SkVector v = (endPoint - controlPoint);
v.normalize();
SkPoint capPoint = endPoint + v*fCurrStrokeRadius;
// Construct a line that incorporates controlPoint so we get a water tight edge with the rest of
// the stroke. The cubic will technically step outside the cap, but we will force it to only
// have one segment, giving edges only at the endpoints.
// Add a join to guarantee we get water tight seaming. Make the join type negative so it's
// double sided.
this->writeJoin(-fCurrStrokeJoinType, controlPoint, endPoint, capPoint);
if (SkPoint* capPatch = this->reservePatch()) {
capPatch[0] = endPoint;
capPatch[1] = controlPoint;
// Straddle the midpoint of the cap because the tessellated geometry emits a center point at
// T=.5, and we need to ensure that point stays inside the cap.
capPatch[2] = endPoint + capPoint - controlPoint;
capPatch[1] = endPoint;
capPatch[2] = capPoint;
capPatch[3] = capPoint;
capPatch[4].set(1, fCurrStrokeRadius);
capPatch[4].set(kStandardCubicType, fCurrStrokeRadius);
}
}
void GrStrokePatchBuilder::writeCaps() {
void GrStrokePatchBuilder::writeCaps(SkPaint::Cap capType) {
if (!fHasPreviousSegment) {
// We don't have any control points to orient the caps. In this case, square and round caps
// are specified to be drawn as an axis-aligned square or circle respectively. Assign
@ -129,15 +111,15 @@ void GrStrokePatchBuilder::writeCaps() {
fCurrentPoint = fCurrContourStartPoint;
}
switch (fCurrStrokeCapType) {
switch (capType) {
case SkPaint::kButt_Cap:
break;
case SkPaint::kRound_Cap:
// A round cap is the same thing as a 180-degree round join.
this->writeJoin(GrStrokeTessellateShader::kRoundJoinType, fCurrContourStartPoint,
fCurrContourFirstControlPoint, fCurrContourFirstControlPoint);
this->writeJoin(GrStrokeTessellateShader::kRoundJoinType, fCurrentPoint,
fLastControlPoint, fLastControlPoint);
this->writeJoin(GrStrokeTessellateShader::kRoundJoinType, fCurrContourFirstControlPoint,
fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->writeJoin(GrStrokeTessellateShader::kRoundJoinType, fLastControlPoint,
fCurrentPoint, fLastControlPoint);
break;
case SkPaint::kSquare_Cap:
this->writeSquareCap(fCurrContourStartPoint, fCurrContourFirstControlPoint);
@ -146,53 +128,6 @@ void GrStrokePatchBuilder::writeCaps() {
}
}
void GrStrokePatchBuilder::addPath(const SkPath& path, const SkStrokeRec& stroke) {
this->beginPath(stroke);
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, rawPts, w] : SkPathPriv::Iterate(path)) {
SkPoint pts[4];
int numPtsInVerb = SkPathPriv::PtsInIter((unsigned)verb);
for (int i = 0; i < numPtsInVerb; ++i) {
// TEMPORORY: Scale all the points up front. SkFind*MaxCurvature and GrWangsFormula::*
// both expect arrays of points. As we refine this class and its math, this scale will
// hopefully be integrated more efficiently.
pts[i] = rawPts[i] * fMatrixScale;
}
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps();
}
this->moveTo(pts[0]);
break;
case SkPathVerb::kClose:
this->close();
break;
case SkPathVerb::kLine:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->lineTo(pts[0], pts[1]);
break;
case SkPathVerb::kQuad:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->quadraticTo(pts);
break;
case SkPathVerb::kCubic:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->cubicTo(pts);
break;
case SkPathVerb::kConic:
SkASSERT(previousVerb != SkPathVerb::kClose);
SkUNREACHABLE;
}
previousVerb = verb;
}
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps();
}
}
static float join_type_from_join(SkPaint::Join join) {
switch (join) {
case SkPaint::kBevel_Join:
@ -205,23 +140,73 @@ static float join_type_from_join(SkPaint::Join join) {
SkUNREACHABLE;
}
void GrStrokePatchBuilder::beginPath(const SkStrokeRec& stroke) {
void GrStrokePatchBuilder::addPath(const SkPath& path, const SkStrokeRec& stroke) {
// We don't support hairline strokes. For now, the client can transform the path into device
// space and then use a stroke width of 1.
SkASSERT(stroke.getWidth() > 0);
fCurrStrokeRadius = stroke.getWidth()/2 * fMatrixScale;
fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
fCurrStrokeCapType = stroke.getCap();
// Find the angle of curvature where the arc height above a simple line from point A to point B
// is equal to 1/kLinearizationIntolerance. (The arc height is always the same no matter how
// long the line is. What we are interested in is the difference in height between the part of
// the stroke whose normal is orthogonal to the line, vs the heights at the endpoints.)
float r = std::max(1 - 1/(fCurrStrokeRadius * kLinearizationIntolerance), 0.f);
fMaxCurvatureCosTheta = 2*r*r - 1;
// This is the number of radial segments we need to add to a triangle strip for each radian of
// rotation, given the current stroke radius. Any fewer radial segments and our error would fall
// outside the linearization tolerance.
fNumRadialSegmentsPerRad = 1 / std::acos(
std::max(1 - 1 / (kLinearizationIntolerance * fCurrStrokeRadius), -1.f));
// Calculate the worst-case numbers of parametric segments our hardware can support for the
// current stroke radius, in the event that there are also enough radial segments to rotate 180
// and 360 degrees respectively. These are used for "quick accepts" that allow us to send almost
// all curves directly to the hardware without having to chop or think any further.
fMaxParametricSegments180 = fMaxTessellationSegments + 1 - std::max(std::ceil(
SK_ScalarPI * fNumRadialSegmentsPerRad), 1.f);
fMaxParametricSegments360 = fMaxTessellationSegments + 1 - std::max(std::ceil(
2*SK_ScalarPI * fNumRadialSegmentsPerRad), 1.f);
fHasPreviousSegment = false;
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, rawPts, w] : SkPathPriv::Iterate(path)) {
SkPoint pts[4];
int numPtsInVerb = SkPathPriv::PtsInIter((unsigned)verb);
for (int i = 0; i < numPtsInVerb; ++i) {
// TEMPORORY: Scale all the points up front. SkFind*MaxCurvature and GrWangsFormula::*
// both expect arrays of points. As we refine this class and its math, this scale will
// hopefully be integrated more efficiently.
pts[i] = rawPts[i] * fMatrixScale;
}
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps(stroke.getCap());
}
this->moveTo(pts[0]);
break;
case SkPathVerb::kClose:
this->close(stroke.getCap());
break;
case SkPathVerb::kLine:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->lineTo(fCurrStrokeJoinType, pts[0], pts[1]);
break;
case SkPathVerb::kQuad:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->quadraticTo(fCurrStrokeJoinType, pts);
break;
case SkPathVerb::kCubic:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->cubicTo(fCurrStrokeJoinType, pts);
break;
case SkPathVerb::kConic:
SkASSERT(previousVerb != SkPathVerb::kClose);
SkUNREACHABLE;
}
previousVerb = verb;
}
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps(stroke.getCap());
}
}
void GrStrokePatchBuilder::moveTo(const SkPoint& pt) {
@ -229,243 +214,149 @@ void GrStrokePatchBuilder::moveTo(const SkPoint& pt) {
fCurrContourStartPoint = pt;
}
void GrStrokePatchBuilder::lineTo(const SkPoint& p0, const SkPoint& p1) {
this->lineTo(fCurrStrokeJoinType, p0, p1);
}
void GrStrokePatchBuilder::lineTo(float leftJoinType, const SkPoint& pt0, const SkPoint& pt1) {
Sk2f p0 = Sk2f::Load(&pt0);
Sk2f p1 = Sk2f::Load(&pt1);
if ((p0 == p1).allTrue()) {
return;
}
this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 1/3.f), lerp(p0, p1, 2/3.f), p1, 1);
}
void GrStrokePatchBuilder::quadraticTo(const SkPoint P[3]) {
this->quadraticTo(fCurrStrokeJoinType, P, SkFindQuadMaxCurvature(P));
}
void GrStrokePatchBuilder::quadraticTo(float leftJoinType, const SkPoint P[3],
float maxCurvatureT) {
if (P[1] == P[0] || P[1] == P[2]) {
this->lineTo(leftJoinType, P[0], P[2]);
void GrStrokePatchBuilder::lineTo(float prevJoinType, const SkPoint& p0, const SkPoint& p1) {
// Zero-length paths need special treatment because they are spec'd to behave differently.
if (p0 == p1) {
return;
}
// Decide a lower bound on the length (in parametric sense) of linear segments the curve will be
// chopped into.
int numSegments = 1 << GrWangsFormula::quadratic_log2(kLinearizationIntolerance, P);
float segmentLength = SkScalarInvert(numSegments);
SkPoint cubic[4] = {p0, p0, p1, p1};
this->writeCubicSegment(prevJoinType, cubic);
}
Sk2f p0 = Sk2f::Load(P);
Sk2f p1 = Sk2f::Load(P+1);
Sk2f p2 = Sk2f::Load(P+2);
Sk2f tan0 = p1 - p0;
Sk2f tan1 = p2 - p1;
// At + B gives a vector tangent to the quadratic.
Sk2f A = p0 - p1*2 + p2;
Sk2f B = p1 - p0;
// Find a line segment that crosses max curvature.
float leftT = maxCurvatureT - segmentLength/2;
float rightT = maxCurvatureT + segmentLength/2;
Sk2f leftTan, rightTan;
if (leftT <= 0) {
leftT = 0;
leftTan = tan0;
rightT = segmentLength;
rightTan = A*rightT + B;
} else if (rightT >= 1) {
leftT = 1 - segmentLength;
leftTan = A*leftT + B;
rightT = 1;
rightTan = tan1;
} else {
leftTan = A*leftT + B;
rightTan = A*rightT + B;
void GrStrokePatchBuilder::quadraticTo(float prevJoinType, const SkPoint p[3], int maxDepth) {
// The stroker relies on p1 to find tangents at the endpoints. (We have to treat the endpoint
// tangents carefully in order to get water tight seams with the join segments.) If p1 is
// colocated on an endpoint then we need to draw this quadratic as a line instead.
if (p[1] == p[0] || p[1] == p[2]) {
this->lineTo(prevJoinType, p[0], p[2]);
return;
}
// Check if curvature is too strong for a triangle strip on the line segment that crosses max
// curvature. If it is, we will chop and convert the segment to a "lineTo" with round joins.
// Ensure our hardware supports enough tessellation segments to render the curve. The first
// branch assumes a worst-case rotation of 180 degrees and checks if even then we have enough.
// In practice it is rare to take even the first branch.
float numParametricSegments = GrWangsFormula::quadratic(kLinearizationIntolerance, p);
if (numParametricSegments > fMaxParametricSegments180 && maxDepth != 0) {
// We still might have enough tessellation segments to render the curve. Check again with
// the actual rotation.
float numRadialSegments = SkMeasureQuadRotation(p) * fNumRadialSegmentsPerRad;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
if (numParametricSegments + numRadialSegments - 1 > fMaxTessellationSegments) {
// The hardware doesn't support enough segments for this curve. Chop and recurse.
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
SkASSERT(maxDepth >= 1);
}
SkPoint chopped[5];
if (numParametricSegments >= numRadialSegments) {
SkChopQuadAtHalf(p, chopped);
} else {
SkChopQuadAtMidTangent(p, chopped);
}
this->quadraticTo(prevJoinType, chopped, maxDepth - 1);
// Use kDoubleSidedRoundJoinType in case we happened to chop at the exact turnaround
// point of a flat quadratic, in which case we would lose 180 degrees of rotation.
this->quadraticTo(kDoubleSidedRoundJoinType, chopped + 2, maxDepth - 1);
return;
}
}
SkPoint cubic[4] = {p[0], lerp(p[0], p[1], 2/3.f), lerp(p[1], p[2], 1/3.f), p[2]};
this->writeCubicSegment(prevJoinType, cubic);
}
void GrStrokePatchBuilder::cubicTo(float prevJoinType, const SkPoint inputPts[4]) {
const SkPoint* p = inputPts;
int numCubics = 1;
SkPoint chopped[10];
double tt[2], ss[2];
if (SkClassifyCubic(p, tt, ss) == SkCubicType::kSerpentine) {
// TEMPORARY: Don't allow cubics to have inflection points.
// TODO: This will soon be moved into the GPU tessellation pipeline and handled more
// elegantly.
float t[2] = {(float)(tt[0]/ss[0]), (float)(tt[1]/ss[1])};
const float* begin = (t[0] > 0 && t[0] < 1) ? t : t+1;
const float* end = (t[1] > 0 && t[1] > t[0] && t[1] < 1) ? t+2 : t+1;
numCubics = (end - begin) + 1;
if (numCubics > 1) {
SkChopCubicAt(p, chopped, begin, end - begin);
p = chopped;
}
} else if (SkMeasureNonInflectCubicRotation(p) > SK_ScalarPI*15/16) {
// TEMPORARY: Don't allow cubics to turn more than 180 degrees. We chop them when they get
// close, just to be sure.
// TODO: This will soon be moved into the GPU tessellation pipeline and handled more
// elegantly.
SkChopCubicAtMidTangent(p, chopped);
p = chopped;
numCubics = 2;
}
for (int i = 0; i < numCubics; ++i) {
this->nonInflectCubicTo(prevJoinType, p + i*3);
// Use kDoubleSidedRoundJoinType in case we happened to chop at an exact cusp or turnaround
// point of a flat cubic, in which case we would lose 180 degrees of rotation.
prevJoinType = kDoubleSidedRoundJoinType;
}
}
void GrStrokePatchBuilder::nonInflectCubicTo(float prevJoinType, const SkPoint p[4], int maxDepth) {
// The stroker relies on p1 and p2 to find tangents at the endpoints. (We have to treat the
// endpoint tangents carefully in order to get water tight seams with the join segments.) If p0
// and p1 are both colocated on an endpoint then we need to draw this cubic as a line instead.
if (p[1] == p[2] && (p[1] == p[0] || p[1] == p[3])) {
this->lineTo(prevJoinType, p[0], p[3]);
return;
}
// Ensure our hardware supports enough tessellation segments to render the curve. The first
// branch assumes a worst-case rotation of 360 degrees and checks if even then we have enough.
// In practice it is rare to take even the first branch.
//
// FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
// would benefit significantly from a quick reject that detects curves that don't need special
// treatment for strong curvature.
if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
SkPoint ptsBuffer[5];
const SkPoint* currQuadratic = P;
if (leftT > 0) {
SkChopQuadAt(currQuadratic, ptsBuffer, leftT);
this->quadraticTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1);
if (rightT < 1) {
rightT = (rightT - leftT) / (1 - leftT);
// NOTE: We could technically assume a worst-case rotation of 180 because cubicTo() chops at
// midtangents and inflections. However, this is only temporary so we leave it at 360 where it
// will arrive at in the future.
float numParametricSegments = GrWangsFormula::cubic(kLinearizationIntolerance, p);
if (numParametricSegments > fMaxParametricSegments360 && maxDepth != 0) {
// We still might have enough tessellation segments to render the curve. Check again with
// the actual rotation.
float numRadialSegments = SkMeasureNonInflectCubicRotation(p) * fNumRadialSegmentsPerRad;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
if (numParametricSegments + numRadialSegments - 1 > fMaxTessellationSegments) {
// The hardware doesn't support enough segments for this curve. Chop and recurse.
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
SkASSERT(maxDepth >= 1);
}
currQuadratic = ptsBuffer + 2;
} else {
this->rotateTo(leftJoinType, currQuadratic[0], currQuadratic[1]);
}
if (rightT < 1) {
SkChopQuadAt(currQuadratic, ptsBuffer, rightT);
this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[2]);
this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 2, /*maxCurvatureT=*/0);
} else {
this->lineTo(kInternalRoundJoinType, currQuadratic[0], currQuadratic[2]);
this->rotateTo(kInternalRoundJoinType, currQuadratic[2],
currQuadratic[2]*2 - currQuadratic[1]);
}
return;
}
if (numSegments > fMaxTessellationSegments) {
SkPoint ptsBuffer[5];
SkChopQuadAt(P, ptsBuffer, 0.5f);
this->quadraticTo(leftJoinType, ptsBuffer, 0);
this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 3, 0);
return;
}
this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 2/3.f), lerp(p1, p2, 1/3.f), p2);
}
void GrStrokePatchBuilder::cubicTo(const SkPoint P[4]) {
float roots[3];
int numRoots = SkFindCubicMaxCurvature(P, roots);
this->cubicTo(fCurrStrokeJoinType, P,
numRoots > 0 ? roots[numRoots/2] : 0,
numRoots > 1 ? roots[0] : kLeftMaxCurvatureNone,
numRoots > 2 ? roots[2] : kRightMaxCurvatureNone);
}
void GrStrokePatchBuilder::cubicTo(float leftJoinType, const SkPoint P[4], float maxCurvatureT,
float leftMaxCurvatureT, float rightMaxCurvatureT) {
if (P[1] == P[2] && (P[1] == P[0] || P[1] == P[3])) {
this->lineTo(leftJoinType, P[0], P[3]);
return;
}
// Decide a lower bound on the length (in parametric sense) of linear segments the curve will be
// chopped into.
int numSegments = 1 << GrWangsFormula::cubic_log2(kLinearizationIntolerance, P);
float segmentLength = SkScalarInvert(numSegments);
Sk2f p0 = Sk2f::Load(P);
Sk2f p1 = Sk2f::Load(P+1);
Sk2f p2 = Sk2f::Load(P+2);
Sk2f p3 = Sk2f::Load(P+3);
Sk2f tan0 = p1 - p0;
Sk2f tan1 = p3 - p2;
// At^2 + Bt + C gives a vector tangent to the cubic. (More specifically, it's the derivative
// minus an irrelevant scale by 3, since all we care about is the direction.)
Sk2f A = p3 + (p1 - p2)*3 - p0;
Sk2f B = (p0 - p1*2 + p2)*2;
Sk2f C = p1 - p0;
// Find a line segment that crosses max curvature.
float leftT = maxCurvatureT - segmentLength/2;
float rightT = maxCurvatureT + segmentLength/2;
Sk2f leftTan, rightTan;
if (leftT <= 0) {
leftT = 0;
leftTan = tan0;
rightT = segmentLength;
rightTan = A*rightT*rightT + B*rightT + C;
} else if (rightT >= 1) {
leftT = 1 - segmentLength;
leftTan = A*leftT*leftT + B*leftT + C;
rightT = 1;
rightTan = tan1;
} else {
leftTan = A*leftT*leftT + B*leftT + C;
rightTan = A*rightT*rightT + B*rightT + C;
}
// Check if curvature is too strong for a triangle strip on the line segment that crosses max
// curvature. If it is, we will chop and convert the segment to a "lineTo" with round joins.
//
// FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
// would benefit significantly from a quick reject that detects curves that don't need special
// treatment for strong curvature.
if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
SkPoint ptsBuffer[7];
p0.store(ptsBuffer);
p1.store(ptsBuffer + 1);
p2.store(ptsBuffer + 2);
p3.store(ptsBuffer + 3);
const SkPoint* currCubic = ptsBuffer;
if (leftT > 0) {
SkChopCubicAt(currCubic, ptsBuffer, leftT);
this->cubicTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1,
(kLeftMaxCurvatureNone != leftMaxCurvatureT)
? leftMaxCurvatureT/leftT : kLeftMaxCurvatureNone,
kRightMaxCurvatureNone);
if (rightT < 1) {
rightT = (rightT - leftT) / (1 - leftT);
SkPoint chopped[7];
if (numParametricSegments >= numRadialSegments) {
SkChopCubicAtHalf(p, chopped);
} else {
SkChopCubicAtMidTangent(p, chopped);
}
if (rightMaxCurvatureT < 1 && kRightMaxCurvatureNone != rightMaxCurvatureT) {
rightMaxCurvatureT = (rightMaxCurvatureT - leftT) / (1 - leftT);
}
currCubic = ptsBuffer + 3;
} else {
SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1];
this->rotateTo(leftJoinType, ptsBuffer[0], c1);
this->nonInflectCubicTo(prevJoinType, chopped, maxDepth - 1);
// Use kDoubleSidedRoundJoinType in case we happened to chop at an exact cusp or
// turnaround point of a flat cubic, in which case we would lose 180 degrees of
// rotation.
this->nonInflectCubicTo(kDoubleSidedRoundJoinType, chopped + 3, maxDepth - 1);
return;
}
if (rightT < 1) {
SkChopCubicAt(currCubic, ptsBuffer, rightT);
this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[3]);
currCubic = ptsBuffer + 3;
this->cubicTo(kInternalRoundJoinType, currCubic, /*maxCurvatureT=*/0,
kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
} else {
this->lineTo(kInternalRoundJoinType, currCubic[0], currCubic[3]);
SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2];
this->rotateTo(kInternalRoundJoinType, currCubic[3], currCubic[3]*2 - c2);
}
return;
}
// Recurse and check the other two points of max curvature, if any.
if (kRightMaxCurvatureNone != rightMaxCurvatureT) {
this->cubicTo(leftJoinType, P, rightMaxCurvatureT, leftMaxCurvatureT,
kRightMaxCurvatureNone);
return;
}
if (kLeftMaxCurvatureNone != leftMaxCurvatureT) {
SkASSERT(kRightMaxCurvatureNone == rightMaxCurvatureT);
this->cubicTo(leftJoinType, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
kRightMaxCurvatureNone);
return;
}
if (numSegments > fMaxTessellationSegments) {
SkPoint ptsBuffer[7];
SkChopCubicAt(P, ptsBuffer, 0.5f);
this->cubicTo(leftJoinType, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
this->cubicTo(kInternalRoundJoinType, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
kRightMaxCurvatureNone);
return;
}
this->writeCubicSegment(leftJoinType, p0, p1, p2, p3);
this->writeCubicSegment(prevJoinType, p);
}
void GrStrokePatchBuilder::rotateTo(float leftJoinType, const SkPoint& anchorPoint,
const SkPoint& controlPoint) {
// Effectively rotate the current normal by drawing a zero length, 1-segment cubic.
// writeCubicSegment automatically adds the necessary join and the zero length cubic serves as
// a glue that guarantees a water tight rasterized edge between the new join and the segment
// that comes after the rotate.
SkPoint pts[4] = {anchorPoint, controlPoint, anchorPoint*2 - controlPoint, anchorPoint};
this->writeCubicSegment(leftJoinType, pts, 1);
}
void GrStrokePatchBuilder::close() {
void GrStrokePatchBuilder::close(SkPaint::Cap capType) {
if (!fHasPreviousSegment) {
// Draw caps instead of closing if the subpath is zero length:
//
@ -474,13 +365,13 @@ void GrStrokePatchBuilder::close() {
//
// (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
//
this->writeCaps();
this->writeCaps(capType);
return;
}
// Draw a line back to the beginning. (This will be discarded if
// fCurrentPoint == fCurrContourStartPoint.)
this->lineTo(fCurrStrokeJoinType, fCurrentPoint, fCurrContourStartPoint);
this->writeJoin(fCurrStrokeJoinType, fCurrContourStartPoint, fLastControlPoint,
this->writeJoin(fCurrStrokeJoinType, fLastControlPoint, fCurrContourStartPoint,
fCurrContourFirstControlPoint);
}

View File

@ -66,40 +66,18 @@ private:
void allocVertexChunk(int minVertexAllocCount);
SkPoint* reservePatch();
// Join types are written as floats in P4.x. See GrStrokeTessellateShader for definitions.
void writeCubicSegment(float leftJoinType, const SkPoint pts[4], float overrideNumSegments = 0);
void writeCubicSegment(float leftJoinType, const Sk2f& p0, const Sk2f& p1, const Sk2f& p2,
const Sk2f& p3, float overrideNumSegments = 0) {
SkPoint pts[4];
p0.store(&pts[0]);
p1.store(&pts[1]);
p2.store(&pts[2]);
p3.store(&pts[3]);
this->writeCubicSegment(leftJoinType, pts, overrideNumSegments);
}
void writeJoin(float joinType, const SkPoint& anchorPoint, const SkPoint& prevControlPoint,
void writeCubicSegment(float prevJoinType, const SkPoint pts[4]);
void writeJoin(float joinType, const SkPoint& prevControlPoint, const SkPoint& anchorPoint,
const SkPoint& nextControlPoint);
void writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint);
void writeCaps();
void writeCaps(SkPaint::Cap);
void beginPath(const SkStrokeRec&);
void moveTo(const SkPoint&);
void lineTo(const SkPoint& p0, const SkPoint& p1);
void quadraticTo(const SkPoint[3]);
void cubicTo(const SkPoint[4]);
void close();
void lineTo(float leftJoinType, const SkPoint& p0, const SkPoint& p1);
void quadraticTo(float leftJoinType, const SkPoint[3], float maxCurvatureT);
static constexpr float kLeftMaxCurvatureNone = 1;
static constexpr float kRightMaxCurvatureNone = 0;
void cubicTo(float leftJoinType, const SkPoint[4], float maxCurvatureT, float leftMaxCurvatureT,
float rightMaxCurvatureT);
// TEMPORARY: Rotates the current control point without changing the current position.
// This is used when we convert a curve to a lineTo, and that behavior will soon go away.
void rotateTo(float leftJoinType, const SkPoint& anchorPoint, const SkPoint& controlPoint);
void lineTo(float prevJoinType, const SkPoint&, const SkPoint&);
void quadraticTo(float prevJoinType, const SkPoint[3], int maxDepth = -1);
void cubicTo(float prevJoinType, const SkPoint[4]);
void nonInflectCubicTo(float prevJoinType, const SkPoint[4], int maxDepth = -1);
void close(SkPaint::Cap);
// These are raw pointers whose lifetimes are controlled outside this class.
GrMeshDrawOp::Target* const fTarget;
@ -115,16 +93,14 @@ private:
// Variables related to the path that we are currently iterating.
float fCurrStrokeRadius;
float fCurrStrokeJoinType; // See GrStrokeTessellateShader for join type definitions .
SkPaint::Cap fCurrStrokeCapType;
// Any curvature on the original curve gets magnified on the outer edge of the stroke,
// proportional to how thick the stroke radius is. This field tells us the maximum curvature we
// can tolerate using the current stroke radius, before linearization artifacts begin to appear
// on the outer edge.
//
// (Curvature this strong is quite rare in practice, but when it does happen, we decompose the
// section with strong curvature into lineTo's with round joins in between.)
float fMaxCurvatureCosTheta;
float fCurrStrokeJoinType; // See GrStrokeTessellateShader for join type definitions.
float fNumRadialSegmentsPerRad;
// These values contain worst-case numbers of parametric segments our hardware can support for
// the current stroke radius, in the event that there are also enough radial segments to rotate
// 180 and 360 degrees respectively. These are used for "quick accepts" that allow us to send
// almost all curves directly to the hardware without having to chop or think any further.
float fMaxParametricSegments180;
float fMaxParametricSegments360;
// Variables related to the specific contour that we are currently iterating.
bool fHasPreviousSegment = false;

View File

@ -13,6 +13,9 @@
#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
constexpr static float kLinearizationIntolerance =
GrTessellationPathRenderer::kLinearizationIntolerance;
class GrStrokeTessellateShader::Impl : public GrGLSLGeometryProcessor {
public:
const char* getMiterLimitUniformName(const GrGLSLUniformHandler& uniformHandler) const {
@ -65,8 +68,8 @@ private:
}
if (!shader.viewMatrix().isIdentity()) {
// Since the view matrix is applied after tessellation, it cannot expand the geometry in
// any direction.
// Since the view matrix is applied after tessellation, it must not expand the geometry
// in any direction.
SkASSERT(shader.viewMatrix().getMaxScale() < 1 + SK_ScalarNearlyZero);
pdman.setSkMatrix(fSkewMatrixUniform, shader.viewMatrix());
}
@ -89,104 +92,165 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
SkString code(versionAndExtensionDecls);
code.append("layout(vertices = 1) out;\n");
// TODO: CCPR stroking was written with a linearization tolerance of 1/8 pixel. Readdress this
// ASAP to see if we can use GrTessellationPathRenderer::kLinearizationIntolerance (1/4 pixel)
// instead.
constexpr static float kIntolerance = 8; // 1/8 pixel.
code.appendf("const float kTolerance = %f;\n", 1/kIntolerance);
code.appendf("const float kCubicK = %f;\n", GrWangsFormula::cubic_k(kIntolerance));
code.appendf("const float kTolerance = %f;\n", 1/kLinearizationIntolerance);
code.appendf("const float kCubicK = %f;\n", GrWangsFormula::cubic_k(kLinearizationIntolerance));
code.appendf("const float kPI = 3.141592653589793238;\n");
code.appendf("const float kMaxTessellationSegments = %i;\n",
shaderCaps.maxTessellationSegments());
const char* miterLimitName = impl->getMiterLimitUniformName(uniformHandler);
code.appendf("uniform float %s;\n", miterLimitName);
code.appendf("#define uMiterLimit %s\n", miterLimitName);
code.append(R"(
in vec2 P[];
in vec2 P[];
out vec4 X[];
out vec4 Y[];
out vec2 fanAngles[];
out vec2 strokeRadii[];
out vec2 outsetClamp[];
// Cubic control points.
out vec4 P01[];
out vec4 P23[];
void main() {
// The 5th point contains the patch type and stroke radius.
float strokeRadius = P[4].y;
// Cubic derivative matrix colums.
out vec3 C_x[];
out vec3 C_y[];
X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
fanAngles[gl_InvocationID /*== 0*/] = vec2(0);
strokeRadii[gl_InvocationID /*== 0*/] = vec2(strokeRadius);
outsetClamp[gl_InvocationID /*== 0*/] = vec2(-1, 1);
out vec4 packedArgs0[];
out vec4 packedArgs1[];
// Calculate how many linear segments to chop this curve into.
// (See GrWangsFormula::cubic().)
float numSegments = sqrt(kCubicK * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
// The built-in atan() is undefined when x == 0. This method relieves that restriction, but also
// can return values larger than 2*kPI. This shouldn't matter for our purposes.
float atan2(vec2 v) {
float bias = 0;
if (abs(v.y) > abs(v.x)) {
v = vec2(v.y, -v.x);
bias = kPI/2;
}
return atan(v.y, v.x) + bias;
}
// A patch can override the number of segments it gets chopped into by passing a
// positive value as P[4].x.
if (P[4].x > 0) {
numSegments = P[4].x;
}
void main() {
// Unpack the 5th input point, which contains the patch type and stroke radius.
float patchType = P[4].x;
float strokeRadius = P[4].y;
// A negative value in P[4].x means this patch actually represents a join instead
// of a stroked cubic. Joins are implemented as radial fans from the junction point.
if (P[4].x < 0) {
// Start by finding the angle between the tangents coming in and out of the
// join.
vec2 c0 = P[1] - P[0];
vec2 c1 = P[3] - P[2];
float theta = atan(determinant(mat2(c0, c1)), dot(c0, c1));
// Calculate the number of evenly spaced (in the parametric sense) segments to chop the
// curve into. (See GrWangsFormula::cubic().) The final tessellated strip will be a
// composition of these parametric segments as well as radial segments.
float numParametricSegments = sqrt(kCubicK * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
if (P[1] == P[0] && P[2] == P[3]) {
// This type of curve is used to represent flat lines, but wang's formula does not
// return 1 segment. Force numParametricSegments to 1.
numParametricSegments = 1;
}
numParametricSegments = max(ceil(numParametricSegments), 1);
// Determine the beginning and end angles of our join.
fanAngles[gl_InvocationID /*== 0*/] = atan(c0.y, c0.x) + vec2(0, theta);
// Find a tangent matrix C' in power basis form. (This gives the derivative scaled by 1/3.)
//
// |C'x C'y|
// dx,dy (divided by 3) = tangent = |T^2 T 1| * | . . |
// | . . |
mat2x3 C_ = mat4x3(-1, 2, -1,
3, -4, 1,
-3, 2, 0,
1, 0, 0) * transpose(mat4x2(P[0], P[1], P[2], P[3]));
float joinType = P[4].x;
if (joinType <= -3) {
// Round join. Decide how many fan segments we need in order to be smooth.
numSegments = abs(theta) / (2 * acos(1 - kTolerance/strokeRadius));
} else if (joinType == -2) {
// Miter join. Draw a fan with 2 segments and lengthen the interior radius
// so it matches the miter point.
// (Or draw a 1-segment fan if we exceed the miter limit.)
float miterRatio = 1.0 / cos(.5 * theta);
strokeRadii[gl_InvocationID /*== 0*/] = strokeRadius * vec2(1, miterRatio);
numSegments = (miterRatio <= uMiterLimit) ? 2.0 : 1.0;
} else {
// Bevel join. Make a fan with only one segment.
numSegments = 1;
}
// Find the beginning and ending tangents. This works for now because it is illegal for the
// caller to send us a curve where P0=P1=P2 or P1=P2=P3.
vec2 tan0 = (P[1] == P[0]) ? P[2] - P[0] : P[1] - P[0];
vec2 tan1 = (P[3] == P[2]) ? P[3] - P[1] : P[3] - P[2];
if (strokeRadius * abs(theta) < kTolerance) {
// The join angle is too tight to guarantee there won't be gaps on the
// inside of the junction. Just in case our join was supposed to only go on
// the outside, switch to an internal bevel that ties all 4 incoming
// vertices together. The join angle is so tight that bevels, miters, and
// rounds will all look the same anyway.
numSegments = 1;
// Paranoia. The next shader uses "fanAngles.x != fanAngles.y" as the test
// to decide whether it is emitting a cubic or a fan. But if theta is close
// enough to zero, that might fail. Assign arbitrary, nonequal values. This
// is fine because we will only draw one segment with vertices at T=0 and
// T=1, and the shader won't use fanAngles on the two outer vertices.
fanAngles[gl_InvocationID /*== 0*/] = vec2(1, 0);
} else if (joinType != -4) {
// This is a standard join. Restrict it to the outside of the junction.
outsetClamp[gl_InvocationID /*== 0*/] = mix(
vec2(-1, 1), vec2(0), lessThan(vec2(-theta, theta), vec2(0)));
}
}
// Determine the curve's start angle.
float angle0 = atan2(tan0);
// Tessellate a "strip" of numSegments quads.
numSegments = max(1, numSegments);
gl_TessLevelInner[0] = numSegments;
gl_TessLevelInner[1] = 2.0;
gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = numSegments;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = numSegments;
// Determine the curve's total rotation. It is illegal for the caller to send us a curve
// that rotates more than 180 degrees or a curve that has inflections, so we only need to
// take the inverse cosine of the dot product.
vec2 tan0norm = normalize(tan0);
vec2 tan1norm = normalize(tan1);
float cosTheta = dot(tan1norm, tan0norm);
float rotation = acos(clamp(cosTheta, -1, +1));
// Adjust sign of rotation to match the direction the curve turns.
if (determinant(mat3x2(.25,1,.5,1,1,0) * C_) < 0) { // i.e., if cross(F'(.5), F''(.5) < 0
rotation = -rotation;
}
// Calculate the number of evenly spaced radial segments to chop the curve into. Radial
// segments divide the curve's rotation into even steps. The final tessellated strip will be
// a composition of both parametric and radial segments.
float numRadialSegments = abs(rotation) / (2 * acos(max(1 - kTolerance/strokeRadius, -1)));
numRadialSegments = max(ceil(numRadialSegments), 1);
// Set up joins.
float innerStrokeRadius = 0; // Used for miter joins.
vec2 strokeOutsetClamp = vec2(-1, 1);
if (patchType != 0) { // A non-zero patchType means this patch is a join.
numParametricSegments = 1; // Joins only have radial segments.
innerStrokeRadius = strokeRadius; // A non-zero innerStrokeRadius designates a join.
float joinType = abs(patchType);
if (joinType == 2) {
// Miter join. Draw a fan with 2 segments and lengthen the interior radius
// so it matches the miter point.
// (Or draw a 1-segment fan if we exceed the miter limit.)
float miterRatio = 1.0 / cos(.5 * rotation);
numRadialSegments = (miterRatio <= uMiterLimit) ? 2.0 : 1.0;
innerStrokeRadius = strokeRadius * miterRatio;
} else if (joinType == 1) {
// Bevel join. Make a fan with only one segment.
numRadialSegments = 1;
}
if (length(tan0norm - tan1norm) * strokeRadius < kTolerance) {
// The join angle is too tight to guarantee there won't be gaps on the inside of the
// junction. Just in case our join was supposed to only go on the outside, switch to
// a double sided bevel that ties all 4 incoming vertices together. The join angle
// is so tight that bevels, miters, and rounds will all look the same anyway.
numRadialSegments = 1;
} else if (patchType > 0) {
// This is join is single-sided. Clamp it to the outer side of the junction.
strokeOutsetClamp = (rotation > 0) ? vec2(-1,0) : vec2(0,1);
}
}
// Finalize the numbers of segments.
numRadialSegments = min(numRadialSegments, kMaxTessellationSegments);
numParametricSegments = min(numParametricSegments,
kMaxTessellationSegments + 1 - numRadialSegments);
// The first and last edges are shared by both the parametric and radial sets of edges, so
// the total number of edges is:
//
// numTotalEdges = numParametricEdges + numRadialEdges - 2
//
// It's also important to differentiate between the number of edges and segments in a strip:
//
// numTotalSegments = numTotalEdges - 1
//
// So the total number of segments in the combined strip is:
//
// numTotalSegments = numParametricEdges + numRadialEdges - 2 - 1
// = numParametricSegments + 1 + numRadialSegments + 1 - 2 - 1
// = numParametricSegments + numRadialSegments - 1
//
float numTotalSegments = numParametricSegments + numRadialSegments - 1;
// Tessellate a "quad strip" with numTotalSegments.
gl_TessLevelInner[0] = numTotalSegments;
gl_TessLevelInner[1] = 2.0;
gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = numTotalSegments;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = numTotalSegments;
// Pack the arguments for the next stage.
P01[gl_InvocationID /*== 0*/] = vec4(P[0], P[1]);
P23[gl_InvocationID /*== 0*/] = vec4(P[2], P[3]);
C_x[gl_InvocationID /*== 0*/] = C_[0];
C_y[gl_InvocationID /*== 0*/] = C_[1];
packedArgs0[gl_InvocationID /*== 0*/] = vec4(numParametricSegments, numRadialSegments,
angle0, rotation / numRadialSegments);
packedArgs1[gl_InvocationID /*== 0*/] = vec4(strokeRadius, innerStrokeRadius,
strokeOutsetClamp);
}
)");
return code;
@ -194,12 +258,18 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps&) const {
const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
auto impl = static_cast<const GrStrokeTessellateShader::Impl*>(glslPrimProc);
SkString code(versionAndExtensionDecls);
code.append("layout(quads, equal_spacing, ccw) in;\n");
// Use a #define to make extra sure we don't prevent the loop from unrolling.
code.appendf("#define MAX_TESSELLATION_SEGMENTS_LOG2 %i\n",
SkNextLog2(shaderCaps.maxTessellationSegments()));
code.appendf("const float kPI = 3.141592653589793238;\n");
const char* skewMatrixName = nullptr;
if (!this->viewMatrix().isIdentity()) {
skewMatrixName = impl->getSkewMatrixUniformName(uniformHandler);
@ -207,57 +277,179 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
}
code.append(R"(
in vec4 X[];
in vec4 Y[];
in vec2 fanAngles[];
in vec2 strokeRadii[];
in vec2 outsetClamp[];
// Cubic control points.
in vec4 P01[];
in vec4 P23[];
uniform vec4 sk_RTAdjust;
// Cubic derivative matrix colums.
in vec3 C_x[];
in vec3 C_y[];
void main() {
float strokeRadius = strokeRadii[0].x;
in vec4 packedArgs0[];
in vec4 packedArgs1[];
mat4x2 P = transpose(mat2x4(X[0], Y[0]));
float T = gl_TessCoord.x;
uniform vec4 sk_RTAdjust;
// Evaluate the cubic at T. Use De Casteljau's for its accuracy and stability.
vec2 ab = mix(P[0], P[1], T);
vec2 bc = mix(P[1], P[2], T);
vec2 cd = mix(P[2], P[3], T);
vec2 abc = mix(ab, bc, T);
vec2 bcd = mix(bc, cd, T);
vec2 position = mix(abc, bcd, T);
void main() {
// Unpack arguments from the previous stage.
mat4x2 P = mat4x2(P01[0], P23[0]);
mat2x3 C_ = mat2x3(C_x[0], C_y[0]);
float numParametricSegments = packedArgs0[0].x;
float numRadialSegments = packedArgs0[0].y;
float angle0 = packedArgs0[0].z;
float radsPerSegment = packedArgs0[0].w;
float strokeRadius = packedArgs1[0].x;
float innerStrokeRadius = packedArgs1[0].y;
vec2 strokeOutsetClamp = packedArgs1[0].zw;
// Find the normalized tangent vector at T.
vec2 tangent = bcd - abc;
if (tangent == vec2(0)) {
// We get tangent=0 if (P0 == P1 and T == 0), of if (P2 == P3 and T == 1).
tangent = (T == 0) ? P[2] - P[0] : P[3] - P[1];
// Find the beginning and ending tangents. This works for now because it is illegal for the
// caller to send us a curve where P0=P1=P2 or P1=P2=P3.
vec2 tan0 = (P[1] == P[0]) ? P[2] - P[0] : P[1] - P[0];
vec2 tan1 = (P[3] == P[2]) ? P[3] - P[1] : P[3] - P[2];
// Our patch is composed of exactly "numTotalSegments + 1" stroke-width edges that run
// orthogonal to the curve and make a strip of "numTotalSegments" quads. Determine which
// discrete edge belongs to this invocation. An edge can either come from a parametric
// segment or a radial one.
float numTotalSegments = numParametricSegments + numRadialSegments - 1;
float totalEdgeID = round(gl_TessCoord.x * numTotalSegments);
// Find the matrix C_sT that produces an (arbitrarily scaled) tangent vector from a
// parametric edge ID:
//
// C_sT * |parametricEdgeID^2 parametricEdgeId 1| = tangent * C
//
vec3 s = vec3(1, numParametricSegments, numParametricSegments * numParametricSegments);
mat3x2 C_sT = transpose(mat2x3(C_[0]*s, C_[1]*s));
// Run an O(log N) search to determine the highest parametric edge that is located on or
// before the totalEdgeID. A total edge ID is determined by the sum of complete parametric
// and radial segments behind it. i.e., find the highest parametric edge where:
//
// parametricEdgeID + floor(numRadialSegmentsAtParametricT) <= totalEdgeID
//
float lastParametricEdgeID = 0;
float maxParametricEdgeID = min(numParametricSegments - 1, totalEdgeID);
vec2 tan0norm = normalize(tan0);
float negAbsRadsPerSegment = -abs(radsPerSegment);
float maxRotation0 = (1 + totalEdgeID) * abs(radsPerSegment);
for (int exp = MAX_TESSELLATION_SEGMENTS_LOG2 - 1; exp >= 0; --exp) {
// Test the parametric edge at lastParametricEdgeID + 2^exp.
float testParametricID = lastParametricEdgeID + (1 << exp);
if (testParametricID <= maxParametricEdgeID) {
vec2 testTan = fma(vec2(testParametricID), C_sT[0], C_sT[1]);
testTan = fma(vec2(testParametricID), testTan, C_sT[2]);
float cosRotation = dot(normalize(testTan), tan0norm);
float maxRotation = fma(testParametricID, negAbsRadsPerSegment, maxRotation0);
maxRotation = min(maxRotation, kPI);
// Is rotation <= maxRotation? (i.e., is the number of complete radial segments
// behind testT, + testParametricID <= totalEdgeID?)
// NOTE: We bias cos(maxRotation) downward for fp32 error. Otherwise a flat section
// following a 180 degree turn might not render properly.
if (cosRotation >= cos(maxRotation) - 1e-5) {
// testParametricID is on or before the totalEdgeID. Keep it!
lastParametricEdgeID = testParametricID;
}
tangent = normalize(tangent);
}
}
// If the fanAngles are not equal, it means this patch actually represents a join
// instead of a stroked cubic. Joins are implemented as radial fans from the
// junction point.
//
// The caller carefully sets up the control points on junctions so the above math
// lines up exactly with the incoming stroke vertices at T=0 and T=1, but for
// interior T values we fall back on the fan's arc equation instead.
if (fanAngles[0].x != fanAngles[0].y && T != 0 && T != 1) {
position = P[0];
float theta = mix(fanAngles[0].x, fanAngles[0].y, T);
tangent = vec2(cos(theta), sin(theta));
// Miters use a larger radius for the internal vertex.
strokeRadius = strokeRadii[0].y;
}
// Find the homogeneous T value of the parametric edge at lastParametricEdgeID.
// (The scalar T value would be "parametricT.s / parametricT.t").
vec2 parametricT = vec2(lastParametricEdgeID, numParametricSegments);
// Determine how far to outset our vertex orthogonally from the curve.
float outset = gl_TessCoord.y * 2 - 1;
outset = clamp(outset, outsetClamp[0].x, outsetClamp[0].y);
outset *= strokeRadius;
// Now that we've identified the highest parametric edge on or before the totalEdgeID,
// the highest radial edge is easy:
float lastRadialEdgeID = totalEdgeID - lastParametricEdgeID;
float radialAngle = fma(lastRadialEdgeID, radsPerSegment, angle0);
vec2 vertexpos = position + vec2(-tangent.y, tangent.x) * outset;
// Find the T value of the edge at lastRadialEdgeID. This is the point whose tangent angle
// is equal to radialAngle, or whose tangent vector is orthogonal to "norm".
vec2 tangent = vec2(cos(radialAngle), sin(radialAngle));
vec2 norm = vec2(-tangent.y, tangent.x);
// Find the T value where the cubic's tangent is orthogonal to norm:
//
// |C'x C'y|
// dot(tangent, norm) == 0: |T^2 T 1| * | . . | * norm == 0
// | . . |
//
// The coeffs for the quadratic equation we need to solve are therefore: C' * norm.
vec3 coeffs = C_ * norm;
float a=coeffs.x, b=coeffs.y, c=coeffs.z;
// Quadratic formula from Numerical Recipes in C. Roots are: q/a and c/q. Combined with our
// method for choosing a root below, this works for all a=0, b=0, and c=0.
float x = sqrt(max(b*b - 4*a*c, 0));
if (b < 0) {
x = -x;
}
float q = -.5 * (b + x);
// Pick the root T value closest to .5. Since we chop serpentines at inflections and all
// other curves at the midtangent, it will always be the case that the root we want is the
// one closest to .5. (But see the note about cusps below.)
// NOTE: we express the root in homogeneous coordinates. The scalar T value would be:
// "radialT.s / radialT.t".
float qa_5 = q*a*.5;
vec2 radialT = (abs(q*q - qa_5) < abs(a*c - qa_5)) ? vec2(q,a) : vec2(c,q);
if (lastRadialEdgeID == 0) {
// On a chopped cusp, the 0th radial edge has orthogonal tangents at T=0 and T=1. Both
// are equally close to 0.5, but luckily we know the 0th radial edge is always at T=0.
radialT = vec2(0,1);
}
radialT *= sign(radialT.t); // Keep the denominator positive.
// Now that we've identified the homogeneous T values of the last parametric and radial
// edges, our final T value for totalEdgeID is whichever is larger.
vec2 T = (determinant(mat2(parametricT, radialT)) > 0) ? parametricT : radialT;
// Evaluate the cubic at T.s/T.t. Use De Casteljau's for its accuracy and stability.
vec2 weights = vec2(T.t - T.s, T.s);
vec2 ab = mat2(P[0], P[1]) * weights;
vec2 bc = mat2(P[1], P[2]) * weights;
vec2 cd = mat2(P[2], P[3]) * weights;
vec2 abc = mat2(ab, bc) * weights;
vec2 bcd = mat2(bc, cd) * weights;
vec2 abcd = mat2(abc, bcd) * weights;
vec2 position = abcd / (T.t * T.t * T.t);
// If we went with T=parametricT, then update the tangent. Otherwise leave it at the radial
// tangent found previously. (In the event that parametricT == radialT, we keep the radial
// tangent.)
if (T != radialT) {
tangent = bcd - abc;
}
if (gl_TessCoord.x == 0) {
// The first edge always uses P[0] and the tangent as identified by the control points.
// This guarantees that adjecent patches always use the same fp32 values for their
// shared edge and get a water tight seam.
tangent = tan0;
position = P[0];
} else if (gl_TessCoord.x == 1) {
// The final edge always uses P[3] and the tangent as identified by the control points.
// This guarantees that adjecent patches always use the same fp32 values for their
// shared edge and get a water tight seam.
tangent = tan1;
position = P[3];
} else if (innerStrokeRadius != 0) {
// Miter joins use a larger radius for the internal vertex in order to reach the miter
// point.
strokeRadius = innerStrokeRadius;
}
// If innerStrokeRadius != 0 then this patch is a join.
if (innerStrokeRadius != 0) {
// ... Aaaand nevermind again if we are a join. Those all rotate around P[1].
position = P[1];
}
// Determine how far to outset our vertex orthogonally from the curve.
float outset = gl_TessCoord.y * 2 - 1;
outset = clamp(outset, strokeOutsetClamp.x, strokeOutsetClamp.y);
outset *= strokeRadius;
vec2 vertexpos = position + normalize(vec2(-tangent.y, tangent.x)) * outset;
)");
// Transform after tessellation. Stroke widths and normals are defined in (pre-transform) local
@ -267,8 +459,8 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
}
code.append(R"(
gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
}
gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
}
)");
return code;

View File

@ -14,34 +14,53 @@
class GrGLSLUniformHandler;
// Tessellates a batch of stroke patches directly to the canvas. A patch is either a "cubic"
// (single stroked bezier curve with butt caps) or a "join". A patch is defined by 5 points as
// follows:
// Tessellates a batch of stroke patches directly to the canvas.
//
// P0..P3 : Represent the cubic control points.
// (P4.x == 0) : The patch is a cubic and the shader decides how many linear segments to produce.
// (P4.x < 0) : The patch is still a cubic, but will be linearized into exactly |P4.x| segments.
// (P4.x == 1) : The patch is an outer bevel join.
// (P4.x == 2) : The patch is an outer miter join.
// (NOTE: If miterLimitOrZero == 0, then miter join patches are illegal.)
// (P4.x == 3) : The patch is an outer round join.
// (P4.x == 4) : The patch is an inner and outer round join.
// P4.y : Represents the stroke radius.
// Current limitations: (These restrictions will hopefully all be lifted in the future.)
//
// If a patch is a join, P0 must equal P3, P1 must equal the control point coming into the junction,
// and P2 must equal the control point going out. It's imperative that a junction's control points
// match the control points of their neighbor cubics exactly, or the rasterization might not be
// water tight. (Also note that if P1==P0 or P2==P3, the junction needs to be given its neighbor's
// opposite cubic control point.)
// * A given curve must not have inflection points. Chop curves at inflection points on CPU
// before sending them down.
//
// * A given curve must not rotate more than 180 degrees. Chop curves that rotate more than 180
// degrees at midtangent before sending them down.
//
// * It is illegal for P1 and P2 to both be coincident with P0 or P3. If this is the case, send
// the curve [P0, P0, P3, P3] instead.
//
// Tessellated stroking works by creating stroke-width, orthogonal edges at set locations along the
// curve and then connecting them with a triangle strip. These orthogonal edges come from two
// different sets: "parametric edges" and "radial edges". Parametric edges are spaced evenly in the
// parametric sense, and radial edges divide the curve's _rotation_ into even steps. The
// tessellation shader evaluates both sets of edges and sorts them into a single triangle strip.
// With this combined set of edges we can stroke any curve, regardless of curvature.
//
// A patch is either a "cubic" (single stroked bezier curve with butt caps) or a "join". A patch is
// defined by 5 points as follows:
//
// P0..P3 : Represent the cubic control points.
// (P4.x == 0) : The patch is a normal cubic.
// (abs(P4.x) == 1) : The patch is a bevel join.
// (abs(P4.x) == 2) : The patch is a miter join.
// (NOTE: If miterLimitOrZero == 0, then miter join patches are illegal.)
// (abs(P4.x) >= 3) : The patch is a round join.
// (P4.x < 0) : The patch join is double sided. (Positive value joins only draw on the
// outer side of their junction.)
// P4.y : Represents the stroke radius.
//
// If a patch is a join, P0 must equal the control point coming into the junction, P1 and P2 must
// equal the junction point, and P3 must equal the control point going out. It's imperative that a
// junction's control points match the control points of their neighbor cubics exactly, or the
// seaming might not be water tight. (Also note that if P1==P0 or P2==P3, the junction needs to be
// given its neighbor's opposite cubic control point.)
//
// To use this shader, construct a GrProgramInfo with a primitiveType of "kPatches" and a
// tessellationPatchVertexCount of 5.
class GrStrokeTessellateShader : public GrPathShader {
public:
constexpr static float kBevelJoinType = -1;
constexpr static float kMiterJoinType = -2;
constexpr static float kRoundJoinType = -3;
constexpr static float kInternalRoundJoinType = -4;
constexpr static float kStandardCubicType = 0;
constexpr static float kBevelJoinType = 1;
constexpr static float kMiterJoinType = 2;
constexpr static float kRoundJoinType = 3;
constexpr static int kNumVerticesPerPatch = 5;