struct Material
{
  vec4 diffuse, emissive, specular;
  vec4 parameters;
};

struct Light
{
  vec4 direction;
  vec4 color;
};

layout(binding = 0) uniform UniformBufferObject
{
  mat4 projViewMat;
  mat4 viewMat;
  mat4 normMat;
} ubo;

layout(binding = 1, std430) buffer MaterialBuffer
{
  Material materials[];
};

layout(binding = 2, std430) buffer LightBuffer
{
  Light lights[];
};

layout(binding = 3, std430) buffer CountBuffer
{
  uint maxSize;
  uint count[];
};

layout(binding = 4, std430) buffer OffsetBuffer
{
  uint maxDepth;
  uint offset[];
};

layout(binding = 5, std430) buffer FragmentBuffer
{
  vec4 fragment[];
};

layout(binding = 6, std430) buffer DepthBuffer
{
  float depth[];
};

layout(binding = 7, std430) buffer OpaqueBuffer
{
  vec4 opaqueColor[];
};

layout(binding = 8, std430) buffer OpaqueDepthBuffer
{
  float opaqueDepth[];
};

#ifdef GPUCOMPRESS
layout(binding=9, std430) buffer indexBuffer
{
  uint index[];
};
#define INDEX(pixel) index[pixel]
#else
#define INDEX(pixel) pixel
#endif

#ifdef NORMAL
layout(location = 1) in vec3 viewPosition;
layout(location = 2) in vec3 norm;
layout(location = 5) in vec4 diffuse;
layout(location = 6) in vec3 specular;
layout(location = 7) in vec3 params; // roughness, metallic, fresnel0
layout(location = 8) in vec4 emissive;
#endif

// PBR material parameters (used by BRDF functions)
vec3 Diffuse;
vec3 Specular;
float Metallic;
float Fresnel0;
float Roughness2;
float Roughness;

layout(location = 0) in vec3 position;

#ifdef MATERIAL
layout(location = 4) flat in int materialIndex;
#endif

layout(push_constant) uniform PushConstants
{
  uvec4 constants;
  // constants[0] = nlights
  // constants[1] = width;
  vec4 background;
} push;

#ifdef TRANSPARENT
vec4 outColor;
#else
layout(location = 0) out vec4 outColor;
#endif

vec3 Emissive;

vec3 normal;

const float gamma=2.2;
const float invGamma=1.0/gamma;

/**
 * @brief Converts linear color (measuring photon count) to srgb (what our brain thinks
 * is the brightness
 * example linearToPerceptual(vec3(0.5)) is approximately vec3(0.729)
 */
vec3 linearToPerceptual(vec3 inColor)
{
  // an actual 0.5 brightness (half amount of photons) would
  // look brighter than what our eyes think is "half" light
  return pow(inColor, vec3(invGamma));
}

#ifdef USE_IBL

layout(binding=11) uniform sampler2D diffuseSampler;
layout(binding=12) uniform sampler2D reflBRDFSampler;
layout(binding=13) uniform sampler3D reflImgSampler;

const float pi=acos(-1.0);
const float piInv=1.0/pi;
const float twopi=2.0*pi;
const float twopiInv=1.0/twopi;

// (x,y,z) -> (r,theta,phi);
// theta -> [0,pi]: colatitude
// phi -> [-pi,pi]: longitude
vec3 cart2sphere(vec3 cart)
{
  float x=cart.x;
  float y=cart.z;
  float z=cart.y;

  float r=length(cart);
  float theta=r > 0.0 ? acos(z/r) : 0.0;
  float phi=atan(y,x);

  return vec3(r,theta,phi);
}

vec2 normalizedAngle(vec3 cartVec)
{
  vec3 sphericalVec=cart2sphere(cartVec);
  sphericalVec.y=sphericalVec.y*piInv;
  sphericalVec.z=0.75-sphericalVec.z*twopiInv;

  return sphericalVec.zy;
}

vec3 IBLColor(vec3 viewDir)
{
  //
  // based on the split sum formula approximation
  // L(v)=\int_\Omega L(l)f(l,v) \cos \theta_l
  // which, by the split sum approiximation (assuming independence+GGX distrubition),
  // roughly equals (within a margin of error)
  // [\int_\Omega L(l)] * [\int_\Omega f(l,v) \cos \theta_l].
  // the first term is the reflectance irradiance integral

  vec3 IBLDiffuse=Diffuse*texture(diffuseSampler,normalizedAngle(normal)).rgb;
  vec3 reflectVec=normalize(reflect(-viewDir,normal));
  vec2 reflCoord=normalizedAngle(reflectVec);
  vec3 IBLRefl=texture(reflImgSampler,vec3(reflCoord,Roughness)).rgb;
  vec2 IBLbrdf=texture(reflBRDFSampler,vec2(dot(normal,viewDir),Roughness)).rg;
  float specularMultiplier=Fresnel0*IBLbrdf.x+IBLbrdf.y;
  vec3 dielectric=IBLDiffuse+specularMultiplier*IBLRefl;
  vec3 metal=Diffuse*IBLRefl;
  return mix(dielectric,metal,Metallic);
}

#else

float NDF_TRG(vec3 h)
{
  float ndoth=max(dot(normal,h),0.0);
  float alpha2=Roughness2*Roughness2;
  float denom=ndoth*ndoth*(alpha2-1.0)+1.0;
  return denom != 0.0 ? alpha2/(denom*denom) : 0.0;
}

float GGX_Geom(vec3 v)
{
  float ndotv=max(dot(v,normal),0.0);
  float ap=1.0+Roughness2;
  float k=0.125*ap*ap;
  return ndotv/((ndotv*(1.0-k))+k);
}

float Geom(vec3 v, vec3 l)
{
  return GGX_Geom(v) * GGX_Geom(l);
}

float Fresnel(vec3 h, vec3 v, float fresnel0)
{
  float a=1.0-max(dot(h,v),0.0);
  float b=a*a;
  return fresnel0+(1.0-fresnel0)*b*b*a;
}

vec3 BRDF(vec3 viewDirection, vec3 lightDirection)
{
  vec3 lambertian=Diffuse;
  // Cook-Torrance model
  vec3 h=normalize(lightDirection+viewDirection);

  float omegain=max(dot(viewDirection,normal),0.0);
  float omegaln=max(dot(lightDirection,normal),0.0);

  float D=NDF_TRG(h);
  float G=Geom(viewDirection,lightDirection);
  float F=Fresnel(h,viewDirection,Fresnel0);

  float denom=4.0*omegain*omegaln;
  float rawReflectance=denom > 0.0 ? (D*G)/denom : 0.0;

  vec3 dielectric=mix(lambertian,rawReflectance*Specular,F);
  vec3 metal=rawReflectance*Diffuse;

  return mix(dielectric,metal,Metallic);
}

#endif

void main() {

  uint nlights = push.constants[0];

#ifdef NORMAL
  outColor = emissive;

  Diffuse = diffuse.rgb;
  Specular = specular.rgb;
  Roughness = params.x;
  Metallic = params.y;
  Fresnel0 = params.z;
  Roughness2 = Roughness * Roughness;

#ifdef ORTHOGRAPHIC
  vec3 viewDirection=vec3(0.0,0.0,1.0);
#else
  vec3 viewDirection=-normalize(viewPosition);
#endif
  normal = normalize(norm);

  if (!gl_FrontFacing)
      normal = -normal;

#ifdef USE_IBL
  outColor=vec4(IBLColor(viewDirection), outColor.a);
#else
  for (int i = 0; i < nlights; i++)
  {
      Light light = lights[i];

      vec3 radiance = max(dot(normal, light.direction.xyz), 0.0) * light.color.rgb;
      outColor += vec4(BRDF(viewDirection, light.direction.xyz) * radiance, 0.0);
  }

  outColor = vec4(outColor.rgb, diffuse.a);
#endif /*USE_IBL*/
#else
  Material mat = materials[materialIndex];
  outColor = mat.emissive;
#endif /*NORMAL*/

  // for reasons, the swapchain/FXAA shader expects a "perceptual" color,
  // while all of our calculations have been linear (i.e. by measuring photon counts)
  // (e.g. our 0.5 is much much brighter than what swap chain/monitor thinks 0.5 is)
  // need to give the output image the color our brain perceives with the same photon count
  // as the original pixel
  vec4 linearColor=outColor;

  // if FXAA is enabled, convert it to perceptual since FXAA needs it
  // otherwise, if OUTPUT_AS_SRGB is enabled, also convert it to perceptual
#if defined(ENABLE_FXAA) || defined(OUTPUT_AS_SRGB)
  // outColor is our output vector, so save what we have as linear color
  vec3 outColorInPerceptualSpace=linearToPerceptual(linearColor.rgb);
  outColor=vec4(outColorInPerceptualSpace,linearColor.a);
#else
  outColor=linearColor;
#endif

#ifndef WIDTH
#if defined(TRANSPARENT) || (!defined(HAVE_INTERLOCK) && !defined(OPAQUE))
  uint pixel=uint(gl_FragCoord.y)*push.constants[1]+uint(gl_FragCoord.x);
  uint element=INDEX(pixel);
  uint listIndex=atomicAdd(offset[element],-1u)-1u;
  fragment[listIndex]=linearColor;
  depth[listIndex]=gl_FragCoord.z;
#ifndef WIREFRAME
  discard;
#endif /*WIREFRAME*/
#else
#if defined(HAVE_INTERLOCK) && !defined(OPAQUE)
  uint pixel=uint(gl_FragCoord.y)*push.constants[1]+uint(gl_FragCoord.x);
  beginInvocationInterlockARB();
  if(opaqueDepth[pixel] == 0.0 || gl_FragCoord.z < opaqueDepth[pixel])
    {
    opaqueDepth[pixel]=gl_FragCoord.z;
    opaqueColor[pixel]=linearColor;
  }
  endInvocationInterlockARB();
#endif
#endif
#endif
}
