diff --git a/shaders/waveform-overlay/0.png b/shaders/waveform-overlay/0.png new file mode 100644 index 0000000..ea442ff Binary files /dev/null and b/shaders/waveform-overlay/0.png differ diff --git a/shaders/waveform-overlay/100.png b/shaders/waveform-overlay/100.png new file mode 100644 index 0000000..5972e47 Binary files /dev/null and b/shaders/waveform-overlay/100.png differ diff --git a/shaders/waveform-overlay/25.png b/shaders/waveform-overlay/25.png new file mode 100644 index 0000000..b6fdf41 Binary files /dev/null and b/shaders/waveform-overlay/25.png differ diff --git a/shaders/waveform-overlay/50.png b/shaders/waveform-overlay/50.png new file mode 100644 index 0000000..d3f6bb1 Binary files /dev/null and b/shaders/waveform-overlay/50.png differ diff --git a/shaders/waveform-overlay/75.png b/shaders/waveform-overlay/75.png new file mode 100644 index 0000000..9e5d27a Binary files /dev/null and b/shaders/waveform-overlay/75.png differ diff --git a/shaders/waveform-overlay/shader.json b/shaders/waveform-overlay/shader.json index 877d70f..e97a1b3 100644 --- a/shaders/waveform-overlay/shader.json +++ b/shaders/waveform-overlay/shader.json @@ -4,21 +4,61 @@ "description": "Draws a lightweight luma waveform overlay along the bottom of the video.", "category": "Utility", "entryPoint": "shadeVideo", + "textures": [ + { + "id": "label0Texture", + "path": "0.png" + }, + { + "id": "label25Texture", + "path": "25.png" + }, + { + "id": "label50Texture", + "path": "50.png" + }, + { + "id": "label75Texture", + "path": "75.png" + }, + { + "id": "label100Texture", + "path": "100.png" + } + ], "parameters": [ { - "id": "overlayHeight", - "label": "Overlay Height", + "id": "overlayScale", + "label": "Overlay Scale", "type": "float", - "default": 0.32, + "default": 0.4, "min": 0.1, "max": 1.0, "step": 0.01 }, + { + "id": "overlayPosition", + "label": "Overlay Position", + "type": "vec2", + "default": [0.24, 0.76], + "min": [0.0, 0.0], + "max": [1.0, 1.0], + "step": [0.01, 0.01] + }, + { + "id": "overlayPadding", + "label": "Overlay Padding", + "type": "float", + "default": 0.08, + "min": 0.0, + "max": 0.25, + "step": 0.01 + }, { "id": "waveformOpacity", "label": "Waveform Opacity", "type": "float", - "default": 0.8, + "default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01 @@ -27,7 +67,7 @@ "id": "backgroundOpacity", "label": "Background", "type": "float", - "default": 0.35, + "default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01 @@ -36,16 +76,52 @@ "id": "lineThickness", "label": "Line Thickness", "type": "float", - "default": 2.0, + "default": 1.5, "min": 0.5, "max": 10.0, "step": 0.1 }, + { + "id": "gridOpacity", + "label": "Grid Opacity", + "type": "float", + "default": 1, + "min": 0.0, + "max": 1.0, + "step": 0.01 + }, + { + "id": "waveformSamples", + "label": "Waveform Samples", + "type": "float", + "default": 64.0, + "min": 8.0, + "max": 96.0, + "step": 1.0 + }, + { + "id": "waveformGain", + "label": "Waveform Gain", + "type": "float", + "default": 12.0, + "min": 1.0, + "max": 32.0, + "step": 0.5 + }, + { + "id": "waveformNoiseReduction", + "label": "Noise Reduction", + "type": "float", + "default": 0.08, + "min": 0.0, + "max": 0.6, + "step": 0.01 + }, { "id": "waveformColor", "label": "Waveform Color", "type": "color", - "default": [0.2, 1.0, 0.65, 1.0] + "default": [1.0, 1.0, 1.0, 1.0] } ] } diff --git a/shaders/waveform-overlay/shader.slang b/shaders/waveform-overlay/shader.slang index fa913e5..f2f2d88 100644 --- a/shaders/waveform-overlay/shader.slang +++ b/shaders/waveform-overlay/shader.slang @@ -1,24 +1,95 @@ +float insideUnit(float2 uv) +{ + return step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0); +} + +float4 blendLabel(float4 base, float4 labelSample, float inside) +{ + float labelMask = saturate(dot(labelSample.rgb, float3(0.2126, 0.7152, 0.0722)) * labelSample.a * inside); + float3 screened = 1.0 - (1.0 - base.rgb) * (1.0 - labelSample.rgb); + return float4(lerp(base.rgb, screened, labelMask), max(base.a, labelMask)); +} + float4 shadeVideo(ShaderContext context) { float4 color = context.sourceColor; - float height = saturate(overlayHeight); - float overlayStart = 1.0 - height; - if (context.uv.y < overlayStart) + + float targetAspect = 16.0 / 9.0; + float resolutionAspect = max(context.outputResolution.x, 1.0) / max(context.outputResolution.y, 1.0); + float width = saturate(overlayScale); + float height = width * resolutionAspect / targetAspect; + 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); + float2 halfSize = float2(width, height) * 0.5; + float2 center = clamp(saturate(overlayPosition), halfSize, float2(1.0) - halfSize); + float2 overlayMin = center - halfSize; + float2 overlayMax = center + halfSize; + + if (context.uv.x < overlayMin.x || context.uv.x > overlayMax.x || + context.uv.y < overlayMin.y || context.uv.y > overlayMax.y) return color; - float2 scopeUv = float2(context.uv.x, (context.uv.y - overlayStart) / max(height, 0.001)); - float luma = dot(sampleVideo(float2(context.uv.x, 0.5)).rgb, float3(0.2126, 0.7152, 0.0722)); - float targetY = 1.0 - saturate(luma); - float pixelThickness = max(lineThickness, 0.5) / max(context.outputResolution.y * height, 1.0); - float wave = 1.0 - smoothstep(pixelThickness, pixelThickness * 2.5, abs(scopeUv.y - targetY)); - - float grid = 0.0; - grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(scopeUv.y - 0.25))); - grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(scopeUv.y - 0.50))); - grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(scopeUv.y - 0.75))); + float2 boxUv = (context.uv - overlayMin) / max(float2(width, height), float2(0.001)); + float2 pad = min(float2(saturate(overlayPadding)), float2(0.45)); + float2 innerUv = (boxUv - pad) / max(float2(1.0) - pad * 2.0, float2(0.001)); float3 bg = lerp(color.rgb, float3(0.0, 0.0, 0.0), saturate(backgroundOpacity)); - float3 withGrid = lerp(bg, float3(0.16, 0.22, 0.28), grid * 0.5); - float3 scoped = lerp(withGrid, waveformColor.rgb, wave * saturate(waveformOpacity) * waveformColor.a); + float labelHeight = min(max(pad.x * 0.95, 0.048), 0.12); + 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; + float y25 = pad.y + 0.25 * (1.0 - pad.y * 2.0); + float y50 = pad.y + 0.50 * (1.0 - pad.y * 2.0); + float y75 = pad.y + 0.75 * (1.0 - pad.y * 2.0); + float y100 = 1.0 - pad.y; + + float4 label = float4(0.0); + float2 labelUv100 = (boxUv - float2(labelX, y100)) / float2(labelWidth, labelHeight) + float2(0.5); + label = blendLabel(label, label100Texture.Sample(labelUv100), insideUnit(labelUv100)); + float2 labelUv75 = (boxUv - float2(labelX, y75)) / float2(labelWidth, labelHeight) + float2(0.5); + label = blendLabel(label, label75Texture.Sample(labelUv75), insideUnit(labelUv75)); + float2 labelUv50 = (boxUv - float2(labelX, y50)) / float2(labelWidth, labelHeight) + float2(0.5); + label = blendLabel(label, label50Texture.Sample(labelUv50), insideUnit(labelUv50)); + float2 labelUv25 = (boxUv - float2(labelX, y25)) / float2(labelWidth, labelHeight) + float2(0.5); + label = blendLabel(label, label25Texture.Sample(labelUv25), insideUnit(labelUv25)); + float2 labelUv0 = (boxUv - float2(labelX, y0)) / float2(labelWidth, labelHeight) + float2(0.5); + label = blendLabel(label, label0Texture.Sample(labelUv0), insideUnit(labelUv0)); + + if (innerUv.x < 0.0 || innerUv.x > 1.0 || innerUv.y < 0.0 || innerUv.y > 1.0) + return float4(lerp(bg, label.rgb, label.a), color.a); + + float pixelThickness = max(lineThickness, 0.5) / max(context.outputResolution.y * height * (1.0 - pad.y * 2.0), 1.0); + float requestedSamples = clamp(waveformSamples, 1.0, 96.0); + float density = 0.0; + + for (int sampleIndex = 0; sampleIndex < 96; sampleIndex++) + { + float samplePosition = float(sampleIndex); + if (samplePosition >= requestedSamples) + break; + + float sourceY = (samplePosition + 0.5) / requestedSamples; + float luma = dot(sampleVideo(float2(innerUv.x, sourceY)).rgb, float3(0.2126, 0.7152, 0.0722)); + float targetY = 1.0 - saturate(luma); + float sampleHit = 1.0 - smoothstep(pixelThickness, pixelThickness * 2.5, abs(innerUv.y - targetY)); + + density += sampleHit; + } + + float wave = saturate(density / requestedSamples * max(waveformGain, 0.0)); + float floor = saturate(waveformNoiseReduction); + wave = smoothstep(floor, 1.0, wave); + + float grid = 0.0; + grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(innerUv.y - 0.00))); + grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(innerUv.y - 0.25))); + grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(innerUv.y - 0.50))); + grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(innerUv.y - 0.75))); + grid = max(grid, 1.0 - smoothstep(0.002, 0.006, abs(innerUv.y - 1.00))); + + float3 withGrid = lerp(bg, float3(0.16, 0.22, 0.28), grid * saturate(gridOpacity)); + float3 withLabel = lerp(withGrid, label.rgb, label.a); + float3 scoped = lerp(withLabel, waveformColor.rgb, wave * saturate(waveformOpacity) * waveformColor.a); return float4(scoped, color.a); }