     return a2 / denom;
 }
 
-float GeometrySchlickGGX(float NdotV, float roughness) {
+float GeometrySchlickGGX(float NdotV, float NdotL, float roughness) {
     const float r = (roughness + 1.0);
     const float k = (r * r) / 8.0;
 
-    const float denom = NdotV * (1.0 - k) + k;
-
-    return NdotV / denom;
-}
-
-float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
-    const float NdotV = max(dot(N, V), 0.0);
-    const float NdotL = max(dot(N, L), 0.0);
-    const float ggx2 = GeometrySchlickGGX(NdotV, roughness);
-    const float ggx1 = GeometrySchlickGGX(NdotL, roughness);
-
-    return ggx1 * ggx2;
+    const float GL = NdotL / (NdotL * (1.0 - k) + k);
+    const float GV = NdotV / (NdotV * (1.0 - k) + k);
+
+    return GL * GV;
 }
 
 //////////////////////////////////////////////////////////////////////////////

    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

float DistributionGGX(vec3 N, vec3 H, float roughness) {
    const float a = roughness * roughness;
    const float a2 = a * a;
    const float NdotH = max(dot(N, H), 0.0);
    const float NdotH2 = NdotH * NdotH;

    float denom = (NdotH2 * (a2 - 1.0) + 1.0);


        // lightDirection is the direction from the fragment to the light
        const vec3 lightDirection = lights[i].type == 1 ? normalize(-lights[i].direction) : normalize(lights[i].position - inPositionWorldSpace);
        const float NdotL = clamp(dot(normalWorldSpace, lightDirection), 0.0, 1.0);

        if (dot(normalWorldSpace, lightDirection) > 0.0) {
            const vec3 halfViewLightDirection = normalize(viewDirection + lightDirection);

            const float NdotV = clamp(dot(normalWorldSpace, viewDirection), 0.0, 1.0);
            const float LdotH = clamp(dot(lightDirection, halfViewLightDirection), 0.0, 1.0);
            const float NdotH = clamp(dot(normalWorldSpace, halfViewLightDirection), 0.0, 1.0);

            // Calculate light radiance
            const float attenuation = lights[i].type == 1 ? 1.0 : lights[i].constantAttenuation + lights[i].linearAttenuation * lightDistance + lights[i].quadraticAttenuation * (lightDistance * lightDistance);
            vec3 radiance = lights[i].color.xyz / attenuation;

            // cook-torrance brdf
            const float NDF = DistributionGGX(NdotH, roughness); // Normal distribution (Distribution of the microfacets)
            const float G = GeometrySchlickGGX(NdotV, NdotL, roughness); // Geometric shadowing term (Microfacets shadowing)
            const vec3 F = fresnelSchlick(NdotV, F0); // Fresnel factor (Reflectance depending on angle of incidence)

            const vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);

            const float denominator = 4 * NdotV * NdotL;
            const vec3 specular = NDF * G * F / denominator;

            const vec3 lightFinalColor = (kD * albedo.xyz / PI + specular) * radiance * NdotL;

            // add to outgoing color to Lo
            if (lights[i].type == 3) { // Spot Light
                const float theta = dot(-lightDirection, normalize(lights[i].direction));

                if (theta > cos(lights[i].falloffAngle)) {
                    const float intensity = max(0.0, pow(theta, lights[i].falloffExponent));
                    Lo += lightFinalColor * intensity;
                }
            } else { // Direction / Point Light
                Lo += lightFinalColor;
            }
        }
    }

    vec3 color = mix(ambient, ambient * occlusion, material.occlusionTextureStrength) + Lo;

    // Tone mapping and gamma correction
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0 / 2.2));

    color += emissive;

    // Final output
    outColor = vec4(color, 1.0);
}
