//Low 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 vec3 fsDirectionVS;
in vec3 fsNormalVS;
in vec3 fsNormalWS;
in vec3 fsSunDirectionVS;
in vec3 fsPositionEye;
in mat4 fsInvView;

out vec4 finalFragColor;

// sAlbedoTexture is a 2D texture sampler for diffuse texture.
uniform sampler2D sColorTableTexture;
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 samplerCube sEnvironmentCubeTexture;

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

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

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

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

void main(void)
{
	// increases texture sharpness. this is a bit more naive than using anisotropic filtering.
	float lod_bias = -1.0;
	vec4 fsColor = 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);

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

	// move to vs
	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);

	// disable this when reflections work...
	//vec3 ambient = vec3(0.125, 0.125, 0.165);

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

	vec3 light_dir = fsSunDirectionVS;
	float ndl = max(0.0, dot(view_nor, light_dir));

	float shadowFactor = smoothstep(0.1, 0.5, ndl);
	shadowFactor = shadowFactor * 0.5 + 0.5;
	//float shadowFactor = 1.0;
	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;

	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 *= fsColor.a;

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