diff options
author | Shawn Rutledge <[email protected]> | 2024-12-04 22:10:09 +0100 |
---|---|---|
committer | Shawn Rutledge <[email protected]> | 2025-09-12 23:25:21 +0200 |
commit | 3f89d9deb0bcdfdfe2b8534603adf235edc4a9f0 (patch) | |
tree | 978174f9608b49963af672228dff55e10942945c /src/quick | |
parent | 6803e9c90862f650cb016dc5554efdcc66978e6b (diff) |
Add ShapePath.cosmeticStroke
QSvgRenderer already obeys the QPen::isCosmetic() setting.
Now we add a ShapePath.cosmeticStroke property and try to support
cosmetic stroking both with SVG and with Shapes, in all renderers.
The curve renderer now starts with skinny triangles: each segment's end
vertices are passed to the vertex shader with their original positions,
and the shader uses the normal and the stroke width to expand the stroke
triangles outwards as needed. We always add triangles for end caps,
regardless whether they are square or round. We get rid of
addBevelTriangle and fix bevels, square caps and miters:
All three of these cases are done by drawing lines adjusted to the
right direction within the respective triangles. And to avoid seeing
rounded ends at any reasonable zoom level, we need the line equation
coefficients to take the line very far outside the actual triangles.
Square caps are really square: we render line segments in those three
triangles, not extensions of the adjacent curve or line.
Miters are also rendered as straight tangent lines.
The bevel's triangle is very short when the join is an acute angle,
and almost as tall as the full stroke width when the join is very
obtuse. But when the triangle is short, we need to diminish the stroke
width rendered in the fragment shader so that the center of the stroke
falls on the inner corner of that triangle, and the edge of the stroke
is rendered along the outer bevel edge rather than trying to go outside.
That's achieved by multiplying the stroke width by the cosine of half
the total angle, AKA the dot product. That is now in the normalExt.z
vertex attribute.
Normals (normalExt.xy) can be premultiplied rather than normalized:
in fact some of them already have length > 1.
In qsgbatchrenderer, Renderer::prepareAlphaBatches() breaks batches
when overlaps occur. Now that we stroke lines with vertices that
represent them as zero-width lines (and thus Element.bounds has
the same x or y coordinates on both corners), we must consider lines
right on top of each other to be overlapping: e.g. the stacks of
horizontal (dashed) line segments in paint-stroke-202-t.svg must be
in separate batches.
At the time QQuickShape::updatePaintNode() is called, the available
transform node (from UpdatePaintNodeData or an individual node's parent
which is a transform node) does not contain the scaling factor that we
need to allow for the stroke width to be adjusted for cosmetic stroking.
But in QQuickShapePrivate::sync(), windowToItemTransform() is known, and
from bde55ad574ac84440e2cdc9c1122a344bb1cb67a we have a precedent in
QSGCurveStrokeMaterialShader::updateUniformData() for using the square
root of the matrix determinant as a scaling approximation (ok when the
scaling is uniform). QQuickShapeSoftwareRenderer::setNode() was already
adjusting a path's bounding rect by its stroke width, and we need a
multiplicative factor there to account for cosmetic stroking, to avoid
excessive clipping in the software renderer. So now we have another use
for the triangulationScale that was introduced in
bcfcaeb87be783d8c329b0bc96323f1c9863982d.
When QQShapeGenericRenderer is used (rendererType == GeometryRenderer),
and any ShapePath has cosmeticStroke, we need it to re-triangulate
whenever scale changes. QQuickShapeGenericRenderer::triangulateStroke()
calls QTriangulatingStroker::setInvScale(1 / triangulationScale), and
QTriangulatingStroker::process() multiplies its m_width by the inverse
scale that was set (since 2009). So this tells us that the intended
meaning of triangulationScale is the inverse of the factor by which we
multiply the pen width. And when QQShapeGenericRenderer is in use,
and there are cosmetic strokes, QQuickShape::itemChange triggers
re-triangulation on changes in scale.
When setting the QQuickShapeCurveRenderer::DebugWireframe debug
visualization flag, we need to repeat the vertex shader calculations to
expand the "skinny" triangles according to stroke width, just as we do
with the actual stroking vertices.
For now customTriangulator2 remains as legacy code, to be removed later
on. It's poorly named, and returns a list of TriangleData which need to
be iterated afterwards ("fix it in post"), looking up the
QQuadPath::Element again in that second loop, which can go wrong when a
path contains a move command. (For example, it could calculate a bevel
between the end-tangent of one subpath and the start-tangent of the
next.) customTriangulator2() was called from only one place,
processStroke(), to which addStrokeTriangleCallback() is given: so the
new way is to just call the callback directly as soon as we've
calculated each triangle. Because we are not iterating again afterwards,
the switch(type) is not needed in that case, and we no longer need
TriangleData::type, except for supporting customTriangulator2().
[ChangeLog][QtQuick][Shapes] ShapePath now has a cosmeticStroke
property which causes strokeWidth to be constant despite scaling.
Set the environment variable QT_QUICKSHAPES_STROKE_EXPANDING to 1 to
enable an experimental method of expanding strokes in the vertex shader,
minimizing the need to re-triangulate when strokeWidth changes.
Task-number: QTBUG-124638
Change-Id: I4eac0ddcd6f623b79bc70c766ff116f4b77736cb
Reviewed-by: Paul Olav Tvete <[email protected]>
Reviewed-by: Eskil Abrahamsen Blomfeldt <[email protected]>
Diffstat (limited to 'src/quick')
-rw-r--r-- | src/quick/CMakeLists.txt | 17 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurveglyphatlas.cpp | 3 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurveprocessor.cpp | 346 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurveprocessor_p.h | 8 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurvestrokenode.cpp | 37 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurvestrokenode_p.cpp | 16 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurvestrokenode_p.h | 25 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurvestrokenode_p_p.h | 39 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgrhiinternaltextnode.cpp | 5 | ||||
-rw-r--r-- | src/quick/scenegraph/shaders_ng/shapestroke.frag | 22 | ||||
-rw-r--r-- | src/quick/scenegraph/shaders_ng/shapestroke.vert | 53 | ||||
-rw-r--r-- | src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag | 34 | ||||
-rw-r--r-- | src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert | 52 |
13 files changed, 564 insertions, 93 deletions
diff --git a/src/quick/CMakeLists.txt b/src/quick/CMakeLists.txt index 1f790fbb39..a2b5a39ce4 100644 --- a/src/quick/CMakeLists.txt +++ b/src/quick/CMakeLists.txt @@ -338,6 +338,8 @@ qt_internal_add_shaders(Quick "scenegraph_curve_shaders" "scenegraph/shaders_ng/shapecurve.vert" "scenegraph/shaders_ng/shapestroke.frag" "scenegraph/shaders_ng/shapestroke.vert" + "scenegraph/shaders_ng/shapestroke_wireframe.frag" + "scenegraph/shaders_ng/shapestroke_wireframe.vert" ) qt_internal_add_shaders(Quick "scenegraph_curve_shaders_derivatives" @@ -361,6 +363,21 @@ qt_internal_add_shaders(Quick "scenegraph_curve_shaders_derivatives" "scenegraph/shaders_ng/shapestroke_derivatives.vert.qsb" ) +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_expanding" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + MULTIVIEW + DEFINES "STROKE_EXPANDING" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapestroke.vert" + OUTPUTS + "scenegraph/shaders_ng/shapestroke_expanding.vert.qsb" +) + qt_internal_add_shaders(Quick "scenegraph_curve_shaders_lg" SILENT BATCHABLE diff --git a/src/quick/scenegraph/qsgcurveglyphatlas.cpp b/src/quick/scenegraph/qsgcurveglyphatlas.cpp index 17460e46ab..0d17597895 100644 --- a/src/quick/scenegraph/qsgcurveglyphatlas.cpp +++ b/src/quick/scenegraph/qsgcurveglyphatlas.cpp @@ -38,10 +38,11 @@ void QSGCurveGlyphAtlas::populate(const QList<glyph_t> &glyphs) Glyph glyph; - QSGCurveProcessor::processStroke(quadPath, 2, 2, Qt::MiterJoin, Qt::FlatCap, + QSGCurveProcessor::processStroke(quadPath, 2, 2, false, Qt::MiterJoin, Qt::FlatCap, [&glyph](const std::array<QVector2D, 3> &s, const std::array<QVector2D, 3> &p, const std::array<QVector2D, 3> &n, + const std::array<float, 3> & /* extrusions */, QSGCurveStrokeNode::TriangleFlags f) { glyph.strokeVertices.append(s.at(0)); glyph.strokeVertices.append(s.at(1)); diff --git a/src/quick/scenegraph/qsgcurveprocessor.cpp b/src/quick/scenegraph/qsgcurveprocessor.cpp index 1b9e803752..f51dc6d857 100644 --- a/src/quick/scenegraph/qsgcurveprocessor.cpp +++ b/src/quick/scenegraph/qsgcurveprocessor.cpp @@ -324,8 +324,9 @@ static bool isIntersecting(const QQuadPath &path, int e1, int e2, QList<std::pai struct TriangleData { TrianglePoints points; - int pathElementIndex = std::numeric_limits<int>::min(); TrianglePoints normals; + std::array<float, 3> extrusions; + int pathElementIndex = std::numeric_limits<int>::min(); private: #ifndef QT_NO_DEBUG_STREAM @@ -453,7 +454,8 @@ QList<TriangleData> simplePointTriangulator(const QList<QVector2D> &pts, const Q int i0 = hull[0]; int i1 = hull[i]; int i2 = hull[i+1]; - ret.append({{pts[i0], pts[i1], pts[i2]}, elementIndex, {normals[i0], normals[i1], normals[i2]}}); + ret.append({{pts[i0], pts[i1], pts[i2]}, {normals[i0], normals[i1], normals[i2]}, + QSGCurveStrokeNode::defaultExtrusions(), elementIndex}); } return ret; } @@ -548,11 +550,13 @@ static std::array<QVector2D, 5> calculateJoin(const QQuadPath::Element *element1 auto tooLong = [penFactor, length](QVector2D p1, QVector2D p2, QVector2D n) -> bool { auto v = p2 - p1; // It's too long if the projection onto the bisector is longer than the bisector - // and the projection onto the normal to the bisector is shorter - // than the pen margin (that projection is just v - proj) + // and half the projection (v - proj) onto the normal to the bisector + // (minus AA margin) is shorter than the pen factor. // (We're also adding a 10% safety margin to length to make room for AA: not exact.) auto projLen = QVector2D::dotProduct(v, n); - return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < penFactor; + return projLen * 0.9f < length && + (QSGCurveStrokeNode::expandingStrokeEnabled() ? ((v - n * projLen).length() - 2.0) * 0.5 + : (v - n * projLen).length() * 0.9) < penFactor; }; // The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating @@ -620,12 +624,12 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW bool simpleCase = !checkTriangleOverlap(t1, t2); if (simpleCase) { - ret.append({{p1, p2, p5}, idx, {n1, n2, controlPointNormal}}); - ret.append({{p3, p4, p5}, idx, {n3, n4, controlPointNormal}}); + ret.append({{p1, p2, p5}, {n1, n2, controlPointNormal}, {}, idx}); + ret.append({{p3, p4, p5}, {n3, n4, controlPointNormal}, {}, idx}); if (controlPointOnRight) { - ret.append({{p1, p3, p5}, idx, {n1, n3, controlPointNormal}}); + ret.append({{p1, p3, p5}, {n1, n3, controlPointNormal}, {}, idx}); } else { - ret.append({{p2, p4, p5}, idx, {n2, n4, controlPointNormal}}); + ret.append({{p2, p4, p5}, {n2, n4, controlPointNormal}, {}, idx}); } } else { ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx)); @@ -740,9 +744,9 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW } else if (squareCap) { QVector2D c1 = p1 + capSpace; QVector2D c2 = p2 + capSpace; - ret.append({{p1, s, c1}, -1, {}}); - ret.append({{c1, s, c2}, -1, {}}); - ret.append({{p2, s, c2}, -1, {}}); + ret.append({{p1, s, c1}, {}, {}, -1}); + ret.append({{c1, s, c2}, {}, {}, -1}); + ret.append({{p2, s, c2}, {}, {}, -1}); } } if (!nextElement) { @@ -753,15 +757,15 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW } else if (squareCap) { QVector2D c3 = p3 + capSpace; QVector2D c4 = p4 + capSpace; - ret.append({{p3, e, c3}, -1, {}}); - ret.append({{c3, e, c4}, -1, {}}); - ret.append({{p4, e, c4}, -1, {}}); + ret.append({{p3, e, c3}, {}, {}, -1}); + ret.append({{c3, e, c4}, {}, {}, -1}); + ret.append({{p4, e, c4}, {}, {}, -1}); } } if (element.isLine()) { - ret.append({{p1, p2, p3}, i, {n1, n2, n3}}); - ret.append({{p2, p3, p4}, i, {n2, n3, n4}}); + ret.append({{p1, p2, p3}, {n1, n2, n3}, {}, i}); + ret.append({{p2, p3, p4}, {n2, n3, n4}, {}, i}); } else { triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4); } @@ -777,27 +781,27 @@ static QList<TriangleData> customTriangulator2(const QQuadPath &path, float penW //const auto inner = e + endInner * penFactor; if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) { - ret.append({{outer1, e, outer2}, -1, {}}); + ret.append({{outer1, e, outer2}, {}, {}, -1}); } else if (roundJoin) { - ret.append({{outer1, e, outer2}, i, {}}); + ret.append({{outer1, e, outer2}, {}, {}, i}); QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penFactor; if (!innerOnRight) nn = -nn; - ret.append({{outer1, outer1 + nn, outer2}, i, {}}); - ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {}}); + ret.append({{outer1, outer1 + nn, outer2}, {}, {}, i}); + ret.append({{outer1 + nn, outer2, outer2 + nn}, {}, {}, i}); } else if (miterJoin) { QVector2D outer = e + outerB * penFactor; - ret.append({{outer1, e, outer}, -2, {}}); - ret.append({{outer, e, outer2}, -2, {}}); + ret.append({{outer1, e, outer}, {}, {}, -2}); + ret.append({{outer, e, outer2}, {}, {}, -2}); } if (!giveUpOnEndJoin) { QVector2D inner = e + endInner * penFactor; - ret.append({{inner, e, outer1}, i, {endInner, {}, endOuter}}); + ret.append({{inner, e, outer1}, {endInner, {}, endOuter}, {}, i}); // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet) int nextIdx = addIdx(i, +1); - ret.append({{inner, e, outer2}, nextIdx, {endInner, {}, nextOuter}}); + ret.append({{inner, e, outer2}, {endInner, {}, nextOuter}, {}, nextIdx}); } } } @@ -1565,24 +1569,30 @@ bool QSGCurveProcessor::solveIntersections(QQuadPath &path, bool removeNestedPat void QSGCurveProcessor::processStroke(const QQuadPath &strokePath, float miterLimit, - float penWidth, + float penWidth, bool cosmetic, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, addStrokeTriangleCallback addTriangle, int subdivisions) { + const bool expandingInVertexShader = cosmetic || QSGCurveStrokeNode::expandingStrokeEnabled(); auto thePath = subdivide(strokePath, subdivisions).flattened(); // TODO: don't flatten, but handle it in the triangulator - auto triangles = customTriangulator2(thePath, penWidth, joinStyle, capStyle, miterLimit); - qCDebug(lcSGCurveProcessor) << thePath << "->" << triangles; auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t) { + qCDebug(lcSGCurveProcessor) << element << "->" << t; QSGCurveStrokeNode::TriangleFlags flags; flags.setFlag(QSGCurveStrokeNode::TriangleFlag::Line, element.isLine()); addTriangle(t.points, { element.startPoint(), element.controlPoint(), element.endPoint() }, - t.normals, flags); + t.normals, t.extrusions, flags); }; + if (!expandingInVertexShader) { + // this block is outdented to preserve git line history + // TODO remove customTriangulator2 in a future version + auto triangles = customTriangulator2(thePath, penWidth, joinStyle, capStyle, miterLimit); + qCDebug(lcSGCurveProcessor) << thePath << "->" << triangles; + auto addBevelTriangle = [&](const TrianglePoints &p, QSGCurveStrokeNode::TriangleFlags flags) { QVector2D fp1 = p[0]; @@ -1606,7 +1616,7 @@ void QSGCurveProcessor::processStroke(const QQuadPath &strokePath, n[2] = (p[2] - p[1]).normalized(); flags.setFlag(QSGCurveStrokeNode::TriangleFlag::Line); - addTriangle(p, { fp1, QVector2D(0.0f, 0.0f), fp2 }, n, flags); + addTriangle(p, { fp1, QVector2D(0.0f, 0.0f), fp2 }, n, {}, flags); }; for (const auto &triangle : std::as_const(triangles)) { @@ -1617,6 +1627,282 @@ void QSGCurveProcessor::processStroke(const QQuadPath &strokePath, const auto &element = thePath.elementAt(triangle.pathElementIndex); addCurveTriangle(element, triangle); } + + return; + } // legacy customTriangulator2 if we aren't using the expanding vertex shader + + auto addEdgeTriangle = [&](QVector2D start, QVector2D end, const TriangleData &t) { + qCDebug(lcSGCurveProcessor) << "line from" << start << "to" << end << "->" << t; + addTriangle(t.points, { start, start, end }, t.normals, t.extrusions, QSGCurveStrokeNode::TriangleFlag::Line); + }; + + const bool bevelJoin = joinStyle == Qt::BevelJoin; + const bool roundJoin = joinStyle == Qt::RoundJoin; + const bool miterJoin = !bevelJoin && !roundJoin; + + // Whether to allow a miter to simply be two adjacent triangle-pair stroke + // quads coming together at an angle: so far, it only works for round joins + // (and even then, only for suitably obtuse angles), because we need + // synthetic line-equation coefficients to get straight-edged joins. + // When we allow the user-provided endpoints to be visible inside the + // triangles, the fragment shader makes round endcaps. + const bool simpleMiter = joinStyle == Qt::RoundJoin; + + Q_ASSERT(miterLimit > 0 || !miterJoin); + const float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0; + const float penFactor = penWidth / 2; + + auto triangulateCurve = [&](int idx, const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, const QVector2D &p4, + const QVector2D &n1, const QVector2D &n2, const QVector2D &n3, const QVector2D &n4) + { + const auto &element = thePath.elementAt(idx); + Q_ASSERT(!element.isLine()); + const auto &s = element.startPoint(); + const auto &c = element.controlPoint(); + const auto &e = element.endPoint(); + // TODO: Don't flatten the path in addCurveStrokeNodes, but iterate over the children here instead + bool controlPointOnRight = determinant(s, c, e) > 0; + QVector2D startNormal = normalVector(element); + QVector2D endNormal = normalVector(element, true); + QVector2D controlPointNormal = (startNormal + endNormal).normalized(); + if (controlPointOnRight) + controlPointNormal = -controlPointNormal; + TrianglePoints t1{p1, p2, c}; + TrianglePoints t2{p3, p4, c}; + bool simpleCase = !checkTriangleOverlap(t1, t2); + if (simpleCase) { + addCurveTriangle(element, {{p1, p2, c}, {n1, n2, controlPointNormal}, + QSGCurveStrokeNode::defaultExtrusions(), idx}); + addCurveTriangle(element, {{p3, p4, c}, {n3, n4, controlPointNormal}, + QSGCurveStrokeNode::defaultExtrusions(), idx}); + if (controlPointOnRight) { + addCurveTriangle(element, {{p1, p3, c}, {n1, n3, controlPointNormal}, + QSGCurveStrokeNode::defaultExtrusions(), idx}); + } else { + addCurveTriangle(element, {{p2, p4, c}, {n2, n4, controlPointNormal}, + QSGCurveStrokeNode::defaultExtrusions(), idx}); + } + } else { + const auto &triangles = simplePointTriangulator({p1, p2, c, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx); + for (const auto &triangle : triangles) + addCurveTriangle(element, triangle); + } + }; + + // Each element is calculated independently, so we don't have to special-case closed sub-paths. + // Take care so the end points of one element are precisely equal to the start points of the next. + // Any additional triangles needed for joining are added at the end of the current element. + + const int count = thePath.elementCount(); + int subStart = 0; + while (subStart < count) { + int subEnd = subStart; + for (int i = subStart + 1; i < count; ++i) { + const auto &e = thePath.elementAt(i); + if (e.isSubpathStart()) { + subEnd = i - 1; + break; + } + if (i == count - 1) { + subEnd = i; + break; + } + } + bool closed = thePath.elementAt(subStart).startPoint() == thePath.elementAt(subEnd).endPoint(); + const int subCount = subEnd - subStart + 1; + + auto addIdx = [&](int idx, int delta) -> int { + int subIdx = idx - subStart; + if (closed) + subIdx = (subIdx + subCount + delta) % subCount; + else + subIdx += delta; + return subStart + subIdx; + }; + auto elementAt = [&](int idx, int delta) -> const QQuadPath::Element * { + int subIdx = idx - subStart; + if (closed) { + subIdx = (subIdx + subCount + delta) % subCount; + return &thePath.elementAt(subStart + subIdx); + } + subIdx += delta; + if (subIdx >= 0 && subIdx < subCount) + return &thePath.elementAt(subStart + subIdx); + return nullptr; + }; + + for (int i = subStart; i <= subEnd; ++i) { + const auto &element = thePath.elementAt(i); + const auto *nextElement = elementAt(i, +1); + const auto *prevElement = elementAt(i, -1); + + const auto &s = element.startPoint(); + const auto &e = element.endPoint(); + + bool startInnerIsRight; + bool startBisectorWithinMiterLimit; // Not used + bool giveUpOnStartJoin; // Not used + auto startJoin = calculateJoin(prevElement, &element, + penFactor, inverseMiterLimit, simpleMiter, + startBisectorWithinMiterLimit, startInnerIsRight, + giveUpOnStartJoin); + const QVector2D &startInner = startJoin[1]; + const QVector2D &startOuter = startJoin[3]; + + bool endInnerIsRight; + bool endBisectorWithinMiterLimit; + bool giveUpOnEndJoin; + auto endJoin = calculateJoin(&element, nextElement, + penFactor, inverseMiterLimit, simpleMiter, + endBisectorWithinMiterLimit, endInnerIsRight, + giveUpOnEndJoin); + const QVector2D endInner = endJoin[0]; + const QVector2D endOuter = endJoin[2]; + const QVector2D nextOuter = endJoin[3]; + const QVector2D outerBisector = endJoin[4]; + const QVector2D startTangent = tangentVector(element, false).normalized(); + const QVector2D endTangent = tangentVector(element, true).normalized(); + + QVector2D n1, n2, n3, n4; + + if (startInnerIsRight) { + n1 = startInner; + n2 = startOuter; + } else { + n1 = startOuter; + n2 = startInner; + } + + // repeat logic above for the other end: + if (endInnerIsRight) { + n3 = endInner; + n4 = endOuter; + } else { + n3 = endOuter; + n4 = endInner; + } + + // When we fill triangles that make up square caps with fake lines, + // we need the line equations to extend way beyond the triangles, + // so that corner roundoff won't occur at any reasonable zoom level. + // But if the virtual lines are too long, AA quality suffers. + // Multiplying by 50 seems to get it to behave reasonably at zoom levels from 0.01 to 50. + static const float artificialLineExtension = 50; + + // End cap triangles + if (capStyle != Qt::FlatCap) { + const QVector2D capNormalNone(0, 0); + // a cap is rendered in 3 triangles: in all cases below, the order is outer, middle, outer + if (!prevElement) { + // If the cap happens to be left-facing, "up" means in the -y direction, + // "down" is in the +y direction and so on; otherwise they are all rotated together + const QVector2D capNormalUp(startTangent.y(), -startTangent.x()); + const QVector2D capNormalDown = -capNormalUp; + // hypoteneuses of triangles made from normalized vectors are longer: not re-normalized + const QVector2D capNormalUpOut = (capNormalUp - startTangent); + const QVector2D capNormalDownOut = (capNormalDown - startTangent); // not in the magic kingdom + Q_ASSERT(capNormalUpOut.length() == capNormalDownOut.length()); + if (capStyle == Qt::RoundCap) { + addCurveTriangle(element, {{s, s, s}, {capNormalUp, capNormalNone, capNormalUpOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{s, s, s}, {capNormalUpOut, capNormalNone, capNormalDownOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{s, s, s}, {capNormalDown, capNormalNone, capNormalDownOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + } else { // SquareCap + addEdgeTriangle(element.startPoint(), element.startPoint() - startTangent * penWidth * artificialLineExtension, + {{s, s, s}, {capNormalUp, capNormalNone, capNormalUpOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + const auto norm = normalVector(element, false).normalized() * penWidth * artificialLineExtension; + addEdgeTriangle(element.startPoint() - norm, element.startPoint() + norm, + {{s, s, s}, {capNormalUpOut, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addEdgeTriangle(element.startPoint(), element.startPoint() - startTangent * penWidth * artificialLineExtension, + {{s, s, s}, {capNormalDown, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i}); + } + } + if (!nextElement) { + const QVector2D capNormalUp(endTangent.y(), -endTangent.x()); + const QVector2D capNormalDown = -capNormalUp; + const QVector2D capNormalUpOut = (capNormalUp - endTangent); + const QVector2D capNormalDownOut = (capNormalDown - endTangent); + Q_ASSERT(capNormalUpOut.length() == capNormalDownOut.length()); + if (capStyle == Qt::RoundCap) { + addCurveTriangle(element, {{e, e, e}, {capNormalDown, capNormalNone, capNormalDownOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{e, e, e}, {capNormalUpOut, capNormalNone, capNormalDownOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{e, e, e}, {capNormalUp, capNormalNone, capNormalUpOut}, + QSGCurveStrokeNode::defaultExtrusions(), i}); + } else { // SquareCap + addEdgeTriangle(element.endPoint() - endTangent * penWidth * artificialLineExtension, element.endPoint(), + {{e, e, e}, {capNormalDown, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i}); + const auto norm = normalVector(element, true).normalized() * penWidth * artificialLineExtension; + addEdgeTriangle(element.endPoint() - norm, element.endPoint() + norm, + {{e, e, e}, {capNormalUpOut, capNormalNone, capNormalDownOut}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addEdgeTriangle(element.endPoint() - endTangent * penWidth * artificialLineExtension, element.endPoint(), + {{e, e, e}, {capNormalUp, capNormalNone, capNormalUpOut}, QSGCurveStrokeNode::defaultExtrusions(), i}); + } + } + } + + if (element.isLine()) { + addCurveTriangle(element, {{s, s, e}, {n1, n2, n3}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{s, e, e}, {n2, n3, n4}, QSGCurveStrokeNode::defaultExtrusions(), i}); + } else { + triangulateCurve(i, s, s, e, e, n1, n2, n3, n4); + } + + bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin; + if (!trivialJoin && nextElement) { + // inside of join (opposite of bevel) is defined by triangle s, e, next.e + bool innerOnRight = endInnerIsRight; + + const auto outer1 = e + endOuter; + const auto outer2 = e + nextOuter; + QVector2D nn = calcNormalVector(outer1, outer2).normalized(); + if (!innerOnRight) + nn = -nn; + const QVector2D endOuterN = (outer1 - e).normalized(); + const QVector2D nextOuterN = (outer2 - e).normalized(); + const QVector2D endOuterBisectorN = (endOuterN + nn.normalized()).normalized(); + const QVector2D nextOuterBisectorN = (nextOuterN + nn.normalized()).normalized(); + + if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) { + const float cosTheta = QVector2D::dotProduct(endOuterN, nextOuterN); // divided by magnitudes, which are 1s + const float cosHalf = cos(acos(cosTheta) / 2); + // draw a line from the end of element to the beginning of the next: + // slope is the average of the two tangents + const auto tan1 = endTangent * penWidth * artificialLineExtension; + const auto tan2 = tangentVector(*nextElement, false).normalized() * penWidth * artificialLineExtension; + const auto bevelTan = (tan2 - tan1) / 2; + // element.endPoint() is not the centerline of the bevel's stroke if full width, so + // we use extrusion (normalExt.z) in the vertex shader to adjust the stroke width so that its edge + // falls in the right place to draw a clean bevel within this triangle: i.e. + // compensate for the triangle becoming smaller as the join angle becomes more acute. + addEdgeTriangle(element.endPoint() - bevelTan, element.endPoint() + bevelTan, + {{e, e, e}, {endOuterN, {}, nextOuterN}, {cosHalf, cosHalf, cosHalf}, i}); + } else if (roundJoin) { + addCurveTriangle(element, {{outer1, e, outer2}, {endOuterN, {}, nextOuterN}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{outer1, outer1 + nn, outer2}, {endOuterN, endOuterBisectorN, nextOuterN}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addCurveTriangle(element, {{outer1 + nn, outer2, outer2 + nn}, {endOuterBisectorN, nextOuterN, nextOuterBisectorN}, QSGCurveStrokeNode::defaultExtrusions(), i}); + } else if (miterJoin) { + addEdgeTriangle(element.endPoint(), element.endPoint() - endTangent * penWidth * artificialLineExtension, + {{e, e, e}, {endOuterN, {}, outerBisector}, QSGCurveStrokeNode::defaultExtrusions(), i}); + addEdgeTriangle(nextElement->startPoint(), + nextElement->startPoint() - tangentVector(*nextElement, false).normalized() * penWidth * artificialLineExtension, + {{e, e, e}, {nextOuterN, {}, outerBisector}, QSGCurveStrokeNode::defaultExtrusions(), i}); + } + + if (!giveUpOnEndJoin) { + addCurveTriangle(element, {{e, e, e}, {endInner, {}, endOuter}, QSGCurveStrokeNode::defaultExtrusions(), i}); + // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet) + int nextIdx = addIdx(i, +1); + addCurveTriangle(*nextElement, {{e, e, e}, {endInner, {}, nextOuter}, QSGCurveStrokeNode::defaultExtrusions(), nextIdx}); + } + } + } + subStart = subEnd + 1; + } } // 2x variant of qHash(float) diff --git a/src/quick/scenegraph/qsgcurveprocessor_p.h b/src/quick/scenegraph/qsgcurveprocessor_p.h index b4f40b5431..bd25204a8a 100644 --- a/src/quick/scenegraph/qsgcurveprocessor_p.h +++ b/src/quick/scenegraph/qsgcurveprocessor_p.h @@ -32,9 +32,10 @@ public: typedef std::function<void(const std::array<QVector2D, 3> &, const std::array<QVector2D, 3> &, uvForPointCallback)> addTriangleCallback; - typedef std::function<void(const std::array<QVector2D, 3> &, - const std::array<QVector2D, 3> &, - const std::array<QVector2D, 3> &, + typedef std::function<void(const std::array<QVector2D, 3> &, // vertex coordinates + const std::array<QVector2D, 3> &, // start, control, end + const std::array<QVector2D, 3> &, // normals + const std::array<float, 3> &, // extrusions QSGCurveStrokeNode::TriangleFlags)> addStrokeTriangleCallback; static void processFill(const QQuadPath &path, @@ -43,6 +44,7 @@ public: static void processStroke(const QQuadPath &strokePath, float miterLimit, float penWidth, + bool cosmetic, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, addStrokeTriangleCallback addTriangle, diff --git a/src/quick/scenegraph/qsgcurvestrokenode.cpp b/src/quick/scenegraph/qsgcurvestrokenode.cpp index f1f95d6cb2..be21eede78 100644 --- a/src/quick/scenegraph/qsgcurvestrokenode.cpp +++ b/src/quick/scenegraph/qsgcurvestrokenode.cpp @@ -11,12 +11,13 @@ QSGCurveStrokeNode::QSGCurveStrokeNode() setFlag(OwnsGeometry, true); qsgnode_set_description(this, QLatin1StringView("curve stroke")); setGeometry(new QSGGeometry(attributes(), 0, 0)); - updateMaterial(); + // defer updateMaterial() until later } void QSGCurveStrokeNode::QSGCurveStrokeNode::updateMaterial() { - m_material.reset(new QSGCurveStrokeMaterial(this)); + const bool expandingInVertexShader = m_cosmetic || expandingStrokeEnabled(); + m_material.reset(new QSGCurveStrokeMaterial(this, expandingInVertexShader)); setMaterial(m_material.data()); } @@ -35,10 +36,17 @@ std::array<QVector2D, 3> QSGCurveStrokeNode::curveABC(const std::array<QVector2D Append a triangle with \a vtx corners within which the fragment shader will draw the visible part of a quadratic curve from ctl[0] to ctl[2] with control point ctl[1] (AKA a quadratic Bézier curve with 3 control points). + The \a normal vectors are used in the vertex shader to expand the triangle + according to its stroke width: therefore, it's ok for the triangle to be + degenerate, and get expanded to size in the vertex shader. Normals are + usually unit vectors, but it's also ok for some to have larger magnitudes, + to handle the case when miter corners need to be extended proportionally + farther as stroke width increases. */ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx, const std::array<QVector2D, 3> &ctl, - const std::array<QVector2D, 3> &normal) + const std::array<QVector2D, 3> &normal, + const std::array<float, 3> &extrusions) { auto abc = curveABC(ctl); @@ -47,7 +55,7 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx, for (int i = 0; i < 3; ++i) { m_uncookedVertexes.append( { vtx[i].x(), vtx[i].y(), abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(), - normal[i].x(), normal[i].y() } ); + normal[i].x(), normal[i].y(), extrusions[i] } ); } m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; } @@ -55,13 +63,18 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx, /*! Append a triangle with \a vtx corners within which the fragment shader will draw the visible part of a line from ctl[0] to ctl[2]. + The \a normal vectors are used in the vertex shader to expand the triangle + according to its stroke width: therefore, it's ok for the triangle to be + degenerate, and get expanded to size in the vertex shader. Normals are + usually unit vectors, but it's also ok for some to have larger magnitudes, + to handle the case when miter corners need to be extended proportionally + farther as stroke width increases. */ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx, const std::array<QVector2D, 2> &ctl, const std::array<QVector2D, 3> &normal, - QSGCurveStrokeNode::TriangleFlags flags) + const std::array<float, 3> &extrusions) { - Q_UNUSED(flags) // We could reduce this to a linear equation by setting A to (0,0). // However, then we cannot use the cubic solution and need an additional // code path in the shader. The following formulation looks more complicated @@ -75,13 +88,14 @@ void QSGCurveStrokeNode::appendTriangle(const std::array<QVector2D, 3> &vtx, for (int i = 0; i < 3; ++i) { m_uncookedVertexes.append( { vtx[i].x(), vtx[i].y(), A.x(), A.y(), B.x(), B.y(), C.x(), C.y(), - normal[i].x(), normal[i].y() } ); + normal[i].x(), normal[i].y(), extrusions[i] } ); } m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; } void QSGCurveStrokeNode::cookGeometry() { + updateMaterial(); // by now, setCosmeticStroke has been called if necessary QSGGeometry *g = geometry(); if (g->indexType() != QSGGeometry::UnsignedIntType) { g = new QSGGeometry(attributes(), @@ -114,10 +128,17 @@ const QSGGeometry::AttributeSet &QSGCurveStrokeNode::attributes() QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //A QSGGeometry::Attribute::createWithAttributeType(2, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //B QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //C - QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalVector + QSGGeometry::Attribute::createWithAttributeType(4, 3, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalExt }; static QSGGeometry::AttributeSet attrs = { 5, sizeof(StrokeVertex), data }; return attrs; } +// TODO remove when we consider the expanding-stroke shader to be stable for full-time use +bool QSGCurveStrokeNode::expandingStrokeEnabled() +{ + static const bool ret = qEnvironmentVariableIntValue("QT_QUICKSHAPES_STROKE_EXPANDING"); + return ret; +} + QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.cpp b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp index b86493b169..f9a92df99b 100644 --- a/src/quick/scenegraph/qsgcurvestrokenode_p.cpp +++ b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp @@ -26,8 +26,11 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate m.scale(localScale); memcpy(buf->data() + viewIndex * 64, m.constData(), 64); } + // determinant is xscale * yscale, as long as Item.transform does not include shearing or rotation float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio() * localScale; memcpy(buf->data() + matrixCount * 64, &matrixScale, 4); + float dpr = state.devicePixelRatio(); + memcpy(buf->data() + matrixCount * 64 + 8, &dpr, 4); changed = true; } @@ -37,6 +40,12 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate changed = true; } + if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) { + float w = newNode->strokeWidth(); + memcpy(buf->data() + matrixCount * 64 + 12, &w, 4); + changed = true; + } + int offset = matrixCount * 64 + 16; if (newNode == nullptr) return changed; @@ -58,18 +67,11 @@ bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMate } offset += 16; - if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) { - float w = newNode->strokeWidth(); - memcpy(buf->data() + offset, &w, 4); - changed = true; - } - offset += 4; if (oldNode == nullptr || newNode->debug() != oldNode->debug()) { float w = newNode->debug(); memcpy(buf->data() + offset, &w, 4); changed = true; } -// offset += 4; return changed; } diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p.h index 11fa72a2df..3c57a1debb 100644 --- a/src/quick/scenegraph/qsgcurvestrokenode_p.h +++ b/src/quick/scenegraph/qsgcurvestrokenode_p.h @@ -39,6 +39,12 @@ public: return m_color; } + void setCosmeticStroke(bool c) + { + m_cosmetic = c; + markDirty(DirtyMaterial); + } + void setStrokeWidth(float width) { m_strokeWidth = width; @@ -47,26 +53,34 @@ public: float strokeWidth() const { - return m_strokeWidth; + // Negative stroke width would not normally mean anything; + // here we use it to mean that the stroke is cosmetic. + return (m_cosmetic ? -1.0 : 1.0) * qAbs(m_strokeWidth); } enum class TriangleFlag { + None = 0, Line = 1 << 0, }; Q_DECLARE_FLAGS(TriangleFlags, TriangleFlag) + static constexpr std::array<float, 3> defaultExtrusions() { return {1.0f, 1.0f, 1.0f}; } + void appendTriangle(const std::array<QVector2D, 3> &vtx, // triangle vertices const std::array<QVector2D, 3> &ctl, // curve points - const std::array<QVector2D, 3> &normal); // vertex normals + const std::array<QVector2D, 3> &normal, // vertex normals + const std::array<float, 3> &extrusions = defaultExtrusions()); void appendTriangle(const std::array<QVector2D, 3> &vtx, // triangle vertices const std::array<QVector2D, 2> &ctl, // line points const std::array<QVector2D, 3> &normal, // vertex normals - QSGCurveStrokeNode::TriangleFlags = {}); + const std::array<float, 3> &extrusions = defaultExtrusions()); void cookGeometry() override; static const QSGGeometry::AttributeSet &attributes(); + static bool expandingStrokeEnabled(); + QVector<quint32> uncookedIndexes() const { return m_uncookedIndexes; @@ -100,14 +114,17 @@ private: float ax, ay; float bx, by; float cx, cy; - float nx, ny; //normal vector: direction to move vertext to account for AA + float nx, ny; // normal vector: direction to move vertex to account for AA + float extrusion; // stroke width multiplier (* uniform strokeWidth) }; void updateMaterial(); static std::array<QVector2D, 3> curveABC(const std::array<QVector2D, 3> &p); + static const bool envStrokeExpanding; QColor m_color; + ushort m_cosmetic = false; // packs alongside QColor; could be turned into flags if needed float m_strokeWidth = 0.0f; float m_debug = 0.0f; float m_localScale = 1.0f; diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h index d5b8c7b2b1..8474062e5d 100644 --- a/src/quick/scenegraph/qsgcurvestrokenode_p_p.h +++ b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h @@ -20,19 +20,29 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + class QSGCurveStrokeNode; class QSGCurveStrokeMaterial; class Q_QUICK_EXPORT QSGCurveStrokeMaterialShader : public QSGMaterialShader { public: - QSGCurveStrokeMaterialShader(bool useDerivatives, int viewCount) + enum class Variant { // flags + Default = 0, + Expanding = 0x01, + Derivatives = 0x02 + }; + + QSGCurveStrokeMaterialShader(int variant, int viewCount) { - QString baseName(u":/qt-project.org/scenegraph/shaders_ng/shapestroke"); - if (useDerivatives) - baseName += u"_derivatives"; - setShaderFileName(VertexStage, baseName + u".vert.qsb", viewCount); - setShaderFileName(FragmentStage, baseName + u".frag.qsb", viewCount); + static constexpr auto baseName = u":/qt-project.org/scenegraph/shaders_ng/shapestroke"_sv; + setShaderFileName(VertexStage, baseName + + (variant & int(Variant::Expanding) ? u"_expanding"_sv : u""_sv) + + u".vert.qsb"_sv, viewCount); + setShaderFileName(FragmentStage, baseName + + (variant & int(Variant::Derivatives) ? u"_derivatives"_sv : u""_sv) + + u".frag.qsb"_sv, viewCount); } bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override; @@ -42,8 +52,8 @@ public: class Q_QUICK_EXPORT QSGCurveStrokeMaterial : public QSGMaterial { public: - QSGCurveStrokeMaterial(QSGCurveStrokeNode *node) - : m_node(node) + QSGCurveStrokeMaterial(QSGCurveStrokeNode *node, bool strokeExpanding = false) + : m_node(node), m_strokeExpanding(strokeExpanding) { setFlag(Blending, true); } @@ -58,15 +68,22 @@ public: protected: QSGMaterialType *type() const override { - static QSGMaterialType t; - return &t; + static QSGMaterialType legacyType; + static QSGMaterialType strokeExpandingType; + return m_strokeExpanding ? &strokeExpandingType : &legacyType; } QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override { - return new QSGCurveStrokeMaterialShader(renderMode == QSGRendererInterface::RenderMode3D, viewCount()); + int variant = int(QSGCurveStrokeMaterialShader::Variant::Default); + if (m_strokeExpanding) + variant |= int(QSGCurveStrokeMaterialShader::Variant::Expanding); + if (renderMode == QSGRendererInterface::RenderMode3D) + variant |= int(QSGCurveStrokeMaterialShader::Variant::Derivatives); + return new QSGCurveStrokeMaterialShader(variant, viewCount()); } QSGCurveStrokeNode *m_node; + bool m_strokeExpanding = false; }; QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgrhiinternaltextnode.cpp b/src/quick/scenegraph/qsgrhiinternaltextnode.cpp index 2bc2665784..cfbdfcaf9b 100644 --- a/src/quick/scenegraph/qsgrhiinternaltextnode.cpp +++ b/src/quick/scenegraph/qsgrhiinternaltextnode.cpp @@ -26,13 +26,14 @@ void QSGRhiInternalTextNode::addDecorationNode(const QRectF &rect, const QColor path.moveTo(QVector2D(rect.left(), c.y())); path.lineTo(QVector2D(rect.right(), c.y())); - QSGCurveProcessor::processStroke(path, 2, rect.height(), Qt::MiterJoin, Qt::FlatCap, + QSGCurveProcessor::processStroke(path, 2, rect.height(), false, Qt::MiterJoin, Qt::FlatCap, [&node](const std::array<QVector2D, 3> &s, const std::array<QVector2D, 3> &p, const std::array<QVector2D, 3> &n, + const std::array<float, 3> &ex, QSGCurveStrokeNode::TriangleFlags f) { Q_ASSERT(f.testFlag(QSGCurveStrokeNode::TriangleFlag::Line)); - node->appendTriangle(s, std::array<QVector2D, 2>{p.at(0), p.at(2)}, n); + node->appendTriangle(s, std::array<QVector2D, 2>{p.at(0), p.at(2)}, n, ex); }); node->cookGeometry(); appendChildNode(node); diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.frag b/src/quick/scenegraph/shaders_ng/shapestroke.frag index 3919438418..459fe0ad58 100644 --- a/src/quick/scenegraph/shaders_ng/shapestroke.frag +++ b/src/quick/scenegraph/shaders_ng/shapestroke.frag @@ -4,8 +4,7 @@ layout(location = 0) in vec4 P; layout(location = 1) in vec2 A; layout(location = 2) in vec2 B; layout(location = 3) in vec2 C; -layout(location = 4) in vec2 HG; -layout(location = 5) in float offset; +layout(location = 4) in vec4 HGOW; // H and G: args to solveDepressedCubic(); O: offset; W: strokeWidth layout(location = 0) out vec4 fragColor; @@ -18,15 +17,15 @@ layout(std140, binding = 0) uniform buf { float matrixScale; float opacity; - float reserved2; - float reserved3; + float devicePixelRatio; + float strokeWidth; vec4 strokeColor; - float strokeWidth; float debug; float reserved5; float reserved6; + float reserved7; } ubuf; float cuberoot(float x) @@ -78,7 +77,8 @@ mat2 qInverse(mat2 matrix) { void main() { - vec3 s = solveDepressedCubic(HG.x, HG.y) - vec3(offset, offset, offset); + float offset = HGOW.z; + vec3 s = solveDepressedCubic(HGOW.x, HGOW.y) - vec3(offset, offset, offset); // choose the value of s that minimizes the distance from our pixel to the point on the curve // stay in the logical coordinate system for now @@ -93,8 +93,8 @@ void main() Qmin = foundNewMin * Q + (1. - foundNewMin) * Qmin; } vec2 n = (P.xy - Qmin) / dmin; - vec2 Q1 = (Qmin + ubuf.strokeWidth / 2. * n); - vec2 Q2 = (Qmin - ubuf.strokeWidth / 2. * n); + vec2 Q1 = (Qmin + HGOW.w / 2. * n); + vec2 Q2 = (Qmin - HGOW.w / 2. * n); // Converting to screen coordinates: #if defined(USE_DERIVATIVES) @@ -136,9 +136,9 @@ void main() float fillCoverage = fillCoverageDia; // The center line is sometimes not filled because of numerical issues. This fixes this. - float centerline = step(ubuf.strokeWidth * 0.01, dmin); - fillCoverage = fillCoverage * centerline + min(1., ubuf.strokeWidth * ubuf.matrixScale) * (1. - centerline); + float centerline = step(HGOW.w * 0.01, dmin); + fillCoverage = fillCoverage * centerline + min(1., HGOW.w * ubuf.matrixScale) * (1. - centerline); - fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity + fragColor = vec4(ubuf.strokeColor.rgb, 1.0) * ubuf.strokeColor.a * fillCoverage * ubuf.opacity + ubuf.debug * vec4(0.0, 0.5, 1.0, 1.0) * (1.0 - fillCoverage) * ubuf.opacity; } diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.vert b/src/quick/scenegraph/shaders_ng/shapestroke.vert index e358e059eb..6e7161d391 100644 --- a/src/quick/scenegraph/shaders_ng/shapestroke.vert +++ b/src/quick/scenegraph/shaders_ng/shapestroke.vert @@ -1,18 +1,16 @@ #version 440 layout(location = 0) in vec4 vertexCoord; -layout(location = 1) in vec2 inA; +layout(location = 1) in vec2 inA; // A B and C: control points in logical coordinates layout(location = 2) in vec2 inB; layout(location = 3) in vec2 inC; -layout(location = 4) in vec2 normalVector; +layout(location = 4) in vec3 normalExt; // x and y: normal vector; z: strokeWidth multiplier (default 1) -layout(location = 0) out vec4 P; -layout(location = 1) out vec2 A; +layout(location = 0) out vec4 P; // stroke edge +layout(location = 1) out vec2 A; // A B and C: control points in logical coordinates layout(location = 2) out vec2 B; layout(location = 3) out vec2 C; -layout(location = 4) out vec2 HG; -layout(location = 5) out float offset; - +layout(location = 4) out vec4 HGOW; // H and G: args to solveDepressedCubic(); O: offset; W: adj strokeWidth layout(std140, binding = 0) uniform buf { #if QSHADER_VIEW_COUNT >= 2 @@ -23,15 +21,15 @@ layout(std140, binding = 0) uniform buf { float matrixScale; float opacity; - float reserved2; - float reserved3; + float devicePixelRatio; + float strokeWidth; vec4 strokeColor; - float strokeWidth; float debug; float reserved5; float reserved6; + float reserved7; } ubuf; #define SQRT2 1.41421356237 @@ -43,7 +41,33 @@ float qdot(vec2 a, vec2 b) void main() { - P = vertexCoord + vec4(normalVector, 0.0, 0.0) * SQRT2/ubuf.matrixScale; + vec4 normalVector = vec4(normalExt.xy, 0.0, 0.0); + // If STROKE_EXPANDING is _not_ defined, vertexCoord starts out in + // logical coordinates, already positioned outwards past an edge of the stroke. + P = vertexCoord + normalVector * SQRT2 / ubuf.matrixScale; + float adjStrokeWidth = abs(ubuf.strokeWidth); // passed to fragment shader in HGOW.w + +#if defined(STROKE_EXPANDING) + // vertexCoord starts out in logical coordinates, in the center of the stroke. + // Move the vertex by stroke width * normalVector to fill the stroke width; + // and in case of a cosmetic stroke, divide by matrixScale to undo the scaling. + // In the case of a miter joint, the tip vertices need to be moved farther, + // so the length of normalVector may be > 1. + P += normalVector * adjStrokeWidth; // expand to stroke width + + // Negative ubuf.strokeWidth means we want a "cosmetic pen", which means + // we make the stroke triangles wider or narrower to adjust for the difference + // between zoomed stroke width and cosmetic stroke width. + if (ubuf.strokeWidth < 0.) { // cosmetic stroke + adjStrokeWidth *= ubuf.devicePixelRatio / ubuf.matrixScale; + float strokeDiff = max(ubuf.devicePixelRatio / ubuf.matrixScale, adjStrokeWidth) * + (ubuf.matrixScale / ubuf.devicePixelRatio - 1) / 2.; + P -= normalVector * strokeDiff; // cosmetic adjustment + } + + // adjust stroke width by the given multiplier (usually 1) + adjStrokeWidth *= normalExt.z; +#endif // if the shader is expected to expand the stroke by moving vertices outwards A = inA; B = inB; @@ -53,7 +77,7 @@ void main() // t^2+H*t+G=0 // that results from the equation // Q'(s).(p-Q(s)) = 0 - // The last parameter is the static offset between s and t: + // O (HGOW.z) is the static offset between s and t: // s = t - b/(3a) // use it to get back the parameter t @@ -69,10 +93,7 @@ void main() // both functions are linear in c and d and thus linear in p float H = (3. * a * c - b * b) / (3. * a * a); float G = (2. * b * b * b - 9. * a * b * c + 27. * a * a * d) / (27. * a * a * a); - HG = vec2(H, G); - offset = b/(3*a); - - + HGOW = vec4(H, G, b / (3 * a), adjStrokeWidth); #if QSHADER_VIEW_COUNT >= 2 gl_Position = ubuf.qt_Matrix[gl_ViewIndex] * P; diff --git a/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag new file mode 100644 index 0000000000..880904492e --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.frag @@ -0,0 +1,34 @@ +#version 440 + +layout(location = 0) in vec3 barycentric; + +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { +#if QSHADER_VIEW_COUNT >= 2 + mat4 qt_Matrix[QSHADER_VIEW_COUNT]; +#else + mat4 qt_Matrix; +#endif + + float matrixScale; + float opacity; + float devicePixelRatio; + float strokeWidth; + + vec4 strokeColor; + + float debug; + float reserved5; + float reserved6; + float reserved7; +} ubuf; + +void main() +{ + float f = min(barycentric.x, min(barycentric.y, barycentric.z)); + float d = fwidth(f * 1.5); + float alpha = smoothstep(0.0, d, f); + + fragColor = vec4(1.0, 0.4, 0.1, 1.0) * (1.0 - alpha); +} diff --git a/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert new file mode 100644 index 0000000000..7c014da82b --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapestroke_wireframe.vert @@ -0,0 +1,52 @@ +#version 440 + +layout(location = 0) in vec4 vertexCoord; +layout(location = 1) in vec3 vertexBarycentric; +layout(location = 2) in vec3 normalExt; // x and y: normal vector; z: strokeWidth multiplier (default 1) +layout(location = 0) out vec3 barycentric; + +layout(std140, binding = 0) uniform buf { +#if QSHADER_VIEW_COUNT >= 2 + mat4 qt_Matrix[QSHADER_VIEW_COUNT]; +#else + mat4 qt_Matrix; +#endif + + float matrixScale; + float opacity; + float devicePixelRatio; + float strokeWidth; + + vec4 strokeColor; + + float debug; + float reserved5; + float reserved6; + float reserved7; +} ubuf; + +#define SQRT2 1.41421356237 + +void main() +{ + // the subset of calculations from shapestroke.vert necessary to get gl_Position + + vec4 normalV = vec4(normalExt.xy, 0.0, 0.0); + vec4 P = vertexCoord // center of stroke + + normalV * abs(ubuf.strokeWidth) // expand to stroke width + + normalV * SQRT2/ubuf.matrixScale; // AA + + if (ubuf.strokeWidth < 0.) { + float adjStrokeWidth = abs(ubuf.strokeWidth) * ubuf.devicePixelRatio / ubuf.matrixScale; + float strokeDiff = max(ubuf.devicePixelRatio / ubuf.matrixScale, adjStrokeWidth) * + (ubuf.matrixScale / ubuf.devicePixelRatio - 1) / 2.; + P -= normalV * strokeDiff; // cosmetic adjustment + } + + barycentric = vertexBarycentric; +#if QSHADER_VIEW_COUNT >= 2 + gl_Position = ubuf.qt_Matrix[gl_ViewIndex] * P; +#else + gl_Position = ubuf.qt_Matrix * P; +#endif +} |