I am building a 3d renderer in C++ using Vulkan.hpp. My renderer implements a PBR lighting model with global illumination components, such as irradiance maps and brdf maps being precompiled using shaders and then used in the lighting stage of a deferred rendering approach. However, I have noticed a few issues with the PBR lighting output. There seems to be an excessive amount of ambient reflection being generated on some surfaces, as though surfaces are more metallic than they the metallic texture input would suggest. Also, when looking at the irradiance map, I don't believe it looks correct. Can you look at the below shader code for the precompiling of the global illumination and radiance maps and also the lighting shader code and verify they are correct or suggest improvements/fixes? Since the main code is in the fragment shader I will only supply that code. Any vertex or geometry shader code is fairly simple and I'm confident it is working correctly. Also note that the lighting shader is complex as it needs to interpret the G buffer and shadow elements, along with other methodology.

irradiance convolution fragment shader:

#version 450
layout(location = 0) out vec4 FragColor;
layout(location = 1) in vec4 WorldPos;

layout(binding = 2) uniform samplerCube environmentMap;

const float PI = 3.14159265359;

void main()
{		
	// The world vector acts as the normal of a tangent surface
    // from the origin, aligned to WorldPos. Given this normal, calculate all
    // incoming radiance of the environment. The result of this radiance
    // is the radiance of light coming from -Normal direction, which is what
    // we use in the PBR shader to sample irradiance.
    vec3 N = normalize(WorldPos.xyz);

    vec3 irradiance = vec3(0.0);   
    
    // tangent space calculation from origin point
    vec3 up    = vec3(0.0, 1.0, 0.0);
    vec3 right = normalize(cross(up, N));
    up         = normalize(cross(N, right));
       
    float sampleDelta = 0.025;
    float nrSamples = 0.0;
    for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
    {
        for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
        {
            // spherical to cartesian (in tangent space)
            vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
            // tangent space to world
            vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 

            irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
            nrSamples++;
        }
    }
    irradiance = PI * irradiance * (1.0 / float(nrSamples));
    
    FragColor = vec4(irradiance, 1.0);
}

prefilter shader:

#version 450
layout(location = 0) out vec4 FragColor;
layout(location = 1) in vec4 WorldPos;

layout(binding = 2) uniform samplerCube environmentMap;
layout(set = 0, binding = 3) uniform Roughness_Data {
    float roughness;
} roughness;

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

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

    return nom / denom;
}
// ----------------------------------------------------------------------------
// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
// efficient VanDerCorpus calculation.
float RadicalInverse_VdC(uint bits) 
{
     bits = (bits << 16u) | (bits >> 16u);
     bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
     bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
     bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
     bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
     return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
	return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}
// ----------------------------------------------------------------------------
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
	float a = roughness*roughness;
	
	float phi = 2.0 * PI * Xi.x;
	float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
	float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
	
	// from spherical coordinates to cartesian coordinates - halfway vector
	vec3 H;
	H.x = cos(phi) * sinTheta;
	H.y = sin(phi) * sinTheta;
	H.z = cosTheta;
	
	// from tangent-space H vector to world-space sample vector
	vec3 up          = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
	vec3 tangent   = normalize(cross(up, N));
	vec3 bitangent = cross(N, tangent);
	
	vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
	return normalize(sampleVec);
}
// ----------------------------------------------------------------------------
void main()
{		
    vec3 N = normalize(WorldPos.xyz);
    
    // make the simplifying assumption that V equals R equals the normal 
    vec3 R = N;
    vec3 V = R;

    const uint SAMPLE_COUNT = 1024u;
    vec3 prefilteredColor = vec3(0.0);
    float totalWeight = 0.0;
    
    for(uint i = 0u; i < SAMPLE_COUNT; ++i)
    {
        // generates a sample vector that's biased towards the preferred alignment direction (importance sampling).
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H = ImportanceSampleGGX(Xi, N, roughness.roughness);
        vec3 L  = normalize(2.0 * dot(V, H) * H - V);

        float NdotL = max(dot(N, L), 0.0);
        if(NdotL > 0.0)
        {
            // sample from the environment's mip level based on roughness/pdf
            float D   = DistributionGGX(N, H, roughness.roughness);
            float NdotH = max(dot(N, H), 0.0);
            float HdotV = max(dot(H, V), 0.0);
            float pdf = D * NdotH / (4.0 * HdotV) + 0.0001; 

            float resolution = 512.0; // resolution of source cubemap (per face)
            float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);
            float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);

            float mipLevel = roughness.roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 
            
            prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL;
            totalWeight      += NdotL;
        }
    }

    prefilteredColor = prefilteredColor / totalWeight;

    FragColor = vec4(prefilteredColor, 1.0);
}

brdf shader:

#version 450
layout(location = 0) out vec2 FragColor;
layout(location = 0) in vec2 TexCoords;

const float PI = 3.14159265359;
// ----------------------------------------------------------------------------
// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
// efficient VanDerCorpus calculation.
float RadicalInverse_VdC(uint bits) 
{
     bits = (bits << 16u) | (bits >> 16u);
     bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
     bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
     bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
     bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
     return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
	return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}
// ----------------------------------------------------------------------------
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
	float a = roughness*roughness;
	
	float phi = 2.0 * PI * Xi.x;
	float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
	float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
	
	// from spherical coordinates to cartesian coordinates - halfway vector
	vec3 H;
	H.x = cos(phi) * sinTheta;
	H.y = sin(phi) * sinTheta;
	H.z = cosTheta;
	
	// from tangent-space H vector to world-space sample vector
	vec3 up          = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
	vec3 tangent   = normalize(cross(up, N));
	vec3 bitangent = cross(N, tangent);
	
	vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
	return normalize(sampleVec);
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
    // note that we use a different k for IBL
    float a = roughness;
    float k = (a * a) / 2.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}
// ----------------------------------------------------------------------------
vec2 IntegrateBRDF(float NdotV, float roughness)
{
    vec3 V;
    V.x = sqrt(1.0 - NdotV*NdotV);
    V.y = 0.0;
    V.z = NdotV;

    float A = 0.0;
    float B = 0.0; 

    vec3 N = vec3(0.0, 0.0, 1.0);
    
    const uint SAMPLE_COUNT = 1024u;
    for(uint i = 0u; i < SAMPLE_COUNT; ++i)
    {
        // generates a sample vector that's biased towards the
        // preferred alignment direction (importance sampling).
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H = ImportanceSampleGGX(Xi, N, roughness);
        vec3 L = normalize(2.0 * dot(V, H) * H - V);

        float NdotL = max(L.z, 0.0);
        float NdotH = max(H.z, 0.0);
        float VdotH = max(dot(V, H), 0.0);

        if(NdotL > 0.0)
        {
            float G = GeometrySmith(N, V, L, roughness);
            float G_Vis = (G * VdotH) / (NdotH * NdotV);
            float Fc = pow(1.0 - VdotH, 5.0);

            A += (1.0 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    A /= float(SAMPLE_COUNT);
    B /= float(SAMPLE_COUNT);
    return vec2(A, B);
}
// ----------------------------------------------------------------------------
void main() 
{
    vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, 1-TexCoords.y);
    FragColor = integratedBRDF;
}

lighting shader:

#version 450

layout(location = 0) out vec4 outColor;

layout(input_attachment_index = 0, set = 0, binding = 0) uniform subpassInputMS gPosition;
layout(input_attachment_index = 1, set = 0, binding = 1) uniform subpassInputMS gNormal;
layout(input_attachment_index = 2, set = 0, binding = 2) uniform subpassInputMS gAlbedo;
layout(input_attachment_index = 3, set = 0, binding = 3) uniform subpassInputMS gSpecular; // R = metallic B = roughness G = ambient occlusion
layout(input_attachment_index = 4, set = 0, binding = 4) uniform subpassInputMS gDirectionalShadow;
layout(input_attachment_index = 5, set = 0, binding = 5) uniform subpassInputMS gPointShadow;
layout(input_attachment_index = 6, set = 0, binding = 13) uniform subpassInputMS gTangent;
layout(input_attachment_index = 7, set = 0, binding = 14) uniform subpassInputMS gBitangent;

layout(location = 0) in vec2 fragTexCoord;

layout(set = 0, binding = 6) uniform Ambient_Data {
    vec3 color;
    float intensity;
} ambient;

layout(set = 0, binding = 7) uniform Directional_Data {
    vec3 position;
    float intensity;
    vec3 color;
} directional;

layout(set = 0, binding = 8) uniform Point_Data {
    vec3 position;
    float intensity;
    vec3 color;
    float constant;
    float linear;
    float quadratic;
} point;

layout(set = 0, binding = 9) uniform Camera_Data {
    vec3 position;
} camera;

layout(set = 0, binding = 10) uniform sampler2D ssao;
layout(binding = 11) uniform Exposure_Data {
    float exposure;
} exposureUBO;

layout(set = 0, binding = 12) uniform sampler2D skybox;

layout(set = 0, binding = 15) uniform samplerCube irradianceMap;
layout(set = 0, binding = 16) uniform samplerCube prefilterMap;
layout(set = 0, binding = 17) uniform sampler2D brdfLUT;

const float kPi = 3.14159265;

float SmithGGXVisibility(vec3 N, vec3 V, vec3 L, float roughness) {
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float a = roughness * roughness;

    float GGXVisibilityV = NdotV / (NdotV * (1.0 - a) + a);
    float GGXVisibilityL = NdotL / (NdotL * (1.0 - a) + a);

    return GGXVisibilityV * GGXVisibilityL;
}

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

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

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}   

vec3 PBR_GGX2(vec3 L, vec3 N, vec3 V, float Dist, vec3 albedo, vec3 color, float metallic, float roughness)
{
    // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 
    // of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // reflectance equation
    vec3 Lo = vec3(0.0);

    // calculate per-light radiance
        //vec3 L = pointLightDir;
        vec3 H = normalize(V + L);
        float distance = Dist;
        float attenuation = 1.0 / (distance * distance + 0.0001); // + 0.0001 to prevent divide by zero
        vec3 radiance = color * attenuation;

        // Cook-Torrance BRDF
        float NDF = DistributionGGX(N, H, roughness);   
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);
           
        vec3 numerator    = NDF * G * F; 
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; // + 0.0001 to prevent divide by zero
        vec3 specular = numerator / denominator;
        
        // kS is equal to Fresnel
        vec3 kS = F;
        // for energy conservation, the diffuse and specular light can't
        // be above 1.0 (unless the surface emits light); to preserve this
        // relationship the diffuse component (kD) should equal 1.0 - kS.
        vec3 kD = vec3(1.0) - kS;
        // multiply kD by the inverse metalness such that only non-metals 
        // have diffuse lighting, or a linear blend if partly metal (pure metals
        // have no diffuse light).
        kD *= 1.0 - metallic;	  

        // scale light by NdotL
        float NdotL = max(dot(N, L), 0.0);        

        // add to outgoing radiance Lo
        Lo += (kD * albedo / kPi + specular) * radiance * NdotL;  // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again

        return Lo;

}

void main() {
    vec3 fragPosition = subpassLoad(gPosition, gl_SampleID).xyz;
    vec3 fragNormal = subpassLoad(gNormal, gl_SampleID).xyz;
    vec3 fragAlbedo = subpassLoad(gAlbedo, gl_SampleID).rgb;
    float directionalShadowValue = subpassLoad(gDirectionalShadow, gl_SampleID).r;
    float pointShadowValue = subpassLoad(gPointShadow, gl_SampleID).r;
    float alpha = subpassLoad(gPosition, gl_SampleID).a;
    float fragSSAO = texture(ssao, fragTexCoord).r;
    fragSSAO += subpassLoad(gSpecular, gl_SampleID).b;

    // Load tangent and bitangent from gBuffer
    vec3 fragTangent = subpassLoad(gTangent, gl_SampleID).xyz;
    vec3 fragBitangent = subpassLoad(gBitangent, gl_SampleID).xyz;

    // Transform vectors to tangent space
    vec3 view_dir = normalize(camera.position - fragPosition);

    // PBR material properties
    float metallic = subpassLoad(gSpecular, gl_SampleID).r;
    float roughness = subpassLoad(gSpecular, gl_SampleID).g;

    //vec3 R = reflect(-view_dir, fragNormal); 
    vec3 R = reflect(view_dir, fragNormal); 

    metallic = clamp(metallic, 0.0, 1.0);
    roughness = clamp(roughness, 0.05, 1.0);

    const float MAX_REFLECTION_LOD = 9.0;
    vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb; 



    // Ambient light
    //vec3 ambientLight = ambient.intensity * ambient.color * fragAlbedo * fragSSAO;

    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, fragAlbedo, metallic);

    vec3 F = fresnelSchlickRoughness(max(dot(fragNormal, view_dir), 0.0), F0, roughness);
    vec2 brdfCoords = vec2(max(dot(fragNormal, view_dir), 0.0), roughness);
    vec2 envBRDF = texture(brdfLUT, vec2(max(dot(fragNormal, view_dir), 0.0), roughness)).rg;
    vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);

    // ambient lighting (we now use IBL as the ambient term)
    vec3 kS = fresnelSchlick(max(dot(fragNormal, view_dir), 0.0), F0);
    vec3 kD = 1.0 - kS;
    kD *= 1.0 - metallic;	  
    vec3 irradiance = texture(irradianceMap, fragNormal).rgb;
    vec3 diffuse      = irradiance * fragAlbedo;
    vec3 ambientLight = ambient.intensity * ambient.color * (kD * diffuse + specular) * fragSSAO;

    // Directional light calculations
    vec3 lightDir = normalize(directional.position);

    //vec3 directionalLight = vec3(0.0);//PBR_GGX(fragNormal, view_dir, lightDir, fragAlbedo, metallic, roughness);
    vec3 directionalLight = PBR_GGX2(lightDir, fragNormal, view_dir, 1.0, fragAlbedo, directional.color, metallic, roughness);
    directionalLight *= directional.intensity;// * directional.color;
    directionalLight *= directionalShadowValue;

    // Point light calculations
    vec3 pointLightDir = normalize(point.position - fragPosition);
    float pointDistance = length(point.position - fragPosition);

    vec3 pointLight = PBR_GGX2(pointLightDir, fragNormal, view_dir, pointDistance, fragAlbedo, point.color, metallic, roughness);
    pointLight *= point.intensity;// * point.color;
    pointLight *= pointShadowValue;

    //vec3 ambientLight = (ambient.intensity+directionalLight) * ambient.color * (kD * diffuse + specular) * fragSSAO;
    // Combine all contributions
    vec3 hdrColor = ambientLight + directionalLight + pointLight;
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposureUBO.exposure);
    //mapped = vec3(ambientLight);

    if (alpha == 0) {
        mapped = vec3(texture(skybox, fragTexCoord) * exposureUBO.exposure);
    }

    outColor = vec4(mapped, 1.0);
}