Files
Pixelplanet--Pixel-VR-Player/shaders/ui.frag
T
Pixelplanet e5f1f733e7 fix(gfx): fix sub-pixel borders, dark color crush & elevate spatial UI contrast
- Enforce minimum 2.2 screen pixel border width in ui.frag using screen-space gradient length (gradLen)
- Increased top-to-bottom luminance gradient on panels and cards from subtle 10% to 70% dynamic range
- Boosted top specular rim catchlight up to 1.7x for crisp chamfered spatial glass effect
- Elevated background, rail, card, and tab color palettes out of near-black color crush into rich smoked glass
- Added dedicated frosted capsule background for header breadcrumb navigation
- Enhanced hover glow halo and card borders for unmistakable spatial visual feedback
2026-10-25 10:07:01 +01:00

211 lines
9.5 KiB
GLSL

#version 450
// Procedural vector/SDF UI fragment shader.
// Supports rounded boxes with antialiased borders, and crisp vector icons.
layout(push_constant) uniform Push {
mat4 mvp;
vec4 color;
vec4 params; // x: shapeType, y: radius, z: borderWidth, w: extra
vec4 borderColor;
} pc;
layout(location = 0) in vec2 vCoord;
layout(location = 0) out vec4 outColor;
// SDF for rounded box
float sdRoundedBox(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + vec2(r);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
// Inigo Quilez 2D triangle SDF
float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
vec2 e0 = p1 - p0, e1 = p2 - p1, e2 = p0 - p2;
vec2 v0 = p - p0, v1 = p - p1, v2 = p - p2;
vec2 pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);
vec2 pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);
vec2 pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);
float s = sign(e0.x * e2.y - e0.y * e2.x);
vec2 d = min(min(vec2(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)),
vec2(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x))),
vec2(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)));
return -sqrt(d.x) * sign(d.y);
}
// SDF line segment
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
void main() {
int shape = int(pc.params.x + 0.5);
vec2 p = vCoord;
float d = 0.0;
bool hasBorder = false;
if (shape == 0) {
// Shape 0: Rounded rectangle (e.g. button body, panel, progress bar)
float r = clamp(pc.params.y, 0.0, 1.0);
d = sdRoundedBox(p, vec2(1.0), r);
hasBorder = (pc.params.z > 0.0);
} else if (shape == 1) {
// Shape 1: Play icon (triangle pointing right)
vec2 pPlay = p - vec2(0.04, 0.0);
d = sdTriangle(pPlay, vec2(-0.45, 0.55), vec2(0.55, 0.0), vec2(-0.45, -0.55)) - 0.06;
} else if (shape == 2) {
// Shape 2: Pause icon (two vertical bars)
vec2 pBar = vec2(abs(p.x) - 0.30, p.y);
d = sdRoundedBox(pBar, vec2(0.12, 0.52), 0.04);
} else if (shape == 3) {
// Shape 3: Rewind (<<) (two left-pointing triangles)
vec2 tri0 = vec2(0.10, 0.45);
vec2 tri1 = vec2(-0.38, 0.0);
vec2 tri2 = vec2(0.10, -0.45);
float d1 = sdTriangle(p - vec2(-0.06, 0.0), tri0, tri1, tri2) - 0.04;
float d2 = sdTriangle(p - vec2(0.42, 0.0), tri0, tri1, tri2) - 0.04;
d = min(d1, d2);
} else if (shape == 4) {
// Shape 4: Fast Forward (>>) (two right-pointing triangles)
vec2 tri0 = vec2(-0.10, 0.45);
vec2 tri1 = vec2(0.38, 0.0);
vec2 tri2 = vec2(-0.10, -0.45);
float d1 = sdTriangle(p - vec2(0.06, 0.0), tri0, tri1, tri2) - 0.04;
float d2 = sdTriangle(p - vec2(-0.42, 0.0), tri0, tri1, tri2) - 0.04;
d = min(d1, d2);
} else if (shape == 5) {
// Shape 5: Files / Folder icon
float body = sdRoundedBox(p - vec2(0.0, -0.10), vec2(0.65, 0.45), 0.08);
float tab = sdRoundedBox(p - vec2(-0.30, 0.40), vec2(0.32, 0.16), 0.06);
d = min(body, tab);
} else if (shape == 6) {
// Shape 6: Settings Gear icon
float r = length(p);
float theta = atan(p.y, p.x);
float teeth = sin(6.0 * theta);
float gearOuter = r - (0.48 + 0.14 * smoothstep(-0.4, 0.4, teeth));
float gearHole = 0.20 - r;
d = max(gearOuter, gearHole);
} else if (shape == 7) {
// Shape 7: Recenter / Target icon
float r = length(p);
float ring = abs(r - 0.52) - 0.06;
float dotCenter = r - 0.16;
float tickH = sdRoundedBox(vec2(abs(p.x) - 0.62, p.y), vec2(0.10, 0.03), 0.01);
float tickV = sdRoundedBox(vec2(p.x, abs(p.y) - 0.62), vec2(0.03, 0.10), 0.01);
d = min(min(ring, dotCenter), min(tickH, tickV));
} else if (shape == 8) {
// Shape 8: Reticle Cursor (outer ring + center dot)
float r = length(p);
float outerRing = abs(r - 0.70) - 0.08;
float centerDot = r - 0.22;
d = min(outerRing, centerDot);
} else if (shape == 9) {
// Shape 9: Close / X icon
float s1 = sdSegment(p, vec2(-0.45, -0.45), vec2(0.45, 0.45)) - 0.10;
float s2 = sdSegment(p, vec2(-0.45, 0.45), vec2(0.45, -0.45)) - 0.10;
d = min(s1, s2);
} else if (shape == 10) {
// Shape 10: Checkmark (✓)
float s1 = sdSegment(p, vec2(-0.45, -0.05), vec2(-0.12, -0.45)) - 0.09;
float s2 = sdSegment(p, vec2(-0.12, -0.45), vec2(0.48, 0.45)) - 0.09;
d = min(s1, s2);
} else if (shape == 11) {
// Shape 11: Stop icon (filled rounded square)
d = sdRoundedBox(p, vec2(0.46), 0.10);
} else if (shape == 12) {
// Shape 12: Download (down arrow onto a base line)
float shaft = sdRoundedBox(p - vec2(0.0, 0.18), vec2(0.09, 0.42), 0.04);
float head = sdTriangle(p - vec2(0.0, -0.20), vec2(-0.36, 0.30),
vec2(0.36, 0.30), vec2(0.0, -0.28)) - 0.03;
float base = sdRoundedBox(p - vec2(0.0, -0.62), vec2(0.52, 0.08), 0.03);
d = min(min(shaft, head), base);
} else if (shape == 13) {
// Shape 13: List / queue (three bars with leading dots) for the
// downloads window, so it is distinct from the plain download arrow.
float b1 = sdRoundedBox(p - vec2(0.10, 0.46), vec2(0.40, 0.07), 0.05);
float b2 = sdRoundedBox(p - vec2(0.10, 0.00), vec2(0.40, 0.07), 0.05);
float b3 = sdRoundedBox(p - vec2(0.10, -0.46), vec2(0.40, 0.07), 0.05);
float p1 = sdRoundedBox(p - vec2(-0.50, 0.46), vec2(0.10, 0.10), 0.05);
float p2 = sdRoundedBox(p - vec2(-0.50, 0.00), vec2(0.10, 0.10), 0.05);
float p3 = sdRoundedBox(p - vec2(-0.50, -0.46), vec2(0.10, 0.10), 0.05);
d = min(min(min(b1, b2), min(b3, p1)), min(p2, p3));
} else if (shape == 14) {
// Shape 14: Hero Glow Ring (circular outline for primary buttons)
float r = length(p);
d = abs(r - 0.72) - 0.08;
} else if (shape == 15) {
// Shape 15: Audio Speaker icon (cone + sound wave)
float box = sdRoundedBox(p - vec2(-0.32, 0.0), vec2(0.12, 0.18), 0.03);
float cone = sdTriangle(p, vec2(-0.20, 0.18), vec2(0.16, 0.45), vec2(-0.20, -0.18)) - 0.02;
float cone2 = sdTriangle(p, vec2(-0.20, -0.18), vec2(0.16, -0.45), vec2(0.16, 0.45)) - 0.02;
float rArc = length(p - vec2(-0.05, 0.0));
float arc1 = max(abs(rArc - 0.46) - 0.06, -p.x + 0.10);
float arc2 = max(abs(rArc - 0.68) - 0.06, -p.x + 0.22);
d = min(min(box, min(cone, cone2)), min(arc1, arc2));
} else if (shape == 16) {
// Shape 16: VR Headset / Display goggles
float gBody = sdRoundedBox(p, vec2(0.55, 0.30), 0.14);
float nose = sdTriangle(p - vec2(0.0, -0.30), vec2(-0.16, -0.08), vec2(0.16, -0.08), vec2(0.0, 0.12)) - 0.04;
float strapL = sdRoundedBox(p - vec2(-0.62, 0.0), vec2(0.08, 0.10), 0.03);
float strapR = sdRoundedBox(p - vec2(0.62, 0.0), vec2(0.08, 0.10), 0.03);
d = max(min(gBody, min(strapL, strapR)), -nose);
} else if (shape == 17) {
// Shape 17: Curved Cinema Display icon
float rScreen = length(p - vec2(0.0, 1.3));
float arcDisp = abs(rScreen - 1.62) - 0.10;
float cutX = abs(p.x) - 0.58;
float cutY = abs(p.y) - 0.32;
d = max(arcDisp, max(cutX, cutY));
} else if (shape == 18) {
// Shape 18: Chevron (>) for breadcrumbs & pagination
float c1 = sdSegment(p, vec2(-0.22, 0.40), vec2(0.24, 0.0)) - 0.08;
float c2 = sdSegment(p, vec2(0.24, 0.0), vec2(-0.22, -0.40)) - 0.08;
d = min(c1, c2);
} else if (shape == 19) {
// Shape 19: Home / Library icon
float roof = sdTriangle(p - vec2(0.0, 0.12), vec2(-0.52, 0.0), vec2(0.52, 0.0), vec2(0.0, 0.42)) - 0.04;
float baseH = sdRoundedBox(p - vec2(0.0, -0.20), vec2(0.35, 0.26), 0.04);
d = min(roof, baseH);
} else {
d = sdRoundedBox(p, vec2(1.0), 0.0);
}
// Exact screen-space gradient magnitude of the distance field along the contour normal.
// Produces a crisp, razor-sharp 1.5 screen-pixel anti-aliased outline without crawling.
float gradLen = length(vec2(dFdx(d), dFdy(d)));
float aa = max(gradLen * 0.75, 0.0005);
float fillAlpha = 1.0 - smoothstep(-aa, aa, d);
if (fillAlpha <= 0.002) {
discard;
}
vec4 fillLin = vec4(pow(max(pc.color.rgb, vec3(0.0)), vec3(2.2)), pc.color.a);
if (shape == 0) {
// Spatial frosted glass top-to-bottom luminance gradient (lit from above)
float topLight = mix(0.80, 1.35, clamp(0.5 * (vCoord.y + 1.0), 0.0, 1.0));
fillLin.rgb *= topLight;
}
vec4 finalColor = fillLin;
if (hasBorder) {
// Enforce at least 2.2 screen pixels for razor-sharp, visible borders at any distance in VR
float minBorder = gradLen * 2.2;
float borderWidth = max(pc.params.z, minBorder);
float innerD = d + borderWidth;
float borderAlpha = 1.0 - smoothstep(-aa, aa, innerD);
vec4 bordLin = vec4(pow(max(pc.borderColor.rgb, vec3(0.0)), vec3(2.2)), pc.borderColor.a);
if (shape == 0) {
// Specular catchlight on upper chamfer/rim
float rimLight = mix(0.70, 1.70, smoothstep(-0.2, 0.95, vCoord.y));
bordLin.rgb *= rimLight;
}
finalColor = mix(bordLin, fillLin, borderAlpha);
}
finalColor.a *= fillAlpha;
outColor = finalColor;
}