mtrebi
mtrebi

Reputation: 267

Tangent space to World Space (TBN Matrix)

I'm building a renderer using rasterization and depth-buffering in the CPU and now I've included normals maps. Everything works like you can see in the next image:

enter image description here

The issue is that, even that it works, I don't understand WHY! The implementation is against what I think. This is the code to get the normal at each fragment:

const Vector3D TexturedMaterial::getNormal(const Triangle3D& triangle_world, const Vector2D& text_coords) const { 
   Vector3D tangent, bitangent;
   calculateTangentSpace(tangent, bitangent, triangle_world);
   // Gets the normal from a RGB Texture [0,1] and maps it to [-1, 1]
   const Vector3D normal_tangent = (Vector3D) getTextureColor(m_texture_normal, m_texture_normal_width, m_texture_normal_height, text_coords);
   const Vector3D normal_world = TangentToWorld(normal_tangent, tangent, bitangent, normal_tangent);

   return normal_world;
}


void TexturedMaterial::calculateTangentSpace(Vector3D& tangent, Vector3D& bitangent, const Triangle3D& triangle_world) const {
  const Vector3D q1 = triangle_world.v2.position - triangle_world.v1.position;
  const Vector3D q2 = triangle_world.v3.position - triangle_world.v2.position;

  const double s1 = triangle_world.v2.texture_coords.x - triangle_world.v1.texture_coords.x;
  const double s2 = triangle_world.v3.texture_coords.x - triangle_world.v2.texture_coords.x;

  const double t1 = triangle_world.v2.texture_coords.y - triangle_world.v1.texture_coords.y;
  const double t2 = triangle_world.v3.texture_coords.y - triangle_world.v2.texture_coords.y;


  tangent = t2 * q1 - t1 * q2;
  bitangent = -s2 * q1 + s1 * q2;

  tangent.normalize();
  bitangent.normalize();
}

My confusion is here:

const Vector3D TexturedMaterial::TangentToWorld(const Vector3D& v, const Vector3D& tangent, const Vector3D& bitangent, const Vector3D& normal) const {
  const int handness = -1; // Left coordinate system
  // Vworld = Vtangent * TBN
  Vector3D v_world = {
    v.x * tangent.x + v.y * bitangent.x + v.z * normal.x,
    v.x * tangent.y + v.y * bitangent.y + v.z * normal.y,
    v.x * tangent.z + v.y * bitangent.z + v.z * normal.z,
  };
  // Vworld = Vtangent * TBN(-1) = V * TBN(T)
  Vector3D v_world2 = {
    v.x * tangent.x   + v.y * tangent.y   + v.z * tangent.z,
    v.x * bitangent.x + v.y * bitangent.y + v.z * bitangent.z,
    v.x * normal.x    + v.y * normal.y    + v.z * normal.z,
  };

  v_world2.normalize();
  // return handness * v_world; --> DOES NOT WORK
  return handness * v_world2; --> WORKS
}

Assuming that I'm working with row vectors:

V = (Vx, Vy, Vz)

      [Tx Ty Tz]
TBN = [Bx By Bz]
      [Nx Ny Nz]

          [Tx Bx Nx]
TBN(-1) = [Ty By Ny] // Assume basis are orthogonal TBN(-1) = TBN(T)
          [Tz Bz Nz]

Then, if T, B and N are the basis vectors of the TBN expressed in the world coordinate system the transformations should be:

 Vworld = Vtangent * TBN
 Vtangent = Vworld * TBN(-1)

But, in my code I am doing exactly the opposite. To transform the normal in tangent space to world space I am multiplying by the inverse of the TBN.

What I am missing or misunderstanding? Is the assumption that T, B and N are expressed in the world coordinate system wrong?

Thank you!

Upvotes: 1

Views: 5165

Answers (1)

Nico Schertler
Nico Schertler

Reputation: 32597

Your reasoning is correct - the second version is wrong. A more intuitive way to see that is analyzing what happens when the tangent space normal is (0, 0, 1). In this case, you obviously want to use the triangle normal, which is exactly what the first version should do.

However, you are feeding a wrong parameter:

const Vector3D normal_world = TangentToWorld(normal_tangent,
                                             tangent, bitangent, normal_tangent);

The last parameter needs to be the triangle normal, not the normal you fetch from the texture.

Upvotes: 2

Related Questions