I am building a 3d renderer in C++ using Vulkan.hpp. I am trying to improve my ambient lighting. Right now the renderer using PBR lighting, with SSAO and IBL ambient lighting from an environment cubemap. This cubemap can be generated within the scene to directly get the surrounding image information to use as an input to IBL. This produces somewhat reasonable lighting, but it is applied across the entire scene, even in areas where there should be shadows cast from where that lighting should be coming from. I do have SSAO, but this seems to be more useful in applying shadow details based on occlusion than actually providing proper ambient shadows. I believe using Volumetric Probes with Spherical Harmonics for diffuse radiance would help to address this issue. This would likely require a decent number of probes within the scene to sample the global illumination values, which should be manageable using Spherical Harmonics. The specular radiance would be handled using my existing prefiltered environmental cubemap, which I can update by rendering to the cubemap from the scene. However, my current IBL setup can only handle a single prefiltered cubemap where I would ideally need the ability to handle multiple and interpolate between them. By having a finer grid for diffuse spherical harmonics and a coarser grid for specular prefiltered cubemaps, I believe I should be able to manage the updates to these within the renderer to handle dynamic lighting. Can you generate any required shader code for the above approach? Please include any vertex, fragment and geometry shader code in your response. Keep in mind I am using Vulkan. Therefore any shader code would need to comply with Vulkan and SPIR-V.

Please also update the following lighting fragment shader code to read and make use of the diffuse radiance input and interpolate between multiple specular cubemaps. It can also help inform how I am structuring UBOs.

Lighting Fragment 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;

vec3 diffuseOrenNayar(vec3 L, vec3 V, vec3 N, float roughness, vec3 albedo) {
    float sigma2 = roughness * roughness;
    float A = 1.0 - 0.5 * sigma2 / (sigma2 + 0.33);
    float B = 0.45 * sigma2 / (sigma2 + 0.09);
    float cosThetaI = max(dot(N, L), 0.0);
    float cosThetaO = max(dot(N, V), 0.0);
    float sinThetaI = sqrt(1.0 - cosThetaI * cosThetaI);
    float sinThetaO = sqrt(1.0 - cosThetaO * cosThetaO);
    float cosPhi = dot(V - N * cosThetaO, L - N * cosThetaI);
    cosPhi = max(cosPhi, 0.0);
    float diffuse = (A + B * cosPhi * sinThetaI * sinThetaO / max(cosThetaI, cosThetaO));
    return diffuse * albedo / kPi;
}

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 + 0.001);
}
// ----------------------------------------------------------------------------
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);

    float subsurfaceStrength = (1.0 - metallic) * roughness;
    vec3 subsurfaceColor = mix(albedo, vec3(0.8, 0.5, 0.3), 0.5);
        float wrap = subsurfaceStrength * 1.5;//0.5; // Adjust for subsurface effect
        float wrappedNdotL = max((dot(N, L) + wrap) / (1.0 + wrap), 0.0);        

    // Backlit term
    float backLit = max(-dot(N, L), 0.0);
    float backLitFactor = backLit * subsurfaceStrength * 0.5;

    // Diffuse and SSS contributions
    vec3 diffuse = kD * albedo / kPi * wrappedNdotL;
    vec3 subsurface = subsurfaceColor * backLitFactor;

    // Combine and apply radiance
    Lo += (diffuse + subsurface + specular) * radiance;

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

        return Lo;

}

vec3 PBR_GGX3(vec3 L, vec3 N, vec3 V, float Dist, vec3 albedo, vec3 color, float metallic, float roughness) {
    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);
    vec3 Lo = vec3(0.0);
    vec3 H = normalize(V + L);
    float distance = Dist;
    float attenuation = 1.0 / (distance * distance + 0.0001);
    vec3 radiance = color * attenuation;

    // BRDF terms (unchanged)
    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;
    vec3 specular = numerator / denominator;

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

    float NdotL = dot(N, L);
    float contribution = NdotL > 0.0 ? smoothstep(0.0, 0.05, NdotL) : 0.0;

    // Use Oren-Nayar diffuse instead of Lambertian
    vec3 diffuse = diffuseOrenNayar(L, V, N, roughness, albedo);
    Lo += (diffuse + specular) * radiance * contribution;

    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;
    float ao = subpassLoad(gSpecular, gl_SampleID).b;
    float ambientOcclusion = clamp(fragSSAO * ao, 0.0, 1.0);

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

    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 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 = vec3(1.0) - kS;
    kD *= 1.0 - metallic;	  
    vec3 irradiance = texture(irradianceMap, fragNormal).rgb;
    vec3 diffuse      = irradiance * fragAlbedo;

vec3 ambientDiffuse = irradiance * fragAlbedo * (1.0 - metallic);
    float subsurfaceStrength = (1.0 - metallic) * roughness;
if (subsurfaceStrength > 0.3) { // Threshold for SSS materials
    float wrap = subsurfaceStrength * 1.0; // Wrap factor for ambient
    vec3 wrappedIrradiance = texture(irradianceMap, normalize(fragNormal + view_dir * wrap)).rgb;
    ambientDiffuse = wrappedIrradiance * fragAlbedo * (1.0 - metallic);
}

vec3 ambientSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y);// * (1.0 - roughness); //modified to weight based on smoothness?
vec3 ambientLight = ambient.intensity * 10 * ambient.color * (ambientDiffuse + ambientSpecular) * ambientOcclusion * pointShadowValue;

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

    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;


    // Combine all contributions
    vec3 hdrColor = ambientLight + directionalLight + pointLight;

    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposureUBO.exposure);


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

    outColor = vec4(mapped, 1.0);
}