static const float PI = 3.14159265358979323846;

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

float3 buildRectilinearRay(float2 screen, float outputAspect, float tanHalfFov)
{
	return normalize(float3(screen.x * outputAspect * tanHalfFov, screen.y * tanHalfFov, 1.0));
}

float3 buildCylindricalRay(float2 screen, float outputAspect, float tanHalfFov)
{
	float horizontalFov = 2.0 * atan(outputAspect * tanHalfFov);
	float yaw = screen.x * horizontalFov * 0.5;
	float vertical = screen.y * tanHalfFov;
	return normalize(float3(sin(yaw), vertical, cos(yaw)));
}

float normalizedFisheyeRadius(float theta, float halfFov)
{
	float safeHalfFov = max(halfFov, 0.0001);

	// Different fisheye lenses map angle to image radius differently. Normalize
	// each model by the selected half-FOV so the outer lens edge stays at 1.0.
	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;
}

float sourceUvRectMask(float2 uv, float2 inputResolution)
{
	float2 pixel = 1.0 / max(inputResolution, float2(1.0, 1.0));
	float cut = max(sourceEdgeCut, 0.0);
	float feather = max(sourceEdgeFeather, 0.0);
	float2 featherSize = max(float2(feather, feather), pixel * 0.5);

	float left = smoothstep(cut, cut + featherSize.x, uv.x);
	float right = 1.0 - smoothstep(1.0 - cut - featherSize.x, 1.0 - cut, uv.x);
	float top = smoothstep(cut, cut + featherSize.y, uv.y);
	float bottom = 1.0 - smoothstep(1.0 - cut - featherSize.y, 1.0 - cut, uv.y);
	return saturate(left * right * top * bottom);
}

float4 shadeVideo(ShaderContext context)
{
	float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
	float outputAspect = context.outputResolution.x / max(context.outputResolution.y, 1.0);

	float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0));
	float tanHalfFov = tan(virtualFov * 0.5);

	// Build a virtual output-camera ray, then rotate it into the fisheye lens
	// coordinate system before asking where that ray lands on the source image.
	float3 ray = outputProjection == 1
		? buildCylindricalRay(screen, outputAspect, tanHalfFov)
		: buildRectilinearRay(screen, outputAspect, tanHalfFov);

	ray = rotateZ(ray, radiansFromDegrees(baseRollDegrees));
	ray = rotateX(ray, radiansFromDegrees(-baseTiltDegrees));
	ray = rotateY(ray, radiansFromDegrees(basePanDegrees));
	ray = rotateZ(ray, radiansFromDegrees(rollDegrees));
	ray = rotateX(ray, radiansFromDegrees(-tiltDegrees));
	ray = rotateY(ray, radiansFromDegrees(panDegrees));

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

	// Polar lens coordinates become UVs inside the circular fisheye image.
	float2 sourceUv = float2(
		center.x + cos(phi) * fisheyeRadius * radius.x,
		center.y - sin(phi) * fisheyeRadius * radius.y
	);

	if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0)
		return outsideColor;

	float sourceMask = sourceUvRectMask(sourceUv, context.inputResolution);
	float4 sourceColor = sampleVideo(sourceUv);
	return saturate(lerp(outsideColor, sourceColor, sourceMask));
}
