+
 #ifdef GL_ES
   #extension GL_EXT_shader_texture_lod : require
   #extension GL_OES_standard_derivatives : require

    ao *= texture2D(uAO, vUV).r;
#endif

    //TODO: No kd? so not really energy conserving
    //we could use disney brdf for irradiance map to compensate for that like in Frostbite
#ifdef USE_NORMALS
#ifdef USE_REFLECTION_PROBES
    float NdotV = saturate( dot( normalWorld, eyeDirWorld ) );
    vec3 reflectance = EnvBRDFApprox( F0, roughness, NdotV );
    vec3 irradianceColor = getIrradiance(eyeDirWorld, normalWorld);
    vec3 reflectionColor = getPrefilteredReflection(eyeDirWorld, normalWorld, roughness);
    vec3 kS = F0;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metallic;
    indirectDiffuse = kD * baseColor * irradianceColor;
    indirectSpecular = reflectionColor * reflectance;
#endif

    //lights
#if NUM_DIRECTIONAL_LIGHTS > 0
    for(int i=0; i<NUM_DIRECTIONAL_LIGHTS; i++) {
        DirectionalLight light = uDirectionalLights[i];

        //shadows
        vec4 lightViewPosition = light.viewMatrix * vec4(vPositionWorld, 1.0);
        float lightDistView = -lightViewPosition.z;
        vec4 lightDeviceCoordsPosition = light.projectionMatrix * lightViewPosition;
        vec2 lightDeviceCoordsPositionNormalized = lightDeviceCoordsPosition.xy / lightDeviceCoordsPosition.w;
        float lightDeviceCoordsZ = lightDeviceCoordsPosition.z / lightDeviceCoordsPosition.w;
        vec2 lightUV = lightDeviceCoordsPositionNormalized.xy * 0.5 + 0.5;

        float illuminated = 0.0;
        if (i == 0) illuminated += directionalShadow(uDirectionalLightShadowMaps[0], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#if NUM_DIRECTIONAL_LIGHTS >= 2
        if (i == 1) illuminated += directionalShadow(uDirectionalLightShadowMaps[1], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 3
        if (i == 2) illuminated += directionalShadow(uDirectionalLightShadowMaps[2], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 4
        if (i == 3) illuminated += directionalShadow(uDirectionalLightShadowMaps[3], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 5
        if (i == 4) illuminated += directionalShadow(uDirectionalLightShadowMaps[4], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 6
        if (i == 5) illuminated += directionalShadow(uDirectionalLightShadowMaps[5], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 7
        if (i == 6) illuminated += directionalShadow(uDirectionalLightShadowMaps[6], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif
#if NUM_DIRECTIONAL_LIGHTS >= 8
        if (i == 7) illuminated += directionalShadow(uDirectionalLightShadowMaps[7], light.shadowMapSize, lightUV, lightDistView - light.bias, light.near, light.far);
#endif

        if (illuminated > 0.0) {
            vec3 L = normalize(-light.direction);
            vec3 N = normalWorld;
            vec3 H = normalize(V + L);
            float NdotL = max(0.0, dot(N, L));
            float HdotV = max(0.0, dot(H, V));
            float NdotV = max(0.0, dot(N, V));

            vec3 F = FresnelSchlick(HdotV, F0);

            vec3 kS = F;
            vec3 kD = vec3(1.0) - kS;

            kD *= 1.0 - metallic;

            float NDF = GGX(N, H, roughness * roughness); // TODO: roughtness^2 or not?

            float G = GeometrySmith(N, V, L, roughness * roughness); // TODO: roughtness^2 or not?

            vec3 nominator = NDF * G * F;
            float denominator = 4.0 * NdotV * NdotL + 0.001;
            vec3 brdf = nominator / denominator;

            vec3 lightColor = decode(light.color, 3).rgb;
            lightColor *= light.color.a; // intensity
            vec3 finalLight = NdotL * lightColor * illuminated;

            directDiffuse += kD * baseColor / PI * finalLight;
            directSpecular += brdf * finalLight;
        }
    }
#endif

#if NUM_POINT_LIGHTS > 0
    for(int i=0; i<NUM_POINT_LIGHTS; i++) {
        PointLight light = uPointLights[i];

        vec3 L = light.position - vPositionWorld;
        float dist = length(L);
        L /= dist;

        float dotNL = max(0.0, dot(normalWorld, L));

        // Based on UE4
        float distanceRatio = clamp(1.0 - pow(dist/light.radius, 4.0), 0.0, 1.0);
        float falloff = (distanceRatio * distanceRatio) / (dist * dist + 1.0);

        vec3 lightColor = decode(light.color, 3).rgb;
        lightColor *= light.color.a;
        //TODO: specular light conservation
        // directDiffuse += baseColor * dotNL * lightColor * falloff;
        // directSpecular += directSpecularGGX(normalWorld, eyeDirWorld, L, roughness, F0) * light.color.rgb * falloff;

        vec3 N = normalWorld;
        vec3 H = normalize(V + L);
        float NdotL = max(0.0, dot(N, L));
        float HdotV = max(0.0, dot(H, V));
        float NdotV = max(0.0, dot(N, V));

        vec3 F = FresnelSchlick(HdotV, F0);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;

        kD *= 1.0 - metallic;

        float NDF = GGX(N, H, roughness);

        float G = GeometrySmith(N, V, L, roughness * roughness);

        vec3 nominator = NDF * G * F;
        float denominator = 4.0 * NdotV * NdotL + 0.001;
        vec3 brdf = nominator / denominator;

        vec3 radiance = lightColor * falloff;

        directDiffuse += kD * baseColor / PI * NdotL * radiance;
        directSpecular += brdf * NdotL * radiance;
    }
#endif

#if NUM_SPOT_LIGHTS > 0
    for(int i=0; i<NUM_SPOT_LIGHTS; i++) {
        SpotLight light = uSpotLights[i];

        const float spotlightLinearAtt = 1.0;
        float dist = distance(vPositionWorld, light.position);
        vec3 vDir = (vPositionWorld - light.position) / dist;
        float distAttenuation = 1.0/(dist * spotlightLinearAtt);

        float fCosine = dot(light.direction, vDir);
        float cutOff = cos(light.angle);

        float fDif = 1.0 - cutOff;
        float fFactor = clamp((fCosine - cutOff)/fDif, 0.0, 1.0);

        fFactor = pow(fFactor, 2.0);

        if (fCosine > cutOff) {
          vec3 L = normalize(-light.direction);

          float intensity = max(dot(normalWorld, L), 0.0);
          //vec4 spec = vec4(0.0);
          //if (intensity > 0.0) {
          //  vec3 eye = vec3(0.0, 0.0, 1.0);
          //  vec3 h = normalize(ecLightDir + eye);
          //  float intSpec = max(dot(h, ecN), 0.0);
          //  spec = vec4(1.0) * pow(intSpec, 128.0);
          //}

          const float distAttenuation = 1.0;
          //return fFactor * distAttenuation * intensity * spotlightColor + spec;
          directDiffuse += baseColor * fFactor * distAttenuation * intensity * light.color.rgb;

        }

        /*
        // Based on UE4
        float distanceRatio = clamp(1.0 - pow(dist/light.radius, 4.0), 0.0, 1.0);
        float falloff = (distanceRatio * distanceRatio) / (dist * dist + 1.0);

        vec3 lightColor = decode(light.color, 3).rgb;
        lightColor *= light.color.a;
        //TODO: specular light conservation
        // directDiffuse += baseColor * dotNL * lightColor * falloff;
        // directSpecular += directSpecularGGX(normalWorld, eyeDirWorld, L, roughness, F0) * light.color.rgb * falloff;

        vec3 N = normalWorld;
        vec3 H = normalize(V + L);
        float NdotL = max(0.0, dot(N, L));
        float HdotV = max(0.0, dot(H, V));
        float NdotV = max(0.0, dot(N, V));

        vec3 F = FresnelSchlick(HdotV, F0);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;

        kD *= 1.0 - metallic;

        float NDF = GGX(N, H, roughness);

        float G = GeometrySmith(N, V, L, roughness);

        vec3 nominator = NDF * G * F;
        float denominator = 4.0 * NdotV * NdotL + 0.001;
        vec3 brdf = nominator / denominator;

        vec3 radiance = lightColor * falloff;

        directDiffuse += kD * baseColor / PI * NdotL * radiance;
        directSpecular += brdf * NdotL * radiance;
        */
    }
#endif

    vec3 indirectArea = vec3(0.0);

#if NUM_AREA_LIGHTS > 0
    for(int i=0; i<NUM_AREA_LIGHTS; i++) {
        AreaLight light = uAreaLights[i];

        // TODO: kD, kS
        //if (length(emissiveColor) == 0.0) {
            indirectArea += evalAreaLight(light, vPositionWorld, normalWorld, baseColor, specularColor, roughness); //TEMP: fix roughness
            //indirectArea = evalAreaLight(light, vPositionWorld, normalWorld,roughness); //TEMP: fix roughness
            //indirectArea = evalAreaLight(light, vPositionView, (uNormalMatrix*normalWorld).xyz, diffuseColor, specularColor, roughness); //TEMP: fix roughness
        //}
    }
#endif
    vec3 color = emissiveColor + ao * indirectDiffuse + ao * indirectSpecular + directDiffuse + directSpecular + indirectArea;
    // color = vec3(metallic);
#else  // USE_NORMALS
    vec3 color = ao * baseColor;
#endif // USE_NORMALS
    gl_FragData[0] = encode(vec4(color, 1.0), uOutputEncoding);
    gl_FragData[1] = encode(vec4(emissiveColor, 1.0), uOutputEncoding);
}

}

vec3 EnvBRDFApprox( vec3 SpecularColor, float Roughness, float NoV ) {
  const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022 );
  const 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;
  vec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw;
  return SpecularColor * AB.x + AB.y;
}

vec3 getPrefilteredReflection(vec3 eyeDirWorld, vec3 normalWorld, float roughness) {
  float maxMipMapLevel = 5.0; //TODO: const
  vec3 reflectionWorld = reflect(-eyeDirWorld, normalWorld);
  //float lod = pow(roughness, 2.0) * maxMipMapLevel; //TODO: verify reflection probe blurring code
  float lod = pow(roughness, 1.5) * maxMipMapLevel;
  float upLod = floor(lod);
  float downLod = ceil(lod);
  vec3 a = decode(texture2D(uReflectionMap, envMapOctahedral(reflectionWorld, 0.0, upLod)), uReflectionMapEncoding).rgb;
  vec3 b = decode(texture2D(uReflectionMap, envMapOctahedral(reflectionWorld, 0.0, downLod)), uReflectionMapEncoding).rgb;
  return mix(a, b, lod - upLod);
}
    
void EvaluateLightProbe(inout PBRData data) {
  float NdotV = clamp( dot( data.normalWorld, data.eyeDirWorld ), 0.0, 1.0);
  vec3 reflectance = EnvBRDFApprox( data.specularColor, data.roughness, NdotV ); //TODO: roughness or alphaRoughness
  //No need to multiply by PI like three.jss as I'm already multiplying by PI in my convolution code
  vec3 irradianceColor = getIrradiance(data.eyeDirWorld, data.normalWorld);
  vec3 reflectionColor = getPrefilteredReflection(data.eyeDirWorld, data.normalWorld, data.roughness);  //TODO: roughness or alphaRoughness
  data.indirectDiffuse += data.diffuseColor * irradianceColor;
  data.indirectSpecular += reflectionColor * reflectance;
}



#ifdef USE_NORMAL_MAP
uniform sampler2D uNormalMap;
    
//http://www.thetenthplanet.de/archives/1180
mat3 cotangentFrame(vec3 N, vec3 p, vec2 uv) {
  // get edge vectors of the pixel triangle
  vec3 dp1 = dFdx(p);
  vec3 dp2 = dFdy(p);
  vec2 duv1 = dFdx(uv);
  vec2 duv2 = dFdy(uv);

  // solve the linear system
  vec3 dp2perp = cross(dp2, N);
  vec3 dp1perp = cross(N, dp1);
  vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
  vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;

  // construct a scale-invariant frame 
  float invmax = 1.0 / sqrt(max(dot(T,T), dot(B,B)));
  return mat3(T * invmax, B * invmax, N);
}

vec3 perturb(vec3 map, vec3 N, vec3 V, vec2 texcoord) {
  mat3 TBN = cotangentFrame(N, -V, texcoord);
  return normalize(TBN * map);
}

void getNormal(inout PBRData data) {
  vec3 normalRGB = texture2D(uNormalMap, data.texCoord0).rgb;
  vec3 normalMap = normalRGB * 2.0 - 1.0;

  // normalMap.y *= -1.0;
  /*normalMap.x *= -1.0;*/
  // vec3 dFdxPos = dFdx( vPositionView );
  // vec3 dFdyPos = dFdy( vPositionView );

  vec3 N = normalize(data.normalView);
  // N = normalize( cross(dFdxPos,dFdyPos ));
  vec3 V = normalize(data.eyeDirView);

  vec3 normalView = perturb(normalMap, N, V, data.texCoord0);
  vec3 normalWorld = vec3(data.inverseViewMatrix * vec4(normalView, 0.0));

  data.normalWorld = normalWorld;
}
#endif

// FIXME: why i can't do #elseif
/*
#ifdef USE_DISPLACEMENT_MAP
uniform sampler2D uDisplacementMap;
uniform float uDisplacement;
uniform float uDisplacementNormalScale;
vec3 getNormal() {
  float scale = uDisplacement * uDisplacementNormalScale;
  float h = scale * texture2D(uDisplacementMap, vTexCoord0).r;
  float hx = scale * texture2D(uDisplacementMap, vTexCoord0 + vec2(1.0 / 2048.0, 0.0)).r;
  float hz = scale * texture2D(uDisplacementMap, vTexCoord0 + vec2(0.0, 1.0 / 2048.0)).r;
  float meshSize = 20.0;
  vec3 a = vec3(0.0, h, 0.0);
  vec3 b = vec3(1.0 / 2048.0 * meshSize, hx, 0.0);
  vec3 c = vec3(0.0, hz, 1.0 / 2048.0 * meshSize);
  vec3 N = normalize(cross(normalize(c - a), normalize(b - a)));
  // FIXME: this is model space normal, need to multiply by modelWorld
  // N = mat3(uModelMatrix) * N;
  return N;
}
#else
*/
#ifndef USE_NORMAL_MAP
void getNormal(inout PBRData data) {
  // NOP
}
#endif   
//#endif
//end of USE_DISPLACEMENT_MAP




#ifdef USE_EMISSIVE_COLOR_MAP
    uniform vec4 uEmissiveColor; // TODO: gltf assumes sRGB color, not linear
    uniform sampler2D uEmissiveColorMap; //assumes sRGB color, not linear
    void getEmissiveColor(inout PBRData data) {
      data.emissiveColor = decode(uEmissiveColor, 3).rgb * decode(texture2D(uEmissiveColorMap, vTexCoord0), 3).rgb;
    }
#else
    uniform vec4 uEmissiveColor; //assumes sRGB color, not linear
    void getEmissiveColor(inout PBRData data) {
      data.emissiveColor = decode(uEmissiveColor, 3).rgb;
    }
#endif


#ifdef USE_OCCLUSION_MAP
uniform sampler2D uOcclusionMap;
#endif    
#endif
    

#ifdef USE_BASE_COLOR_MAP
uniform vec4 uBaseColor; // TODO: gltf assumes sRGB color, not linear
uniform sampler2D uBaseColorMap; //assumes sRGB color, not linear
void getBaseColor (inout PBRData data) {
  vec4 texelColor = texture2D(uBaseColorMap, data.texCoord0);
  
  data.baseColor = decode(uBaseColor, 3).rgb * decode(texelColor, 3).rgb;
  data.opacity = uBaseColor.a * texelColor.a;
}
#else
uniform vec4 uBaseColor; // TODO: gltf assumes sRGB color, not linear
void getBaseColor (inout PBRData data) {
  data.baseColor = decode(uBaseColor, 3).rgb;
  data.opacity = uBaseColor.a;
}
#endif

    
#ifdef USE_METALLIC_ROUGHNESS_WORKFLOW


#ifdef USE_METALLIC_ROUGHNESS_MAP
  // R = ?, G = roughness, B = metallic
  uniform sampler2D uMetallicRoughnessMap;
  // TODO: sampling the same texture twice
  void getMetallic(inout PBRData data) {
    vec4 texelColor = texture2D(uMetallicRoughnessMap, vTexCoord0);
    data.metallic = texelColor.b;
    data.roughness = texelColor.g;
  }
  void getRoughness(inout PBRData data) {
    //NOP, already read in getMetallic
  }
    
#else
  #ifdef USE_METALLIC_MAP
    uniform sampler2D uMetallicMap; //assumes linear, TODO: check gltf
    uniform float uMetallic;
    void getMetallic(inout PBRData data) {
      data.metallic = uMetallic * texture2D(uMetallicMap, vTexCoord0).r;
    }
  #else
    uniform float uMetallic;
    void getMetallic(inout PBRData data) {
      data.metallic = uMetallic;
    }
  #endif

  #ifdef USE_ROUGHNESS_MAP
    uniform float uRoughness;
    uniform sampler2D uRoughnessMap; //assumes linear, TODO: check glTF
    void getRoughness(inout PBRData data) {
      data.roughness = uRoughness * texture2D(uRoughnessMap, vTexCoord0).r + 0.01;
    }
  #else
    uniform float uRoughness;
    void getRoughness(inout PBRData data) {
      data.roughness = uRoughness + 0.01;
    }
  #endif
#endif
//end of USE_METALLIC_ROUGHNESS_MAP

#endif
    
#ifdef USE_SPECULAR_GLOSSINESS_WORKFLOW

#ifdef USE_SPECULAR_GLOSSINESS_WORKFLOW
uniform vec4 uDiffuse;
uniform vec3 uSpecular;
uniform float uGlossiness;

#ifdef USE_DIFFUSE_MAP
    uniform sampler2D uDiffuseMap;
    uniform float uDiffuseMapEncoding;
    vec4 getDiffuse() {
      // assumes sRGB texture
      vec4 texelColor = texture2D(uDiffuseMap, vTexCoord0);
      return vec4(decode(uDiffuse, 3).rgb, uDiffuse.a) * vec4(decode(texelColor, 3).rgb, texelColor.a);
    }
#else
    vec4 getDiffuse() {
      return vec4(decode(uDiffuse, 3).rgb, uDiffuse.a);
    }
//USE_DIFFUSE_MAP
#endif

#ifdef USE_SPECULAR_GLOSSINESS_MAP
    uniform sampler2D uSpecularGlossinessMap;
    vec4 getSpecularGlossiness() {
      // assumes specular is sRGB and glossiness is linear
      vec4 specGloss = texture2D(uSpecularGlossinessMap, vTexCoord0);
      //TODO: should i move uSpecular to linear?
      return vec4(uSpecular, uGlossiness) * vec4(decode(vec4(specGloss.rgb, 1.0), 3).rgb, specGloss.a);
    }
#else
    vec4 getSpecularGlossiness() {
      return vec4(uSpecular, uGlossiness);
    }
//USE_SPECULAR_GLOSSINES_MAP
#endif

// assumes linear color
