blob: d0f42292709effbee61d9b173987be52c2776549 [file] [log] [blame]
/*
* Copyright 2020 Google LLC.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
#include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
#include "src/gpu/glsl/GrGLSLVarying.h"
#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
class GrTessellateStrokeShader::Impl : public GrGLSLGeometryProcessor {
public:
const char* getMiterLimitUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fMiterLimitUniform);
}
private:
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
const auto& shader = args.fGP.cast<GrTessellateStrokeShader>();
args.fVaryingHandler->emitAttributes(shader);
fMiterLimitUniform = args.fUniformHandler->addUniform(
nullptr, kTessControl_GrShaderFlag, kFloat_GrSLType, "miterLimit", nullptr);
const char* colorUniformName;
fColorUniform = args.fUniformHandler->addUniform(
nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType, "color", &colorUniformName);
// The vertex shader is pure pass-through. Stroke widths and normals are defined in local
// path space, so we don't apply the view matrix until after tessellation.
args.fVertBuilder->declareGlobal(GrShaderVar("P", kFloat2_GrSLType,
GrShaderVar::TypeModifier::Out));
args.fVertBuilder->codeAppendf("P = inputPoint;");
// The fragment shader just outputs a uniform color.
args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
args.fFragBuilder->codeAppendf("%s = half4(1);", args.fOutputCoverage);
}
void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc,
const CoordTransformRange& transformRange) override {
const auto& shader = primProc.cast<GrTessellateStrokeShader>();
if (fCachedMiterLimitValue != shader.fMiterLimit) {
pdman.set1f(fMiterLimitUniform, shader.fMiterLimit);
fCachedMiterLimitValue = shader.fMiterLimit;
}
if (fCachedColorValue != shader.fColor) {
pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
fCachedColorValue = shader.fColor;
}
this->setTransformDataHelper(pdman, transformRange);
}
GrGLSLUniformHandler::UniformHandle fMiterLimitUniform;
GrGLSLUniformHandler::UniformHandle fColorUniform;
float fCachedMiterLimitValue = -1;
SkMatrix fCachedViewMatrixValue = SkMatrix::I();
SkPMColor4f fCachedColorValue = {-1, -1, -1, -1};
};
SkString GrTessellateStrokeShader::getTessControlShaderGLSL(
const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
auto impl = static_cast<const GrTessellateStrokeShader::Impl*>(glslPrimProc);
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));
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[];
out vec4 X[];
out vec4 Y[];
out vec2 fanAngles[];
out vec2 strokeRadii[];
out vec2 outsetClamp[];
void main() {
// The 5th point contains the patch type and stroke radius.
float strokeRadius = P[4].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);
// 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]))));
// A patch can override the number of segments it gets chopped into by passing a
// negative value as P[4].x. (Square caps do this to only draw one segment.)
if (P[4].x < 0) {
numSegments = -P[4].x;
}
// A positive 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));
// Determine the beginning and end angles of our join.
fanAngles[gl_InvocationID /*== 0*/] = atan(c0.y, c0.x) + vec2(0, theta);
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;
}
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)));
}
}
// 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;
}
)");
return code;
}
SkString GrTessellateStrokeShader::getTessEvaluationShaderGLSL(
const GrGLSLPrimitiveProcessor*, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler&, const GrShaderCaps&) const {
SkString code(versionAndExtensionDecls);
code.append(R"(
layout(quads, equal_spacing, ccw) in;
in vec4 X[];
in vec4 Y[];
in vec2 fanAngles[];
in vec2 strokeRadii[];
in vec2 outsetClamp[];
uniform vec4 sk_RTAdjust;
void main() {
float strokeRadius = strokeRadii[0].x;
mat4x2 P = transpose(mat2x4(X[0], Y[0]));
float T = gl_TessCoord.x;
// 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);
// 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];
}
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;
}
// 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;
vec2 vertexpos = position + vec2(-tangent.y, tangent.x) * outset;
)");
// Transform after tessellation. Stroke widths and normals are defined in (pre-transform) local
// path space.
if (!this->viewMatrix().isIdentity()) {
SK_ABORT("Non-identity matrices not supported.");
// TODO: implement.
}
code.append(R"(
gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
}
)");
return code;
}
GrGLSLPrimitiveProcessor* GrTessellateStrokeShader::createGLSLInstance(
const GrShaderCaps&) const {
return new Impl;
}