I am building a 3d renderer in C++ using Vulkan.hpp. I have a deferred lighting shader setup, which has a number of complicated elements such as PBR lighting and parallax occlusion shading, with directional and point shadow inputs. Between the geometry and lighting shader stages I also run a SSAO shader stage that feeds into the lighting shader. I have noticed some issues with normal when testing using a cube located inside an inverted cube with a point light rotating around the inner cube. Firstly, not all sides of both cubes have consistent parallax mapping. Some sides seem to have an inverted depth effect. Secondly, and most noticeably, the specular lighting does not affect the sides of the cubes properly. It is offset from where you would expect to see it, with specular only occurring on one corner of the face. The location of the specular effect is also different for some sides of the cube. The issue is most easily seen on the inverted cube, but also exists on the central cube. I believe this may be due to the handling of normal, both within the geometry shader, which would account for the parallax shading issues, and the lighting shader. Can you look at the shader code below and tell me what the issue could be and how to fix it?

Geometry Vertex Shader:

#version 450

layout(binding = 0) buffer ModelBufferObject {
    mat4 models[];
} modelUBO;

layout(binding = 1) uniform ViewBufferObject {
    mat4 view;
} viewUBO;

layout(binding = 2) uniform ProjBufferObject {
    mat4 proj;
} projUBO;

layout(binding = 3) uniform LightSpaceBufferObject {
    mat4 light;
} lightUBO;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 3) in vec3 inNormal;
layout(location = 4) in vec3 inTangent;
layout(location = 5) in vec3 inBitangent;

layout(location = 0) out vec3 fragPosition;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec3 fragColor;
layout(location = 3) out vec2 fragTexCoord;
layout(location = 4) out vec4 fragPosLightSpace;
layout(location = 5) out mat3 TBN;

const mat4 biasMat = mat4( 
	0.5, 0.0, 0.0, 0.0,
	0.0, 0.5, 0.0, 0.0,
	0.0, 0.0, 1.0, 0.0,
	0.5, 0.5, 0.0, 1.0 );

void main() {
    mat4 model = modelUBO.models[gl_InstanceIndex]; // Use the model matrix for the current instance
    gl_Position = projUBO.proj * viewUBO.view * model * vec4(inPosition, 1.0);
    fragPosition = (model * vec4(inPosition, 1.0)).xyz;//gl_Position.xyz;
    mat3 normalMatrix = transpose(inverse(mat3(model)));
    if (determinant(normalMatrix) < 0.0) {
        normalMatrix = -normalMatrix;
    }
    fragNormal = normalize(normalMatrix * inNormal);
    fragColor = vec3(1.0,1.0,1.0);//inColor;
    fragTexCoord = inTexCoord;
    fragPosLightSpace = biasMat * lightUBO.light * vec4(fragPosition, 1.0);

    vec3 T = normalize(normalMatrix * inTangent);
    vec3 B = normalize(normalMatrix * inBitangent);
    vec3 N = normalize(normalMatrix * inNormal);

    T = normalize(T - dot(T, N) * N);
    B = normalize(B - dot(B, N) * N - dot(B, T) * T);
    TBN = transpose(mat3(T, B, N));
}

Geometry Fragment Shader:

#version 450

layout(location = 0) in vec3 fragPosition;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragColor;
layout(location = 3) in vec2 fragTexCoord;
layout(location = 4) in vec4 fragPosLightSpace;
layout(location = 5) in mat3 TBN;

layout(binding = 4) uniform sampler2D directionalShadowSampler; // Sampler for directional shadows
layout(binding = 5) uniform samplerCube pointShadowSampler; // Sampler for point shadow cube map

layout(set = 0, binding = 6) uniform Specular_Data {
    float metallic;
    float roughness;
    float heightscale;
} specular;

layout(binding = 7) uniform PointShadowBufferObject {
    vec3 lightPos;
    float far_plane;
} pointshadowUBO;

layout(binding = 8) uniform ShadowMatrixObject {
    mat4 shadowMatrices[6];
} shadowmatrixUBO;

layout(binding = 9) uniform sampler2D albedoTextureSampler; // Sampler for albedo
layout(binding = 10) uniform sampler2D metallicTextureSampler; // Sampler for metallic
layout(binding = 11) uniform sampler2D roughnessTextureSampler; // Sampler for roughness
layout(binding = 12) uniform sampler2D aoTextureSampler; // Sampler for ambient occlusion
layout(binding = 13) uniform sampler2D normalTextureSampler; // Sampler for normals
layout(binding = 14) uniform sampler2D depthTextureSampler; // Sampler for depth

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

layout(location = 0) out vec4 gPosition;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gAlbedo;
layout(location = 3) out vec4 gSpecular;
layout(location = 4) out vec4 gDirectionalShadow; // Existing output for directional shadows
layout(location = 5) out vec4 gPointShadow;      // New output for point shadows in screen space

float ShadowCalculation(vec4 fragPosLightSpace) {
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; // Perspective divide
    projCoords = projCoords * 0.5 + 0.5; // Transform to [0, 1] range

    // Apply bias to prevent shadow acne
    projCoords.z += 0.001;

    float closestDepth = texture(directionalShadowSampler, projCoords.xy).r; // Get closest depth from light's perspective
    float currentDepth = projCoords.z;
    float shadow = (currentDepth > closestDepth) ? 1.0 : 0.0; // Shadow computation
    return 1-shadow;
}

vec3 sampleOffsetDirections[20] = vec3[]
(
   vec3( 1,  1,  1), vec3(1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1), 
   vec3( 1,  1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
   vec3( 1,  1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
   vec3( 1,  0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1,  0, -1),
   vec3( 0,  1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0,  1, -1)
);   

float PointShadowCalculation(vec3 fragPos)
{
    vec3 fragToLight = fragPos - pointshadowUBO.lightPos;
    float currentDepth = length(fragToLight);
    float shadow = 0.0;
    float bias   = 0.15;
    int samples  = 10;
    float diskRadius = 0.05;  
    for(int i = 0; i < samples; ++i)
    {
        float closestDepth = texture(pointShadowSampler, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
        closestDepth *= pointshadowUBO.far_plane;   // undo mapping [0;1]
        if(currentDepth - bias > closestDepth)
            shadow += 1.0;
    }
    shadow /= float(samples);  

    return 1-shadow;
}  

// Parallax Occlusion Mapping function

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir, sampler2D depthMap, float heightScale)
{ 
    // number of depth layers
    const float minLayers = 8;
    const float maxLayers = 32;
    float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));  
    // calculate the size of each layer
    float layerDepth = 1.0 / numLayers;
    // depth of current layer
    float currentLayerDepth = 0.0;
    // the amount to shift the texture coordinates per layer (from vector P)
    vec2 P = viewDir.xy / viewDir.z * heightScale; 
    vec2 deltaTexCoords = P / numLayers;

    // get initial values
    vec2  currentTexCoords     = texCoords;
    float currentDepthMapValue = texture(depthMap, currentTexCoords).r;

    while(currentLayerDepth < currentDepthMapValue)
    {
        // shift texture coordinates along direction of P
        currentTexCoords -= deltaTexCoords;
        // get depthmap value at current texture coordinates
        currentDepthMapValue = texture(depthMap, currentTexCoords).r;  
        // get depth of next layer
        currentLayerDepth += layerDepth;  
    }

    // get texture coordinates before collision (reverse operations)
    vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

    // get depth after and before collision for linear interpolation
    float afterDepth  = currentDepthMapValue - currentLayerDepth;
    float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;

    // interpolation of texture coordinates
    float weight = afterDepth / (afterDepth - beforeDepth);
    vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

    return finalTexCoords;
}

void main() {
    gPosition = vec4(fragPosition, 1.0);

    vec3 TangentViewPos  = TBN * camera.position;
    vec3 TangentFragPos  = TBN * fragPosition;

    // Compute view direction
    vec3 viewDir = normalize(TangentViewPos - TangentFragPos);

    // Invert Y-component for Vulkan compatibility
    viewDir.y *= -1.0;

    // Calculate parallax offset
    vec2 parallaxTexCoords = ParallaxMapping(fragTexCoord, viewDir, depthTextureSampler, specular.heightscale);

    gAlbedo = texture(albedoTextureSampler, parallaxTexCoords);
    gSpecular = vec4(specular.metallic * texture(metallicTextureSampler, parallaxTexCoords).x,
                     specular.roughness * texture(roughnessTextureSampler, parallaxTexCoords).x,
                     texture(aoTextureSampler, parallaxTexCoords).r, 1.0);

    // Sample normal map
    vec3 normalMap = texture(normalTextureSampler, parallaxTexCoords).rgb;
    normalMap = normalize(normalMap * 2.0 - 1.0); // Convert from [0,1] to [-1,1]

    // Transform normal from tangent space to world space
    vec3 worldNormal = normalize(TBN * normalMap);
    gNormal = vec4(worldNormal, 1.0);

    // Directional shadow calculation
    float directionalShadow = ShadowCalculation(fragPosLightSpace);
    gDirectionalShadow = vec4(directionalShadow, directionalShadow, directionalShadow, directionalShadow);

    // Point shadow calculation
    float pointShadow = 0.0;
    vec3 fragWorldPosition = fragPosition;  // Assume this is already in world space
    vec3 lightPosition = pointshadowUBO.lightPos;

    // Invert Y-component of light position for Vulkan compatibility
    lightPosition.y *= -1.0;

    float farPlane = pointshadowUBO.far_plane;

    pointShadow = PointShadowCalculation(fragWorldPosition);
    gPointShadow = vec4(pointShadow, pointShadow, pointShadow, pointShadow);
}

Lighting Vertex Shader:

#version 450

layout(location = 0) out vec2 fragTexCoord;

void main() {
    const vec2 positions[6] = vec2[6](
        vec2(-1.0, -1.0),  // bottom-left
        vec2( 1.0, -1.0),  // bottom-right
        vec2( 1.0,  1.0),  // top-right
        vec2(-1.0, -1.0),  // bottom-left
        vec2( 1.0,  1.0),  // top-right
        vec2(-1.0,  1.0)   // top-left
    );

    const vec2 texCoords[6] = vec2[6](
        vec2(0.0, 0.0),    // bottom-left
        vec2(1.0, 0.0),    // bottom-right
        vec2(1.0, 1.0),    // top-right
        vec2(0.0, 0.0),    // bottom-left
        vec2(1.0, 1.0),    // top-right
        vec2(0.0, 1.0)     // top-left
    );

    fragTexCoord = texCoords[gl_VertexIndex];
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

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(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;

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;
}

// Function to calculate the GGX BRDF
vec3 PBR_GGX(vec3 N, vec3 V, vec3 L, vec3 albedo, float metallic, float roughness) {
    vec3 F0 = mix(vec3(0.04), albedo, metallic);

    // Calculate the reflection direction
    vec3 R = reflect(-L, N);

    // Calculate the dot products
    float dotNL = max(dot(N, L), 0.0);
    float dotVR = max(dot(V, R), 0.0);

    // Calculate the GGX distribution function (D)
    float a = roughness * roughness;
    float a2 = a * a;
    float NDL = dotNL;
    float denom = NDL * (a2 - 1.0) + 1.0;
    float D = a2 / (denom * denom + 1e-8);

    // Calculate the Smith microfacet geometry function (G)
    float GGXVisibility = SmithGGXVisibility(N, V, L, roughness);

    // Calculate the Fresnel term (F)
    vec3 F = F0 + (vec3(1.0) - F0) * pow(1.0 - dotVR, 5.0);

    // Combine the terms
    vec3 specular = D * GGXVisibility * F / (4.0 * dotNL + 1e-8);
    vec3 diffuse = (1.0 - F) * (1.0 - metallic) * albedo / kPi;

    return (diffuse + specular) * dotNL;
}

void main() {
    vec3 fragPosition = subpassLoad(gPosition, gl_SampleID).xyz;
    vec3 fragNormal = normalize(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;
    vec3 view_dir = normalize(camera.position - fragPosition);

    // Invert Y-component of view direction for Vulkan compatibility
    view_dir.y *= -1.0;

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

    metallic = clamp(metallic, 0.0, 1.0);
    roughness = clamp(roughness, 0.05, 1.0); // Add a minimum roughness to avoid NaNs

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

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

    // Invert Y-component of directional light direction
    lightDir.y *= -1.0;

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

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

    // Invert Y-component of point light direction
    pointLightDir.y *= -1.0;

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

    // Attenuation calculation
    float pointDistance = length(point.position - fragPosition);
    float attenuation = 1.0 / (point.constant + point.linear * pointDistance + point.quadratic * pointDistance * pointDistance);
    pointLight *= attenuation;

    // Combine all contributions
    vec3 hdrColor = ambientLight + directionalLight + pointLight;
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposureUBO.exposure);
    //mapped = vec3(fragSSAO);
    if (alpha == 0) {
        mapped = vec3(texture(skybox, fragTexCoord) * exposureUBO.exposure);
    }
    outColor = vec4(mapped, 1.0);
}