//Turbo Boom! High Detail Fragment Shader
#version 150

// This came from some tutorial, mentioning some drivers required this to function
//   properly but did not delve into why.
precision highp float;

in vec4 fsColor;
in vec2 fsTextureUV;
in float fsDepth;
in vec4 shadowCoordinates[4];

out vec4 finalFragColor;

in vec3 fsDirectionVS;
in vec3 fsNormalWS;
in vec3 fsNormalVS;
in vec3 fsSunDirectionVS;
in vec3 fsPositionEye;
in vec4 fsPositionWS;
//in vec3 fsNormalEye;
in mat4 fsInvView;

// sAlbedoTexture is a 2D texture sampler for diffuse texture.
uniform sampler2D sAlbedoTexture;
uniform vec4 uAmbientLightColor = vec4(1.0, 1.0, 1.0, 1.0);
uniform vec4 uSunLightColor = vec4(1.0, 1.0, 1.0, 1.0);
uniform vec4 uSunLightDirection = vec4(-0.447766751, 0.373415232, 0.812444329, 0.422158539);  //Expected to be a unit vector and in world space already.
uniform vec4 uMaterialColor = vec4(1.0, 1.0, 1.0, 1.0);
uniform mat4 uWorldToView;
uniform int uFrameCounterBeat = 0;
uniform float uFarPlane = 15000.0;
uniform float uNearPlane = 0.1;
uniform vec3 uPlayerPositionWS;
uniform sampler2D sColorTableTexture;

uniform bool uIsHighDynamicRange;
uniform bool uIsAmbientOcclusionEnabled;
uniform vec4 uCameraWhiteAndExposure;

uniform bool uUseOcclusionFade = false;

uniform samplerCube sEnvironmentCubeTexture;

//Uniforms related to material:
uniform float uMetalness = 0.25;
uniform float uRoughness = 0.5;
uniform sampler2D sMetalnessTexture;
uniform sampler2D sRoughnessTexture;

//These uniforms control the bias of each cascaded map.
uniform int uShadowPoissonSamples = 8;
uniform sampler2DShadow sShadowTexture;
uniform vec4 uShadowPoissonScales = vec4(1500.0, 2000.0, 2250.0, 2500.0);
uniform vec4 uShadowBias = vec4(0.0, 0.0, 0.0001, 0.0002);

const vec2 poissonDisk[16] = vec2[](
	vec2( -0.94201624, -0.39906216 ),
	vec2( 0.94558609, -0.76890725 ),
	vec2( -0.094184101, -0.92938870 ),
	vec2( 0.34495938, 0.29387760 ),
	vec2( -0.91588581, 0.45771432 ),
	vec2( -0.81544232, -0.87912464 ),
	vec2( -0.38277543, 0.27676845 ),
	vec2( 0.97484398, 0.75648379 ),
	vec2( 0.44323325, -0.97511554 ),
	vec2( 0.53742981, -0.47373420 ),
	vec2( -0.26496911, -0.41893023 ),
	vec2( 0.79197514, 0.19090188 ),
	vec2( -0.24188840, 0.99706507 ),
	vec2( -0.81409955, 0.91437590 ),
	vec2( 0.19984126, 0.78641367 ),
	vec2( 0.14383161, -0.14100790 )
);

// 2025-01-07: Directly from iceShaderApi.
vec3 ToGamma(vec3 value) { return pow(value, vec3(1.0/2.2)); }
vec4 ToGamma(vec4 value) { return vec4(pow(value.rgb, vec3(1.0/2.2)), value.a); }

vec3 tonemap_aces(vec3 x) {
	float a = 2.51;
	float b = 0.03;
	float c = 2.43;
	float d = 0.59;
	float e = 0.14;
	return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}

float schlick_ior_fresnel(float ior, float LdotH)
{
	float f0 = pow((ior-1.0)/(ior+1.0), 2.0);
	float x = clamp(1.0-LdotH, 0.0, 1.0);
	float x2 = x*x;
	return f0 + (1.0 - f0) * (x2*x2*x);
}

float schlick_ggx_gsf(float ndl, float ndv, float roughness) {
	float k = roughness / 2.0;
	float sl = (ndl) / (ndl * (1.0 - k) + k);
	float sv = (ndv) / (ndv * (1.0 - k) + k);
	return sl * sv;
}

const float pi = 3.1415926535;

float trowbridge_reitz_ndf(float ndh, float roughness) {
	float r2 = roughness*roughness;
	float d2 = ndh*ndh * (r2-1.0) + 1.0;
	d2 *= d2;
	return r2 / (pi * d2);
}

float to_linear(float v) { return pow(v, 2.2); }
float to_gamma(float v) { return pow(v, 1.0/2.2); }
vec3 to_linear(vec3 v) { return pow(v, vec3(2.2)); }
vec3 to_gamma(vec3 v) { return pow(v, vec3(1.0/2.2)); }
vec4 to_linear(vec4 v) { return vec4(pow(v.rgb, vec3(2.2)), v.a); }
vec4 to_gamma(vec4 v) { return vec4(pow(v.rgb, vec3(1.0/2.2)), v.a); }

//
//
//
float sample_csm_viewports()
{
	float shadowFactor = 1.0;
	float margin = 0.005; //Can be made accurate to 1 texel
	float inverseuShadowPoissonSamples = 1.0 / float(uShadowPoissonSamples);

	for (int i = 0; i < 4; ++i)
	{
		vec4 biasedShadowCoordinate = shadowCoordinates[i];
		biasedShadowCoordinate.z -= uShadowBias[i];

		vec2 texCoord = biasedShadowCoordinate.xy / biasedShadowCoordinate.w;
		bool outside = any(greaterThan(texCoord.xy, vec2(1.0 - margin))) || any(lessThan(texCoord.xy, vec2(margin)));
		if (false == outside)
		{
			//TODO: Send the crop matrices as uniforms.
			float x = float(i % 2);
			float y = float(i / 2);
			mat4 crop = mat4(
				0.5, 0.0, 0.0, x * 0.5,
				0.0, 0.5, 0.0, y * 0.5,
				0.0, 0.0, 1.0, 0.0,
				0.0, 0.0, 0.0, 1.0
			);

			shadowFactor = 0.0;
			for (int j = 0; j < uShadowPoissonSamples; ++j)
			{
				vec4 poissonOffset = vec4(poissonDisk[j], 0.0, 0.0) / uShadowPoissonScales[i];
				vec4 shadowCoords = (biasedShadowCoordinate * crop) + poissonOffset;
				shadowFactor += inverseuShadowPoissonSamples * textureProj(sShadowTexture, shadowCoords);
			}

			break;
		}
	}

	return shadowFactor;
}

// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
vec2 prefilteredBRDF(float NoV, float roughness) {
	vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
	vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
	vec4 r = roughness * c0 + c1;
	float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
	return vec2(-1.04, 1.04) * a004 + r.zw;
}

float dither17(vec2 pos, float index_mod_4) {
	return fract(dot(vec3(pos.xy, index_mod_4), vec3(2.0, 7.0, 23.0) / 17.0));
}

vec4 sampleLUT(sampler2D tex, vec3 coord) {
	float size = float(textureSize(tex, 0).y);
	float slice_size = 1.0 / size;
	float slice_pixel_size = slice_size / size;
	float width = size - 1.0;
	float z0 = floor(coord.z * width);
	float z1 = min(z0 + 1.0, width);
	float x_offset = slice_pixel_size * 0.5 + coord.x * (slice_pixel_size * width);
	float y_range = (coord.y * width + 0.5) / size;
	float s0 = z0 * slice_size + x_offset;
	float s1 = z1 * slice_size + x_offset;
	float adjust_range = slice_size * slice_size * 0.5;
	vec4 s0_color = textureLod(tex, vec2(s0, y_range), 0.0);
	vec4 c1_color = textureLod(tex, vec2(s1, y_range), 0.0);
	float z_offset = mod(coord.z * width, 1.0);
	vec2 cl = vec2(0.0, 1.0); // swizzling is free
	return clamp(mix(s0_color, c1_color, z_offset) * (1.0 + adjust_range) - adjust_range, cl.xxxx, cl.yyyy);
}

void main() {
	// increases texture sharpness. this is a bit more naive than using anisotropic filtering.
	float lod_bias = -1.0;

	float shadowFactor = sample_csm_viewports();

	vec4 texturedFragmentColor = fsColor * uMaterialColor * texture(sAlbedoTexture, fsTextureUV, lod_bias);

	vec3 view_dir = normalize(fsDirectionVS);
	vec3 view_nor = normalize(fsNormalVS);
	float ndv = max(0.05, dot(view_nor, view_dir));

	float metalness = to_linear(texture(sMetalnessTexture, fsTextureUV, lod_bias).g) * uMetalness;
	float fresnel = schlick_ior_fresnel(1.45 + 2.0*metalness, ndv);

	float roughness = to_linear(texture(sRoughnessTexture, fsTextureUV, lod_bias).r) * uRoughness * 0.95;	//.95 inteded to prevent numerical problems.
	vec2 size = textureSize(sEnvironmentCubeTexture, 0);
	float mip_max = pow(2.5, floor(log2(min(size.x,size.y))));
	float mip_bias = 0.5;
	mip_max -= mip_bias;

	roughness = mix(roughness, 1.0, 0.35);
	// metalness = mix(metalness, 1.0, 1.0);

	// at glancing angles, roughness tends toward 0
	float mip_roughness = mix(0.05, roughness, ndv);
	float mip_irr = mip_max;
	float mip = mip_bias + mip_max * pow(mip_roughness, 2.0);

	// view space normal
	vec3 mirror_dir = reflect(-view_dir, view_nor);
	mirror_dir = vec3(fsInvView * vec4(mirror_dir, 0.0));

	float refl = mix(0.25, 0.5, metalness);
	vec3 radiance = refl * 1.25 * to_gamma(textureLod(sEnvironmentCubeTexture, mirror_dir, mip).rgb);
	vec3 irradiance = refl * 2.0 * to_gamma(textureLod(sEnvironmentCubeTexture, normalize(fsNormalWS), mip_irr).rgb);

	vec3 albedo = vec3(texturedFragmentColor);// * vec3(uSunLightColor);

	vec3 light_dir = fsSunDirectionVS;
	float ndl = max(0.0, dot(view_nor, light_dir));
	shadowFactor *= smoothstep(0.1, 0.5, ndl);
	shadowFactor = shadowFactor * 0.5 + 0.5;

	// bias toward metallic specular, makes things a bit more colorful.
	float tint = mix(0.5, 1.0 + shadowFactor * 0.75, metalness);
	vec3 f0 = mix(vec3(0.04), albedo, tint);
	vec2 lookup = prefilteredBRDF(ndv, roughness);
	vec3 specular_base = (f0 + lookup.r + lookup.g) * mix(radiance, radiance * albedo, tint); // over color

	//vec3 diffuse = albedo;
	//shakesoda proclaims this as a hack to make things colorful again.
	vec3 diffuse = mix(albedo * (albedo * (1.0 - metalness)), albedo, metalness);
	diffuse *= pow(ndl, 1.0-roughness);
	diffuse *= 1.0 - metalness * 0.95;

	vec3 halfv = normalize(view_dir + light_dir);
	float ndh = max(0.0, dot(view_nor, halfv));

	vec3 specular = specular_base * 1.0 + metalness * 0.25;
	specular *= trowbridge_reitz_ndf(ndh, roughness);

	float light_power = 1.5;
	vec3 direct = light_power * uSunLightColor.rgb * (diffuse + specular);
	direct *= shadowFactor;
	irradiance *= mix(shadowFactor * 0.5 + 0.5, shadowFactor, metalness);

	vec3 env_diffuse = albedo * irradiance * (1.0 - fresnel);
	vec3 env_reflect = fresnel * mix(radiance, specular_base, ndv) * (shadowFactor * 0.25 + 0.75); // how much shadow to take out of reflections
	vec3 indirect = env_diffuse + env_reflect;
	vec3 finalColor = max(direct + indirect, vec3(0.0));

	// vec3 white = tonemap_aces(vec3(1000.0));
	// vec3 white_point = vec3(4.25, 4.4, 3.5);
	// float exposure = 1.5;
	// finalColor *= exp2(exposure);
	// finalColor = tonemap_aces(finalColor / white_point) * white;

	if (true == uUseOcclusionFade) {
		float dither = dither17(gl_FragCoord.xy, uFrameCounterBeat);

		float wdist = min(1.0, distance(fsPositionWS.xyz, uPlayerPositionWS.xyz) / 5.0);
		wdist = smoothstep(0.125, 1.0, wdist);
		float ydiff = min(max(0.0, uPlayerPositionWS.y - (fsPositionWS.y - 2.5)) / 2.0, 1.0);
		ydiff = smoothstep(0.125, 1.0, ydiff);
		vec3 player_pos = (vec4(uPlayerPositionWS, 1.0) * uWorldToView).xyz;
		float xdiff = min(abs(fsPositionEye.x - player_pos.x) / 10.0, 1.0);
		float zdiff = min(1.0, max(0.0, fsPositionEye.z - (player_pos.z - 2.5)) / 6.0);
		zdiff = 1.0 - smoothstep(0.125, 1.0, zdiff);
		zdiff = max(zdiff, max(ydiff, wdist));
		// zdiff = max(ydiff, wdist);

		float near_fade = pow(smoothstep(0.0, 1.0, zdiff), 2.0);
		near_fade = min(1.0, near_fade + near_fade * dither);
		texturedFragmentColor.a *= near_fade;

		// if you use msaa + A2C, you don't need this.
		if (near_fade < 0.5) {
			discard;
		}
	}

		// 2025-01-07: This is what WAS happening today before trying to add the MakeFinalOutputColor() below.
		// float exposure = 1.5;
		// finalColor *= exp2(exposure);
		// finalFragColor = vec4(finalColor, 1.0);
		// //finalFragColor.rgb = sampleLUT(sColorTableTexture, finalFragColor.rgb).rgb;
		// finalFragColor *= texturedFragmentColor.a;
		
	//float gamma = 2.2;
	//finalFragColor = vec4(pow(finalColor, vec3(1.0 / gamma, 1.0 / gamma, 1.0 / gamma)), 1.0);
	//finalFragColor.rgb = sampleLUT(sColorTableTexture, finalFragColor.rgb).rgb;
	//finalFragColor *= texturedFragmentColor.a;


	//Basically taken from ICE SHADER API::MakeFinalOutputColor()
	float alpha = texturedFragmentColor.a;
	vec3 color = finalColor;
	vec4 outputColor = vec4(color, 1.0);

	float exposure = uCameraWhiteAndExposure.a;
	color *= exp2(exposure);

	if (false == uIsHighDynamicRange)
	{
		vec3 white = tonemap_aces(vec3(1000.0));
		vec3 whitePoint = uCameraWhiteAndExposure.rgb;
		color = tonemap_aces(color / whitePoint) * white;

		outputColor = ToGamma(vec4(color, 1.0));
	}
	else
	{
		outputColor = vec4(color, 1.0);
	}

	outputColor *= alpha;
	finalFragColor = outputColor;
	////// End of MakeFinalOutputColor

	if (finalFragColor.a < 0.25)
	{
		discard;
	}
}
