PDA

View Full Version : Tangent Space Vector for my Terrain mesh

Mars_999
01-12-2006, 04:43 PM
Ok, I have looked at Eric's code and since my math skills are long but gone been 5 years since Calculus and not sure what I can edit out of his code to just calculate tangent space from my vertex xyz and texture coordinate st... Or can I get away with calling my tangent vector 1,0,0 due to my terrain mesh normals are 0,1,0? I would like to make a function to calculate the tangent space vectors but as of now can't figure out what I need and don't need with Eric's code. Thanks for all the help.

fpo
01-13-2006, 03:48 AM
Here is a sample code from 3dsmax sdk. It calculates the tangent vectors for a single triangle (using 3 vertices and 3 texcoords as input).

So you first compute the tangent vectors for each trangle in your mesh (and normalize as max code does not return them normalized). Then for each vertex, average together the tangent vectors for all triangles using that vertex.

void ComputeBumpVectors(
const Point3 tv[3], // texture coords
const Point3 v[3], // vertex pos
Point3 tvec[2]) // result tangent vectors
{
float uva,uvb,uvc,uvd,uvk;
Point3 v1,v2;

uva = tv[1].x-tv[0].x;
uvb = tv[2].x-tv[0].x;

uvc = tv[1].y-tv[0].y;
uvd = tv[2].y-tv[0].y;

uvk = uvb*uvc - uva*uvd;

v1 = v[1]-v[0];
v2 = v[2]-v[0];

if (uvk!=0)
{
tvec[0] = (uvc*v2-uvd*v1)/uvk;
tvec[1] = (uva*v2-uvb*v1)/uvk;
}
else
{
if (uva!=0)
tvec[0] = v1/uva;
else if (uvb!=0)
tvec[0] = v2/uvb;
else
tvec[0] = Point3(0.0f,0.0f,0.0f);

if (uvc!=0)
tvec[1] = v1/uvc;
else if (uvd!=0)
tvec[1] = v2/uvd;
else
tvec[1] = Point3(0.0f,0.0f,0.0f);
}
}

Mars_999
01-15-2006, 05:30 PM
Thanks for the reply FPO but I am still working on the calculations and was wondering what tangent values should I be seeing if I have a normal of 0,1,0 or (0, .9, 0)?? Thanks

Zengar
01-15-2006, 08:43 PM
You probably have noticed that it fully depends on your texture coordinates. If I am not mistaken, for normals like (0, 1, 0) you will get a tangent like t2.u - t1.u, there t1.u, t2.1 are first texture coordinates of different vertices of your triangle.

knackered
01-16-2006, 10:08 AM
There are certain assumptions you can make about terrain height map grids, etc.
When doing my 'projected grid' water using texture sampling in the vertex shader, I had to calculate the tangent/binormal vectors in the vertex shader itself.
Here's the relevant bit of GLSL vertex shader code, in case you're interested:

const vec3 rightVec = vec3(1.0, 0.0, 0.0);
vec3 N = worldNormal;
vec3 T = cross(rightVec, N);
vec3 B = cross(T, N);So N is the normal, T is the tangent, B is the binormal.

Mars_999
01-16-2006, 03:17 PM
Here is my code so far, I am not getting the correct values as far as I can tell.

unsigned int x = 0;
unsigned int z = 0;
float deltaT0 = 0.0f;
float deltaT1 = 0.0f;
float deltaS0 = 0.0f;
float deltaS1 = 0.0f;
Vector3D sTangent = Vector3D(0.0f, 0.0f, 0.0f);
Vector3D tTangent = Vector3D(0.0f, 0.0f, 0.0f);
Vector3D side0 = Vector3D(0.0f, 0.0f, 0.0f);
Vector3D side1 = Vector3D(0.0f, 0.0f, 0.0f);
Vector3D tangentCross = Vector3D(0.0f, 0.0f, 0.0f);
Vector3D normal = Vector3D(0.0f, 0.0f, 0.0f);

for(z = 0; z < mapData.map_Z - 1; z++)
{
for(x = 0; x < mapData.map_X - 1; x++)
{
side0.x = terrain.mesh[(z + 1) * mapData.map_Z + x].vx - terrain.mesh[z * mapData.map_Z + x].vx;
side0.y = terrain.mesh[(z + 1) * mapData.map_Z + x].vy - terrain.mesh[z * mapData.map_Z + x].vy;
side0.z = terrain.mesh[(z + 1) * mapData.map_Z + x].vz - terrain.mesh[z * mapData.map_Z + x].vz;

side1.x = terrain.mesh[z * mapData.map_Z + x].vx - terrain.mesh[z * mapData.map_Z + x + 1].vx;
side1.y = terrain.mesh[z * mapData.map_Z + x].vy - terrain.mesh[z * mapData.map_Z + x + 1].vy;
side1.z = terrain.mesh[z * mapData.map_Z + x].vz - terrain.mesh[z * mapData.map_Z + x + 1].vz;

deltaT0 = (terrain.mesh[(z + 1) * mapData.map_Z + x].t * mapData.tileFactorTerrain) - (terrain.mesh[z * mapData.map_Z + x].t * mapData.tileFactorTerrain);
deltaT1 = (terrain.mesh[z * mapData.map_Z + x].t * mapData.tileFactorTerrain) - (terrain.mesh[z * mapData.map_Z + x + 1].t * mapData.tileFactorTerrain);

sTangent.x = deltaT1 * side0.x - deltaT0 * side1.x;
sTangent.y = deltaT1 * side0.y - deltaT0 * side1.y;
sTangent.z = deltaT1 * side0.z - deltaT0 * side1.z;

sTangent = sTangent.Normalize();

deltaS0 = (terrain.mesh[(z + 1) * mapData.map_Z + x].s * mapData.tileFactorTerrain) - (terrain.mesh[z * mapData.map_Z + x].s * mapData.tileFactorTerrain);
deltaS1 = (terrain.mesh[z * mapData.map_Z + x].s * mapData.tileFactorTerrain) - (terrain.mesh[z * mapData.map_Z + x + 1].s * mapData.tileFactorTerrain);

tTangent.x = deltaS1 * side0.x - deltaS0 * side1.x;
tTangent.y = deltaS1 * side0.y - deltaS0 * side1.y;
tTangent.z = deltaS1 * side0.z - deltaS0 * side1.z;

tTangent = tTangent.Normalize();

tangentCross = sTangent.CrossProduct(tTangent);

normal.x = terrain.mesh[z * mapData.map_Z + x].nx;
normal.y = terrain.mesh[z * mapData.map_Z + x].ny;
normal.z = terrain.mesh[z * mapData.map_Z + x].nz;

if(tangentCross.DotProduct(normal, tangentCross) < 0.0f)
{
sTangent.x *= -1.0f;
sTangent.y *= -1.0f;
sTangent.z *= -1.0f;
tTangent.x *= -1.0f;
tTangent.y *= -1.0f;
tTangent.z *= -1.0f;
}

terrain.mesh[z * mapData.map_Z + x].tx = tangentCross.x;
terrain.mesh[z * mapData.map_Z + x].ty = tangentCross.y;
terrain.mesh[z * mapData.map_Z + x].tz = tangentCross.z;

fout << "Normal x " << terrain.mesh[z * mapData.map_Z + x].nx << endl <<
"Normal y " << terrain.mesh[z * mapData.map_Z + x].ny << endl <<
"Normal z " << terrain.mesh[z * mapData.map_Z + x].nz << endl <<
"Tangent x " << terrain.mesh[z * mapData.map_Z + x].tx << endl <<
"Tangent y " << terrain.mesh[z * mapData.map_Z + x].ty << endl <<
"Tangent z " << terrain.mesh[z * mapData.map_Z + x].tz << endl;
}
}

knackered
01-17-2006, 06:31 AM
Ok, ok, I'll rephrase my answer using your weird maths library functions and structures:-

normal.x = terrain.mesh[z * mapData.map_Z + x].nx;
normal.y = terrain.mesh[z * mapData.map_Z + x].ny;
normal.z = terrain.mesh[z * mapData.map_Z + x].nz;

Vector3D tangent = Vector3D(1.0, 0.0, 0.0).CrossProduct(normal);
Vector3D binormal = tangent.CrossProduct(normal);If you don't need the binormal, just delete the binormal line.

Mars_999
01-17-2006, 10:07 AM
Originally posted by knackered:
Ok, ok, I'll rephrase my answer using your weird maths library functions and structures:-

normal.x = terrain.mesh[z * mapData.map_Z + x].nx;
normal.y = terrain.mesh[z * mapData.map_Z + x].ny;
normal.z = terrain.mesh[z * mapData.map_Z + x].nz;

Vector3D tangent = Vector3D(1.0, 0.0, 0.0).CrossProduct(normal);
Vector3D binormal = tangent.CrossProduct(normal);If you don't need the binormal, just delete the binormal line.And this way is going to work for terrain that is rough? Now what I am seeing for values is if my normal is 0,1,0 my tangent is 0,0,1 shouldn't the tangent be 1,0,0 and the bitangent be 0,0,1 Thanks for the help

knackered
01-17-2006, 12:18 PM
Tangent, bitangent, binormal, they're all just words. There's no rule that says the tangent should travel along the x axis or the z axis - in the end, they're just vectors to put in the upper 3x3 of the matrix.
If it bothers you, just rename the 'tangent' variable to 'bitangent', and rename 'binormal' to 'tangent'.
Oh, and of course it will work if the terrain is rough - why would I waste my/your time answering you if I was talking about a flat grid?!
If your normals are smooth (ie. vertex normals, not face normals), then your tangent vectors will be smooth too.
It works, trust me - I've used it lots of times.

fpo
01-17-2006, 01:46 PM
I think you should always make the tangent vector aligned with the U texture space direction and the binormal aligned with the V texture direction. There are infinite number of valid tangent/binormal vectors at a vertex but only one will match the texture mapping axis.

A few texture based effects requires tangent vectors to be aligned to the texture space axis. Like parallax mapping and relief mapping for example.

Mars_999
01-17-2006, 04:35 PM
Originally posted by fpo:
I think you should always make the tangent vector aligned with the U texture space direction and the binormal aligned with the V texture direction. There are infinite number of valid tangent/binormal vectors at a vertex but only one will match the texture mapping axis.

A few texture based effects requires tangent vectors to be aligned to the texture space axis. Like parallax mapping and relief mapping for example.I am using it for bumpmapping, parallax mapping and probably spherical harmonics eventually. But I am only planning on doing bumpmapping to the terrain and spherical harmonics. I know for my water I have one quad and the Normal is 0,1,0 and tangent is 1,0,0 and the binormal is 0,0,1. I am confused on what I am to be doing because Eric's tangent space calculations are for arbitrary meshs. So when would I need to use his example then if I don't need it for my terrain rendering? Sorry if I seem dense on this just it's not clicking...

knackered
01-18-2006, 01:30 AM
for arbitary meshes, of course.
That is, meshes where the topology of the surface is defined by arbitarily placed texture coordinates, placed by an artist, or a lod algorithm.
For a terrain, you know the topology of the surface.

I'm not sure if I'm using the word 'topology' in the correct context here, but you get the gist.

Mars_999
01-19-2006, 05:42 PM
Originally posted by knackered:
for arbitary meshes, of course.
That is, meshes where the topology of the surface is defined by arbitarily placed texture coordinates, placed by an artist, or a lod algorithm.
For a terrain, you know the topology of the surface.

I'm not sure if I'm using the word 'topology' in the correct context here, but you get the gist.Ok, if I understand you correctly I should do this

normal.x = terrain.mesh[z * mapData.map_Z + x].nx;normal.y = terrain.mesh[z * mapData.map_Z + x].ny;normal.z = terrain.mesh[z * mapData.map_Z + x].nz;Vector3D tangent = Vector3D(1.0, 0.0, 0.0).CrossProduct(normal);just take my normals per vertex and use the 1,0,0 for all my vertices in my terrain mesh and use the returned value from the cross product as my tangent vectors. :) Thanks for all the help and patience with me.

Zulfiqar Malik
01-19-2006, 08:23 PM
Sorry, i was busy earlier.
I haven't read the entire post so please forgive me if this has been mentioned in some earlier post. This method is applicable to regular meshes (e.g. terrains).
The ideal way of calculating terrain normals for any vertex matching a particular index within the height map is to take a cross product between two vectors which are calculated as follows:
Lets assume as h being the height map index of the vertex whose normal needs to be calculated. Let r be the height map value on the right of h (along +u tex-coord axis) and l be the value on the left of h (along -u tex-coord axis). Similarly let t be the height map value above (along +v) and b be the height map value below (along -v). Now since the mesh is regular therefore we have fixed offsets between the current vertex and the vertices on its four sides (left, top, right and bottom). Lets call the fixed distance between adjacent vertices along x-axis be x and along y-axis y. Then the non-normalized tangent will become (assuming x-axis points in the u direction and y-axis points in the v direction):

tangent = (x, 0, r - l)

Similarly the bitangent will become

bitangent = (0, y, t - b)

A cross between the normalized versions of these vectors will give you the normal. This way you can easily calculate normalized normals, tangents and bitangents for regular terrain meshes (using heightmaps).
Its quite easy to notice from the above equations that x and y are constants and the only variable factor is the z-axis. For a "well behaved" height map the difference between adjacent height values is never too large (assuming that there are no sudden spikes in the heightmap). You can actually calculate a look-up table (LUT) that pre-calculates normals, tangents and bitangents for different heightmap differences. This table doesn't take much space and can be arranged such that a quick difference in height map values gives an index to the array which contains the normals, tangents and bitangents.

Zulfiqar Malik
01-19-2006, 08:29 PM
Mars, your method of assuming (1, 0, 0) to be the bitangent is wrong and will definitely fail most of the time. It is quite evident that a normal such as (x, y, z) where x, y, z != 0 will give a vector that is not orthagonal to your bitangent and will thus give the wrong tangent.
In the equations i mentioned above my tangent is actually your bitangent, which i will re-iterate over here
tangent = (x, 0, r - l)
Your technique seems to work sometimes because your x = 1 (regular mesh) the difference between r and l is negligible i.e. not too large which gives a vector
tangent = (1, 0, diff) where diff -> 0
However on steep slopes, the difference will not be negligible and your method will fail to give proper normals.

Zulfiqar Malik
01-19-2006, 09:01 PM
Originally posted by Zulfiqar Malik

Your technique seems to work sometimes because your x = 1 (regular mesh) the difference between r and l is negligible i.e. not too large which gives a vector
tangent = (1, 0, diff) where diff -> 0

Sorry i meant that x = n (where n is some constant) therefore
tangent = (n, 0, diff) {diff -> 0}
normalizing it gives
tangent = (1, 0, diff) {diff -> 0}

Mars_999
01-20-2006, 03:55 PM
Thanks Zulfiqar Malik for the reply. So when you say I am assuming 1,0,0 for my parameter to send to my cross procduct function with my normal to get the tangent is wrong?

const vec3 rightVec = vec3(1.0, 0.0, 0.0);
vec3 N = worldNormal;
vec3 T = cross(rightVec, N);
vec3 B = cross(T, N);knackered posted that and assuming I can send 1,0,0 to a cross product with my normal and get a tangent vector for that vertex then... THanks

Zulfiqar Malik
01-20-2006, 06:30 PM
Yes, that's the wrong method as i explained earlier. But seems to work sometimes because of general terrain behavior. Consider this, your right vector is (1, 0, 0) and lets suppose your normal of a vertex that's part of a vertical surface is (-1, 0, 0). Taking a cross product gives a (0, 0, 0) tangent, which is clearly wrong.
Use the method i mentioned and your life will be a lot easier and the LUT speeds things up a lot. Btw, how are you calculating normals in the first place? Encoded in the texture?
If you are calculating normals at run-time, then the tangent and bitangent cross (as mentioned earlier) is the fastest method to my knowledge.

Mars_999
01-20-2006, 06:45 PM
Originally posted by Zulfiqar Malik:
Yes, that's the wrong method as i explained earlier. But seems to work sometimes because of general terrain behavior. Consider this, your right vector is (1, 0, 0) and lets suppose your normal of a vertex that's part of a vertical surface is (-1, 0, 0). Taking a cross product gives a (0, 0, 0) tangent, which is clearly wrong.
Use the method i mentioned and your life will be a lot easier and the LUT speeds things up a lot. Btw, how are you calculating normals in the first place? Encoded in the texture?
If you are calculating normals at run-time, then the tangent and bitangent cross (as mentioned earlier) is the fastest method to my knowledge.I am calculating them like the "OpenGL Game Programming" book shows how to by the staff at gamedev... I am away from my computer right now so I can't do much of anything till Sunday night. Thanks and I will try it out later this weekend.

knackered
01-21-2006, 08:44 AM
Well, of course there's the safe-guard of calculating the dot product between the normal and the 1,0,0, but I'd have come to that once he'd actually tried the simple bit.
However, the circumstance where the normal is aligned with the 1,0,0 vector is the circumstance where your texture will be stretched to the point where it would be unusable as a bumpmap anyway.

Zulfiqar Malik
01-21-2006, 09:23 AM
@knackered: Well, there is no need for hacks when there is a proper, documented method. This method gives you non-orthagonal tangent space vectors, which, among other things is just plain wrong. There is a better method and its the cheapest means of calculating the tangent space. I don't understand why one would opt a wrong method when the proper method is the cheapest method especially after the construction of a LUT which gets rid of the cross product.
The example was wild, because well-behaved meshes usually do not have totally vertical surfaces, but steep slopes are frequent, which can still give a lot of problems.

Mars_999
01-21-2006, 02:52 PM
Ok, I am still lost... What I have is an array that holds my terrain data for normals,vertex,tex coord,tangent. So with
this code

for(z = 0; z < mapData.map_Z - 1; z++)
{
for(x = 0; x < mapData.map_X - 1; x++)
{
//terrain[z * mapData.map_Z + x].nx //normals nx,ny,nz
//terrain[z * mapData.map_Z + x].vx //vertex vx,vy,vz
//terrain[z * mapData.map_Z + x].s // tex coord
//terrain[z * mapData.map_Z + x].t // tex coord
//terrain[z * mapData.map_Z + x].tx //tangent tx,ty,tz

//what should I be doing here...
}
}Ok, what I understand so far is vertex data means nothing normals and tex coords are whats needed...

knackered
01-22-2006, 12:28 PM
Originally posted by Zulfiqar Malik:
@knackered: Well, there is no need for hacks when there is a proper, documented method.Oh, I forgot you didn't read the whole thread.
Well, my method doesn't require knowledge of the neighbouring vertices, and so fits a vertex shader implementation.

BTW,

your method of assuming (1, 0, 0) to be the bitangent is wrong and will definitely fail most of the time.I assume you meant to say "will definitely fail in extreme circumstances".

Zulfiqar Malik
01-23-2006, 01:32 AM
Well, i don't know what exactly are you trying to do. But here is my code that calculates the normal LUT

for (int x = -g_halfHeight/2; x <= g_halfHeight/2; x++)
{
float fHeightX = (float)x;
vec3f vTangent = vec3f(fDist, 0, fHeightX);
vTangent.Normalize();
for (int y = -g_halfHeight/2; y <= g_halfHeight/2; y++)
{
float fHeightY = (float)y;
vec3f vBitangent = vec3f(0, fDist, fHeightY);
vBitangent.Normalize();
vec3f vNormal = vTangent.Cross(vBitangent);
vNormal.Normalize();
m_arrNormalLUT[x + g_maxHeight][y + g_maxHeight] = vNormal;
}
}where g_maxHeight is the maximum height that can be reached, 256 in my case. As far as your problem goes, g_halfHeight is the same i.e. 256. fDist is the distance along x-axis between adjacent vertices for tangent and along y-axis for bitangent. Its the same in my case.
Then you can easily do a lookup at runtime using:

int x = (heightPosX - heightNegX) + g_maxHeight;
int y = (heightPosY - heightNegY) + g_maxHeight;
return m_arrNormalLUT[x][y];Where heightPosX is the heightmap value on the right of current vertex and heightNegX is the value on the left. heightPosY is the value above and heightNegY is the value below the height map index of the current vertex.

Mars_999
01-23-2006, 08:48 AM
Thanks for the post Zulfiqar Malik. So the LUT is all the tangents for each vertex then or the normals, as you have sent the normal vector to the array to be updated... I have normals already calculated in my code. Thanks

Zulfiqar Malik
01-23-2006, 08:37 PM
Its the normals in my case but vTangent is the tangent and vBitangent is the bitangent. You can keep them in an array similar to the normals array at the exact same index as the normals. The lookup indices can then be used for those arrays as well (or you can make one small struct and keep all three in one array).