请勿转载.
clayman
Blog:http://blog.csdn.net/soilwork
clayman_joe@yahoo.com.cn
又一篇学习笔记,参考Mathematics for 3D Game Programming and Computer Graphics和ShaderX4上一篇关于tangent space计算的文章写的东西。对于计算时需要分裂顶点的内容看的还不是太清楚-_-b。另外,目前的算法还不能完美处理镜像或者在纹理不连续处可能出现的问题,就算在Farcry中,很多问题也是通过美工来“隐藏”的,再一次应证了之前对美工重要性的结论^^。
算法:
Tangent space在Bump Map中有着重要作用,通常需要把灯光转换到tangent space进行计算。对由参数方程计算出的规则曲面(比如,球体,圆环)来说,很容易通过方程计算出tangent space,但对任意的三角形网格来说,则没有那么简单。
Tangent space是一个三维空间。对3D空间中的一个顶点来说,切空间的三条座标轴分别对应该点的法线N,切线T,和副法线(binormal)B,显然,对不同的顶点来说,切空间是不同的。那么在已知三角形三个顶点及其纹理坐标的时候,如何计算出N,T,B呢?
目前已知的数据有三角形的三个顶点在世界坐标中的位置: P0, P1,P2, 以及相应的纹理坐标在纹理空间中的位置C0 (U0,V0),C1,C2,则有:
P10 = P1 – P0,
P20 = P2 - P1 ,
C10 = C1 – C0 = (U1-U0, V1-V0) = ( U10 ,V10)
C20 = C2 – C0.= (U2-U0, V2-V0) = ( U20 ,V20)
注意,P10在世界坐标中的方向和C10在纹理空间中的方向是一致的(这一点确实比较抽象,偶画图研究了好久才弄明白-_-),同样,P20和C20也是如此,发现这一点很重要,可以说是整个计算的基石。进一步来说,T,B分别和纹理坐标轴U,V是平行的。因此我们有:
P10 = U10T + V10B
P20 = U20T + V20B
把矢量展开得到:
两边乘以[C 10 C 20]的逆矩阵,最后得到
法线N = T x B
这样我们就得到了坐标从切空间转变到世界坐标下的变换矩阵M = [ T B N ],当然,更加常用的是M的逆矩阵。注意,这里计算得出的只是面法线,如果需要计算每个顶点的法线,则应该对共享该顶点的多个面的法线取均值,求出结果。
需求:
下面来看看在Farcry 1.3补丁和Crytek的Polybump Previewer中计算tangent space的具体实现:
首先,对这个计算tangent space的实现有以下要求:
1. 很容易集成到现有项目中:使用统一的借口,输入和输出数据的格式都应该是独立的。
2. 计算和内存占用都应该很高效:既能用于预处理模型,也适合实时程序在加载模型时计算。
3. 支持镜像:无论几何体或UV的镜像操作都会改变纹理空间中的三角形朝向(纹理空间中,三角形朝向可以是顺时针或逆时针)。
4. 最小化顶点分裂的数量:在镜像或纹理坐标不连续的区域都有可能需要分裂顶点。
5. 可预测的结果:三角形的顺序不会影响计算结果,结果只与直接相连的面有关(顶点法线将有邻接的面法线计算得出)。
6. 结果与面的镶嵌细节无关。
7. 支持tiling texture:纹理坐标不一定在0…1的范围之内。
8. 独立于应用程序的平滑/硬边定义:maya和3d max中定义边缘的方式是不一样的。
实现:
整个算法只需要实现TangentSpaceCalculation.h头文件(参考Polybump Previewer源代码)。程序中使用了一个代理类,因此只需要实现这个代理类就行了,已经写好的代码都能很方便的复用。
// derive your proxy from this class
// not virtual to save the call overhead
struct ITriangleInputProxy
{
unsigned int GetTriangleCount() const;
void GetTriangleIndices( const unsigned int indwTriNo, unsigned int outdwPos[3], unsigned int outdwNorm[3], unsigned int outdwUV[3] ) const;
void GetPos( const unsigned int indwPos, float outfPos[3] ) const;
void GetUV( const unsigned int indwPos, float outfUV[2] ) const;
};
注意计算并不需要法线信息,法线将是计算的副产品。但是需要为每个三角形返回法线的索引,以便定义硬边。执行计算的代码应该是这样(假设代理的名称为CtriangleInputProxy):
CTangentSpaceCalculation< CTriangleInputProxy > tangents;
CtriangleInputProxy input ( inoutData);
tangents.CalculateTangentSpace ( input );
返回数据的代码应该类似于这样:
unsigned int dwCnt = tangents.GetBaseCount();
unsigned int dwTriNo;
for ( dwTriNo=0; dwTriNo<dwTriCount;++dwTriNo)
{
unsigned int dwBaseIndex[3];
tangents.GetTriangleBaseIndices ( dwTriNo, dwBaseIndex);
//……store the base index for the 3 triangle corners
}
for( unsigned int dwC=0; dwC< dwCnt;++dwC)
{
float vU,vV,Vn;
tangents.GetBase ( dwC,vU,vV,vN);
//…store T or invert and store T_inv
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
地震了,国外网站基本不能访问了,郁闷中.....
csdn的blog还是那么烂,这都好几周了,还是不能传图片,偶在Flickr的图片空间这几天也访问不了,找个地方传图片还真是麻烦。-_-#.