diff --git a/shaders/balatro-swirl/shader.slang b/shaders/balatro-swirl/shader.slang index 340ff1d..76c16fd 100644 --- a/shaders/balatro-swirl/shader.slang +++ b/shaders/balatro-swirl/shader.slang @@ -6,6 +6,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset; float uvLength = length(uv); + // First warp: convert to polar space and twist the angle more near the + // center, creating the large spiral motion. float speed = spinRotation * spinEase * 0.2; if (isRotate) speed = time * speed; @@ -19,6 +21,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se speed = (time + seed * 17.0) * spinSpeed; float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y); + // Second warp: a short iterative feedback loop turns the spiral into + // painterly bands while preserving a fixed compile-time loop bound. for (int i = 0; i < 5; ++i) { uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv; @@ -32,6 +36,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes)); float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes)); float c3p = 1.0 - min(1.0, c1p + c2p); + // Three soft band weights drive the palette; lighting rides on the brightest + // bands so the swirl keeps dimensional highlights. float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0); float safeContrast = max(contrast, 0.001); diff --git a/shaders/fisheye-equirectangular-mirror/shader.slang b/shaders/fisheye-equirectangular-mirror/shader.slang index 9a6c4ae..970be2f 100644 --- a/shaders/fisheye-equirectangular-mirror/shader.slang +++ b/shaders/fisheye-equirectangular-mirror/shader.slang @@ -31,6 +31,8 @@ float normalizedFisheyeRadius(float theta, float halfFov) { float safeHalfFov = max(halfFov, 0.0001); + // Match common fisheye projection families while keeping the selected FOV + // normalized to the same source-image radius. if (fisheyeModel == 1) { return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001); @@ -49,6 +51,7 @@ float normalizedFisheyeRadius(float theta, float halfFov) float3 equirectangularRay(float2 uv) { + // Convert equirectangular UVs into longitude/latitude on the unit sphere. float longitude = (uv.x - 0.5) * TWO_PI; float latitude = (0.5 - uv.y) * PI; float latitudeCos = cos(latitude); @@ -82,6 +85,8 @@ float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context) float inwardLength = max(length(inward), 0.000001); inward /= inwardLength; + // Outside the fisheye image, sample back inward from the nearest edge so the + // fill looks like stretched lens content instead of a hard color plate. float blurDistance = max(edgeBlur, 0.0); float4 color = sampleVideo(clampedUv) * 0.32; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26; @@ -114,6 +119,7 @@ float4 shadeVideo(ShaderContext context) float phi = atan2(ray.y, ray.x); float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov); + // Project the mirrored sphere ray back into the circular fisheye source. float2 sourceUv = float2( center.x + cos(phi) * fisheyeRadius * radius.x, center.y - sin(phi) * fisheyeRadius * radius.y diff --git a/shaders/fisheye-reproject/shader.slang b/shaders/fisheye-reproject/shader.slang index 31e4381..92838f2 100644 --- a/shaders/fisheye-reproject/shader.slang +++ b/shaders/fisheye-reproject/shader.slang @@ -43,6 +43,8 @@ float normalizedFisheyeRadius(float theta, float halfFov) { float safeHalfFov = max(halfFov, 0.0001); + // Different fisheye lenses map angle to image radius differently. Normalize + // each model by the selected half-FOV so the outer lens edge stays at 1.0. if (fisheyeModel == 1) { return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001); @@ -67,6 +69,8 @@ float4 shadeVideo(ShaderContext context) float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0)); float tanHalfFov = tan(virtualFov * 0.5); + // Build a virtual output-camera ray, then rotate it into the fisheye lens + // coordinate system before asking where that ray lands on the source image. float3 ray = outputProjection == 1 ? buildCylindricalRay(screen, outputAspect, tanHalfFov) : buildRectilinearRay(screen, outputAspect, tanHalfFov); @@ -86,6 +90,7 @@ float4 shadeVideo(ShaderContext context) float phi = atan2(ray.y, ray.x); float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov); + // Polar lens coordinates become UVs inside the circular fisheye image. float2 sourceUv = float2( center.x + cos(phi) * fisheyeRadius * radius.x, center.y - sin(phi) * fisheyeRadius * radius.y diff --git a/shaders/greenscreen-key/shader.slang b/shaders/greenscreen-key/shader.slang index 6598afb..ffd4734 100644 --- a/shaders/greenscreen-key/shader.slang +++ b/shaders/greenscreen-key/shader.slang @@ -23,6 +23,8 @@ float3 matteSampleColor(float2 uv, ShaderContext context) if (blur <= 0.0001) return center; + // Pre-blur only the color used for screen comparison; the final image keeps + // its original detail and alpha is refined in a later pass. float2 radius = pixel * blur; float3 color = center * 0.36; color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16; @@ -37,6 +39,8 @@ float keyDistanceAt(float2 uv, ShaderContext context) float3 color = matteSampleColor(uv, context); float3 keyColor = saturate(screenColor.rgb); float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65; + // Direction distance is less sensitive to brightness, while chroma distance + // follows broadcast-style color difference; screenBalance blends the two. float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55; return lerp(directionDistance, chromaDistance, saturate(screenBalance)); } @@ -65,6 +69,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context) if (aaRadius > 0.0001) { + // A small fixed kernel smooths edges and collects min/max alpha for + // black/white cleanup without needing dynamic loops or arrays. float2 radius = pixel * aaRadius; float2 halfRadius = radius * 0.5; float alphaMin = centerAlpha; @@ -126,6 +132,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context) alpha = centerAlpha; } + // Final matte shaping happens after blur/cleanup so clip and contrast affect + // the refined edge rather than the raw screen-distance estimate. alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001)); alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5); alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001)); @@ -135,6 +143,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context) float spillAmountForColor(float3 color) { float3 keyColor = saturate(screenColor.rgb); + // Measure spill as color energy aligned with the screen color minus the + // strongest opposing channel, leaving neutral highlights mostly intact. float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))); float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b)); return saturate(keyComponent - opposingComponent + despillBias); @@ -187,6 +197,8 @@ float4 applyKey(ShaderContext context) float cropMask = cropMaskAt(context.uv, context); alpha *= cropMask; + // Edge recovery is strongest around 50% alpha, where fringing usually lives, + // and fades away for solid foreground/background pixels. float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0)); despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover)); diff --git a/shaders/happy-accident/shader.slang b/shaders/happy-accident/shader.slang index 55f1aa9..4bf038e 100644 --- a/shaders/happy-accident/shader.slang +++ b/shaders/happy-accident/shader.slang @@ -36,6 +36,8 @@ float4 shadeVideo(ShaderContext context) float4 accumulated = float4(0.0, 0.0, 0.0, 0.0); float clampedSteps = clamp(raySteps, 1.0, 77.0); + // Ray-march a folded procedural field. distanceToSurface advances the ray, + // while inverse-distance accumulation creates the glowing filaments. for (int i = 0; i < 77; ++i) { if (float(i) >= clampedSteps) @@ -49,11 +51,14 @@ float4 shadeVideo(ShaderContext context) position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy); position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy); + // Color comes from pre-fold space so the palette varies smoothly even as + // the geometry folds into repeated cells. float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition); float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0)); palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy); position = abs(frac(position) - 0.5); + // Distance to a tiny box/cross primitive inside each repeated cell. distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001; accumulated += palette.w * palette / distanceToSurface; } diff --git a/shaders/lut-apply/shader.slang b/shaders/lut-apply/shader.slang index 4f6a627..6b41482 100644 --- a/shaders/lut-apply/shader.slang +++ b/shaders/lut-apply/shader.slang @@ -7,6 +7,8 @@ float3 sampleLutCell(float3 index) float g = floor(index.g + 0.5); float b = floor(index.b + 0.5); + // The 33^3 cube is packed as blue slices laid horizontally, with red across + // each slice and green down the atlas. float atlasWidth = LUT_SIZE * LUT_SIZE; float2 lutUv; lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth; @@ -30,6 +32,9 @@ float3 applyLut33(float3 color) float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b)); float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b)); + // Tetrahedral interpolation chooses one of six paths through the cube. + // This avoids the muddy diagonals that simple trilinear LUT sampling can + // introduce for strong grades. if (blend.r > blend.g) { if (blend.g > blend.b) @@ -55,6 +60,8 @@ float hash12(float2 value) float3 outputDither(float2 pixel) { + // Subtract paired hashes to center the dither around zero, then scale to + // roughly one 8-bit code value. float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0)); float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0)); float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0)); diff --git a/shaders/singularity/shader.slang b/shaders/singularity/shader.slang index d77dad1..de2a27e 100644 --- a/shaders/singularity/shader.slang +++ b/shaders/singularity/shader.slang @@ -20,6 +20,8 @@ float4 shadeVideo(ShaderContext context) float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale; p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035; + // Build a skewed coordinate system around an offset "black hole" so the + // waves pinch and stretch instead of staying radially symmetric. float iterator = 0.2; float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35)); float2 blackholeCenter = p - iterator * diagonal; @@ -30,6 +32,8 @@ float4 shadeVideo(ShaderContext context) float2 v = singularitySpiral(c, time, iterator); float2 waves = float2(0.0001, 0.0001); + // Iterative sine feedback creates the accretion texture; the iterator value + // also damps later steps to keep the pattern stable. for (; iterator < 9.0; iterator += 1.0) { waves += 1.0 + sin(v); @@ -40,6 +44,8 @@ float4 shadeVideo(ShaderContext context) float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius; float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001); float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness; + // Exponential falloff turns the accumulated wave field into bright rims and + // a darker center without hard thresholds. float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift); float4 waveColor = waves.xyyx; diff --git a/shaders/vhs/shader.slang b/shaders/vhs/shader.slang index 257afbf..2a31c4c 100644 --- a/shaders/vhs/shader.slang +++ b/shaders/vhs/shader.slang @@ -8,6 +8,8 @@ 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))); @@ -59,6 +61,8 @@ float grainScalar(float2 uv) 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; float2 drift = float2(time * 19.7, time * 23.3); @@ -87,6 +91,8 @@ float valueNoise2(float2 p) 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; @@ -102,6 +108,8 @@ float3 analogStatic(float2 uv, float time, float2 outputResolution) 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 = goldNoise(goldPixel, seed + 0.1); float snowB = goldNoise(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2); @@ -146,6 +154,8 @@ float3 blurVhs(float2 uv, float d, int sampleCount) 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) @@ -170,6 +180,8 @@ float4 buildTapeSmear(ShaderContext 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); @@ -202,6 +214,8 @@ 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); @@ -219,6 +233,8 @@ float4 finishVhs(ShaderContext context) 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; @@ -229,6 +245,8 @@ float4 finishVhs(ShaderContext context) 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); diff --git a/shaders/video-cube/shader.slang b/shaders/video-cube/shader.slang index 6c35111..21fd5df 100644 --- a/shaders/video-cube/shader.slang +++ b/shaders/video-cube/shader.slang @@ -17,6 +17,8 @@ bool intersectCube(float3 rayOrigin, float3 rayDirection, float halfExtent, out float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent); float3 boxMax = float3(halfExtent, halfExtent, halfExtent); + // Slab intersection: find the ray interval that overlaps all three box + // axes, then keep the nearest positive hit. float3 invDir = 1.0 / rayDirection; float3 t0 = (boxMin - rayOrigin) * invDir; float3 t1 = (boxMax - rayOrigin) * invDir; @@ -43,6 +45,8 @@ float2 cubeFaceUv(float3 hitPoint, float halfExtent, float zoom) float2 uv = float2(0.5, 0.5); float safeZoom = max(zoom, 0.001); + // The dominant coordinate tells which face was hit; the other two axes + // become that face's local UVs. if (face.x >= face.y && face.x >= face.z) { uv = hitPoint.x > 0.0 @@ -79,6 +83,8 @@ float4 shadeVideo(ShaderContext context) float yaw = spin; float pitch = spin * 0.61 + 0.35; + // Rotate the camera ray into cube-local space instead of rotating the cube + // geometry, which keeps the intersection math axis-aligned. float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw); float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw); @@ -96,6 +102,8 @@ float4 shadeVideo(ShaderContext context) float3 normal; float3 face = abs(localHit); + // Reconstruct the face normal from the hit point so lighting follows the + // same face choice used for UV lookup. if (face.x >= face.y && face.x >= face.z) normal = float3(sign(localHit.x), 0.0, 0.0); else if (face.y >= face.x && face.y >= face.z) diff --git a/shaders/waveform-overlay/shader.slang b/shaders/waveform-overlay/shader.slang index d6eff81..25184fe 100644 --- a/shaders/waveform-overlay/shader.slang +++ b/shaders/waveform-overlay/shader.slang @@ -18,6 +18,8 @@ float4 shadeVideo(ShaderContext context) float resolutionAspect = max(context.outputResolution.x, 1.0) / max(context.outputResolution.y, 1.0); float width = saturate(overlayScale); float height = width * resolutionAspect / targetAspect; + // Keep the scope in a 16:9 frame, then shrink it if the requested scale + // would push the overlay beyond the screen bounds. float fitScale = min(1.0 / max(width, 0.001), 1.0 / max(height, 0.001)); width *= min(fitScale, 1.0); height *= min(fitScale, 1.0); @@ -36,6 +38,8 @@ float4 shadeVideo(ShaderContext context) float3 bg = lerp(color.rgb, float3(0.0, 0.0, 0.0), saturate(backgroundOpacity)); float labelHeight = min(max(pad.x * 0.95, 0.048), 0.12); + // Label textures are authored in UV space, so compensate for the overlay + // and output aspect ratios to keep the glyphs from stretching. float labelWidth = labelHeight * height * max(context.outputResolution.y, 1.0) / max(width * max(context.outputResolution.x, 1.0), 0.001); float labelX = max(pad.x * 0.5, labelWidth * 0.55); float y0 = pad.y; @@ -63,6 +67,8 @@ float4 shadeVideo(ShaderContext context) float requestedSamples = clamp(waveformSamples, 1.0, 96.0); float density = 0.0; + // For each output pixel, march through source rows at the same X coordinate + // and accumulate hits where sampled luma lands near this pixel's Y level. for (int sampleIndex = 0; sampleIndex < 96; sampleIndex++) { float samplePosition = float(sampleIndex); diff --git a/shaders/xyla-exposure-chart/shader.slang b/shaders/xyla-exposure-chart/shader.slang index bfcfaf1..e1a5715 100644 --- a/shaders/xyla-exposure-chart/shader.slang +++ b/shaders/xyla-exposure-chart/shader.slang @@ -1,5 +1,7 @@ float boxMask(float2 point, float2 halfSize, float feather) { + // Signed-distance box mask gives the chart and border pixel-sized feathered + // edges without branching per side. float2 distanceToEdge = abs(point) - halfSize; float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))); float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0); @@ -31,6 +33,8 @@ float applyToneCurve(float linearLevel) float patchBrightness(int patchIndex, int count) { int clampedIndex = clamp(patchIndex, 0, max(count - 1, 0)); + // Each patch is one stop brighter than the previous patch until it clips at + // the requested peak level, matching the Xyla-style exposure ramp. float linearLevel = baseLevel * exp2(float(clampedIndex)); linearLevel = min(linearLevel, peakLevel); return applyToneCurve(linearLevel); @@ -60,6 +64,8 @@ float4 shadeVideo(ShaderContext context) if (reverseOrder) patchIndex = count - 1 - patchIndex; + // Build each patch as a slot along the main axis, then mask the cross-axis + // extents so vertical and horizontal charts share the same logic. float patchSlotCenter = (floor(patchPosition) + 0.5) / float(count); float localAxis = abs(normalizedAxis - patchSlotCenter) * float(count) * 2.0; float safeGapSize = saturate(gapSize);