//	GGVR_GPU_Functions.h
//
//	© 2026 by Jeff Weeks
//	See TermsOfUse.txt

#ifndef GGVR_GPU_Functions_h
#define GGVR_GPU_Functions_h

#include <simd/simd.h>

//	Define shared GPU functions right here in this .h file,
//	so the compiler can inline them into whatever app-specific
//	GPU function calls them. (In other words, make sure they
//	end up in the same "compilation unit".)
//
//		Note: The compiler would probably inline them
//		with or without the 'inline' keyword. We include it
//		not only to make our intentions clear, but also
//		to avoid a linker error in the unlikely event that
//		we ever include this .h file in than one .metal file
//		in the same project.
//

inline float4 azimuthalEquidistantMap(
	float4	positionInBridgeOfNoseSpace,
	uchar	geometryCode,	//	I couldn't figure out how to share function constants
	float	spaceRadius,	//		among different .metal files,
	float	spaceRadiusInv)	//		so I passed them as parameters instead.
{
	float	factor = 1.0;	//	overwritten below
	float4	positionInTangentSpace;

	//	Given that the positionInBridgeOfNoseSpace sits
	//	at some distance r from the player (as measured
	//	in the non-Euclidean space itself), in some direction v,
	//	we want to find a point in the Euclidean tangent space
	//	(based at the bridge of the player's nose) that
	//	sits in that same direction v at that same distance r,
	//	but in the Euclidean tangent space rather than
	//	in the non-Euclidean space itself.
	//
	//	Let's compute the positionInTangentSpace as explained and
	//	illustrated in §5 of
	//
	//		Jeff Weeks,
	//		Body coherence in curved-space virtual reality games,
	//		Computers & Graphics 97 (2021) 28–41.
	//
	//		  https://www.sciencedirect.com/science/article/pii/S0097849321000443
	//		or
	//		  https://arxiv.org/abs/2011.00510
	//
	
	if (geometryCode == 's')
	{
		float cosTheta = clamp(
							positionInBridgeOfNoseSpace.w * spaceRadiusInv,
							-1.0,
							+1.0);
		float theta = acos(cosTheta);
		float sinTheta = sin(theta);
		
		if (sinTheta > 0.000001)	//	typical case
		{
			factor = theta / sinTheta;
			positionInTangentSpace = float4(
					factor * positionInBridgeOfNoseSpace.x,
					factor * positionInBridgeOfNoseSpace.y,
					factor * positionInBridgeOfNoseSpace.z,
					spaceRadius);
		}
		else	//	sin(θ) ≤ 0.000001
		{
			if (cosTheta > 0.0)	//	near north pole, cos(θ) ≈ +1.0
			{
				//	Near the north pole, the angle θ is small and the difference
				//
				//		θ - sin(θ) = θ - (θ - θ³/6 + θ⁵/120 - …)
				//				   = θ³/6 - θ⁵/120 + …
				//				   ≈ θ³/6
				//
				//	is much much smaller. So the "factor" computed above
				//	is extremely close to 1.0 and the positionInTangentSpace
				//	is extremely close to the positionInBridgeOfNoseSpace.
				//	Note that this conclusion remains valid even when
				//	the vertex sits exactly at the north pole,
				//	where θ = 0.0 and the factor = θ/sin(θ) = “0.0/0.0” = 1.0.
				
				positionInTangentSpace = float4(
						positionInBridgeOfNoseSpace.x,
						positionInBridgeOfNoseSpace.y,
						positionInBridgeOfNoseSpace.z,
						spaceRadius);	//	w coordinate almost same as before
			}
			else	//	near south pole, cos(θ) ≈ -1.0
			{
				//	At the south pole the situation is a bit trickier:
				//	we know that the south pole itself should map to a point
				//	exactly (π * spaceRadius) meters from the observer,
				//	but its direction is completely undefined.
				//	Nevertheless, we have to send the south pole somewhere,
				//	so let's put it directly in front of the player at
				//
				//			(0, 0, π * spaceRadius, spaceRadius)
				//
				positionInTangentSpace = float4(
						0.0,
						0.0,
						M_PI_F * spaceRadius,
						spaceRadius);
			}
		}
	}

	if (geometryCode == 'e')
	{
		positionInTangentSpace = positionInBridgeOfNoseSpace;
	}

	if (geometryCode == 'h')
	{
		float coshTheta = max(
							positionInBridgeOfNoseSpace.w * spaceRadiusInv,
							+1.0);
		float theta = acosh(coshTheta);
		float sinhTheta = sinh(theta);
		factor = (sinhTheta > 0.000001 ?
					theta / sinhTheta :	//	typical case
					1.0);				//	near north pole
		positionInTangentSpace = float4(
				factor * positionInBridgeOfNoseSpace.x,
				factor * positionInBridgeOfNoseSpace.y,
				factor * positionInBridgeOfNoseSpace.z,
				spaceRadius);
	}

	return positionInTangentSpace;
}


// MARK: -
// MARK: Euclidean headset-based messages

struct MessageVertexInput
{
	float2	tex [[ attribute(MessageVertexAttributeTexCoords)	]];
};

struct MessageVertexOutput
{
	float4	position	[[ position				  ]];
	float2	texCoords	[[ user(messageTexcoords) ]];
};
struct MessageFragmentInput
{
	float2	texCoords	[[ user(messageTexcoords) ]];
};

vertex MessageVertexOutput messageVertexFunction(
			 MessageVertexInput	in			[[ stage_in								]],
			 ushort				amp_id		[[ amplification_id ]],	//	strange name!
	constant MessageUniforms	& uniforms	[[ buffer(MessageBufferIndexUniforms)	]]
)
{
	MessageVertexOutput	out;
	float4				position;
	
	position = float4(
				(-0.5 + in.tex.x) * uniforms.width,		//	centered horizontally
				    in.tex.y      * uniforms.height,	//	above center vertically
				-0.75,	//	distance from player to message, in meters
				1.0 );
	
	out.position  = uniforms.modelViewProjection[amp_id] * position;
	out.texCoords = in.tex;
	
	return out;
}

fragment half4 messageFragmentFunction(
	MessageFragmentInput	in				[[ stage_in						]],
	constant half			& opacity		[[ buffer(BufferIndexOpacity)	]],
	texture2d<half>			texture			[[ texture(MessageTextureIndex)	]],
	sampler					textureSampler	[[ sampler(MessageSamplerIndex) ]]
)
{
	half4	tmpTexelColor,
			tmpPixelColor;

	tmpTexelColor = texture.sample(textureSampler, in.texCoords.xy);
	
	tmpPixelColor = opacity * tmpTexelColor;	//	pre-multiplied alpha

	return tmpPixelColor;
}

#endif /* GGVR_GPU_Functions_h */
