mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-22 12:01:39 +02:00
- Add Hidden Vault system: - Secure directory storage under ~/.pixelvr_vault with 0700 permissions. - Cheat-code combo unlocking triggered on full grip squeeze (A-B-B-A-X-Y default, customizable). - Headset removal / face detection gating and inactivity auto-lock. - Automatic download history clearing upon vault lock. - Add File Actions Submenu (Button Y): - In-browser file/folder submenu with Move to Vault, Delete, Move, and Rename. - Recursive folder move to vault with collision handling. - Folder deletion with confirmation dialog. - Enhance VR controllers and interactions: - Dynamic left and right Steam Frame controller render models. - Single laser pointer priority with hand swapping via full grip squeeze. - Fix video format detection: - Correct auto-detection and default projection for high-resolution VR180 SBS media (e.g. 7200x3600). - Visual sharpness & rendering stability: - Render swapchain at native 2160x2160 per eye. - SDF font rendering with RMS pixel footprint and stem darkening. - Razor-sharp procedural UI anti-aliasing and crisp border highlights without distortion. - Add unit test suite (tests/test_vault.cpp) and documentation specs.
200 lines
9.0 KiB
GLSL
200 lines
9.0 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);
|
|
vec4 finalColor = fillLin;
|
|
if (hasBorder) {
|
|
// Crisp, razor-sharp border highlight line matching requested thickness
|
|
float borderWidth = pc.params.z;
|
|
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);
|
|
finalColor = mix(bordLin, fillLin, borderAlpha);
|
|
}
|
|
|
|
finalColor.a *= fillAlpha;
|
|
outColor = finalColor;
|
|
}
|