From d3e23d6cfd5151b3a4c04889f09a31b65b5aa3cb Mon Sep 17 00:00:00 2001 From: Lemmy Date: Sun, 22 Mar 2026 11:14:29 -0400 Subject: [PATCH] fix(graph): improved AA on graph shader --- Shaders/frag/graph.frag | 57 +++++++++++++++++++++++++++++-------- Shaders/qsb/graph.frag.qsb | Bin 5977 -> 7632 bytes 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Shaders/frag/graph.frag b/Shaders/frag/graph.frag index fd7afd16f..940f2088e 100644 --- a/Shaders/frag/graph.frag +++ b/Shaders/frag/graph.frag @@ -34,7 +34,6 @@ float fetchData(float idx, int ch) { float cubicHermite(float y0, float y1, float y2, float y3, float t) { float m1 = (y2 - y0) * 0.25; float m2 = (y3 - y1) * 0.25; - float t2 = t * t; float t3 = t2 * t; return (2.0 * t3 - 3.0 * t2 + 1.0) * y1 @@ -56,6 +55,38 @@ float evalCurve(float dataIdx, int ch) { ); } +// Squared distance from point p to line segment a→b +float segDistSq(vec2 p, vec2 a, vec2 b) { + vec2 ab = b - a; + float len2 = dot(ab, ab); + float t = len2 > 0.0 ? clamp(dot(p - a, ab) / len2, 0.0, 1.0) : 0.0; + vec2 proj = a + t * ab; + vec2 d = p - proj; + return dot(d, d); +} + +// Minimum distance from fragment to curve via multi-segment sampling. +// Samples the curve at 9 half-pixel-spaced x-positions (±2px neighborhood) +// and returns the minimum distance to the 8 line segments between them. +float curveDistance(float dataIdx, float pixStep, float normY, int ch) { + vec2 frag = vec2(0.0, normY * resY); + + float px = -2.0; + float py = evalCurve(dataIdx - 2.0 * pixStep, ch) * resY; + vec2 d0 = frag - vec2(px, py); + float best = dot(d0, d0); + + for (int i = 1; i <= 8; i++) { + float cx = -2.0 + float(i) * 0.5; + float cy = evalCurve(dataIdx + cx * pixStep, ch) * resY; + best = min(best, segDistSq(frag, vec2(px, py), vec2(cx, cy))); + px = cx; + py = cy; + } + + return sqrt(best); +} + // Premultiplied alpha over compositing vec4 blendOver(vec4 src, vec4 dst) { return src + dst * (1.0 - src.a); @@ -72,9 +103,9 @@ void main() { if (count1 >= 4.0) { float segs = count1 - 3.0; float di = 2.0 + scroll1 + uv.x * segs; - float pixStep1 = dFdx(di); + float pixStep = dFdx(di); float cy = evalCurve(di, 0); - float cyNext = evalCurve(di + pixStep1, 0); + float cyNext = evalCurve(di + pixStep, 0); // Fill below curve (gradient: opaque at top, transparent at bottom) if (graphFillOpacity > 0.0 && normY <= cy) { @@ -82,11 +113,13 @@ void main() { result = blendOver(vec4(lineColor1.rgb * a, a), result); } - // Perpendicular distance to the line segment between adjacent samples + // Multi-segment distance for accurate AA at peaks and steep sections. + // AA width derived analytically from curve slope: (|sinθ|+|cosθ|) + // gives the ideal SDF fwidth (~1.0–1.41) without GPU derivative noise. + float dist = curveDistance(di, pixStep, normY, 0); float slope1 = (cyNext - cy) * resY; - float vDist1 = (normY - cy) * resY; - float dist1 = abs(vDist1) * inversesqrt(slope1 * slope1 + 1.0); - float sa = smoothstep(halfW + aaSize, halfW, dist1) * lineColor1.a; + float aa = (abs(slope1) + 1.0) * inversesqrt(slope1 * slope1 + 1.0) * aaSize * 2.0; + float sa = smoothstep(halfW + aa, halfW, dist) * lineColor1.a; result = blendOver(vec4(lineColor1.rgb * sa, sa), result); } @@ -94,19 +127,19 @@ void main() { if (count2 >= 4.0) { float segs = count2 - 3.0; float di = 2.0 + scroll2 + uv.x * segs; - float pixStep2 = dFdx(di); + float pixStep = dFdx(di); float cy = evalCurve(di, 1); - float cyNext = evalCurve(di + pixStep2, 1); + float cyNext = evalCurve(di + pixStep, 1); if (graphFillOpacity > 0.0 && normY <= cy) { float a = graphFillOpacity * normY * lineColor2.a; result = blendOver(vec4(lineColor2.rgb * a, a), result); } + float dist = curveDistance(di, pixStep, normY, 1); float slope2 = (cyNext - cy) * resY; - float vDist2 = (normY - cy) * resY; - float dist2 = abs(vDist2) * inversesqrt(slope2 * slope2 + 1.0); - float sa = smoothstep(halfW + aaSize, halfW, dist2) * lineColor2.a; + float aa = (abs(slope2) + 1.0) * inversesqrt(slope2 * slope2 + 1.0) * aaSize * 2.0; + float sa = smoothstep(halfW + aa, halfW, dist) * lineColor2.a; result = blendOver(vec4(lineColor2.rgb * sa, sa), result); } diff --git a/Shaders/qsb/graph.frag.qsb b/Shaders/qsb/graph.frag.qsb index e89ec293f9277524bb59d4d66e4173262cfeb1f5..16cdee2945df037c953f15718e0e25e7bf4807a1 100644 GIT binary patch literal 7632 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 literal 5977 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 F{tx=;1p)v7