I'm trying to do normal mapping in a deferred renderer and I'm stuck on how to implement normal maps. I have a bool that passes whether or not to use a normal mapped value and thus, whether to calculate the TBN matrix. My vertex code for the geometry pass looks as follows:

#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent; // Optional Texture coordinates 
layout (location = 4) in vec3 aBitangent; // Optional Texture coordinates 

out vec3 FragPos;
out vec2 TexCoords;
out vec3 Normal;
out mat3 TBN;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform bool hasNormalMap;

void main()
    vec4 worldPos = model * vec4(aPos, 1.0);
    FragPos = worldPos.xyz; 
    TexCoords = aTexCoords;
    Normal = transpose(inverse(mat3(model))) * aNormal;

        vec3 T = normalize(vec3(model * vec4(aTangent, 0.0)));
        vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
        // re-orthogonalize T with respect to N
        T = normalize(T - dot(T, N) * N);
        // then retrieve perpendicular vector B with the cross product of T and N
        vec3 B = cross(N, T);
        mat3 TBN = mat3(T, B, N);

    gl_Position = projection * view * worldPos;

Here is where I am confused:

In my calculation, I multiplied T and N by the model matrix which should have moved it into world space. Now transposing (T,B,N) should move me back into model space (I think, I'm not sure). In the fragment shader how do I use the TBN to calculate the normal in world space?

If there are better approaches, they are welcome. Thank you.

Update: I removed the transposing of the TBN as there's no reason to transform into tangent space if we want to pass it in world space. Now that we have the TBN matrix in the fragment shader, how do we apply it so that the normal is the correct value for lighting? Currently I've done:

#version 410 core

layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;
in mat3 TBN;

struct Material {
    sampler2D diffuseMap;
    sampler2D specularMap;
    sampler2D normalMap;
    float shininess;

uniform Material material;
uniform bool hasNormalMap;

void main()
    // store the fragment position vector in the first gbuffer texture
    gPosition = FragPos;

    // also store the per-fragment normals into the gbuffer
    gNormal = normalize(Normal);

        gNormal = texture(material.normalMap, TexCoords).rgb * TBN;
        gNormal = normalize(gNormal);

    // and the diffuse per-fragment color
    gAlbedoSpec.rgb = texture(material.diffuseMap, TexCoords).rgb;

    // store specular intensity in gAlbedoSpec's alpha component
    gAlbedoSpec.a = texture(material.specularMap, TexCoords).r;

but that doesn't feel right. I imagine that I would transform the sampled value from the normal map by the TBN matrix to get it in world space. Am I missing something?


1 Answer 1


When using a deferred rendering pipeline, your normal-mapping workflow is as follows:

Writing to GBuffer:

Vertex shader:

Compute T, B, and N vertex properties. Multiply by normal matrix. Construct the TBN matrix. Pass it to the fragment shader.

vec3 T = normalize(vec3(normalMat * ModelSpaceTangent));
vec3 B = normalize(vec3(normalMat * ModelSpaceBiTangent));
vec3 N = normalize(vec3(normalMat * ModelSpaceNormal));
mat3 tbn = mat3(T, B, N);
outdata.TBN = tbn;

Fragment shader Sample the normal map. Convert the normal map sample to something usable. Transform from texture space, to world space. Store the world space normal in the GBuffer texture

vec3 normal_rgb = texture(normalMap,txc).rgb; 
normal_rgb = normalize(normal_rgb * 2.0 - 1.0); 
normal_rgb = normalize(indata.TBN * normal_rgb); 
normal_buffer_out = vec4(normal_rgb,1.0); 

Then use it as you see fit for lighting/reflection/refraction calculations in the lighting/shading stage of your renderer.


You must log in to answer this question.

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