more shaders and updates/changes
This commit is contained in:
102
shaders/crt-bulge/shader.json
Normal file
102
shaders/crt-bulge/shader.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "crt-bulge",
|
||||
"name": "CRT Bulge",
|
||||
"description": "Warps the image like convex CRT glass, with optional rounded screen edges and vignette darkening.",
|
||||
"category": "Distortion",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "bulgeAmount",
|
||||
"label": "Bulge",
|
||||
"type": "float",
|
||||
"default": -0.04,
|
||||
"min": -0.5,
|
||||
"max": 0.8,
|
||||
"step": 0.01,
|
||||
"description": "Positive values swell the center outward; negative values pinch it inward."
|
||||
},
|
||||
{
|
||||
"id": "zoom",
|
||||
"label": "Zoom",
|
||||
"type": "float",
|
||||
"default": 1.04,
|
||||
"min": 0.5,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Scales the source before distortion, useful for hiding warped edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeRoundness",
|
||||
"label": "Edge Roundness",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0,
|
||||
"max": 0.35,
|
||||
"step": 0.01,
|
||||
"description": "Rounds the visible screen corners like older CRT glass."
|
||||
},
|
||||
{
|
||||
"id": "edgeFeather",
|
||||
"label": "Edge Feather",
|
||||
"type": "float",
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 24,
|
||||
"step": 0.1,
|
||||
"description": "Softens the rounded screen edge in pixels."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeFeather",
|
||||
"label": "Source Edge Feather",
|
||||
"type": "float",
|
||||
"default": 1.5,
|
||||
"min": 0,
|
||||
"max": 16,
|
||||
"step": 0.1,
|
||||
"description": "Antialiases warped source edges when the distortion reveals outside-frame pixels."
|
||||
},
|
||||
{
|
||||
"id": "vignetteAmount",
|
||||
"label": "Vignette",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Darkens the glass toward the screen edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeMode",
|
||||
"label": "Edge Mode",
|
||||
"type": "enum",
|
||||
"default": "black",
|
||||
"options": [
|
||||
{
|
||||
"value": "black",
|
||||
"label": "Black"
|
||||
},
|
||||
{
|
||||
"value": "clamp",
|
||||
"label": "Clamp"
|
||||
},
|
||||
{
|
||||
"value": "mirror",
|
||||
"label": "Mirror"
|
||||
}
|
||||
],
|
||||
"description": "Chooses how warped samples outside the source frame are filled."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used outside the curved screen or source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
71
shaders/crt-bulge/shader.slang
Normal file
71
shaders/crt-bulge/shader.slang
Normal file
@@ -0,0 +1,71 @@
|
||||
float mirroredCoordinate(float coordinate)
|
||||
{
|
||||
float wrapped = frac(coordinate * 0.5) * 2.0;
|
||||
return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
|
||||
}
|
||||
|
||||
float roundedBoxMask(float2 point, float2 halfSize, float radius, float feather)
|
||||
{
|
||||
float2 distanceToEdge = abs(point) - (halfSize - radius);
|
||||
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))) - radius;
|
||||
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
|
||||
float signedDistance = outsideDistance + insideDistance;
|
||||
return 1.0 - smoothstep(0.0, max(feather, 0.00001), signedDistance);
|
||||
}
|
||||
|
||||
float sourceBoundsMask(float2 uv, float2 resolution)
|
||||
{
|
||||
float2 pixel = 1.0 / max(resolution, float2(1.0, 1.0));
|
||||
float2 feather = pixel * max(sourceEdgeFeather, 0.0);
|
||||
float left = smoothstep(0.0, max(feather.x, 0.00001), uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - max(feather.x, 0.00001), 1.0, uv.x);
|
||||
float top = smoothstep(0.0, max(feather.y, 0.00001), uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - max(feather.y, 0.00001), 1.0, uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float2 applyBulge(float2 uv, float2 resolution)
|
||||
{
|
||||
float2 centered = uv * 2.0 - 1.0;
|
||||
float aspect = resolution.x / max(resolution.y, 1.0);
|
||||
float2 aspectCentered = float2(centered.x * aspect, centered.y);
|
||||
float radiusSq = dot(aspectCentered, aspectCentered);
|
||||
float amount = clamp(bulgeAmount, -0.95, 0.95);
|
||||
float scale = 1.0 / max(1.0 + amount * radiusSq, 0.05);
|
||||
return centered * scale / max(zoom, 0.001) * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
|
||||
{
|
||||
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||
|
||||
if (edgeMode == 1)
|
||||
return sampleVideo(clamp(uv, 0.0, 1.0));
|
||||
if (edgeMode == 2)
|
||||
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||
|
||||
float edgeMask = sourceBoundsMask(uv, resolution);
|
||||
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
|
||||
return lerp(outsideColor, color, edgeMask);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 sourceUv = applyBulge(context.uv, resolution);
|
||||
|
||||
bool insideSource = false;
|
||||
float4 color = sampleWarped(sourceUv, resolution, insideSource);
|
||||
|
||||
float2 centered = context.uv * 2.0 - 1.0;
|
||||
float feather = max(edgeFeather, 0.0) / min(resolution.x, resolution.y);
|
||||
float screenMask = roundedBoxMask(centered, float2(1.0, 1.0), saturate(edgeRoundness), feather);
|
||||
color = lerp(outsideColor, color, screenMask);
|
||||
|
||||
float2 aspectCentered = float2(centered.x * resolution.x / max(resolution.y, 1.0), centered.y);
|
||||
float edgeDistance = saturate(length(aspectCentered) * 0.72);
|
||||
float vignette = lerp(1.0, 1.0 - saturate(vignetteAmount), smoothstep(0.35, 1.05, edgeDistance));
|
||||
color.rgb *= vignette;
|
||||
|
||||
return saturate(color);
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
"id": "vignetteAmount",
|
||||
"label": "Vignette",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"default": 0.3,
|
||||
"min": 0,
|
||||
"max": 0.6,
|
||||
"step": 0.01,
|
||||
@@ -154,6 +154,46 @@
|
||||
"max": 6,
|
||||
"step": 0.05,
|
||||
"description": "Scale of the generated noise pattern."
|
||||
},
|
||||
{
|
||||
"id": "scanlineAmount",
|
||||
"label": "Scanlines",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0,
|
||||
"max": 0.35,
|
||||
"step": 0.005,
|
||||
"description": "Subtle alternating-field luma modulation."
|
||||
},
|
||||
{
|
||||
"id": "chromaCrawlAmount",
|
||||
"label": "Chroma Crawl",
|
||||
"type": "float",
|
||||
"default": 0.035,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.005,
|
||||
"description": "Moving color shimmer around high-contrast edges."
|
||||
},
|
||||
{
|
||||
"id": "generationLoss",
|
||||
"label": "Generation Loss",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Raises blacks, softens detail, lowers contrast, and desaturates chroma like copied tape."
|
||||
},
|
||||
{
|
||||
"id": "sharpnessDrift",
|
||||
"label": "Sharpness Drift",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0,
|
||||
"max": 0.6,
|
||||
"step": 0.01,
|
||||
"description": "Slowly varies picture softness to mimic unstable tape focus."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,11 +46,16 @@ float noiseHash(float2 p)
|
||||
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
// Gold Noise (c)2015 dcerisano@standard3d.com, adapted for Slang.
|
||||
float goldNoise(float2 xy, float seed)
|
||||
float staticHash(float2 p)
|
||||
{
|
||||
const float phi = 1.61803398874989484820459;
|
||||
return frac(tan(distance(xy * phi, xy) * seed) * xy.x);
|
||||
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)
|
||||
@@ -65,11 +70,13 @@ float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float
|
||||
// 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);
|
||||
float frame = floor(time * 59.94);
|
||||
|
||||
float r = grainScalar(grainUv + drift + float2(13.1, 71.7));
|
||||
float g = grainScalar(grainUv * float2(1.03, 0.97) + drift * 1.11 + float2(47.2, 19.4));
|
||||
float b = grainScalar(grainUv * float2(0.96, 1.05) + drift * 0.91 + float2(83.6, 53.8));
|
||||
// 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;
|
||||
}
|
||||
@@ -111,15 +118,16 @@ float3 analogStatic(float2 uv, float time, float2 outputResolution)
|
||||
// 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);
|
||||
float snowC = goldNoise(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
|
||||
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 = goldNoise(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
|
||||
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 fleck = smoothstep(0.988, 1.0, snowA) - smoothstep(0.0, 0.012, snowC);
|
||||
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;
|
||||
@@ -146,6 +154,85 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
||||
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);
|
||||
@@ -241,6 +328,10 @@ float4 finishVhs(ShaderContext context)
|
||||
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));
|
||||
@@ -262,6 +353,8 @@ float4 finishVhs(ShaderContext context)
|
||||
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);
|
||||
|
||||
121
shaders/video-plane-3d/shader.json
Normal file
121
shaders/video-plane-3d/shader.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"id": "video-plane-3d",
|
||||
"name": "Video Plane 3D",
|
||||
"description": "Places the video on a perspective 2D plane in 3D space with camera FOV, XYZ position, and pan/tilt/roll controls.",
|
||||
"category": "Projection",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "fovDegrees",
|
||||
"label": "FOV",
|
||||
"type": "float",
|
||||
"default": 45,
|
||||
"min": 5,
|
||||
"max": 150,
|
||||
"step": 0.1,
|
||||
"description": "Virtual camera vertical field of view in degrees."
|
||||
},
|
||||
{
|
||||
"id": "positionX",
|
||||
"label": "X",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -4,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Horizontal plane position in world units."
|
||||
},
|
||||
{
|
||||
"id": "positionY",
|
||||
"label": "Y",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -4,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Vertical plane position in world units."
|
||||
},
|
||||
{
|
||||
"id": "positionZ",
|
||||
"label": "Z",
|
||||
"type": "float",
|
||||
"default": 2.2,
|
||||
"min": 0.1,
|
||||
"max": 10,
|
||||
"step": 0.01,
|
||||
"description": "Depth of the plane in front of the virtual camera."
|
||||
},
|
||||
{
|
||||
"id": "panDegrees",
|
||||
"label": "Pan",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the plane left/right around its vertical axis."
|
||||
},
|
||||
{
|
||||
"id": "tiltDegrees",
|
||||
"label": "Tilt",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the plane up/down around its horizontal axis."
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the plane around its face normal."
|
||||
},
|
||||
{
|
||||
"id": "planeScale",
|
||||
"label": "Plane Scale",
|
||||
"type": "float",
|
||||
"default": 1.4,
|
||||
"min": 0.05,
|
||||
"max": 6,
|
||||
"step": 0.01,
|
||||
"description": "Height of the video plane in world units; width follows the source aspect ratio."
|
||||
},
|
||||
{
|
||||
"id": "edgeFeather",
|
||||
"label": "Edge Feather",
|
||||
"type": "float",
|
||||
"default": 1.5,
|
||||
"min": 0,
|
||||
"max": 24,
|
||||
"step": 0.1,
|
||||
"description": "Softens the plane edge in source pixels."
|
||||
},
|
||||
{
|
||||
"id": "backgroundMix",
|
||||
"label": "Background Mix",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mixes the original video behind the projected plane."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the camera ray misses the plane."
|
||||
}
|
||||
]
|
||||
}
|
||||
84
shaders/video-plane-3d/shader.slang
Normal file
84
shaders/video-plane-3d/shader.slang
Normal file
@@ -0,0 +1,84 @@
|
||||
static const float PI = 3.14159265358979323846;
|
||||
|
||||
float radiansFromDegrees(float degrees)
|
||||
{
|
||||
return degrees * (PI / 180.0);
|
||||
}
|
||||
|
||||
float3 rotateX(float3 p, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
|
||||
}
|
||||
|
||||
float3 rotateY(float3 p, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
|
||||
}
|
||||
|
||||
float3 rotateZ(float3 p, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
|
||||
}
|
||||
|
||||
float3 rotateWorldToPlane(float3 value)
|
||||
{
|
||||
float pan = radiansFromDegrees(panDegrees);
|
||||
float tilt = radiansFromDegrees(tiltDegrees);
|
||||
float roll = radiansFromDegrees(rollDegrees);
|
||||
return rotateZ(rotateX(rotateY(value, -pan), -tilt), -roll);
|
||||
}
|
||||
|
||||
float planeEdgeMask(float2 uv, float2 inputResolution)
|
||||
{
|
||||
float2 feather = max(edgeFeather, 0.0) / max(inputResolution, float2(1.0, 1.0));
|
||||
feather = max(feather, float2(0.00001, 0.00001));
|
||||
|
||||
float left = smoothstep(0.0, feather.x, uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - feather.x, 1.0, uv.x);
|
||||
float top = smoothstep(0.0, feather.y, uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - feather.y, 1.0, uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 outputResolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float outputAspect = outputResolution.x / outputResolution.y;
|
||||
float sourceAspect = context.inputResolution.x / max(context.inputResolution.y, 1.0);
|
||||
float tanHalfFov = tan(radiansFromDegrees(clamp(fovDegrees, 5.0, 150.0)) * 0.5);
|
||||
|
||||
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
|
||||
float3 rayOrigin = float3(0.0, 0.0, 0.0);
|
||||
float3 rayDirection = normalize(float3(screen.x * outputAspect * tanHalfFov, screen.y * tanHalfFov, 1.0));
|
||||
|
||||
float3 planePosition = float3(positionX, positionY, max(positionZ, 0.001));
|
||||
float3 localOrigin = rotateWorldToPlane(rayOrigin - planePosition);
|
||||
float3 localDirection = rotateWorldToPlane(rayDirection);
|
||||
|
||||
float backgroundAmount = saturate(backgroundMix);
|
||||
float4 background = float4(lerp(outsideColor.rgb, context.sourceColor.rgb, backgroundAmount), 1.0);
|
||||
if (abs(localDirection.z) < 0.00001)
|
||||
return background;
|
||||
|
||||
float hitDistance = -localOrigin.z / localDirection.z;
|
||||
if (hitDistance <= 0.0)
|
||||
return background;
|
||||
|
||||
float3 localHit = localOrigin + localDirection * hitDistance;
|
||||
float halfHeight = max(planeScale, 0.001) * 0.5;
|
||||
float halfWidth = halfHeight * sourceAspect;
|
||||
float2 planeUv = float2(
|
||||
localHit.x / max(halfWidth * 2.0, 0.0001) + 0.5,
|
||||
0.5 - localHit.y / max(halfHeight * 2.0, 0.0001)
|
||||
);
|
||||
|
||||
float mask = planeEdgeMask(planeUv, max(context.inputResolution, float2(1.0, 1.0)));
|
||||
float4 planeColor = sampleVideo(clamp(planeUv, 0.0, 1.0));
|
||||
return saturate(lerp(background, planeColor, mask));
|
||||
}
|
||||
@@ -47,6 +47,35 @@
|
||||
"step": 0.1,
|
||||
"description": "Rotates the source image around the frame center."
|
||||
},
|
||||
{
|
||||
"id": "cropAspect",
|
||||
"label": "Crop Aspect",
|
||||
"type": "enum",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{
|
||||
"value": "none",
|
||||
"label": "None"
|
||||
},
|
||||
{
|
||||
"value": "4x3",
|
||||
"label": "4:3"
|
||||
},
|
||||
{
|
||||
"value": "3x2",
|
||||
"label": "3:2"
|
||||
},
|
||||
{
|
||||
"value": "1x1",
|
||||
"label": "1:1"
|
||||
},
|
||||
{
|
||||
"value": "9x16",
|
||||
"label": "9:16"
|
||||
}
|
||||
],
|
||||
"description": "Crops the visible image to a centered preset aspect ratio without squeezing the source."
|
||||
},
|
||||
{
|
||||
"id": "edgeMode",
|
||||
"label": "Edge Mode",
|
||||
|
||||
@@ -28,8 +28,42 @@ float2 applyEdgeMode(float2 uv, out bool inside)
|
||||
return uv;
|
||||
}
|
||||
|
||||
float selectedCropAspect()
|
||||
{
|
||||
if (cropAspect == 1)
|
||||
return 4.0 / 3.0;
|
||||
if (cropAspect == 2)
|
||||
return 3.0 / 2.0;
|
||||
if (cropAspect == 3)
|
||||
return 1.0;
|
||||
if (cropAspect == 4)
|
||||
return 9.0 / 16.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
bool insideCropWindow(float2 uv, float2 resolution)
|
||||
{
|
||||
float targetAspect = selectedCropAspect();
|
||||
if (targetAspect <= 0.0)
|
||||
return true;
|
||||
|
||||
float outputAspect = resolution.x / max(resolution.y, 1.0);
|
||||
float2 cropSize = float2(1.0, 1.0);
|
||||
if (outputAspect > targetAspect)
|
||||
cropSize.x = targetAspect / outputAspect;
|
||||
else
|
||||
cropSize.y = outputAspect / targetAspect;
|
||||
|
||||
float2 cropMin = (1.0 - cropSize) * 0.5;
|
||||
float2 cropMax = cropMin + cropSize;
|
||||
return uv.x >= cropMin.x && uv.x <= cropMax.x && uv.y >= cropMin.y && uv.y <= cropMax.y;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
if (!insideCropWindow(context.uv, max(context.outputResolution, float2(1.0, 1.0))))
|
||||
return outsideColor;
|
||||
|
||||
float safeZoom = max(zoom, 0.001);
|
||||
float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5;
|
||||
sourceUv -= pan;
|
||||
|
||||
Reference in New Issue
Block a user