Delta Engine Blog

All about multiplatform and game development

The Trick with Mesh.ComputeTangent

Back to more useful programming stuff. Yesterday our modelers encountered a problem when exporting certain objects in 3D Studio Max. After some research I found out that the Tangent data isn't exported from 3D Studio and was rebuilt automatically with ComputeTangent in the engine. Usually this works fine, but several objects (not even very complex ones) had problems with the generated tangents. The texture mapping did fit perfectly, the normal map fits as well, all normals where fine and smoothed, but the TangentMatrix was messed up in the vertex shader.

First I thought there must be something wrong with the exporter and maybe there is just some export tangents option missing. I tested several exporters for the DirectX x file format: Panda exporter (the best exporter for .X files currently available), The Microsoft DirectX Exporter for 3DS Max (doesn't support much), XPorter (warning: Japanese site) and several other. None of them exports any tangent data (or binormals).

I tested developing my own exporter with help of the 3DS Max SDK (first a simple x file, than an ascii exporter and then the IGameExporter method), but there are no get tangent data functions available with the 3DS Max 6 SDK. I found some information in the net about the IGameMesh class in 3DS Max SDK 7 or 8 (8 isn't out yet), but I couldn't get any sample to compile. I also read that the GetTangent methods might return null and then I am where I was before. I also don't think the developer support from 3DS Max (discreet) is any good, it is very hard to get any information or downloads and most (80-90%) of the message board questions are just unanswered.

Well, back to my tangent problem. While searching for solutions for exporting tangent data in 3DS Max , I saw this article (NVMeshMender) from NVidia explaining how to generate tangent and binormal data for vertices. But after a short trip to FxComposer (btw: A new version 1.8 was released, check it out) I saw that NVMeshMender wasn't really the solution. As you can see on the following screenshot the 3DS Max exported mesh is still messed up (again, all position, normal, texture, etc. information is perfectly ok, only the generated tangent data is wrong).
Note: All models shown here are just test models and do not represent any final art. This is just a FxComposer screenshot:

So instead of spending more hours trying to implement some way to generate the tangent data in the 3DS Max exporter, I thought how could I generate the correct tangent data in my engine.

Here is the trick:

  • First check if the vertex declaration is wrong, make sure the used vertex shader declaration is used.
  • Generate texture coordinates and normals if they are missing (usually not for exported objects).
  • Weld (collapse) any vertices with the exact same data (position, normal, texture coordinates and tangent), this optimizes further processing and can improve rendering and minimizes the vertex buffer size.
  • Now if we didn't had any valid tangents, generate them in the following way:
    • Clone the mesh, because we are going to reduce the vertices again and will kill texture coordinates to generate proper tangents.
    • Weld (collapse) the vertices again, this time ignore any texture coordinates and put everything in one big smoothing group if exported model is smoothed (same normals). This won't change vertices with multiple different normals (e.g. a sharp box), only smooth surfaces!
    • Now compute the tangent data with Mesh.ComputeTangent (in my example I didn't need the binormals, which are calculated in the vertex shader)
    • And finally copy all generated tangents back to the original mesh with the untouched texture coordinates (this will duplicate tangents if texture coordinates are different for the same point at different faces).
  • Thats it, optimize the mesh and we are done!

And this is the difference between my method and just using ComputeTangent or NVMeshMender as before (2 simple screenshots from the engine):

Before (just computing tangents based on the vertices):
->

->
And after using my method with the compute tangent helper mesh:

And here is the source code for the main helper method. I use it to convert all meshes (no matter if generated in the engine or loaded from external files) to be compatible for the shader techniques.
Please note that some methods might not be available to you (e.g. the TangentVertex struct or my Graphics class, but these classes are not really used, replace them with your used vertex struct and your DirectX class).

  1. /// <summary>
  2. /// Generate normals and tangents if not present.
  3. /// This method is very important for using shaders, most
  4. /// shader techniques will expect the TangentVertex format!
  5. /// </summary>
  6. /// <param name="someMesh">Mesh we are going to manipulate</param>
  7. public static void GenerateNormalsAndTangentsIfNotPresent(
  8.   ref Mesh someMesh)
  9. {
  10.   if (someMesh == null)
  11.     throw new ArgumentNullException("someMesh",
  12.       "Can't generate normals and tangents without valid mesh.");
  13.  
  14.   // Quick check if vertex declaration of mesh already fits.
  15.   VertexElement[] decl = someMesh.Declaration;
  16.   if (TangentVertex.IsTangentVertexDeclaration(decl))
  17.     // Everything looks fine, leave it that way.
  18.     return;
  19.  
  20.   bool hadNormals = false;
  21.   bool hadTangents = false;
  22.   // Check the first couple of declaration usages
  23.   for (int i = 0; i < 6 && i < decl.Length; i++)
  24.   {
  25.     if (decl[i].DeclarationUsage == DeclarationUsage.Normal)
  26.     {
  27.       hadNormals = true;
  28.       break;
  29.     } // if (decl[i].DeclarationUsage)
  30.     if (decl[i].DeclarationUsage == DeclarationUsage.Tangent)
  31.     {
  32.       hadTangents = true;
  33.       break;
  34.     } // if (decl[i].DeclarationUsage)
  35.   } // for (int)
  36.  
  37.   // Create new mesh and clone everything
  38.   Mesh tempMesh = someMesh.Clone(
  39.     someMesh.Options.Value,
  40.     TangentVertex.VertexElements,
  41.     Graphics.GetDirectXDevice());
  42.   // Destroy current mesh, use the new one
  43.   someMesh.Dispose();
  44.   someMesh = tempMesh;
  45.  
  46.   // Check if we got texture coordinates, if not, generate them!
  47.   bool gotMilkErmTexCoords = false;
  48.   bool gotValidNormals = true;
  49.   bool gotValidTangents = true;
  50.   TangentVertex[] verts =
  51.     (TangentVertex[])someMesh.LockVertexBuffer(
  52.     typeof(TangentVertex),
  53.     LockFlags.None,
  54.     new int[1] { someMesh.NumberVertices });
  55.   // Check all vertices
  56.   for (int num = 0; num < verts.Length; num++)
  57.   {
  58.     // We only need at least 1 texture coordinate different from (0, 0)
  59.     if (verts[num].u != 0.0f ||
  60.       verts[num].v != 0.0f)
  61.       gotMilkErmTexCoords = true;
  62.  
  63.     // All normals and tangents must be valid, else generate them below.
  64.     if (verts[num].normal.IsZero())
  65.       gotValidNormals = false;
  66.     if (verts[num].tangent.IsZero())
  67.       gotValidTangents = false;
  68.  
  69.     // If we found valid texture coordinates and no normals or tangents,
  70.     // there isn't anything left to check here.
  71.     if (gotMilkErmTexCoords == true &&
  72.       gotValidNormals == false &&
  73.       gotValidTangents == false)
  74.       break;
  75.   } // for (num, <, ++)
  76.  
  77.   // If declaration had normals, but we found no valid normals,
  78.   // set hadNormals to false and generate valid normals (see below).
  79.   if (gotValidNormals == false)
  80.     hadNormals = false;
  81.   // Same check for tangents
  82.   if (gotValidTangents == false)
  83.     hadTangents = false;
  84.  
  85.   // Generate dummy texture coordinates, not only useful for tangent
  86.   // generation, but also unit tests display better visual meshes.
  87.   if (gotMilkErmTexCoords == false)
  88.   {
  89.     for (int num = 0; num < verts.Length; num++)
  90.     {
  91.       // Similar stuff as in GenerateTextureCoordinates, very simple and
  92.       // dummy way to generate texture coordinates from object position.
  93.       // Usually only test objects don't have texture coordinates.
  94.       verts[num].u = -0.75f + verts[num].pos.x / 2.0f;
  95.       verts[num].v = +0.75f - verts[num].pos.y / 2.0f +
  96.         verts[num].pos.z / 2.0f;
  97.     } // for (num, <, ++)
  98.   } // if (gotMilkErmTexCoords)
  99.   someMesh.UnlockVertexBuffer();
  100.  
  101.   // Generate normals if this mesh hadn't any.
  102.   if (hadNormals == false)
  103.     someMesh.ComputeNormals();
  104.  
  105.   // Ok, first weld vertices which should be together anyway.
  106.   // This optimizes rendering and enables us to do correct tangent
  107.   // calculations below. For example a mesh using around 100 faces might
  108.   // have around 300 vertices, if each face has its own 3 vertices. But
  109.   // when collapsing same vertices together we can get this down to 100-150
  110.   // vertices (which saves half of the bandwidth and vertex memory).
  111.   WeldEpsilons weldEpsilons = new WeldEpsilons();
  112.  
  113.   // Position and normal should be the same (or nearly the same)
  114.   weldEpsilons.Position = 0.0001f;
  115.   weldEpsilons.Normal = 0.0001f;
  116.  
  117.   // Rest of the weldEpsilons values can stay 0, we don't use them
  118.   // or if they are used (like texture coord or already generated tangent
  119.   // data, they must be the same for vertices we want to collapse).
  120.   someMesh.WeldVertices(
  121.     // Don't collapse faces that are not smoothend together.
  122.     WeldEpsilonsFlags.WeldPartialMatches,
  123.     // Use the epsilon values defined above
  124.     weldEpsilons,
  125.     // Let WeldVertices generate the adjacency
  126.     null);
  127.  
  128.   // Need to generate tangents because mesh doesn't provide any yet?
  129.   if (hadTangents == false)
  130.   {
  131.     // Huston, we might have a problem!
  132.     // If the vertices for a smoothend point exist several times the
  133.     // DirectX ComputeTangent method is not able to threat them all the
  134.     // same way (see Screenshots on my post on abi.exdream.com).
  135.     // To circumvent this, we collapse all vertices in a cloned mesh
  136.     // even if the texture coordinates don't fit. Then we copy the
  137.     // generated tangents back to the original mesh vertices (duplicating
  138.     // the tangents for vertices at the same point with the same normals
  139.     // if required). This happens usually with models exported from 3DSMax.
  140.  
  141.     // Clone mesh just for tangent generation
  142.     Mesh dummyTangentGenerationMesh = someMesh.Clone(
  143.       someMesh.Options.Value,
  144.       someMesh.Declaration,
  145.       Graphics.GetDirectXDevice());
  146.  
  147.     // Reuse weldEpsilons, just change the TextureCoordinates, which we
  148.     // don't care about anymore. TextureCoordinate expects 8 float values.
  149.     weldEpsilons.TextureCoordinate =
  150.       new float[] { 1, 1, 1, 1, 1, 1, 1, 1 };
  151.     // Rest of the weldEpsilons values can stay 0, we don't use them.
  152.     dummyTangentGenerationMesh.WeldVertices(
  153.       // Don't collapse faces that are not smoothend together.
  154.       WeldEpsilonsFlags.WeldPartialMatches,
  155.       // Use the defined epsilon values
  156.       weldEpsilons,
  157.       // Let WeldVertices generate the adjacency
  158.       null);
  159.  
  160.     // Compute tangents with texture channel 0,
  161.     // tangents are in stream 0, binormals are not required
  162.     // and wrapping doesn't help or work anyways (last 0 parameter).
  163.     dummyTangentGenerationMesh.ComputeTangent(0, 0, D3DX.Default, 0);
  164.  
  165.     // Ok, time to copy the smoothly generated tangents back :)
  166.     TangentVertex[] tangentVerts =
  167.       (TangentVertex[])dummyTangentGenerationMesh.LockVertexBuffer(
  168.       typeof(TangentVertex),
  169.       LockFlags.None,
  170.       new int[1] { dummyTangentGenerationMesh.NumberVertices });
  171.     verts =
  172.       (TangentVertex[])someMesh.LockVertexBuffer(
  173.       typeof(TangentVertex),
  174.       LockFlags.None,
  175.       new int[1] { someMesh.NumberVertices });
  176.     for (int num = 0; num < verts.Length; num++)
  177.     {
  178.       // Search for tangent vertex with the exact same position and normal.
  179.       for (int tangentVertexNum = 0; tangentVertexNum <
  180.         tangentVerts.Length; tangentVertexNum++)
  181.         if (verts[num].pos == tangentVerts[tangentVertexNum].pos &&
  182.           verts[num].normal == tangentVerts[tangentVertexNum].normal)
  183.         {
  184.           // Copy the tangent over
  185.           verts[num].tangent = tangentVerts[tangentVertexNum].tangent;
  186.           // No more checks required, proceed with next vertex
  187.           break;
  188.         } // for if (verts[num].pos)
  189.     } // for (num)
  190.     someMesh.UnlockVertexBuffer();
  191.     dummyTangentGenerationMesh.UnlockVertexBuffer();
  192.   } // if (hadTangents)
  193.  
  194.   // Finally optimize the mesh for the current graphics cards vertex cache.
  195.   int[] adj = someMesh.ConvertPointRepsToAdjacency((GraphicsStream)null);
  196.   someMesh.OptimizeInPlace(MeshFlags.OptimizeVertexCache, adj);
  197. } // GenerateNormalsAndTangentsIfNotPresent(someMesh)

Here are the basics for the TangentVertex struct:

  1. public struct TangentVertex
  2. {
  3.   /// <summary>
  4.   /// Position
  5.   /// </summary>
  6.   public Vector3 pos;
  7.   /// <summary>
  8.   /// Texture coordinates
  9.   /// </summary>
  10.   public float u, v;
  11.   /// <summary>
  12.   /// Normal
  13.   /// </summary>
  14.   public Vector3 normal;
  15.   /// <summary>
  16.   /// Tangent
  17.   /// </summary>
  18.   public Vector3 tangent;
  19.  
  20.   [constructors, helper methods, etc.]
  21. } // struct TangentVertex

And finally the unit test for the GenerateNormalsAndTangentsIfNotPresent method.

  1. /// <summary>
  2. /// Test generate normals and tangents if not present method.
  3. /// </summary>
  4. [Test]
  5. public void TestGenerateNormalsAndTangentsIfNotPresent()
  6. {
  7.   // Create dummy DirectX device
  8.   new Graphics().InitGraphics(DirectXHelper.BuildDummyDeviceSettings());
  9.  
  10.   // Create 3 meshes, 1 sphere and 2 boxes
  11.   Mesh sphere = Mesh.Sphere(Graphics.GetDirectXDevice(), 1, 12, 12);
  12.   Mesh box1 = Mesh.Box(Graphics.GetDirectXDevice(), 1, 1, 1);
  13.   Mesh box2 = Mesh.Box(Graphics.GetDirectXDevice(), 1, 1, 1);
  14.  
  15.   // First test the sphere, we expect the same amount of vertices
  16.   // for the resulting mesh and it should include normal and tangent
  17.   // information.
  18.   int sphereVerticesBefore = sphere.NumberVertices;
  19.   GenerateNormalsAndTangentsIfNotPresent(ref sphere);
  20.   Assert.AreEqual(sphereVerticesBefore, sphere.NumberVertices);
  21.   // Get a tangent value and check if it is correct
  22.   TangentVertex[] verts =
  23.     (TangentVertex[])sphere.LockVertexBuffer(
  24.     typeof(TangentVertex),
  25.     LockFlags.None,
  26.     new int[1] { sphere.NumberVertices });
  27.   Assert.IsFalse(verts[0].normal.IsZero());
  28.   Assert.IsFalse(verts[0].tangent.IsZero());
  29.   sphere.UnlockVertexBuffer();
  30.  
  31.   // Check if declaration is correct
  32.   Assert.IsTrue(TangentVertex.IsTangentVertexDeclaration(
  33.     sphere.Declaration));
  34.  
  35.   // Now send box 1 to our method
  36.   int box1VerticesBefore = box1.NumberVertices;
  37.   GenerateNormalsAndTangentsIfNotPresent(ref box1);
  38.   // Number of vertices shouldn't have changed (each face has its own
  39.   // vertices, but they shouldn't be merged).
  40.   Assert.AreEqual(box1VerticesBefore, box1.NumberVertices,
  41.     "Box with each face having its own normals vertices count has " +
  42.     "changed. The vertices at the edges shouldn't be merged.");
  43.  
  44.   // The first normal should be (0, 0, -1) (the bottom of the box).
  45.   // The tangent for that should be (0, -1, 0)
  46.   verts =
  47.     (TangentVertex[])box1.LockVertexBuffer(
  48.     typeof(TangentVertex),
  49.     LockFlags.None,
  50.     new int[1] { box1.NumberVertices });
  51.   Assert.AreEqual(new Vector3(0, 0, -1), verts[0].normal);
  52.   Assert.AreEqual(new Vector3(0, -1, 0), verts[0].tangent);
  53.   box1.UnlockVertexBuffer();
  54.  
  55.   // Ok, it is time for box2. We are going to normalize it before
  56.   // sending it to our normal and tangent generation method to force
  57.   // this method to collapse all vertices with the same position and
  58.   // normal!
  59.   // ComputeNormals won't work because every mesh has its own vertices.
  60.   // We will just set all normals based on the vector to the origin.
  61.   box2 = box2.Clone(
  62.     box2.Options.Value,
  63.     CustomVertex.PositionNormal.Format,
  64.     Graphics.GetDirectXDevice());
  65.   CustomVertex.PositionNormal[] normalVerts =
  66.     (CustomVertex.PositionNormal[])box2.LockVertexBuffer(
  67.     typeof(CustomVertex.PositionNormal),
  68.     LockFlags.None,
  69.     new int[1] { box2.NumberVertices });
  70.   for (int num = 0; num < verts.Length; num++)
  71.   {
  72.     normalVerts[num].Normal = -normalVerts[num].Position;
  73.     normalVerts[num].Normal.Normalize();
  74.   } // for (num)
  75.   box2.UnlockVertexBuffer();
  76.  
  77.   int box2VerticesBefore = box2.NumberVertices;
  78.   Assert.AreEqual(24, box2VerticesBefore);
  79.   GenerateNormalsAndTangentsIfNotPresent(ref box2);
  80.   // Number of vertices should have changed, vertices can be collapsed.
  81.   Assert.IsFalse(box2VerticesBefore == box2.NumberVertices,
  82.     "Box 2 has duplicate vertices (count=" + box2.NumberVertices +
  83.     ") which can be collapsed!");
  84.   // We should now have only 8 vertices.
  85.   Assert.AreEqual(8, box2.NumberVertices);
  86. } // TestGenerateNormalsAndTangentsIfNotPresent()

Thats it. I hope after the introduction the source code is pretty self-explanatory. If you got any questions, just post a comment. I hope this helps if you have troubles with generating tangents, I'm just posting this because there is so few information about tangent generation around.