Swin.C

 

[翻译]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)得到.

 

代码
#include "Vector4D.h"


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中使用时,固定用手习惯

 

代码
D3DXVECTOR3 bi;
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) );
}

 

 

 

posted on 2010-12-10 15:26  Swin.C  阅读(1059)  评论(1编辑  收藏  举报

导航