Having a problem with tangent/bitangent calculation for my meshes

Hi, I am new to the forum and somewhat new to openGL - my last endevour into openGL leads back to 2004 …
As stated in my headline of the topic, I have some issues with normal-mapping, in detail with the calcualtion (I think at least) of the tangent and bitangent for my vertices.
So what I am trying to do: I try to write a very simple 3D engine just for fun, following the tutorials at “LearnOpenGL by Joey de Vries”. So far, it is working well. I am now trying to implement normal mapping to the engine, following the tutorial found here: “LearnOpenGL/Advanced Lighting/Normal mapping”. I have a very simple shader with a point light:

Vertex Shader:

#version 460 core
layout (location = 0) in vec3 vertexPosition_modelspace;
layout (location = 1) in vec3 vertexNormal;
layout (location = 2) in vec2 texture_coord;
layout (location = 4) in vec3 aTangent;
layout (location = 5) in vec3 aBitangent;

struct pointLight{
   vec3 position;
   float constant;
   float linear;
   float quadratic;
   vec3 ambient;
   vec3 diffuse;
   vec3 specular;
   bool present;
};

uniform mat4 model;
uniform mat4 view;
uniform vec3 viewPos;
uniform mat4 projection;
uniform pointLight light;

out VS_OUT{
   vec3 normal;
   vec3 FragPos;
   vec3 vColor;
   vec2 texCoord;
   vec3 TangentLightPos;
   vec3 TangentViewPos;
   vec3 TangentFragPos;
} vs_out;
void main()
{
   vs_out.vColor = vec3(1.0);
   vs_out.FragPos = vec3(model * vec4(vertexPosition_modelspace, 1.0));

   mat3 normalMatrix = transpose(inverse(mat3(model)));
   vec3 T = normalize(normalMatrix * aTangent);
   vec3 N = normalize(normalMatrix * vertexNormal);
   T = normalize(T - dot(T, N) * N);
   vec3 B = cross(N, T);

   mat3 TBN = transpose(mat3(T, B, N));    
   vs_out.TangentLightPos = TBN * light.position;
   vs_out.TangentViewPos  = TBN * viewPos;
   vs_out.TangentFragPos  = TBN * vs_out.FragPos;

   vs_out.normal = vec3(vec4(normalMatrix * vertexNormal, 0.0));
   vs_out.texCoord = texture_coord;
   gl_Position = projection * view * vec4(vs_out.FragPos, 1.0);
}

Fragment Shader:

#version 460 core
struct Material{
   float roughness;
   float shininess;
   float alpha;
   vec3 ambient;
   vec3 diffuse;
   vec3 specular;
   sampler2D texture_diffuse1;
   sampler2D texture_specular1;
   sampler2D texture_normal1;
};

struct pointLight{
    vec3 position;
    
    float constant;
    float linear;
    float quadratic;
    
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    bool present;
};

//input data
in VS_OUT{
    vec3 normal;
    vec3 FragPos;
    vec3 vColor;
    vec2 texCoord;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;

// Values that stay constant for the whole mesh.
uniform vec3 viewPos;
uniform pointLight point_light;
uniform Material material;

// output data
out vec4 FragColor ;

vec3 CalcPointLight(pointLight light, vec3 normal, vec3 viewDir, vec3 material_tex, vec3 material_tex_specular);

void main()
{
    vec3 material_tex = texture(material.texture_diffuse1, fs_in.texCoord).rgb;
    vec3 material_tex_specular = texture(material.texture_specular1, fs_in.texCoord).rgb;

    // obtain normal from normal map in range [0,1]
    vec3 norm = texture(material.texture_normal1, fs_in.texCoord).rgb;
    // transform normal vector to range [-1,1]
    norm = normalize(norm * 2.0 - 1.0);  // this normal is in tangent space

    vec3 result_light = CalcPointLight(point_light, norm, normalize(fs_in.TangentViewPos - fs_in.TangentFragPos), material_tex, material_tex_specular);
    FragColor = vec4(result_light.rgb, material.alpha);
}

vec3 CalcPointLight(pointLight light, vec3 normal, vec3 viewDir, vec3 material_tex, vec3 material_tex_specular){
    // ambient
    vec3 ambient = light.ambient * material_tex;
    
    // diffuse 
    vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = light.diffuse * diff * material_tex;  
    
    //specular blinn shading:
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(normal, halfwayDir), 0.0), material.shininess);

    vec3 specular = light.specular * spec * material_tex_specular;  
    
    // attenuation
    float dist    = length(light.position - fs_in.FragPos);
    float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * (dist * dist));    

    ambient  *= attenuation;  
    diffuse   *= attenuation;
    specular *= attenuation;   
        
    if(material.shininess <= 0.0){
        return (ambient + diffuse);
    }else{
        return (ambient + diffuse + specular);
    }
}

With a mesh imported with the assimp importer library (GitHub - assimp/assimp: The official Open-Asset-Importer-Library Repository. Loads 40+ 3D-file-formats into one unified and clean data structure.), this shader works really well - no issues at all. But in this case the tangent and bitangent are calculated by the assimp importer lib.

However, when it comes to my own meshes, it is a whole different story. For my own meshes I have to generate the tangent and bitangent myself. Therefore I used the algorithm shown at LearnOpenGL/Normalmapping and extended it to be able to go over several triangles in a mesh (btw: my meshes are all index vertex arrays). This looks like this:

void my3DEngineVertexObject::calculateTangentsIndexed(vertex* vertex_array, size_t nr_vertices, GLuint* indices, size_t nr_indices) {
    //iterate the indices array
    for(size_t i = 0; i < nr_indices; i+=3){ //we need to handle 3 vertices --> one triangle
        //calculate indices
        unsigned int i1 = indices[i];
        unsigned int i2 = indices[i + 1];
        unsigned int i3 = indices[i + 2];

        glm::vec3 edge1 = glm::vec3(vertex_array[i2].pos.x, vertex_array[i2].pos.y, vertex_array[i2].pos.z) - glm::vec3(vertex_array[i1].pos.x, vertex_array[i1].pos.y, vertex_array[i1].pos.z);
        glm::vec3 edge2 = glm::vec3(vertex_array[i3].pos.x, vertex_array[i3].pos.y, vertex_array[i3].pos.z) - glm::vec3(vertex_array[i1].pos.x, vertex_array[i1].pos.y, vertex_array[i1].pos.z);
        glm::vec2 deltaUV1 = glm::vec2(vertex_array[i2].texCoord.u, vertex_array[i2].texCoord.v) - glm::vec2(vertex_array[i1].texCoord.u, vertex_array[i1].texCoord.v);
        glm::vec2 deltaUV2 = glm::vec2(vertex_array[i3].texCoord.u, vertex_array[i3].texCoord.v) - glm::vec2(vertex_array[i1].texCoord.u, vertex_array[i1].texCoord.v);

        // calculate tangent.
        float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
        glm::vec3 tangent = f * (deltaUV2.y * edge1 - deltaUV1.y * edge2);

        // calculate bitangent.
        glm::vec3 bitangent = f * (-deltaUV2.x * edge1 + deltaUV1.x * edge2);


        vertex_array[i1].tangent = tangent;
        vertex_array[i2].tangent = tangent;
        vertex_array[i3].tangent = tangent;

        vertex_array[i1].bitangent = bitangent;
        vertex_array[i2].bitangent = bitangent;
        vertex_array[i3].bitangent = bitangent;
    }
}

Since it worked for the tutorial, I thought I can extrapolate this to my vertex arrays and it should work these to - but it didn’t, as you can see in the pictures below. My own plane (500x500 vertices) lit by a single point light and a waveiy normal map fount on the internet, it looks quite off:

It seems that the normal mapping is “ok” for one half of the plane, the other half looks like it lays in darkness.

Similar problem for the cube (the one with the yellow vertex normals) with the brick wall (different normal map, the one from the LearnOpenGL normal mapping tutorial).

I am not sure if it matters, but these are the vertices used for the cube:

static vertex cube_vertices[] = {
    // positions             // normals             // TexCoord    // color
    // back (red)
    {{-0.5f, -0.5f, -0.5f},  { 0.0f,  0.0f, -1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f, -0.5f, -0.5f},  { 0.0f,  0.0f, -1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f, -0.5f},  { 0.0f,  0.0f, -1.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f,  0.5f, -0.5f},  { 0.0f,  0.0f, -1.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    // front (green)
    {{-0.5f, -0.5f,  0.5f},  { 0.0f,  0.0f,  1.0f}, {0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f, -0.5f,  0.5f},  { 0.0f,  0.0f,  1.0f}, {1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f,  0.5f},  { 0.0f,  0.0f,  1.0f}, {1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f,  0.5f,  0.5f},  { 0.0f,  0.0f,  1.0f}, {0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    // left (blue)
    {{-0.5f, -0.5f, -0.5f},  {-1.0f,  0.0f,  0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f, -0.5f,  0.5f},  {-1.0f,  0.0f,  0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f,  0.5f,  0.5f},  {-1.0f,  0.0f,  0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f,  0.5f, -0.5f},  {-1.0f,  0.0f,  0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    // right (yellow)
    {{ 0.5f, -0.5f, -0.5f},  { 1.0f,  0.0f,  0.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f, -0.5f,  0.5f},  { 1.0f,  0.0f,  0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f,  0.5f},  { 1.0f,  0.0f,  0.0f}, {1.0f, 1.0f}, {1.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f, -0.5f},  { 1.0f,  0.0f,  0.0f}, {0.0f, 1.0f}, {1.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    // underside (magenta)
    {{-0.5f, -0.5f, -0.5f},  { 0.0f, -1.0f,  0.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f, -0.5f, -0.5f},  { 0.0f, -1.0f,  0.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f, -0.5f,  0.5f},  { 0.0f, -1.0f,  0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f, -0.5f,  0.5f},  { 0.0f, -1.0f,  0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    // upperside (cyan)
    {{-0.5f,  0.5f, -0.5f},  { 0.0f,  1.0f,  0.0f}, {0.0f, 0.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f, -0.5f},  { 0.0f,  1.0f,  0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{ 0.5f,  0.5f,  0.5f},  { 0.0f,  1.0f,  0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}},
    {{-0.5f,  0.5f,  0.5f},  { 0.0f,  1.0f,  0.0f}, {0.0f, 1.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}}
};

And this is my algorithm to generate the plane:

my3DEngineObjectPlane::my3DEngineObjectPlane(GLuint xRes, GLuint yRes) {
    if (xRes > 1 && yRes > 1) {
        vertex* vertices = new vertex[xRes * yRes]; //memory for vertices

        unsigned int index = 0;

        // create vertices
        for (unsigned int y = 0; y < yRes; y++) {
            for (unsigned int x = 0; x < xRes; x++) {

                vertices[index].pos.x = ((GLfloat)x / (GLfloat)(xRes - 1)) - 0.5f; // -0.5 for centering the origin of the plane in the center of the plane
                vertices[index].pos.y = 0.0f;
                vertices[index].pos.z = ((GLfloat)y / (GLfloat)(yRes - 1)) - 0.5f;

                vertices[index].color = glm::vec3(1.0f, 0.0f, 1.0f);

                vertices[index].texCoord.u = ((GLfloat)x / (GLfloat)(xRes-1));
                vertices[index].texCoord.v = ((GLfloat)y / (GLfloat)(yRes-1));

                vertices[index].normal = glm::vec3(0.0f, 1.0f, 0.0f);

                vertices[index].tangent = glm::vec3(0.0f, 0.0f, 0.0f);
                vertices[index].bitangent = glm::vec3(0.0f, 0.0f, 0.0f);
                index++;
            }
        }

        // create indices for GL_TRIANGLES
        GLuint* indices = new GLuint[(yRes - 1) * (xRes - 1) * 2 * 3]; // memory for indices - 2 triangles per square, 3 indices per triangle
        index = 0;
        for (unsigned int y = 0; y < yRes - 1; y++) {
            for (unsigned int x = 0; x < xRes - 1; x++) {
                indices[index++] = yRes * y + x;
                indices[index++] = yRes * y + x + xRes;
                indices[index++] = yRes * y + x + xRes + 1;

                indices[index++] = yRes * y + x;
                indices[index++] = yRes * y + x + xRes + 1;
                indices[index++] = yRes * y + x + 1;
            }
        }

        size_t vertex_size = (size_t)xRes * (size_t)yRes * sizeof(vertex);
        size_t index_size = (size_t)((xRes - 1) * (yRes - 1) * 2 * 3) * sizeof(GLuint);

        calculateTangentsIndexed(vertices, (size_t)(xRes*yRes), indices, (size_t)((yRes - 1) * (xRes - 1) * 2 * 3));

        addVertexDataIndexed((GLfloat*)vertices, indices, vertex_size, index_size, GL_TRIANGLES, true, true, true, true);

        if (vertices != nullptr) delete[] vertices;
        if (indices != nullptr) delete[] indices;
    }
}

I desperatly need some help from far more seasoned people then me here. It seems I am close, but missing some crucial detail. I first thought, that my base normals are off, but as you can see at the cube, the yellow normals seem alright for me. However, for the plane the normals seem to be off - laying flat on the plane pointing in the diagonal.

I found the problem for the plane - it was indeed a normal issue, but other than I expected:
I organized my vertex arrays for the shader as follows:

layout (location = 0) in vec3 vertexPosition_modelspace;
layout (location = 1) in vec3 vertexNormal;
layout (location = 2) in vec2 texture_coord;
layout (location = 3) in vec3 vertex color;
layout (location = 4) in vec3 aTangent;
layout (location = 5) in vec3 aBitangent;

but I organized my struct which is used for the float array passed to opengl as follows:

struct vertex {
   vertex_pos pos;
   vertex_color color;
   vertex_tex_coord texCoord;
   vertex_normal normal;
   vertex_tangent tangent;
   vertex_bitangent bitangent;
};

So the normal and colors got mixed up. Once fixed the normal mapping for the plane works like a charme:

The problem with the cube still exists:

But I think, this have to be a different bug, since the lighting and normal debug display (yellow lines) indicate that the normals are correct for the cube.
Since the normal mapping for the plane now works, the shader and the tangent/bitangent calculation have to be correct now also. Since they are also used by the cube, the bug needs to be elsewhere.

I have now also found the problem with the cube. In fact it was not a problem of the cube, the position of the light was not transferred correctly to the shader.