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

bool intersectCube(float3 rayOrigin, float3 rayDirection, float halfExtent, out float hitDistance)
{
	float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent);
	float3 boxMax = float3(halfExtent, halfExtent, halfExtent);

	// Slab intersection: find the ray interval that overlaps all three box
	// axes, then keep the nearest positive hit.
	float3 invDir = 1.0 / rayDirection;
	float3 t0 = (boxMin - rayOrigin) * invDir;
	float3 t1 = (boxMax - rayOrigin) * invDir;

	float3 tMin3 = min(t0, t1);
	float3 tMax3 = max(t0, t1);

	float tMin = max(max(tMin3.x, tMin3.y), tMin3.z);
	float tMax = min(min(tMax3.x, tMax3.y), tMax3.z);

	if (tMax < max(tMin, 0.0))
	{
		hitDistance = 0.0;
		return false;
	}

	hitDistance = tMin > 0.0 ? tMin : tMax;
	return hitDistance > 0.0;
}

float2 cubeFaceUv(float3 hitPoint, float halfExtent, float zoom)
{
	float3 face = abs(hitPoint);
	float2 uv = float2(0.5, 0.5);
	float safeZoom = max(zoom, 0.001);

	// The dominant coordinate tells which face was hit; the other two axes
	// become that face's local UVs.
	if (face.x >= face.y && face.x >= face.z)
	{
		uv = hitPoint.x > 0.0
			? float2(-hitPoint.z, hitPoint.y)
			: float2(hitPoint.z, hitPoint.y);
	}
	else if (face.y >= face.x && face.y >= face.z)
	{
		uv = hitPoint.y > 0.0
			? float2(hitPoint.x, -hitPoint.z)
			: float2(hitPoint.x, hitPoint.z);
	}
	else
	{
		uv = hitPoint.z > 0.0
			? float2(hitPoint.x, hitPoint.y)
			: float2(-hitPoint.x, hitPoint.y);
	}

	uv = uv / (halfExtent * 2.0 * safeZoom) + 0.5;
	return frac(uv);
}

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

	float3 rayOrigin = float3(0.0, 0.0, 2.7);
	float3 rayDirection = normalize(float3(centeredUv, -2.1));

	float spin = context.time * spinSpeed + context.startupRandom * 6.2831853;
	float yaw = spin;
	float pitch = spin * 0.61 + 0.35;

	// Rotate the camera ray into cube-local space instead of rotating the cube
	// geometry, which keeps the intersection math axis-aligned.
	float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw);
	float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw);

	float halfExtent = max(cubeScale, 0.05);
	float hitDistance = 0.0;

	float3 background = lerp(float3(0.02, 0.02, 0.03), context.sourceColor.rgb, saturate(backgroundMix));

	if (!intersectCube(localOrigin, localDirection, halfExtent, hitDistance))
		return float4(background, 1.0);

	float3 localHit = localOrigin + localDirection * hitDistance;
	float2 faceUv = cubeFaceUv(localHit, halfExtent, faceZoom);
	float4 faceColor = sampleVideo(faceUv);

	float3 normal;
	float3 face = abs(localHit);
	// Reconstruct the face normal from the hit point so lighting follows the
	// same face choice used for UV lookup.
	if (face.x >= face.y && face.x >= face.z)
		normal = float3(sign(localHit.x), 0.0, 0.0);
	else if (face.y >= face.x && face.y >= face.z)
		normal = float3(0.0, sign(localHit.y), 0.0);
	else
		normal = float3(0.0, 0.0, sign(localHit.z));

	normal = rotateX(rotateY(normal, yaw), pitch);

	float3 lightDir = normalize(float3(0.5, 0.8, 0.6));
	float diffuse = saturate(dot(normal, lightDir)) * 0.75 + 0.25;
	float edge = 1.0 - saturate(max(face.x, max(face.y, face.z)) - halfExtent + 0.03) / 0.03;

	float3 shaded = faceColor.rgb * diffuse;
	shaded = lerp(shaded, shaded + 0.12, edge * 0.35);

	return float4(saturate(shaded), 1.0);
}
