fix: blobs now allow to be drawn in device pixels to stay sharp
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
This commit is contained in:
@@ -30,7 +30,6 @@ float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
|
||||
}
|
||||
|
||||
float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) {
|
||||
// r = (topRight, bottomRight, bottomLeft, topLeft)
|
||||
p -= center;
|
||||
r.xy = (p.x > 0.0) ? r.xy : r.wz;
|
||||
r.x = (p.y > 0.0) ? r.y : r.x;
|
||||
@@ -44,7 +43,6 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) {
|
||||
}
|
||||
|
||||
float smin(float a, float b, float k) {
|
||||
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
|
||||
float h = max(k - abs(a - b), 0.0) / k;
|
||||
return min(a, b) - h * h * h * k * (1.0 / 6.0);
|
||||
}
|
||||
@@ -55,9 +53,6 @@ float smax(float a, float b, float k) {
|
||||
}
|
||||
|
||||
float smaxSharpA(float a, float b, float k) {
|
||||
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
|
||||
// Used for the frame inner side, so the outer edge keeps its own shape and
|
||||
// always fills to the edges.
|
||||
float h = max(k - abs(a - b), 0.0) / k;
|
||||
float blend = h * h * h * k * (1.0 / 6.0);
|
||||
blend *= smoothstep(0.0, k * 0.5, -a);
|
||||
@@ -67,10 +62,6 @@ float smaxSharpA(float a, float b, float k) {
|
||||
void main() {
|
||||
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
|
||||
|
||||
// Phase 1: compute per-rect SDF, and track which rect owns the pixel for the
|
||||
// per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner:
|
||||
// myIndex stays -2, so the ownership test is skipped and this pass writes every
|
||||
// pixel the quad covers. Excluded pairs still need the pairwise pass below.
|
||||
float dArr[16];
|
||||
int owner = -2;
|
||||
float minDist = 1e10;
|
||||
@@ -84,12 +75,8 @@ void main() {
|
||||
vec4 radii =
|
||||
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
|
||||
|
||||
// Offset center for asymmetric deformation
|
||||
vec2 center = rect.xy + props.yz;
|
||||
|
||||
// AABB early-out: skip rects far from this pixel. The reach has to include the
|
||||
// shadow, otherwise a rect that is only inside the shadow range would be
|
||||
// skipped and its distance could not be used.
|
||||
vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius);
|
||||
if (abs(pixel.x - center.x) > extent.x ||
|
||||
abs(pixel.y - center.y) > extent.y) {
|
||||
@@ -97,19 +84,13 @@ void main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply pre-computed inverse deformation to the evaluation point
|
||||
mat2 invDeform = mat2(invDm.xy, invDm.zw);
|
||||
vec2 transformedPixel = center + invDeform * (pixel - center);
|
||||
|
||||
// Use pre-computed effective per-corner radii
|
||||
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
|
||||
|
||||
// Use pre-computed minimum eigenvalue for SDF correction
|
||||
d *= max(props.w, 0.01);
|
||||
|
||||
// Scale SDF on the axis facing a nearby border to narrow the smin blend
|
||||
// zone in that direction only, without reducing k (which would cause sharp
|
||||
// edges).
|
||||
if (hasInverted != 0) {
|
||||
vec2 screenHalf = sh.xy;
|
||||
|
||||
@@ -122,26 +103,21 @@ void main() {
|
||||
float distX1 =
|
||||
(invertedInner.x + invertedInner.z) - (center.x - screenHalf.x);
|
||||
|
||||
// 0 = far from border, 1 = at border (max compression)
|
||||
float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0),
|
||||
smoothstep(0.0, smoothFactor, distY1));
|
||||
float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0),
|
||||
smoothstep(0.0, smoothFactor, distX1));
|
||||
|
||||
// Smooth axis weights: gradient-based at corners, face-based inside.
|
||||
vec2 q = abs(pixel - center) - screenHalf;
|
||||
vec2 qp = max(q, vec2(0.0));
|
||||
float cornerLen = length(qp);
|
||||
|
||||
// Gradient direction in corner region (smooth 90-degree rotation)
|
||||
float gradX = qp.x / max(cornerLen, 0.001);
|
||||
float gradY = qp.y / max(cornerLen, 0.001);
|
||||
|
||||
// Smooth face weights for inside/edge (no hard branch)
|
||||
float faceY = smoothstep(-4.0, 4.0, q.y - q.x);
|
||||
float faceX = 1.0 - faceY;
|
||||
|
||||
// Blend: gradient in corner region, face-based inside
|
||||
float t = smoothstep(0.0, 2.0, cornerLen);
|
||||
float xWeight = mix(faceX, gradX, t);
|
||||
float yWeight = mix(faceY, gradY, t);
|
||||
@@ -158,15 +134,11 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: hard-min baseline over all rects.
|
||||
float mergedSdf = 1e10;
|
||||
for (int i = 0; i < rectCount; i++) {
|
||||
mergedSdf = min(mergedSdf, dArr[i]);
|
||||
}
|
||||
|
||||
// Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin
|
||||
// <= min, so taking the min over all non-excluded pair smins gives the
|
||||
// smoothly-merged SDF.
|
||||
for (int i = 0; i < rectCount; i++) {
|
||||
if (dArr[i] >= 1e9)
|
||||
continue;
|
||||
@@ -176,7 +148,6 @@ void main() {
|
||||
continue;
|
||||
if ((excludeMask & (1 << j)) != 0)
|
||||
continue;
|
||||
// smin only deviates from min within smoothFactor
|
||||
if (abs(dArr[i] - dArr[j]) >= smoothFactor)
|
||||
continue;
|
||||
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor));
|
||||
@@ -184,16 +155,12 @@ void main() {
|
||||
}
|
||||
|
||||
if (hasInverted != 0) {
|
||||
// Outer edge rounds with its own per-corner radii. With all-zero radii
|
||||
// sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is
|
||||
// unchanged. smaxSharpA keeps this boundary authoritative for the fill.
|
||||
float dOuter =
|
||||
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
|
||||
invertedOuterRadii) - 1.0;
|
||||
float dInner =
|
||||
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
|
||||
|
||||
// Border sinks: track the opposite rect edge, clamped to border thickness
|
||||
float innerTop = invertedInner.y - invertedInner.w;
|
||||
float innerBot = invertedInner.y + invertedInner.w;
|
||||
float innerLeft = invertedInner.x - invertedInner.z;
|
||||
@@ -203,62 +170,51 @@ void main() {
|
||||
float outerLeft = invertedOuter.x - invertedOuter.z;
|
||||
float outerRight = invertedOuter.x + invertedOuter.z;
|
||||
|
||||
float topZone =
|
||||
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
|
||||
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
|
||||
float leftZone =
|
||||
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
|
||||
float rightZone =
|
||||
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
|
||||
|
||||
float sinkValue = 0.0;
|
||||
for (int i = 0; i < rectCount; i++) {
|
||||
vec4 rect = rectData[i * 5];
|
||||
vec4 sinkProps = rectData[i * 5 + 1];
|
||||
vec2 sinkSh = rectData[i * 5 + 3].xy;
|
||||
float preOff = smoothFactor * (1.0 / 6.0);
|
||||
float s = smoothFactor * 2.0;
|
||||
|
||||
// Screen-space center (with offset) and pre-computed AABB half-extents
|
||||
vec2 ctr = rect.xy + sinkProps.yz;
|
||||
if (topZone > 0.0 || botZone > 0.0 || leftZone > 0.0 || rightZone > 0.0) {
|
||||
for (int i = 0; i < rectCount; i++) {
|
||||
vec4 rect = rectData[i * 5];
|
||||
vec4 sinkProps = rectData[i * 5 + 1];
|
||||
vec2 sinkSh = rectData[i * 5 + 3].xy;
|
||||
|
||||
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
|
||||
float preOff = smoothFactor * (1.0 / 6.0);
|
||||
vec2 ctr = rect.xy + sinkProps.yz;
|
||||
|
||||
// Top border: track rect's BOTTOM edge, only within border thickness
|
||||
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
|
||||
innerTop - outerTop);
|
||||
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
|
||||
innerTop - outerTop);
|
||||
|
||||
// Bottom border: track rect's TOP edge
|
||||
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
|
||||
outerBot - innerBot);
|
||||
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
|
||||
outerBot - innerBot);
|
||||
|
||||
// Left border: track rect's RIGHT edge
|
||||
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
|
||||
innerLeft - outerLeft);
|
||||
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
|
||||
innerLeft - outerLeft);
|
||||
|
||||
// Right border: track rect's LEFT edge
|
||||
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
|
||||
outerRight - innerRight);
|
||||
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
|
||||
outerRight - innerRight);
|
||||
|
||||
// Lateral distance from pixel to rect's extent along each edge
|
||||
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
|
||||
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
|
||||
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
|
||||
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
|
||||
|
||||
// Perpendicular proximity: full strength in border, fade inside inner
|
||||
// area
|
||||
float topZone =
|
||||
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
|
||||
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
|
||||
float leftZone =
|
||||
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
|
||||
float rightZone =
|
||||
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
|
||||
|
||||
float s = smoothFactor * 2.0;
|
||||
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
|
||||
botPen * smoothstep(s, 0.0, hLat) * botZone),
|
||||
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
|
||||
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
|
||||
sinkValue = max(sinkValue, sink);
|
||||
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
|
||||
botPen * smoothstep(s, 0.0, hLat) * botZone),
|
||||
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
|
||||
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
|
||||
sinkValue = max(sinkValue, sink);
|
||||
}
|
||||
}
|
||||
|
||||
dInner -= sinkValue;
|
||||
|
||||
// dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the
|
||||
// outer arc is the active boundary; a smax term added there would pull the
|
||||
// arc's sides inward (curvature/tangent mismatch), so the corner would not be
|
||||
// a circle. Only blend on the inner side, where the inner boundary is active.
|
||||
float negInner = -dInner;
|
||||
float dFrame = dOuter >= negInner ? dOuter
|
||||
: smaxSharpA(dOuter, negInner, smoothFactor);
|
||||
@@ -269,37 +225,22 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// A pixel this far from every rect cannot have a value, so it is not written.
|
||||
// The one-unit margin keeps the outermost pixels of the quad out of the AA: their
|
||||
// derivatives are undefined (the quad neighbour is outside the primitive), which
|
||||
// is what drew a line along the coverage edge.
|
||||
float reach = max(smoothFactor, shadowRadius);
|
||||
if (mergedSdf > reach - 1.0)
|
||||
float pxWidth = max(length(vec2(dFdx(mergedSdf), dFdy(mergedSdf))), 1e-5);
|
||||
if (mergedSdf > reach - max(2.0 * pxWidth, 1.0))
|
||||
discard;
|
||||
|
||||
// Only the owner writes a pixel, which is what makes the per-shape path look
|
||||
// like one surface: qt_Opacity is applied once per pixel instead of once per
|
||||
// overlapping draw. The single group draw has no owner (myIndex == -2), so the
|
||||
// test is skipped and the value below is the only value a pixel can have.
|
||||
if (myIndex >= -1 && owner != myIndex)
|
||||
discard;
|
||||
|
||||
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad
|
||||
// boundary cannot make a wide edge.
|
||||
float fw = min(fwidth(mergedSdf), 1.0);
|
||||
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
|
||||
float materialAlpha = clamp(0.5 - mergedSdf / pxWidth, 0.0, 1.0);
|
||||
|
||||
// Phase 2: the shadow, as a distance falloff from the merged surface. The
|
||||
// quadratic reaches zero with zero slope at its outer edge, so that edge needs no
|
||||
// AA of its own. Only the single group draw writes a shadow: in the per-shape
|
||||
// path overlapping draws would darken the same area twice.
|
||||
float shadowAlpha = 0.0;
|
||||
if (shadowRadius > 0.0 && myIndex == -2) {
|
||||
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
|
||||
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
|
||||
}
|
||||
|
||||
// Material over shadow, premultiplied.
|
||||
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
|
||||
vec3 rgb = color.rgb * materialAlpha
|
||||
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user