This commit is contained in:
2026-05-02 17:13:53 +10:00
parent 1a4c33b9dc
commit fb1bf01fef
19 changed files with 780 additions and 69 deletions

72
shaders/vhs/shader.json Normal file
View File

@@ -0,0 +1,72 @@
{
"id": "vhs",
"name": "VHS",
"description": "VHS with wiggle, smear, and YIQ-style color separation inspired by the Godot shader reference.",
"category": "Built-in",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "wiggle",
"label": "Wiggle",
"type": "float",
"default": 0.03,
"min": 0.0,
"max": 1.5,
"step": 0.01
},
{
"id": "wiggleSpeed",
"label": "Wiggle Speed",
"type": "float",
"default": 25.0,
"min": 0.0,
"max": 100.0,
"step": 1.0
},
{
"id": "smear",
"label": "Smear",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.01
},
{
"id": "blurSamples",
"label": "Blur Samples",
"type": "float",
"default": 15.0,
"min": 3.0,
"max": 15.0,
"step": 1.0
},
{
"id": "vignetteAmount",
"label": "Vignette",
"type": "float",
"default": 0.18,
"min": 0.0,
"max": 0.6,
"step": 0.01
},
{
"id": "aberrationAmount",
"label": "Aberration",
"type": "float",
"default": 0.75,
"min": 0.0,
"max": 3.0,
"step": 0.05
},
{
"id": "halationAmount",
"label": "Halation",
"type": "float",
"default": 0.12,
"min": 0.0,
"max": 0.5,
"step": 0.01
}
]
}

115
shaders/vhs/shader.slang Normal file
View File

@@ -0,0 +1,115 @@
float onOff(float a, float b, float c, float framecount)
{
return step(c, sin((framecount * 0.001) + a * cos((framecount * 0.001) * b)));
}
float2 jumpy(float2 uv, float framecount)
{
float2 look = uv;
float m = frac(framecount / 4.0);
float dy = look.y - m;
float window = 1.0 / (1.0 + 80.0 * dy * dy);
look.x += 0.05 * sin(look.y * 10.0 + framecount) / 20.0 * onOff(4.0, 4.0, 0.3, framecount) * (0.5 + cos(framecount * 20.0)) * window;
float vShift = (0.1 * wiggle) * 0.4 * onOff(2.0, 3.0, 0.9, framecount) * (sin(framecount) * sin(framecount * 20.0) + (0.5 + 0.1 * sin(framecount * 200.0) * cos(framecount)));
look.y = frac(look.y - 0.01 * vShift);
return look;
}
float2 circle(float start, float points, float point)
{
float rad = 6.28318530718 * (1.0 / points) * (point + start);
return float2(-(.3 + rad), cos(rad));
}
float3 rgb2yiq(float3 c)
{
return float3(
0.2989 * c.x + 0.5959 * c.y + 0.2115 * c.z,
0.5870 * c.x - 0.2744 * c.y - 0.5229 * c.z,
0.1140 * c.x - 0.3216 * c.y + 0.3114 * c.z
);
}
float3 yiq2rgb(float3 c)
{
return float3(
1.0 * c.x + 1.0 * c.y + 1.0 * c.z,
0.956 * c.x - 0.2720 * c.y - 1.1060 * c.z,
0.6210 * c.x - 0.6474 * c.y + 1.7046 * c.z
);
}
float3 blurVhs(float2 uv, float d, int sampleCount)
{
float3 sum = float3(0.0, 0.0, 0.0);
float weight = 1.0 / max(float(sampleCount), 1.0);
float start = 2.0 / max(float(sampleCount), 1.0);
float2 pixelOffset = float2(d, 0.0);
float2 scale = 0.66 * 8.0 * pixelOffset;
for (int i = 0; i < 15; ++i)
{
if (i >= sampleCount)
break;
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
sum += sampleVideo(frac(uv + offset)).rgb * weight;
}
return sum;
}
float4 shadeVideo(ShaderContext context)
{
float2 uv = context.uv;
float framecount = frac(context.time * wiggleSpeed / 7.0) * 7.0;
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
float d = 0.1 - round(frac(context.time / 3.0)) * 0.1;
uv = jumpy(uv, framecount);
float s = 0.0001 * -d + 0.0001 * wiggle * sin(context.time * wiggleSpeed);
float e = min(0.30, pow(max(0.0, cos(uv.y * 4.0 + 0.3) - 0.75) * (s + 0.5), 3.0)) * 25.0;
float r = 250.0 * (2.0 * s);
uv.x += abs(r * pow(min(0.003, (-uv.y + (0.01 * frac(context.time / 5.0) * 5.0))) * 3.0, 2.0)) * wiggle;
d = 0.051 + abs(sin(s / 4.0));
float c = max(0.0001, 0.002 * d) * smear;
float3 yBlur = blurVhs(uv, c + c * uv.x, sampleCount);
float y = rgb2yiq(yBlur).r;
uv.x += 0.01 * d;
c *= 6.0;
float3 iBlur = blurVhs(uv, c, sampleCount);
float i = rgb2yiq(iBlur).g;
uv.x += 0.005 * d;
c *= 2.5;
float3 qBlur = blurVhs(uv, c, sampleCount);
float q = rgb2yiq(qBlur).b;
float3 color = yiq2rgb(float3(y, i, q)) - pow(s + e * 2.0, 3.0);
float2 centered = context.uv * 2.0 - 1.0;
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
float redAberration = sampleVideo(frac(context.uv + aberrationOffset)).r;
float blueAberration = sampleVideo(frac(context.uv - aberrationOffset)).b;
color.r = lerp(color.r, redAberration, 0.35);
color.b = lerp(color.b, blueAberration, 0.35);
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
float3 halationSource =
sampleVideo(frac(context.uv + halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv - halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv + halationOffset * 2.0)).rgb * 0.2;
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
return float4(saturate(color), 1.0);
}