369 lines
14 KiB
Plaintext
369 lines
14 KiB
Plaintext
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;
|
|
// Localize the horizontal tear to a moving scanline window instead of
|
|
// bending the whole frame equally.
|
|
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
|
|
);
|
|
}
|
|
|
|
float noiseHash(float2 p)
|
|
{
|
|
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
|
}
|
|
|
|
float staticHash(float2 p)
|
|
{
|
|
float3 p3 = frac(float3(p.x, p.y, p.x) * 0.1031);
|
|
p3 += dot(p3, p3.yzx + 33.33);
|
|
return frac((p3.x + p3.y) * p3.z);
|
|
}
|
|
|
|
float seededStaticHash(float2 p, float seed)
|
|
{
|
|
return staticHash(p + float2(seed * 37.13, seed * 17.71));
|
|
}
|
|
|
|
float grainScalar(float2 uv)
|
|
{
|
|
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
|
|
}
|
|
|
|
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
|
{
|
|
float safeGrainSize = max(grainSize, 0.001);
|
|
// Quantize the coordinates first so larger grain sizes become visible
|
|
// chroma blocks rather than simply lower-frequency smooth noise.
|
|
float2 baseUv = uv * outputResolution * float2(0.85, 0.95) / safeGrainSize;
|
|
float2 grainUv = floor(baseUv) + 0.5;
|
|
float frame = floor(time * 59.94);
|
|
|
|
// Change the grain field per frame instead of drifting it through UV space;
|
|
// continuous drift can alias into horizontal bands that march down-frame.
|
|
float r = staticHash(grainUv + float2(frame * 17.0 + 13.1, frame * 3.0 + 71.7));
|
|
float g = staticHash(grainUv * float2(1.03, 0.97) + float2(frame * 11.0 + 47.2, frame * 5.0 + 19.4));
|
|
float b = staticHash(grainUv * float2(0.96, 1.05) + float2(frame * 7.0 + 83.6, frame * 13.0 + 53.8));
|
|
|
|
return float3(r, g, b) * 2.0 - 1.0;
|
|
}
|
|
|
|
float valueNoise2(float2 p)
|
|
{
|
|
float2 cell = floor(p);
|
|
float2 f = frac(p);
|
|
float2 u = f * f * (3.0 - 2.0 * f);
|
|
|
|
float a = noiseHash(cell);
|
|
float b = noiseHash(cell + float2(1.0, 0.0));
|
|
float c = noiseHash(cell + float2(0.0, 1.0));
|
|
float d = noiseHash(cell + float2(1.0, 1.0));
|
|
|
|
return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y);
|
|
}
|
|
|
|
float tapeLineNoise(float2 uv, float time, float2 outputResolution)
|
|
{
|
|
float y = floor(uv.y * outputResolution.y);
|
|
// Combine stable per-line noise with frame-rate noise so bands have both
|
|
// slow tape wander and fast electronic shimmer.
|
|
float slowLine = valueNoise2(float2(y * 0.021, floor(time * 10.0)));
|
|
float fastLine = noiseHash(float2(y * 1.73, floor(time * 59.94)));
|
|
float line = (slowLine * 0.7 + fastLine * 0.3) * 2.0 - 1.0;
|
|
|
|
float band = sin(uv.y * outputResolution.y * 0.42 + time * 36.0);
|
|
return line * (0.65 + 0.35 * band);
|
|
}
|
|
|
|
float3 analogStatic(float2 uv, float time, float2 outputResolution)
|
|
{
|
|
float2 safeResolution = max(outputResolution, float2(1.0, 1.0));
|
|
float2 pixel = floor(uv * safeResolution / max(noiseSize, 0.25));
|
|
float frame = floor(time * 59.94);
|
|
float seed = frac(time);
|
|
|
|
// Several differently skewed hashes keep the snow from forming obvious
|
|
// diagonal or grid patterns at broadcast frame cadence.
|
|
float2 goldPixel = pixel + float2(0.37, 0.61) + frame;
|
|
float snowA = seededStaticHash(goldPixel, seed + 0.1);
|
|
float snowB = seededStaticHash(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2);
|
|
float snowC = seededStaticHash(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
|
|
float snow = (snowA * 0.72 + snowB * 0.28) * 2.0 - 1.0;
|
|
|
|
float lineNoise = tapeLineNoise(uv, time, safeResolution);
|
|
float dropoutSeed = seededStaticHash(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
|
|
float dropout = smoothstep(0.965, 1.0, dropoutSeed);
|
|
float fleckSeed = seededStaticHash(pixel + float2(frame * 13.0, -frame * 7.0), seed + 0.5);
|
|
float fleck = smoothstep(0.992, 1.0, fleckSeed) - smoothstep(0.0, 0.008, snowC);
|
|
|
|
float scan = sin(uv.y * safeResolution.y * 3.14159265);
|
|
float scanMask = 0.55 + 0.45 * scan * scan;
|
|
float lumaNoise = snow * 0.55 + lineNoise * staticLines * 0.45 + fleck * 0.7 + dropout * lineNoise * 1.2;
|
|
|
|
return float3(lumaNoise * scanMask, lumaNoise * 0.42, lumaNoise * 0.72);
|
|
}
|
|
|
|
float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
|
{
|
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
|
float2 dx = float2(pixel.x * radius, 0.0);
|
|
float2 dy = float2(0.0, pixel.y * radius);
|
|
|
|
float3 sum = sampleVideo(frac(uv)).rgb * 0.28;
|
|
sum += sampleVideo(frac(uv + dx)).rgb * 0.14;
|
|
sum += sampleVideo(frac(uv - dx)).rgb * 0.14;
|
|
sum += sampleVideo(frac(uv + dy)).rgb * 0.14;
|
|
sum += sampleVideo(frac(uv - dy)).rgb * 0.14;
|
|
sum += sampleVideo(frac(uv + dx + dy)).rgb * 0.075;
|
|
sum += sampleVideo(frac(uv + dx - dy)).rgb * 0.075;
|
|
sum += sampleVideo(frac(uv - dx + dy)).rgb * 0.075;
|
|
sum += sampleVideo(frac(uv - dx - dy)).rgb * 0.075;
|
|
return sum;
|
|
}
|
|
|
|
float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
|
|
{
|
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
|
float2 offset = pixel * radius;
|
|
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
|
|
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
|
|
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
|
|
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
|
|
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
|
|
return sum;
|
|
}
|
|
|
|
float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolution)
|
|
{
|
|
float amount = saturate(chromaCrawlAmount);
|
|
if (amount <= 0.0001)
|
|
return color;
|
|
|
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
|
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
|
|
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
|
|
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
|
|
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
|
|
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
|
|
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
|
|
|
|
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
|
|
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
|
|
float3 crawled = color;
|
|
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
|
|
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
|
|
return crawled;
|
|
}
|
|
|
|
float3 applyGenerationLoss(float3 color, float2 uv, float2 outputResolution)
|
|
{
|
|
float loss = saturate(generationLoss);
|
|
if (loss <= 0.0001)
|
|
return color;
|
|
|
|
float3 softened = softCrossBlur(uv, outputResolution, 0.85 + loss * 2.2);
|
|
color = lerp(color, softened, loss * 0.42);
|
|
|
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
|
float3 gray = float3(luma, luma, luma);
|
|
color = lerp(color, gray, loss * 0.32);
|
|
color = (color - 0.5) * (1.0 - loss * 0.18) + 0.5;
|
|
color = color * (1.0 - loss * 0.08) + float3(0.035, 0.035, 0.04) * loss;
|
|
return color;
|
|
}
|
|
|
|
float3 applySharpnessDrift(float3 color, float2 uv, float time, float2 outputResolution)
|
|
{
|
|
float drift = saturate(sharpnessDrift);
|
|
if (drift <= 0.0001)
|
|
return color;
|
|
|
|
float wobble = 0.5 + 0.5 * sin(time * 1.7 + sin(time * 0.37) * 2.0);
|
|
float radius = 0.35 + wobble * 2.25;
|
|
float3 softened = softCrossBlur(uv, outputResolution, radius);
|
|
return lerp(color, softened, drift * (0.35 + 0.65 * wobble));
|
|
}
|
|
|
|
float3 applySubtleScanlines(float3 color, float2 uv, float time, float2 outputResolution)
|
|
{
|
|
float amount = saturate(scanlineAmount);
|
|
if (amount <= 0.0001)
|
|
return color;
|
|
|
|
float scan = sin((uv.y * outputResolution.y + floor(time * 59.94) * 0.5) * 3.14159265);
|
|
float field = 0.5 + 0.5 * scan;
|
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
|
float visibility = lerp(1.0, 0.45, saturate(luma));
|
|
float modulation = 1.0 - amount * visibility * (0.35 + 0.65 * field);
|
|
color.rgb *= modulation;
|
|
color.rgb += amount * 0.015 * (1.0 - field);
|
|
return color;
|
|
}
|
|
|
|
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;
|
|
|
|
// The circular tap pattern approximates soft tape smear while keeping the
|
|
// maximum loop bound fixed for shader compilation.
|
|
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;
|
|
}
|
|
|
|
float distortedTapeTime(ShaderContext context)
|
|
{
|
|
return context.time + context.startupRandom * 113.0;
|
|
}
|
|
|
|
float4 buildTapeSmear(ShaderContext context)
|
|
{
|
|
float2 uv = context.uv;
|
|
float time = distortedTapeTime(context);
|
|
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
|
|
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
|
|
|
|
// Split the source into YIQ, smear each component by a different amount,
|
|
// then recombine to mimic luma/chroma bandwidth mismatch on tape.
|
|
float d = 0.1 - round(frac(time / 3.0)) * 0.1;
|
|
uv = jumpy(uv, framecount);
|
|
float s = 0.0001 * -d + 0.0001 * wiggle * sin(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(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);
|
|
return float4(saturate(color), 1.0);
|
|
}
|
|
|
|
float4 finishVhs(ShaderContext context)
|
|
{
|
|
float time = distortedTapeTime(context);
|
|
float3 color = sampleVideo(context.uv).rgb;
|
|
|
|
// Radial red/blue offsets create lens and deck misregistration before the
|
|
// wider tape effects are layered in.
|
|
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;
|
|
|
|
// Bloom and fade are applied as separate layers so highlights glow without
|
|
// flattening the full picture into the faded black level.
|
|
float3 bloomSource = softBloom(context.uv, context.outputResolution, 2.0 + smear * 2.5);
|
|
float bloomLuma = dot(bloomSource, float3(0.299, 0.587, 0.114));
|
|
float bloomMask = smoothstep(0.32, 1.0, bloomLuma) * bloomAmount;
|
|
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
|
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
|
|
|
color = applySharpnessDrift(color, context.uv, time, context.outputResolution);
|
|
color = applyGenerationLoss(color, context.uv, context.outputResolution);
|
|
color = applyChromaCrawl(color, context.uv, time, context.outputResolution);
|
|
|
|
float3 speckle = animatedChromaGrain(context.uv, time, context.outputResolution, noiseSize);
|
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
|
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
|
float chunkiness = lerp(1.0, 2.4, saturate((noiseSize - 1.0) / 5.0));
|
|
// Push darker regions harder: analog noise reads most naturally in shadows
|
|
// and avoids washing out bright highlights.
|
|
float3 chromaNoise = float3(speckle.x * 1.2, speckle.y * 0.28, speckle.z * 1.35);
|
|
color += chromaNoise * noiseAmount * noiseMask * chunkiness;
|
|
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.2 * chunkiness, 0.35);
|
|
color.b = lerp(color.b, color.b + speckle.z * noiseAmount * 0.28 * chunkiness, 0.5);
|
|
|
|
float3 staticNoise = analogStatic(context.uv, time, context.outputResolution);
|
|
float staticMask = lerp(0.45, 1.15, 1.0 - saturate(luma));
|
|
color += staticNoise * staticAmount * staticMask;
|
|
color = lerp(color, color + float3(staticNoise.r * 0.22, staticNoise.g * 0.08, -staticNoise.b * 0.08), saturate(staticAmount * 2.0));
|
|
|
|
float3 grayscale = float3(luma, luma, luma);
|
|
color = lerp(color, grayscale, fadeAmount * 0.18);
|
|
color = color * (1.0 - fadeAmount * 0.08) + float3(0.055, 0.055, 0.065) * fadeAmount;
|
|
color = lerp(color, softBloom(context.uv, context.outputResolution, 1.0 + smear), fadeAmount * 0.12);
|
|
|
|
color = applySubtleScanlines(color, context.uv, time, context.outputResolution);
|
|
|
|
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);
|
|
}
|
|
|
|
float4 shadeVideo(ShaderContext context)
|
|
{
|
|
return finishVhs(context);
|
|
}
|