HLSL Normal Mapping Matrix Multiplication

dk123 picture dk123 · May 15, 2013 · Viewed 8.3k times · Source

I'm currently working in directx9 and have the following code for my normal mapping:

(Vertex Shader):

float4x4 gWorldMatrix;
float4x4 gWorldViewProjectionMatrix;

float4 gWorldLightPosition;
float4 gWorldCameraPosition;

struct VS_INPUT 
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
   float3 mTangent: TANGENT;
   float3 mBinormal: BINORMAL;
   float2 mUV: TEXCOORD0;
};

struct VS_OUTPUT 
{
   float4 mPosition : POSITION;
   float2 mUV: TEXCOORD0;
   float3 mLightDir: TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 T: TEXCOORD3;
   float3 B: TEXCOORD4;
   float3 N: TEXCOORD5;
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );

   Output.mUV = Input.mUV;

   float4 worldPosition = mul( Input.mPosition, gWorldMatrix );

   float3 lightDir = worldPosition.xyz - gWorldLightPosition.xyz;
   Output.mLightDir = normalize( lightDir );

   float3 viewDir = normalize( worldPosition.xyz - gWorldCameraPosition.xyz );
   Output.mViewDir = viewDir;

   //object space=>world space
   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );
   Output.N = normalize( worldNormal );

   float3 worldTangent = mul( Input.mTangent, (float3x3)gWorldMatrix );
   Output.T = normalize( worldTangent );

   float3 worldBinormal = mul( Input.mBinormal, (float3x3)gWorldMatrix );
   Output.B = normalize( worldBinormal);

   return Output;
}

(Pixel Shader)

struct PS_INPUT
{
   float2 mUV : TEXCOORD0;
   float3 mLightDir: TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 T: TEXCOORD3;
   float3 B: TEXCOORD4;
   float3 N: TEXCOORD5;
};

sampler2D DiffuseSampler;
sampler2D SpecularSampler;
sampler2D NormalSampler;

float3 gLightColor;

float4 ps_main(PS_INPUT Input) : COLOR
{
   //read normal from tex
   float3 tangentNormal = tex2D( NormalSampler, Input.mUV ).xyz;
   tangentNormal = normalize( tangentNormal * 2 - 1 ); //convert 0~1 to -1~+1.

   //read from vertex shader
   float3x3 TBN = float3x3( normalize(Input.T), normalize(Input.B),
      normalize(Input.N) ); //transforms world=>tangent space

   TBN = transpose( TBN ); //transform tangent space=>world

   float3 worldNormal = mul( TBN, tangentNormal ); //note: mat * scalar
   //(since TBN is row matrix)

   float3 lightDir = normalize( Input.mLightDir ); 
   float3 diffuse = saturate( dot(worldNormal, -lightDir) );

   float4 albedo = tex2D( DiffuseSampler, Input.mUV );
   diffuse = gLightColor * albedo.rgb * diffuse;

   float3 specular = 0;
   if ( diffuse.x > 0 )
   {
      float3 reflection = reflect( lightDir, worldNormal );
      float3 viewDir = normalize( Input.mViewDir );

      specular = saturate( dot(reflection, -viewDir) );
      specular = pow( specular, 20.0f );

      //further adjustments to specular (since texture is 2D)
      float specularIntensity = tex2D( SpecularSampler, Input.mUV );
      specular *= specularIntensity * gLightColor;
   }

   float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;

   return float4(ambient + diffuse + specular, 1);
}

The code works, but I don't quite understand why I need to do the

TBN = transpose( TBN ); in the pixel shader.

The TBN values I passed through the Vertex Shader are those in world space (hence why I multiplied gWorldMatrix), yet I'm told that

float3x3 TBN = float3x3( normalize(Input.T), normalize(Input.B), normalize(Input.N) );

transforms world=>tangent(surface) space.

Why is this?

Answer

Gnietschow picture Gnietschow · May 15, 2013

You need the line

TBN = transpose( TBN ); 

because you're multipling your tangent-space normal from the right to the matrix. Therefore it's considered as a column vector, while the base vectors are in the rows of the matrix. So the matrix must be transposed, that the base transformation can be applied. You can omit the transposition, if youre switch the multiplication to

float3 worldNormal = mul( tangentNormal, TBN );

Because your multiplied the T,N and B vector with the worldmatrix your TBN matrix transforms from tangent space to world space (TBN transforms into object-space, after that world transforms into worldspace). Other implementations multiply the TBN with the world inverse transpose matrix. With the resulting TBN you can transform the light vector from world into tangent space and compare it to the tangent normal. So I think the one who told you that TBN transforms world to tangent space uses this approach (It saves some performance, because the heavy matrix-operations are done in the vertexshader).