I have normal mapping with a directional light in place. I noticed the lighting looks off but am unsure why it is happening. I believe it is related to the TBN matrix and possibly the handedness fix that is in place. Lighting is being converted into tangent space. The two images below compare the lighting with and without the handedness impacting the TBN matrix:

You can see the issues with the spartan, especially when looking at the abdomen/chest area. Spartan Handedness

Outputting the light direction also looks funky with and without handedness fix in place. Spartan Handedness Light Direction

It appears that disabling the handedness fix for calculating the TBN matrix seems to almost fix the issue. However, you can see that I still get some funky lighting issues (looking at the underside of the visor).

  • Vertex Shader
#version 330 core
// Vertex Shader
// Normal Mapping
// OGL4 Shader Language Cookbook Pg. 159

layout (location = 0) in vec3 vertex_position;
layout (location = 1) in vec3 vertex_normal;
layout (location = 2) in vec2 tex_coords;
layout (location = 3) in vec3 vertex_tangent;
layout (location = 4) in vec3 vertex_bittangent;

// ======== Structure Definitions =========
struct LightInfo {
    vec4 light_in_world;

// ============ Global Uniforms ============
layout (std140) uniform uniform_matrices {
    mat4 projection_matrix;
    mat4 view_matrix;

// ============ Shader Uniforms ============
uniform mat4 model_matrix;
uniform mat3 normal_matrix;
uniform LightInfo light_info;

// =============== Output ==================
out vec3 vector_towards_viewer_tangent;
out vec3 light_direction_from_surface_to_lightsource_tangent;
out vec2 frag_tex_coords;

// ========= Function Prototypes ==========
mat3 get_tbn_matrix(vec3 vertex_tangent, vec3 vertex_bittangent, vec3 vertex_normal);
vec3 get_light_direction_eye(vec3 vertex_position_eye_space);

// ================== Main =================
void main(){        
    vec3 vertex_position_eye = (view_matrix * model_matrix * vec4(vertex_position, 1.0)).xyz;
    vec3 light_direction_eye = get_light_direction_eye(vertex_position_eye);

    // TBN Matrix (View/eye space to tangent space)
    mat3 TBN = get_tbn_matrix(vertex_tangent, vertex_bittangent, vertex_normal);
    // Output
    light_direction_from_surface_to_lightsource_tangent = TBN * light_direction_eye;
    vector_towards_viewer_tangent = TBN * normalize(-vertex_position_eye);
    frag_tex_coords = tex_coords;
    // Convert vertex position to clip space for fragment shader
    gl_Position = projection_matrix * view_matrix * model_matrix * vec4(vertex_position, 1.0);

// ========= Function Definitions ===========

mat3 get_tbn_matrix(vec3 vertex_tangent, vec3 vertex_bittangent, vec3 vertex_normal){

    // Convert vertex normal and vertex tangent to eye space
    vec3 vertex_tangent_eye_T = normalize(normal_matrix * vertex_tangent);
    vec3 vertex_normal_eye_N = normalize(normal_matrix * vertex_normal);
    vec3 vertex_bitangent_eye_B = normalize(normal_matrix * vertex_bittangent);

    // Matrix for converting from tangent space to view space
    mat3 TBN = mat3(vertex_tangent_eye_T, vertex_bitangent_eye_B, vertex_normal_eye_N);
    // Matrix for converting from view space to tangent space (inverse); In this case the transpose works 
    // because the TBN matrix is orthogonal (so its the same as doing the inverse but faster)
    TBN =  transpose(TBN);
    return TBN;

vec3 get_light_direction_eye(vec3 vertex_position_eye_space){
    // Convert light to eye space       
    vec3 light_in_world_eye = vec3(view_matrix * light_info.light_in_world);

    // Determine if light_position_in_world is a positional light or directional light
    vec3 light_direction_from_surface_to_lightsource = vec3(0.0f);
    if(light_info.light_in_world.w == 0.0f){
        // Directional light from object to lightsource
        light_direction_from_surface_to_lightsource = normalize(light_in_world_eye);
    } else {
        // Positional light
        // Convert light position to eye space & get direction
        light_direction_from_surface_to_lightsource = normalize(light_in_world_eye - vertex_position_eye_space);

    return light_direction_from_surface_to_lightsource;


  • Fragment Shader
#version 330 core
// Fragment Shader
// Normal Mapping
// OGL4 Shader Language Cookbook Pg. 159

struct SceneLight {
    vec3 scale_light_intensity;
    vec3 ambient_intensity;
    vec3 diffuse_intensity;
    vec3 specular_intensity;

struct DiffuseMaterial {
    sampler2D m_sampler;

struct NormalMaterial {
    sampler2D m_sampler;

// ======= Input from Vertex Shader ========
in vec3 vector_towards_viewer_tangent;
in vec3 light_direction_from_surface_to_lightsource_tangent;
in vec2 frag_tex_coords;

// ============ Shader Uniforms ============
// Lights
uniform SceneLight scenelight;

// Texture
uniform DiffuseMaterial diffuse_material;
uniform NormalMaterial normal_material;

// =============== Output ==================
out vec4 fragment_color;

// ========= Function Prototypes ==========
vec3 phong_lighting(vec3 light_direction_surface_to_source_tangent);

// ================== Main =================
void main() {   
    fragment_color = vec4(phong_lighting(light_direction_from_surface_to_lightsource_tangent), 1.0f);   

    // Testing =============================
    // fragment_color = vec4(light_direction_from_surface_to_lightsource_tangent, 1.0f);
    // =================================

vec3 get_ambient_light(){
    return scenelight.ambient_intensity * vec3(texture(diffuse_material.m_sampler, frag_tex_coords));

vec3 get_diffuse_light(float light_dir_to_normals_dot){
    return scenelight.diffuse_intensity * light_dir_to_normals_dot * vec3(texture(diffuse_material.m_sampler, frag_tex_coords));

vec3 phong_lighting(vec3 light_direction_surface_to_source_tangent){
    vec3 mapped_normals_tangent = texture(normal_material.m_sampler, frag_tex_coords).xyz;
    // Textures store values from 0 to 1.  Normal vectors have values from -1 to 1.
    // This rescales the value to be between -1 and 1;
    mapped_normals_tangent = normalize(2.0 * mapped_normals_tangent - 1.0);

    // Lighting Calculations
    // The value here gives us a scalar for how strong our light is (if light_dir and vertex_normals were at 90 degrees, the result would be zero): pg 80
    float light_dir_to_normals_dot = max(dot(light_direction_surface_to_source_tangent, mapped_normals_tangent), 0.0f);

    // Ambient
    vec3 ambient_light = get_ambient_light();

    // Diffuse
    vec3 diffuse_light = get_diffuse_light(light_dir_to_normals_dot);

    // Combine lights (Note that diffuse and specular are multiplied by the intensity)
    return ambient_light + diffuse_light * scenelight.scale_light_intensity;

  • Handedness Fix (Performed as the model is loaded)
    void update_handedness(){
        // Handedness fix for symmetric UV's
        if (glm::dot(glm::cross(m_normals, m_tangent), m_bitangent) < 0.0f) {
            m_tangent = m_tangent * -1.0f;

In both image examples, light is being supplied as a directional (vec4) light directly above the model: glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)

It seems to me that when the TBN matrix has the handedness fix in place, the lighting looks incorrect. Disabling the fix corrects the lighting, but not fully (as shown by the visor in the 2nd image).

How I can make this lighting look correct? I would prefer to keep the handedness fix in place, and it's not clear to me why this is impacting the lighting so much. Additionally, even when it's disabled, the lighting looks mostly correct, but the visor in the 2nd image gives me some doubt.


1 Answer 1


Update: I have a handedness fix that looks to be correct when it's applied. I still get the weird visor issue, but I'm thinking perhaps it's something funky with the model itself.

The solution I am using is from OpenGL 4 Shading Language Cookbook:

  1. Tangent is now a vec4. The w component is used to track handedness in the shader
  2. w is calculated using the following:
// Vec4 Version             
glm::vec3 tangent_temp = glm::vec3(m_tangent.x, m_tangent.y, m_tangent.z);

// Re-orthogonalize T with respect to N
tangent_temp = glm::normalize(tangent_temp - (glm::dot(m_normals, tangent_temp) * m_normals));

// Handedness fix for symmetric UV's
const float w = glm::dot(glm::cross(m_normals, tangent_temp), m_bitangent) < 0.0f ? -1.0f : 1.0f;

// Update tangent
m_tangent = glm::vec4(tangent_temp.x, tangent_temp.y, tangent_temp.z, w);
  1. In the vertex shader, tangent is now a vec4. The TBN matrix is calculated as follows:
mat3 get_tbn_matrix(vec4 vertex_tangent, vec3 vertex_bittangent, vec3 vertex_normal){

  // Convert vertex normal and vertex tangent to eye space
  vec3 vertex_tangent_eye_T = normalize(normal_matrix * vertex_tangent.xyz);
  vec3 vertex_normal_eye_N = normalize(normal_matrix * vertex_normal);

  // Compute bittangent and update handedness
  vec3 vertex_bitangent_eye_B = normalize(cross(vertex_normal_eye_N, vertex_tangent_eye_T)) * vertex_tangent.w;

  // Matrix for converting from view space to tangent space
  mat3 TBN = mat3(
      vertex_tangent_eye_T.x, vertex_bitangent_eye_B.x, vertex_normal_eye_N.x,
      vertex_tangent_eye_T.y, vertex_bitangent_eye_B.y, vertex_normal_eye_N.y,
      vertex_tangent_eye_T.z, vertex_bitangent_eye_B.z, vertex_normal_eye_N.z 

  return TBN;
  1. Lastly, the following variables are updated and sent to the fragment shader:
    // Output
  light_direction_from_surface_to_lightsource_tangent = TBN * light_direction_eye;
  vector_towards_viewer_tangent = TBN * normalize(-vertex_position_eye);
  frag_tex_coords = tex_coords;

I am happy with this solution, as it allows me to keep the handedness fix in place and have lighting that looks to be correct. I will leave this question open for a few more days in case anyone has something to add (in particular, the underside visor weirdness that still occurs).


You must log in to answer this question.

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