Try to normalize tangents more robustly in stroke tessellators
I noticed the old version of cosine_between_vectors was causing issues after https://skia-review.googlesource.com/c/skia/+/532014 landed, which had the effect that tan0 and tan1 tended to be larger for lines. That said, I could find instances of the same visual glitches on other devices before that change landed, so depending on the hardware we were running into issues either way. This changes the cosine function to assume the tangents are already normalized, and restructures the curve evaluation logic to assume that as well. This is a reduction in work that hopefully makes up for the work of always normalizing tan0 and tan1, whereas before the cosine_ function attempted to avoid that, and do a single inversesqrt call. From a precision standpoint, just switching to calling normalize() on tan0 and tan1 before getting the cosine improves the problem compared to the precision required to store (dot(tan0,tan0)*dot(tan1,tan1)) before normalization. However, I also added some additional logic for when coordinates get very large to try and scale the two end points of the line to a more normalized exponent range. Since it returns the unit vector, it should be equivalent but more accurate. Bug: skia:11268, skia:12703, skia:13056 Change-Id: I34b6e1df1f57a8913443d75ed78710fcd27731e6 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/532777 Auto-Submit: Michael Ludwig <michaelludwig@google.com> Reviewed-by: Jim Van Verth <jvanverth@google.com> Commit-Queue: Jim Van Verth <jvanverth@google.com>
This commit is contained in:
parent
d663bdb900
commit
5ce8f07271
@ -105,12 +105,30 @@ GrStrokeTessellationShader::GrStrokeTessellationShader(const GrShaderCaps& shade
|
||||
SkASSERT(fAttribs.count() <= kMaxAttribCount);
|
||||
}
|
||||
|
||||
const char* GrStrokeTessellationShader::Impl::kCosineBetweenVectorsFn = R"(
|
||||
float cosine_between_vectors(float2 a, float2 b) {
|
||||
// FIXME(crbug.com/800804,skbug.com/11268): This can overflow if we don't normalize exponents.
|
||||
float ab_cosTheta = dot(a,b);
|
||||
float ab_pow2 = dot(a,a) * dot(b,b);
|
||||
return (ab_pow2 == 0.0) ? 1.0 : clamp(ab_cosTheta * inversesqrt(ab_pow2), -1.0, 1.0);
|
||||
const char* GrStrokeTessellationShader::Impl::kRobustNormalizeDiffFn = R"(
|
||||
float2 robust_normalize_diff(float2 a, float2 b) {
|
||||
if (a == b) {
|
||||
return float2(0.0, 0.0);
|
||||
} else {
|
||||
float2 magXY = max(abs(a), abs(b));
|
||||
float mag = max(magXY.x, magXY.y);
|
||||
if (mag > 16777216.0) { // 2^24, when f32 loses 1px precision
|
||||
// This brings the components of a and b to be within [-1, 1] before the vector's length
|
||||
// is calculated inside the normalize() call.
|
||||
float2 scaled_diff = (a/mag) - (b/mag);
|
||||
return normalize(scaled_diff);
|
||||
} else {
|
||||
// assume standard f32 precision will be sufficiently accurate
|
||||
return normalize(a - b);
|
||||
}
|
||||
}
|
||||
})";
|
||||
|
||||
const char* GrStrokeTessellationShader::Impl::kCosineBetweenUnitVectorsFn = R"(
|
||||
float cosine_between_unit_vectors(float2 a, float2 b) {
|
||||
// Since a and b are assumed to be normalized, the cosine is equal to the dot product, although
|
||||
// we clamp that to ensure it falls within the expected range of [-1, 1].
|
||||
return clamp(dot(a, b), -1.0, 1.0);
|
||||
})";
|
||||
|
||||
// Extends the middle radius to either the miter point, or the bevel edge if we surpassed the miter
|
||||
@ -163,8 +181,8 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
// bool isFinalEdge;
|
||||
// float numParametricSegments;
|
||||
// float radsPerSegment;
|
||||
// float2 tan0;
|
||||
// float2 tan1;
|
||||
// float2 tan0; // Must be pre-normalized
|
||||
// float2 tan1; // Must be pre-normalized
|
||||
// float strokeOutset;
|
||||
//
|
||||
code->appendf(R"(
|
||||
@ -215,8 +233,6 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
//
|
||||
float lastParametricEdgeID = 0.0;
|
||||
float maxParametricEdgeID = min(numParametricSegments - 1.0, combinedEdgeID);
|
||||
// FIXME(crbug.com/800804,skbug.com/11268): This normalize() can overflow.
|
||||
float2 tan0norm = normalize(tan0);
|
||||
float negAbsRadsPerSegment = -abs(radsPerSegment);
|
||||
float maxRotation0 = (1.0 + combinedEdgeID) * abs(radsPerSegment);
|
||||
for (int exp = %i - 1; exp >= 0; --exp) {
|
||||
@ -225,7 +241,7 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
if (testParametricID <= maxParametricEdgeID) {
|
||||
float2 testTan = fma(float2(testParametricID), A, B_);
|
||||
testTan = fma(float2(testParametricID), testTan, C_);
|
||||
float cosRotation = dot(normalize(testTan), tan0norm);
|
||||
float cosRotation = dot(normalize(testTan), tan0);
|
||||
float maxRotation = fma(testParametricID, negAbsRadsPerSegment, maxRotation0);
|
||||
maxRotation = min(maxRotation, PI);
|
||||
// Is rotation <= maxRotation? (i.e., is the number of complete radial segments
|
||||
@ -244,11 +260,12 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
// combinedEdgeID, the highest radial edge is easy:
|
||||
float lastRadialEdgeID = combinedEdgeID - lastParametricEdgeID;
|
||||
|
||||
// Find the angle of tan0, or the angle between tan0norm and the positive x axis.
|
||||
float angle0 = acos(clamp(tan0norm.x, -1.0, 1.0));
|
||||
angle0 = tan0norm.y >= 0.0 ? angle0 : -angle0;
|
||||
// Find the angle of tan0, i.e. the angle between tan0 and the positive x axis.
|
||||
float angle0 = acos(clamp(tan0.x, -1.0, 1.0));
|
||||
angle0 = tan0.y >= 0.0 ? angle0 : -angle0;
|
||||
|
||||
// Find the tangent vector on the edge at lastRadialEdgeID.
|
||||
// Find the tangent vector on the edge at lastRadialEdgeID. By construction it is already
|
||||
// normalized.
|
||||
float radialAngle = fma(lastRadialEdgeID, radsPerSegment, angle0);
|
||||
tangent = float2(cos(radialAngle), sin(radialAngle));
|
||||
float2 norm = float2(-tangent.y, tangent.x);
|
||||
@ -304,7 +321,9 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
// tangent found previously. (In the event that parametricT == radialT, we keep the radial
|
||||
// tangent.)
|
||||
if (T != radialT) {
|
||||
tangent = (w >= 0.0) ? bc*u - ab*v : bcd - abc;
|
||||
// We must re-normalize here because the tangent is determined by the curve coefficients
|
||||
tangent = w >= 0.0 ? robust_normalize_diff(bc*u, ab*v)
|
||||
: robust_normalize_diff(bcd, abc);
|
||||
}
|
||||
|
||||
strokeCoord = (w >= 0.0) ? abc/uv : abcd;
|
||||
@ -316,8 +335,8 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
|
||||
})", shader.maxParametricSegments_log2() /* Parametric/radial sort loop count. */);
|
||||
|
||||
code->append(R"(
|
||||
// FIXME(crbug.com/800804,skbug.com/11268): This normalize() can overflow.
|
||||
float2 ortho = normalize(float2(tangent.y, -tangent.x));
|
||||
// At this point 'tangent' is normalized, so the orthogonal vector is also normalized.
|
||||
float2 ortho = float2(tangent.y, -tangent.x);
|
||||
strokeCoord += ortho * (STROKE_RADIUS * strokeOutset);)");
|
||||
|
||||
if (!shader.stroke().isHairlineStyle()) {
|
||||
|
@ -89,10 +89,17 @@ private:
|
||||
// emitTessellationCode and emitFragment code.
|
||||
class GrStrokeTessellationShader::Impl : public ProgramImpl {
|
||||
protected:
|
||||
// float cosine_between_vectors(float2 a, float2 b) { ...
|
||||
// float2 robust_normalize_diff(float2 a, float b) { ... }
|
||||
//
|
||||
// Returns dot(a, b) / (length(a) * length(b)).
|
||||
static const char* kCosineBetweenVectorsFn;
|
||||
// Returns the normalized difference between a and b, i.e. normalize(a - b), with care taken for
|
||||
// if b and/or a have large coordinates.
|
||||
static const char* kRobustNormalizeDiffFn;
|
||||
|
||||
// float cosine_between_unit_vectors(float2 a, float2 b) { ...
|
||||
//
|
||||
// Returns the cosine of the angle between a and b, assuming a and b are unit vectors already.
|
||||
// Guaranteed to be between [-1, 1].
|
||||
static const char* kCosineBetweenUnitVectorsFn;
|
||||
|
||||
// float miter_extent(float cosTheta, float miterLimit) { ...
|
||||
//
|
||||
|
@ -54,7 +54,7 @@ void GrStrokeTessellationShader::HardwareImpl::onEmitCode(EmitArgs& args, GrGPAr
|
||||
v->declareGlobal(GrShaderVar("vsColor", SkSLType::kHalf4, TypeModifier::Out));
|
||||
}
|
||||
|
||||
v->insertFunction(kCosineBetweenVectorsFn);
|
||||
v->insertFunction(kCosineBetweenUnitVectorsFn);
|
||||
v->insertFunction(kMiterExtentFn);
|
||||
v->insertFunction(kUncheckedMixFn);
|
||||
if (shader.hasDynamicStroke()) {
|
||||
@ -150,7 +150,7 @@ void GrStrokeTessellationShader::HardwareImpl::onEmitCode(EmitArgs& args, GrGPAr
|
||||
}
|
||||
|
||||
// Calculate the number of segments to chop the join into.
|
||||
float cosTheta = cosine_between_vectors(prevJoinTangent, tan0);
|
||||
float cosTheta = cosine_between_unit_vectors(normalize(prevJoinTangent), normalize(tan0));
|
||||
float joinRotation = (cosTheta == 1) ? 0 : acos(cosTheta);
|
||||
if (cross_length_2d(prevJoinTangent, tan0) < 0) {
|
||||
joinRotation = -joinRotation;
|
||||
@ -351,7 +351,7 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessControlShaderGLSL(
|
||||
}
|
||||
|
||||
code.append(GrTessellationShader::WangsFormulaSkSL());
|
||||
code.append(kCosineBetweenVectorsFn);
|
||||
code.append(kCosineBetweenUnitVectorsFn);
|
||||
code.append(kMiterExtentFn);
|
||||
code.append(R"(
|
||||
float cross2d(vec2 a, vec2 b) {
|
||||
@ -444,7 +444,8 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessControlShaderGLSL(
|
||||
|
||||
// Determine the curve's total rotation. The vertex shader ensures our curve does not rotate
|
||||
// more than 180 degrees or inflect, so the inverse cosine has enough range.
|
||||
float cosTheta = cosine_between_vectors(tangents[0], tangents[1]);
|
||||
float cosTheta = cosine_between_unit_vectors(normalize(tangents[0]),
|
||||
normalize(tangents[1]));
|
||||
float rotation = acos(cosTheta);
|
||||
|
||||
// Adjust sign of rotation to match the direction the curve turns.
|
||||
@ -582,6 +583,7 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessEvaluationShaderGLSL(
|
||||
uniform vec4 sk_RTAdjust;)");
|
||||
|
||||
code.append(kUncheckedMixFn);
|
||||
code.append(kRobustNormalizeDiffFn);
|
||||
|
||||
code.append(R"(
|
||||
void main() {
|
||||
@ -628,7 +630,10 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessEvaluationShaderGLSL(
|
||||
numParametricSegments = tcsTessArgs[2].y;
|
||||
radsPerSegment = tcsTessArgs[2].z;
|
||||
}
|
||||
float2 tan1 = tcsEndPtEndTan.zw;
|
||||
// emitTessellationCode() expects unit vectors for tan0 and tan1
|
||||
tan0 = normalize(tan0);
|
||||
float2 tan1 = normalize(tcsEndPtEndTan.zw);
|
||||
|
||||
bool isFinalEdge = (gl_TessCoord.x == 1);
|
||||
float w = -1.0; // w<0 means the curve is an integral cubic.
|
||||
if (isinf(p3.y)) {
|
||||
|
@ -26,7 +26,8 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
|
||||
if (shader.hasDynamicStroke()) {
|
||||
args.fVertBuilder->insertFunction(kNumRadialSegmentsPerRadianFn);
|
||||
}
|
||||
args.fVertBuilder->insertFunction(kCosineBetweenVectorsFn);
|
||||
args.fVertBuilder->insertFunction(kRobustNormalizeDiffFn);
|
||||
args.fVertBuilder->insertFunction(kCosineBetweenUnitVectorsFn);
|
||||
args.fVertBuilder->insertFunction(kMiterExtentFn);
|
||||
args.fVertBuilder->insertFunction(kUncheckedMixFn);
|
||||
args.fVertBuilder->insertFunction(GrTessellationShader::WangsFormulaSkSL());
|
||||
@ -138,8 +139,9 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
|
||||
|
||||
args.fVertBuilder->codeAppend(R"(
|
||||
// Find the starting and ending tangents.
|
||||
float2 tan0 = ((p0 == p1) ? (p1 == p2) ? p3 : p2 : p1) - p0;
|
||||
float2 tan1 = p3 - ((p3 == p2) ? (p2 == p1) ? p0 : p1 : p2);
|
||||
// (p0 == p1) ? ((p1 == p2) ? p3 : p2) : p1
|
||||
float2 tan0 = robust_normalize_diff((p0 == p1) ? ((p1 == p2) ? p3 : p2) : p1, p0);
|
||||
float2 tan1 = robust_normalize_diff(p3, (p3 == p2) ? ((p2 == p1) ? p0 : p1) : p2);
|
||||
if (tan0 == float2(0)) {
|
||||
// The stroke is a point. This special case tells us to draw a stroke-width circle as a
|
||||
// 180 degree point stroke instead.
|
||||
@ -163,7 +165,8 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
|
||||
// of the join twice: once full width and once restricted to half width. This guarantees
|
||||
// perfect seaming by matching the vertices from the join as well as from the strokes on
|
||||
// either side.
|
||||
float joinRads = acos(cosine_between_vectors(p0 - lastControlPoint, tan0));
|
||||
float2 prevTan = robust_normalize_diff(p0, lastControlPoint);
|
||||
float joinRads = acos(cosine_between_unit_vectors(prevTan, tan0));
|
||||
float numRadialSegmentsInJoin = max(ceil(joinRads * NUM_RADIAL_SEGMENTS_PER_RADIAN), 1);
|
||||
// +2 because we emit the beginning and ending edges twice (see above comment).
|
||||
float numEdgesInJoin = numRadialSegmentsInJoin + 2;
|
||||
@ -205,13 +208,13 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
|
||||
// means the join is disabled, and to disable it with the existing code we can leave
|
||||
// tan0 equal to tan1.
|
||||
if (lastControlPoint != p0) {
|
||||
tan0 = p0 - lastControlPoint;
|
||||
tan0 = robust_normalize_diff(p0, lastControlPoint);
|
||||
}
|
||||
turn = cross_length_2d(tan0, tan1);
|
||||
}
|
||||
|
||||
// Calculate the curve's starting angle and rotation.
|
||||
float cosTheta = cosine_between_vectors(tan0, tan1);
|
||||
float cosTheta = cosine_between_unit_vectors(tan0, tan1);
|
||||
float rotation = acos(cosTheta);
|
||||
if (turn < 0) {
|
||||
// Adjust sign of rotation to match the direction the curve turns.
|
||||
|
Loading…
Reference in New Issue
Block a user