Tangent space calculation at UVs bounds


I’m trying to implement correct normal mapping with tangents and binormals pre-calculation. To do this pre-calculation, I’ve applied the method found there : http://www.terathon.com/code/tangent.html .
With this calculation method, the lighting is generally fine, but discontinuities appear on the model though. After debugging in the shader, I found out that it was a tangents and binormals discontinuity problem, happening at texture bounds (see attachments).

According to what I understood, the method calculates tangents and binormals depending on uvs variations in each directions, therefore there could be a uvs gradient discontinuity that leads to miscalculation ? (I’m only guessing)

Doesn’t this method handle this case, or is there a way to correct it ?

Yes, you need correct UVs in order to calculate correct tangents and bitangent with Lengyel’y method. Looking at the UV screen shot it is apparent that you’ve got a huge discontinuity at approx. v=0.5

You should redefined the UVs for the object manually or use a better unwrapping method. Where did you author this cube like shape? Blender?

Edit: By correct I mean of course UVs that make the textures appear correct. Mathematically you get what you ask for even with apparently incorrect UVs so the calculation is not invalid in itself but the result appears to be invalid. You also checkout the complementary material by former Crytek employee Martin Mittring here.

Thanks for the response :slight_smile:
My UVs were calculated and exported with ZBrush (I put the colormap in attachment).
If these UVs aren’t correct, how should I create them ?
And also : if I display my mesh in 3DS Max with the same maps and Uvs, this discontinuities don’t appear.

This unwrap looks funny for a cube. :slight_smile:

Please post your shader code.

This unwrap looks funny for a cube. :slight_smile:

Well, it seems to be the ZBrush-style for unwrapping uvs from arbitrary meshes :slight_smile:

My shader code :

Vertex Shader :

#version 330

uniform mat4 un_projMatrix;
uniform mat4 un_modelViewMatrix;

in vec3 in_vPosition;
in vec3 in_vNormal;
in vec2 in_vTexCoord;
in vec3 in_vTangent;
in vec3 in_vBinormal;

smooth out vec2 var_vTexCoord;
smooth out vec3 var_vNormalT;

smooth out vec3 var_vToEye;
smooth out vec3 var_vToLight;

const vec3 cst_lightPos = vec3(0.0, 0.0, 1.0);

void main()
    gl_Position = un_projMatrix * un_modelViewMatrix * vec4(in_vPosition, 1.0);

    // Note : uncorrect normal matrix if non uniform scales are applied
    mat3 normalMatrix = mat3(un_modelViewMatrix);

    mat3 TBNMatrixTp = normalMatrix * mat3(in_vTangent, in_vBinormal, in_vNormal);

    var_vNormalT = normalMatrix * in_vNormal;
    var_vTexCoord = in_vTexCoord;

    vec3 vPositionT = (un_viewMatrix * vec4(in_vPosition, 1.0)).xyz;
    var_vToEye = (- vPositionT) * TBNMatrixTp;

    vec3 lightPosT = cst_lightPos; // Use non transformed light position
    var_vToLight = (lightPosT - vPositionT) * TBNMatrixTp;

Fragment Shader :

#version 330

uniform sampler2D un_normalMap;
uniform sampler2D un_colorMap;

smooth in vec2 var_vTexCoord;
smooth in vec3 var_vNormalT;

smooth in vec3 var_vToEye;
smooth in vec3 var_vToLight;

out vec4 out_fragColor;

const vec3 cst_lightAmbient = vec3(0.2, 0.2, 0.2);
const vec3 cst_lightDiffuse = vec3(0.6, 0.6, 0.6);
const vec3 cst_lightSpecular = vec3(1.0, 1.0, 1.0);

const float cst_matAmbientFactor = 1.0;
const float cst_matDiffuseFactor = 1.0;
const float cst_matSpecularFactor = 1.0;
const float cst_matShininess = 20.0;

vec3 GetDiffuseColor()
    return texture2D(un_colorMap, var_vTexCoord).rgb;

vec3 GetNormal()
    return texture2D(un_normalMap, var_vTexCoord).rgb;

vec4 CalculateLightingFactor()
    vec3 vToEyeN = normalize(var_vToEye);
    vec3 vToLightN = normalize(var_vToLight);

    vec3 normal = normalize(GetNormal() * 2.0 - vec3(1.0));

    vec3 ambientFactor = cst_matAmbientFactor * cst_lightAmbient;
    vec3 diffuseFactor = cst_matDiffuseFactor * cst_lightDiffuse * max(dot(vToLightN, normal), 0.0);
    vec3 specularFactor = cst_matSpecularFactor * cst_lightSpecular * pow(max(dot(reflect(- vToLightN, normal), vToEyeN), 0.0), cst_matShininess);

    return vec4(ambientFactor + diffuseFactor + specularFactor, 1.0);

void main()
    vec4 color = vec4(GetDiffuseColor(), 1.0);
    vec4 littenColor = CalculateLightingFactor() * color;

    out_fragColor = littenColor;

Once again, 3DS Max displays perfectly my mesh, so the problem may come either from the tangent space calculation or from the shaders…I hope it is my shaders :slight_smile:

Once again, 3DS Max displays perfectly my mesh, so the problem may come either from the tangent space calculation or from the shaders…I hope it is my shaders

Nope, discontinuous UVs are not the result of the tangent space calculation and, as far as I can tell, not the result of the shader.

However, are you sure you upload the UVs correctly to your buffer object? I feel that if 3DSMax displays it correctly the coordinates should be correct in principle but may be incorrectly handled by your application code.

The Vbos upload looks alright to me, but there still might be something wrong somewhere :whistle:

My Vbos initialization looks like this :

    glGenBuffers(1, &m_positionsBufferId);
    glGenBuffers(1, &m_normalsBufferId);
    glGenBuffers(1, &m_texCoordsBufferId);
    glGenBuffers(1, &m_tangentsBufferId);
    glGenBuffers(1, &m_binormalsBufferId);
    glGenBuffers(1, &m_indexesBufferId);

    glBindBuffer(GL_ARRAY_BUFFER, m_positionsBufferId);
    glBufferData(GL_ARRAY_BUFFER, m_positionsVector.size() * sizeof(GLfloat), &m_positionsVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, m_normalsBufferId);
    glBufferData(GL_ARRAY_BUFFER, m_normalsVector.size() * sizeof(GLfloat), &m_normalsVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, m_texCoordsBufferId);
    glBufferData(GL_ARRAY_BUFFER, m_texCoordsVector.size() * sizeof(GLfloat), &m_texCoordsVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, m_tangentsBufferId);
    glBufferData(GL_ARRAY_BUFFER, m_tangentsVector.size() * sizeof(GLfloat), &m_tangentsVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, m_binormalsBufferId);
    glBufferData(GL_ARRAY_BUFFER, m_binormalsVector.size() * sizeof(GLfloat), &m_binormalsVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexesBufferId);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexesVector.size() * sizeof(GLuint), &m_indexesVector.front(), GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

And the Vbos drawing :


    // ...
    // Send uniforms to shader

    GLuint positionsAttribLocation = 0;
    GLuint normalsAttribLocation = 0;
    GLuint texCoordsAttribLocation = 0;
    GLuint tangentsAttribLocation = 0;
    GLuint binormalsAttribLocation = 0;

    positionsAttribLocation = m_shader->GetAttribLocation("in_vPosition");
    glBindBuffer(GL_ARRAY_BUFFER, m_positionsBufferId);
    glVertexAttribPointer(positionsAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, NULL);

    normalsAttribLocation = m_shader->GetAttribLocation("in_vNormal");
    glBindBuffer(GL_ARRAY_BUFFER, m_normalsBufferId);
    glVertexAttribPointer(normalsAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, NULL);

    texCoordsAttribLocation = m_shader->GetAttribLocation("in_vTexCoord");
    glBindBuffer(GL_ARRAY_BUFFER, m_texCoordsBufferId);
    glVertexAttribPointer(texCoordsAttribLocation, 2, GL_FLOAT, GL_FALSE, 0, NULL);

    tangentsAttribLocation = m_shader->GetAttribLocation("in_vTangent");
    glBindBuffer(GL_ARRAY_BUFFER, m_tangentsBufferId);
    glVertexAttribPointer(tangentsAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, NULL);

    binormalsAttribLocation = m_shader->GetAttribLocation("in_vBinormal");
    glBindBuffer(GL_ARRAY_BUFFER, m_binormalsBufferId);
    glVertexAttribPointer(binormalsAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, NULL);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexesBufferId);

    glDrawElements(GL_TRIANGLES, 3 * m_trianglesCount, GL_UNSIGNED_INT, 0);


    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);


I’ve also tested it on the UDK, and same as 3DS Max : no problem…
Anyway, thanks for the help, I will have a look at it again in some days with a clearer mind :slight_smile: