BlockLeftTop, PRELOAD BlockLeftBottom, PRELOAD BlockLeftStretch, PRELOAD BlockTop, PRELOAD BlockBottom, PRELOAD BlockStretch, PRELOAD BlockRightTop, PRELOAD BlockRightBottom, PRELOAD BlockRightStretch, PRELOAD

The Trick with Mesh.ComputeTangent

by Benjamin Nitschke 28. August 2005 02:58
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.");
  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;
  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)
  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;
  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;
  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;
  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, <, ++)
  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;
  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();
  101.   // Generate normals if this mesh hadn't any.
  102.   if (hadNormals == false)
  103.     someMesh.ComputeNormals();
  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();
  113.   // Position and normal should be the same (or nearly the same)
  114.   weldEpsilons.Position = 0.0001f;
  115.   weldEpsilons.Normal = 0.0001f;
  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);
  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
  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.
  141.     // Clone mesh just for tangent generation
  142.     Mesh dummyTangentGenerationMesh = someMesh.Clone(
  143.       someMesh.Options.Value,
  144.       someMesh.Declaration,
  145.       Graphics.GetDirectXDevice());
  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);
  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);
  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)
  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;
  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());
  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);
  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();
  31.   // Check if declaration is correct
  32.   Assert.IsTrue(TangentVertex.IsTangentVertexDeclaration(
  33.     sphere.Declaration));
  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.");
  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();
  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();
  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.

Raid explained.

by Benjamin Nitschke 26. August 2005 23:05
I just found this picture and I think it explains the different Raid modes for harddisk configurations pretty good.

Google Talk?

by Benjamin Nitschke 24. August 2005 11:10
Check out Google Talk (a another new voice talk program, which just came out today). Similar to Skype it allows you to chat and call your friends and talk to them. You will need a gmail account to login, the rest is simple.

Usually I like it when google makes everything plain and simple, but this program is the first I would say is way to simple and has way to less features. The quality is not as good as skype (it uses maybe half the bandwidth), the background noise reduction filter isn't very good (it is very noisy if noone is talking, even if the other party hasn't got a mic) and you don't have any way to set any volume, quality or threshold.
The chat feature looks way to boring too, there are no similies, everything is put right below the last messages, there are no timestamps, etc.

I don't like it, but its still beta, but I don't think it has anything to do with that, some programs are not made for google's plain and simple ideology. I hope they are not going to release any game soon :)

Pics from the Games Convention

by Benjamin Nitschke 22. August 2005 16:51
We are back from the Games Convention 2005 in Leipzig and took some pictures for you. Check them out here (or just click on the images).
I'm getting arrested


Even more babes

exDream entertainment at the Games Convention 2005

by Benjamin Nitschke 16. August 2005 13:15
You can find exDream entertainment (and obviously me too) at the  Games Convention 2005 in Leipzig (Germany) in Hall 2/E71 (Business Center, Northstar developers).

I was planning to write some other blog entries about recent developments as well, but I had not much time and a lot of troubles with one of my tooth (root canal resection was done 2 weeks ago and there is still a lot of pain going on, I'm only at 25% health, I guess I need some health packs).

However, one thing I worked on last weekend was Lua2IL, the homepage from the Lua .NET Guys (Roberto Ierusalimschy (founder of Lua), Renato Cequeira and Fabio Mascarenhas) was updated a couple of weeks back and you can download the Lua2IL Binary and Sourcecode files there and play around with them. I think this is very powerful stuff, you can now compile Lua bytecode directly to .NET IL code (and save it for example in a .dll assembly file) and use that directly from your .NET code. For example: I use currently a similar approach as LuaInterface, where all the Lua code is compiled natively and then executed with help of the Lua50.dll with some interop calls from .NET to call methods and set values. This is pretty nice for some smaller problems (some data stored, maybe some AI code), but for my fancy Effect-Particle-etc. System I wrote a while back for Lost Squadron it was quite a lot of work to syncronize the Lua code with the .NET code and everytime I changed some tiny bit of any table or method, I had to adjust some .NET caller code as well.

With Lua bytecode compiled directly to .NET IL and use it as a simple .NET assembly this problems are not longer an issue, because you can call methods directly now and use your .NET code in Lua much easier. Currently I've done just some simple tests and ported some old code to .NET 2.0, but the performance is also very nice (read about it here: PDF paper about Lua2IL from Fabio Mascarenhas, Roberto Ierusalimschy). When I have more time in the next couple of weeks I will hopefully implement more stuff in Lua and play with on the fly generated code a bit more.

Links of Lua2IL project:

Disclaimer: The opinions expressed in this blog are own personal opinions and do not represent the companies view.
© 2000-2011 exDream GmbH & MobileBits GmbH. All rights reserved. Legal/Impressum


Which platform should Soulcraft be released on next?

Show Results Poll Archive

Recent Games



Jobs @ exDream


<<  July 2014  >>