[翻译]Tangent Space Calculation
原文:http://www.terathon.com/code/tangent.html
bump mapping(也有被称为normal mapping)需要为网格的每一个顶点
计算其切平面基准向量(互相垂直的三个坐标轴)。
这篇文章描述了为几何三角形网格逐顶点计算切空间的理论
并提供了实现正确数学计算的源文件
数学推导
这个推导出现在
Mathematics for 3D Game Programming and Computer Graphics, 2nd ed., Section 6.8.
首先,我们需要将切空间与纹理坐标轴对齐。例如x坐标与bump map中的u方向对齐,
y坐标与bump map中的v方向对齐。那么,如果Q代表一个在三角形内的点,
我们可以写出这样的式子:
Q - P0 = ( u - u0 ) * T + ( v - v0 ) * B
T和B是与texture map对齐的切向量.P0是三角形的一个顶点,(u0,v0)是顶点的纹理坐标。
字母B代表了bitangent,也有因为在切空间bump mapping开始传播开时一些很复杂的原因
而被称为binormal。
我们有一个三角形,三个点分别为P0,P1,P2。然后,三个纹理坐标分别是
(u0,v0),(u1,v1),(u2,v2)。通过顶点P0,我们的计算可以相当简单,所以我们令
Q1 = P1 - P0
Q2 = P2 - P0
和
(s1, t1) = (u1 - u0, v1 - v0)
(s2, t2) = (u2 - u0, v2 - v0).
我们需要解以下的等式
Q1 = s1 * T + t1 * B
Q2 = s2 * T + t2 * B
这是一个有6个未知量和6条等式的线性系统(两个向量等式的x,y,z分量)。
我们可以写成一个matrix,如下.
(Q1)x , (Q1)y , (Q1)z s1 , t1 Tx , Ty , Tz
= *
(Q2)x , (Q2)y , (Q2)z s2 , t2 Bx , By , Bz
两边同时乘以(s,t)的逆矩阵,我们得到:
Tx , Ty , Tz t2 , -t1 (Q1)x , (Q1)y , (Q1)z
= 1 / ( s1 * t2 - s2 * t1 ) * *
Bx , By , Bz -s2 , s1 (Q2)x , (Q2)y , (Q2)z
这里可以得到P0,P1,P2所成三角形的T和B切向量(未单位化)。
要找到单个顶点的切向量,
我们对共享该顶点的所有三角形的切向量求平均,使用类似于求普通计算顶点法向量的方式。
在邻接三角形的texture mapping不连续的情况,沿边的顶点如果有不同的
mapping coordinates,都是已经复制出来的了。我们不需要对这些三角形的切向量也求平均,
因为这个结果对于任何一个三角形都不会准确代表bump map的方向。
一旦我们有了一个顶点的法向量N,切向量T和B,我们就可以使用matrix
从一个切空间转换到object空间
Tx , Bx , Nx
Ty , By , Ny
Tz , Bz , Nz
要沿一个逆方向转换(从object空间到tangent空间)
我们可以简单地使用上述的逆矩阵。
切向量不需要总是与法向量和另一个切向量互相垂直,所以这个矩阵的逆并不总是等于它的置换。
但是无论如何,我们可以安全地认为三个向量至少会接近于正交,所以使用
Gram-Schmidt算法去正交化他们不会造成不可接受的扭曲。
使用这个过程,可以得到新的切向量T'和B'(未单位化)
T' = T - ( N * T ) * N
B' = B - ( N * B ) * N - ( T' * B ) * T' / T'^2
单位化这三个向量,其中两个是作为顶点的tangent,bitangent,使用matrix保存。
T'x , T'y , T'z
B'x , B'y , B'z (*)
Nx , Ny , Nz
要把光源的方向从object空间转换到tangent空间。
需要将光线方向与bump map对应取样值做一个点乘,
然后计算正确的Lambertian漫反射光值。
使用一个额外的数组保存每个顶点的bitangent是不必要的,
因为叉乘N×T′可以用于获取mB',m=±1代表切空间的用手习惯。
m值必须每个顶点地保存因为这个bitangent B′可能从N×T′得到的方向是错误的。
这个m的值等于在等式(*)中矩阵的determinant(行列式值)。这个可以很方便地保存在
tangent vector T'的第4维中,也不是x,y,z,w中的w用于保存m值。然后
bitangent B′可以使用公式
B′ = T′w(N × T′)
计算得到。
这个叉乘需要忽略w坐标。
避免使用额外的数组去保存每个顶点的m值,这可以在顶点着色器中很好地工作。
Bitangent与Binormal的对比
这个词binormal普遍就于其名是第二个tangent direction般使用(这是与表面法向量与u切方向垂直)。
这是一个误称。这个词在曲线的研究中突然出现的,在有名的Frenet框架中表示一个在曲线上特别的点
在这个曲线中的点有一个单切方向和两个正交方向,就是normal和binormal。
但是当我们讨论点在一个坐标系统的某个表面上时,
这个点应该是有一个法向量和两个切向量,而这两个切向量应该称为tangent和bitangent;
源文件
以下代码生成一个在本地坐标系统上的4维tangent T,其坐标用手习惯保存在第四维w中,
用手习惯值为±1。这个bitangent vector B是从B = (N × T)得到.
struct Triangle
{
unsigned short index[3];
};
void CalculateTangentArray(long vertexCount, const Point3D *vertex,
const Vector3D *normal,
const Point2D *texcoord, long triangleCount,
const Triangle *triangle, Vector4D *tangent)
{
Vector3D *tan1 = new Vector3D[vertexCount * 2];
Vector3D *tan2 = tan1 + vertexCount;
ZeroMemory(tan1, vertexCount * sizeof(Vector3D) * 2);
for (long a = 0; a < triangleCount; a++)
{
long i1 = triangle->index[0];
long i2 = triangle->index[1];
long i3 = triangle->index[2];
const Point3D& v1 = vertex[i1];
const Point3D& v2 = vertex[i2];
const Point3D& v3 = vertex[i3];
const Point2D& w1 = texcoord[i1];
const Point2D& w2 = texcoord[i2];
const Point2D& w3 = texcoord[i3];
float x1 = v2.x - v1.x;
float x2 = v3.x - v1.x;
float y1 = v2.y - v1.y;
float y2 = v3.y - v1.y;
float z1 = v2.z - v1.z;
float z2 = v3.z - v1.z;
float s1 = w2.x - w1.x;
float s2 = w3.x - w1.x;
float t1 = w2.y - w1.y;
float t2 = w3.y - w1.y;
float r = 1.0F / (s1 * t2 - s2 * t1);
Vector3D sdir((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r);
Vector3D tdir((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r);
tan1[i1] += sdir;
tan1[i2] += sdir;
tan1[i3] += sdir;
tan2[i1] += tdir;
tan2[i2] += tdir;
tan2[i3] += tdir;
triangle++;
}
for (long a = 0; a < vertexCount; a++)
{
const Vector3D& n = normal[a];
const Vector3D& t = tan1[a];
// Gram-Schmidt orthogonalize
tangent[a] = (t - n * Dot(n, t)).Normalize();
// Calculate handedness
tangent[a].w = (Dot(Cross(n, t), tan2[a]) < 0.0F) ? -1.0F : 1.0F;
}
delete[] tan1;
}
代码在DX中使用时,固定用手习惯
if( D3DXVec3Dot( D3DXVec3Cross( &bi , &n , &t ) , &data[a].binormal ) > 0.0f )
{
D3DXVec3Normalize( &data[a].binormal , &data[a].binormal );
}
else
{
D3DXVec3Normalize( &data[a].binormal , &(-data[a].binormal) );
}