float3 safeNormalize(float3 value) { float len = max(length(value), 0.0001); return value / len; } float luma709(float3 color) { return dot(color, float3(0.2126, 0.7152, 0.0722)); } float2 chroma709(float3 color) { float y = luma709(color); return float2((color.b - y) * 0.5647, (color.r - y) * 0.7132); } float3 matteSampleColor(float2 uv, ShaderContext context) { float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0)); float blur = max(screenPreBlur, 0.0); float3 center = saturate(sampleVideo(saturate(uv)).rgb); 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; color += saturate(sampleVideo(saturate(uv - float2(radius.x, 0.0))).rgb) * 0.16; color += saturate(sampleVideo(saturate(uv + float2(0.0, radius.y))).rgb) * 0.16; color += saturate(sampleVideo(saturate(uv - float2(0.0, radius.y))).rgb) * 0.16; return color; } 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)); } float rawAlphaAt(float2 uv, ShaderContext context) { float keyDistance = keyDistanceAt(uv, context); float matteCenter = threshold + erodeDilate; float matteFeather = max(softness, 0.0005); float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, keyDistance); return saturate(alpha); } float matteAlphaAt(float2 uv) { return saturate(sampleVideo(saturate(uv)).a); } float refinedAlphaFromMatte(float2 uv, ShaderContext context) { float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0)); float blur = max(matteBlur, 0.0); float aaRadius = max(blur, 0.65); float centerAlpha = matteAlphaAt(uv); float alpha = centerAlpha * 0.30; 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; float alphaMax = centerAlpha; float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0)); alpha += sampleAlpha * 0.065; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0)); alpha += sampleAlpha * 0.065; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y)); alpha += sampleAlpha * 0.065; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y)); alpha += sampleAlpha * 0.065; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0)); alpha += sampleAlpha * 0.06; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0)); alpha += sampleAlpha * 0.06; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y)); alpha += sampleAlpha * 0.06; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y)); alpha += sampleAlpha * 0.06; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + radius); alpha += sampleAlpha * 0.05; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv - radius); alpha += sampleAlpha * 0.05; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y)); alpha += sampleAlpha * 0.05; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y)); alpha += sampleAlpha * 0.05; alphaMin = min(alphaMin, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha); alpha = lerp(alpha, alphaMin, saturate(blackCleanup)); alpha = lerp(alpha, alphaMax, saturate(whiteCleanup)); } else { 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)); return saturate(alpha); } 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); } float3 despillColor(float3 color, float alpha) { float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001))); float spill = spillAmountForColor(color) * despill * (1.0 - alpha * 0.35); float neutral = luma709(color); float3 neutralized = color - keyColor * spill; neutralized = max(neutralized, float3(0.0, 0.0, 0.0)); neutralized = lerp(neutralized, float3(neutral, neutral, neutral), spill * 0.18); neutralized = lerp(neutralized, neutralized * saturate(spillTint.rgb), saturate(spill)); return saturate(neutralized); } float cropMaskAt(float2 uv, ShaderContext context) { float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0)); float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x); float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x); float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y); float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y); return saturate(left * right * top * bottom); } float4 buildRawMatte(ShaderContext context) { float4 src = context.sourceColor; float3 color = saturate(src.rgb); float alpha = rawAlphaAt(context.uv, context); return float4(color, alpha); } float4 refineMatte(ShaderContext context) { float4 raw = sampleVideo(context.uv); float alpha = refinedAlphaFromMatte(context.uv, context); return float4(saturate(raw.rgb), alpha); } float4 applyKey(ShaderContext context) { float4 keyed = sampleVideo(context.uv); float3 color = saturate(keyed.rgb); float alpha = saturate(keyed.a); float spill = spillAmountForColor(color); float3 despilled = despillColor(color, alpha); 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)); if (viewMode == 1) return float4(alpha, alpha, alpha, 1.0); if (viewMode == 2) return float4(spill, spill * 0.55, 0.0, 1.0); if (viewMode == 3) return float4(despilled, 1.0); if (viewMode == 4) { float rawAlpha = rawAlphaAt(context.uv, context) * cropMask; return float4(rawAlpha, alpha, spill, 1.0); } float3 premultiplied = saturate(despilled) * alpha; return float4(premultiplied, alpha); } float4 shadeVideo(ShaderContext context) { return applyKey(context); }