I've been trying to implement normal mapping into my custom OpenGL shader. The result I'm getting is almost there, but it looks like the Point Light is not illuminating the mesh evenly.

For context, I recently added lighting, added a Point Light and a rectangle with a texture to do regular lighting and got this result:


This is a desired result, the Point Light (it's the small yellow cube above the rectangle) illuminates the rectangle with the brick texture evenly.

I then wanted to add a normal map, so instead of using the vertex normals, I'd use normals from another texture. The result I'm getting is this:


The result is almost there, the normal map is being applied, and the bricks almost look 3D. The only problem is that one corner of the mesh remains dark, even though the light is exactly in the middle of the rectangle, so I'd expect the illumination to be even.

In case it helps, here's a gif that shows the Point Light moving in an 8 pattern above the rectangle:


My current attempt is to get the normals from the normal map and convert them from Tangent Space to View Space (where the lighting takes effect already).

Here is the Vertex Shader I'm using:

// vertex attributes
in vec3 colour;
in vec3 normal;
in vec2 texCoord;
in vec3 tangent;
in vec3 bitangent;

// output to fragment shader
out vec4 outColour;
out vec2 outTexCoord;
out vec3 outNormal_view;
out vec3 outLightPos_view;
out vec3 outPos_view;
out mat3 outTBN;

// light position in global space
uniform vec3 lightPos;

void main() {
    // vertex positions
    vec4 pos = matrix_projection *matrix_view *matrix_model *position;
    gl_Position = pos;

    // convert light and vertex positions to view space and pass them to fragment shader
    vec3 lightPos_view = vec3((matrix_view *vec4(lightPos, 1.0)).rgb);
    vec3 pos_view = vec3(matrix_view *matrix_model *position);
    outLightPos_view = lightPos_view;
    outPos_view = pos_view;

    // convert vertex normals to view spacce and pass them to fragment shader
    vec3 normal_world = mat3(transpose(inverse(matrix_view *matrix_model))) *normal.rgb;
    outNormal_view = normal_world;

    // TBN matrix to convert normals from tangent space to view space
    vec3 normal_view    = normalize( vec3(matrix_view *matrix_model * vec4(normal   , 0.0)) );
    vec3 tangent_view   = normalize( vec3(matrix_view *matrix_model * vec4(tangent  , 0.0)) );
    vec3 bitangent_view = normalize( vec3(matrix_view *matrix_model * vec4(bitangent, 0.0)) );
    mat3 TBN = mat3( tangent_view, bitangent_view, normal_view );
    outTBN = TBN;

    outColour = vec4(colour.rgb, 1.0);

    outTexCoord = texCoord;

And here is the Fragment Shader:

// input from vertex shader
in vec4 outColour;
in vec2 outTexCoord;
in vec3 outNormal_view;
in vec3 outLightPos_view;
in vec3 outPos_view;
in mat3 outTBN;

// texture and it's normal map
uniform sampler2D image;
uniform sampler2D image_normal;

void main() {

    // normals from texture to view space
    vec3 norm = avdl_texture(image_normal, outTexCoord).rgb;
    norm = normalize(norm * 2.0 - 1.0);
    norm = normalize(outTBN *norm);

    // light values in view space
    vec3 lightColour = vec3(1.0, 1.0, 1.0);
    vec3 lightDir = normalize(outLightPos_view -outPos_view.rgb);

    // `outNnormal_view` is using vertex normals and it works
    // `norm` is using normal map normals, and it shows the dark corner
    vec3 normal_view = outNormal_view;
    //vec3 normal_view = norm;
    float diff = max(dot(normal_view.rgb, lightDir), 0.0);
    vec3 diffuse = diff * lightColour;

    gl_FragColor = vec4(outColour.rgb *diffuse, 1.0) *texture2D(image, outTexCoord);


I'm still learning, but I can confirm in this specific scenario, the rectangle vertices have the Normal (0, 1, 0), Tangent (1, 0, 0) and Bitangent (0, 0, 1). From what I understand this is how they should look like on a flat surface that points up, but I'm not sure if I'm messing something up in the TBN calculations.

For reference, I've been using the tutorial from here: https://learnopengl.com/Advanced-Lighting/Normal-Mapping - the texture and normal map were taken directly from there, for learning purposes.


1 Answer 1


Unsurprisingly I missed something that was throwing all the normal calculations off. I recently added gamma correction into my renderer, so the following line:

vec3 norm = avdl_texture(image_normal, outTexCoord).rgb;`

Returns the normal in Linear Space. All I needed to do is convert that to sRGB space with the following lines:

float gamma = 2.2;
norm = pow(norm.rgb, vec3(1.0/gamma));

After that, all the rest of the calculations do the trick, and this is the result I'm getting:

bump map working

  • 2
    \$\begingroup\$ You probably want to store your normal map texture in linear space and not enable sRGB sampling on it. Otherwise you're losing precision in one direction and clumping it in the other. \$\endgroup\$
    – DMGregory
    Dec 21, 2023 at 0:06
  • \$\begingroup\$ that still looks wrong though \$\endgroup\$
    – Raildex
    Dec 21, 2023 at 10:03
  • \$\begingroup\$ @DMGregory Thanks! Saving the texture in linear space makes sense, and will save me more headaches in the future. \$\endgroup\$ Dec 21, 2023 at 16:36
  • \$\begingroup\$ @Raildex Could you elaborate? I was only aiming for the ground to look 3D with a bump map, nothing else, the result looks exactly as I wanted it. Have you got any advice on what's missing? \$\endgroup\$ Dec 21, 2023 at 16:36
  • \$\begingroup\$ @TomTsagkatos when the light is above the center of the platform, it's radius gets smaller \$\endgroup\$
    – Raildex
    Dec 21, 2023 at 17:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .