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);

	// 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);
	}
	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)
{
	// 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);

	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;

	// 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;
	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);

	// 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
	);

	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);
}
