static const float PI = 3.14159265358979323846; static const float TWO_PI = 6.28318530717958647692; float radiansFromDegrees(float degrees) { return degrees * (PI / 180.0); } float3 rotateX(float3 ray, float angle) { float s = sin(angle); float c = cos(angle); return float3(ray.x, c * ray.y - s * ray.z, s * ray.y + c * ray.z); } float3 rotateY(float3 ray, float angle) { float s = sin(angle); float c = cos(angle); return float3(c * ray.x + s * ray.z, ray.y, -s * ray.x + c * ray.z); } float3 rotateZ(float3 ray, float angle) { float s = sin(angle); float c = cos(angle); return float3(c * ray.x - s * ray.y, s * ray.x + c * ray.y, ray.z); } float normalizedFisheyeRadius(float theta, float halfFov) { float safeHalfFov = max(halfFov, 0.0001); if (fisheyeModel == 1) { return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001); } else if (fisheyeModel == 2) { return tan(theta * 0.5) / max(tan(safeHalfFov * 0.5), 0.0001); } else if (fisheyeModel == 3) { return sin(theta) / max(sin(safeHalfFov), 0.0001); } return theta / safeHalfFov; } float3 equirectangularRay(float2 uv) { float longitude = (uv.x - 0.5) * TWO_PI; float latitude = (0.5 - uv.y) * PI; float latitudeCos = cos(latitude); return normalize(float3( sin(longitude) * latitudeCos, sin(latitude), cos(longitude) * latitudeCos )); } float sourceUvOutsideDistance(float2 uv) { float2 lower = max(-uv, float2(0.0, 0.0)); float2 upper = max(uv - 1.0, float2(0.0, 0.0)); return max(max(lower.x, lower.y), max(upper.x, upper.y)); } float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context) { float outsideDistance = sourceUvOutsideDistance(sourceUv); if (outsideDistance <= 0.0) return sampleVideo(sourceUv); float fillDistance = max(edgeFill, 0.0); if (outsideDistance > fillDistance) return outsideColor; float2 clampedUv = saturate(sourceUv); float2 inward = clampedUv - sourceUv; float inwardLength = max(length(inward), 0.000001); inward /= inwardLength; float blurDistance = max(edgeBlur, 0.0); float4 color = sampleVideo(clampedUv) * 0.32; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.75)) * 0.20; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.20)) * 0.14; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.75)) * 0.08; float edgeFade = smoothstep(fillDistance * 0.78, fillDistance, outsideDistance); return lerp(color, outsideColor, edgeFade); } float4 shadeVideo(ShaderContext context) { float3 ray = equirectangularRay(context.uv); ray = rotateZ(ray, radiansFromDegrees(rollDegrees)); ray = rotateX(ray, radiansFromDegrees(-pitchDegrees)); ray = rotateY(ray, radiansFromDegrees(yawDegrees)); // Mirror the rear hemisphere into the front-facing fisheye image so one // circular lens source fills both halves of the equirectangular output. ray.z = abs(ray.z); ray = normalize(ray); float halfFov = radiansFromDegrees(clamp(lensFovDegrees, 1.0, 220.0) * 0.5); float theta = acos(clamp(ray.z, -1.0, 1.0)); if (theta > halfFov) return outsideColor; float phi = atan2(ray.y, ray.x); float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov); float2 sourceUv = float2( center.x + cos(phi) * fisheyeRadius * radius.x, center.y - sin(phi) * fisheyeRadius * radius.y ); float2 guard = 0.5 / max(context.inputResolution, float2(1.0, 1.0)); if (edgeFill <= 0.0 && (sourceUv.x < -guard.x || sourceUv.x > 1.0 + guard.x || sourceUv.y < -guard.y || sourceUv.y > 1.0 + guard.y)) return outsideColor; return sampleEdgeFilledVideo(sourceUv, context); }