Search code examples
openglglslfragment-shadervertex-shader

Implementing Normal Mapping using OpenGL/GLSL


I'm learning GLSL and trying to implement some lighting and mapping tricks. I'm working with ShaderDesigner tool. After coding normal mapping I recognized that my model illumination looks not real. Here is my code and some pictures. If it possible tell me what is my problem.

Vertex Shader

#define MAX_LIGHTS 1

struct LightProps
{
    vec3 direction[MAX_LIGHTS];
};

attribute vec3 tangent;
attribute vec3 bitangent;

varying LightProps lights;

void main()
{
    vec3 N = normalize(gl_NormalMatrix*gl_Normal);
    vec3 T = normalize(gl_NormalMatrix*tangent);
    vec3 B = normalize(gl_NormalMatrix*bitangent);

    mat3 TBNMatrix = mat3(T,B,N);

    vec4 vertex = gl_ModelViewMatrix*gl_Vertex;
    for(int i = 0; i < MAX_LIGHTS; i++)
    {
        vec4 lightPos = gl_LightSource[i].position;
        lights.direction[i] = vec3(lightPos.w > 0 ? lightPos-vertex : lightPos);
        lights.direction[i] *= TBNMatrix;
    }

    gl_TexCoord[0] = gl_MultiTexCoord0;
    gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
} 

Fragment Shader

#define MAX_LIGHTS 1
struct LightProps
{
    vec3 direction[MAX_LIGHTS];
};

uniform sampler2D textureUnit;
uniform sampler2D normalTextureUnit;
uniform vec4 TexColor;

varying LightProps lights;

void main()
{
    vec3 N = normalize(texture2D(normalTextureUnit,gl_TexCoord[0].st).rgb*2.0-1.0);

    vec4 color = vec4(0,0,0,0);
    for(int i = 0; i < MAX_LIGHTS; i++)
    {
        vec3 L = lights.direction[i];
        float dist = length(L);
        L = normalize(L);

        float NdotL = max(dot(N,L),0.0);

        if(NdotL > 0)
        {
            float att = 1.0;
            if(gl_LightSource[i].position.w > 0)
            {
                att = 1.0/ (gl_LightSource[i].constantAttenuation +
                gl_LightSource[i].linearAttenuation * dist +
                gl_LightSource[i].quadraticAttenuation * dist * dist);
            }
            
            vec4 ambient = gl_FrontLightProduct[i].ambient;
            vec4 diffuse = clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
        
            color += att*(ambient+diffuse);
        }
    }

    vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
    gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
}

I set TexColor to (0.3,0.3,0.3,1.0) and take screenshots:

Screenshot

  Screenshot

There is little bit lighting when I rotate camera and light to left, but when I rotate to right the plane got fully illuminated.I think there is something wrong because without normal mapping plane looks same from to sides. Here is normal texture. Thanks in advance.

Normal Map:

  Normal Texture


Solution

  • That normal map is in tangent-space, but you are treating it as object-space.

    You need a bitangent and/or tangent vector per-vertex in addition to your normal in order to form the basis to perform transformation into and out of tangent-space. This matrix is often referred to as simply TBN.

    You have two options here:

    1. Transform all of your lighting direction vectors into tangent-space

      • Useful for forward-shading, can be done in a vertex shader
    2. Transform your normal map from tangent-space back to view-space

      • Required by deferred-shading, must be done in fragment shader

    Both options require the construction of a TBN matrix, and if your tangent-space basis is orthogonal (modeling software like Assimp can be configured to do this for you) you can transpose the TBN matrix to do either one.

    You are implementing forward-shading, so solution #1 is the approach you should take.


    Below is a rough overview of the necessary steps for solution #1. Ordinarily you would do the calculation of the lighting direction vector in the vertex shader for better performance.

    Vertex Shader to Transform of Lighting Vectors into Tangent-space:

    attribute vec3 tangent;
    attribute vec3 bitangent;
    
    varying vec3 N;
    varying vec3 V;
    varying vec3 E;
    
    varying vec3 T;
    varying vec3 B;
    
    void main()
    {
        N = normalize(gl_NormalMatrix*gl_Normal);
        V = vec3(gl_ModelViewMatrix*gl_Vertex);
        E = normalize(-V);
    
        T = normalize(gl_NormalMatrix*tangent);
        B = normalize(gl_NormalMatrix*bitangent);
    
        gl_TexCoord[0] = gl_MultiTexCoord0;
        gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
    }
    

    Fragment Shader to Transform Lighting Vectors into Tangent-space:

    varying vec3 N;
    varying vec3 V;
    varying vec3 E;
    
    varying vec3 B;
    varying vec3 T;
    
    uniform sampler2D textureUnit;
    uniform sampler2D normalTextureUnit;
    uniform vec4 TexColor;
    
    #define MAX_LIGHTS 1
    
    void main()
    {
        // Construct Tangent Space Basis
        mat3 TBN = mat3 (T, B, N);
    
        vec3 normal = normalize (texture2D(normalTextureUnit,gl_TexCoord[0].st).xyz*2.0 - 1.0);
    
        vec4 color = vec4(0,0,0,0);
        for(int i = 0; i < MAX_LIGHTS; i++)
        {
            vec4 lightPos = gl_LightSource[i].position;
            vec3 L = lightPos.w > 0 ? lightPos.xyz - V : lightPos;
    
            L *= TBN; // Transform into tangent-space
    
            float dist = length(L);
            L = normalize(L);
    
            float NdotL = max(dot(L,N),0.0);
            if(NdotL > 0)
            {
                float att = 1.0;
                if(lightPos.w > 0)
                {
                    att = 1.0/ (gl_LightSource[i].constantAttenuation +
                    gl_LightSource[i].linearAttenuation * dist +
                    gl_LightSource[i].quadraticAttenuation * dist * dist);
                }
    
                vec4 diffuse =  clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
                color += att*gl_FrontLightProduct[i].ambient + diffuse;
            }
        }
    
        vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
        gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
    }
    

    There is a tutorial here that should fill in the gaps and explain how to compute tangent and bitangent.