切线空间计算

去年做normalmap的时候下载了一个crytek的ppt《Triangle mesh tangent space calculation》。

链接地址:http://www.crytek.com/cryengine/presentations/triangle-mesh-tangent-space-calculation

由于很多细节ppt上没有标注,直接看代码的时候很多地方都不清楚原理,不看原论文果然吃大亏。下面是部分相关的中文资料。



传统的做法

从Max中导出的数据包括三角形的数据以及顶点的数据,通常对于多个面共享的顶点都需要将这些点复制。对于一个点来说它的法线等于共享它的所有的面的法线之和,切线副法线也一样。

这样做理论上貌似可行,但是实际工程中常常碰到一些问题,例如uv镜像会造成法线贴图反向、uv严重旋转效果不理想的问题等等。

 

目标

针对传统做法的不足,论文中提出了以下目标:

1、容易集成、高效、鲁棒。

2、支持uv镜像

3、尽量少的顶点分裂

4、和Tesselation无关,解决L型问题。

5、支持UV大于1或者小于0

6、支持常见的法线贴图压缩格式

7、无浮点异常

8、易测试

9、能够支持光滑组

10、能够定义hard edge

 

分析问题

L型问题

L型问题指的是上图这样,有2个三角形共享顶点B,而有3个三角形共享顶点A,如果按照传统的方法来计算顶点法线的话,那么结果就是图中红色的法线。这个问题叫做Tesselation相关。

解决这个问题的方法是,计算出所有的面法线相加,同时需要用角度做权值,例如:B点的法线,使用面ABE的法线乘以∠ABE,加上面ABD的法线乘以∠ABD。

 

UV镜像的问题

如图,在UV空间中,三角形ABC和ACD关于AC对称的,A、C两点被两个三角形共享。

UV镜像是一个很简单的问题,一般来说如果一个顶点被N个面共享,那么这个顶点会分裂成N个。

CryEngine中需求是将顶点分裂减到最低,所以CryEngine中只有检测到一个顶点是经过了UV镜像才分裂。

解决镜像问题的第一个问题是:如何判断是否存在镜像关系?

A、C分别隶属于两个三角形,所以A、C分别都具有两组TBN。

方法:对于A点来说,取其中每一组TBN,计算法线和T、B叉乘后的夹角,分别比较这些夹角,如果的方向不一致,即点积后的符号不一致,那么就证明存在有uv镜像,需要分裂顶点。

第二个问题是,如何处理镜像的UV?

镜像的UV照成的结果是切线空间的手相性反了,解决的办法是在顶点中保存手相性,用+1或者-1保存即可,如果T和B的叉积和法线的点积小于0的话,就证明手相性反向了。

顶点中保存的手相性将会被传入到VertexShader中。

 

UV旋转问题

上图中三角形ABD,和三角形BCD是mesh中的2个三角形。如果美术在Max中使用UV修改器单独旋转了三角形BCD的UV,如果旋转度数比较大的话,需要分裂顶点。

如果检查到顶点的uv是由某一个uv旋转某个角度得来的,且该角度大于90度时,那么就需要分裂顶点。

判断uv旋转的方法是:

取三角面的T、B相加得到的向量HalfUV,将该向量绕着轴k旋转一个角度a,角度a是顶点法线和面法线的夹角,轴k是顶点法线和面法线的叉积结果。

将旋转后的HalfUV和三角形中(T+B)做点积,如果<0的话,就证明有大于90度的旋转。

 

计算流程

计算TBN的过程发生在CMeshCompiler::Compile中,这个函数中会进行多个操作,计算顶点的TBN只是其中一步,从函数的注释可见一斑:

// . Sort|Group faces by materials

// . Create vertex buffer with sequence of (possibly non-unique) vertices, 3 verts per face

// . For each (non-unique) vertex calculate the tangent base

// . Index the mesh (Compact Vertices): detect and delete duplicate vertices

// . Remove degenerated triangles in the generated mesh (GetIndices())

// . Sort vertices and indices for GPU cache

其中,计算顶点TBN分为了4个步骤

1、计算每个面的TBN

法线计算使用三角形两边的叉乘实现。计算切线副法线就是老掉牙的方法:

在切线空间中,三角形任意一个向量可以表示为TB线性拟合。


例如:


 
 

所以,


注意点:

1、如果求逆矩阵的时候,判别式=0,即uv重合的情况,要返回错误

2、切线和副法线要乘以三角形面积,意思是用三角形面积做权值。

 

2、计算每个顶点的法线

遍历每一个三角面,收集三角形中每一个顶点法线。计算顶点法线就是将共享该定点的所有三角面的法线相加,同时为了解决L型问题,

需要用三角形中顶点的两条边的夹角做权重乘以面法线。

 

这个过程中并不需要计算顶点分裂。

通过该过程,初步得出了所有顶点的法线。

接下来还需要计算每个顶点的T、B以及根据uv情况决定是否还要分裂顶点。

 

3、根据顶点T、B计算是否需要分裂顶点

遍历每一个三角面,计算三角形中每一个顶点的T、B。计算的方法和法线一样,将共享某个顶点的所有三角面的T或者B相加起来。

同时为了解决L型问题,需要用三角形中顶点的两条边的夹角做权重乘以面的T或者B。

 

这里需要根据每个顶点的T、B计算是否有UV镜像,如果有的话分裂顶点。

还需要计算是否有比较大的UV旋转,如果有的话也需要分裂顶点。

这一步完成以后,一个mesh的全部的顶点切线空间的基就计算完毕了,剩下的只是进行正交化和压缩了。

 

4、正交化

采用思密特正交化的方法。将u和v分别投影到n所在的平面,然后通过叉积计算新的n,最后归一化。

 

5、压缩、保存手相性

将每个32位浮点数压缩成16位的定点数,压缩的方式是:

_inline int16f tPackF2B(const float f)

{

return (int16f)(f * 32767.0f);

}

自然,解压缩的方式是:

_inline float tPackB2F(const int16f i)

{

return (float)((float)i / 32767.0f);

}

前面说过,当uv镜像时可能出现手相性的问题,需要给每个切线基保存一个符号,表示手相性。

if ((Tangent cross Binormal) dot TNormal < 0)

{

pBasises[i].Tangent.w =-1

    pBasises[i].Binormal.w = -1

}

杂项

论文的最后给出了几条关于法线贴图的Tips,我觉得这写Tips是整个文章中最有用的部分。

其中,最后一条提到,由于使用8位的贴图保存法线贴图,会造成精度丢失的问题,相当于是把-1到1的浮点数转化到8位定点数,

这照成的最大问题就是,当把采样出来的值再映射到-1到1的区间时,0这个值丢失掉了。这很容易证明:

保存法线贴图的时候,首先将数值转化到0到1,所以原本是0的值变成了0.5,然后再乘以255,丢弃掉小数就变成了127。

122这个数再转化为浮点数就就再也变不回0。

当法线贴图和坐标轴平行时,这个问题比较容易看出来。

所以,文章中建议在Shader中不要使用*2-1的方法而是使用*(255/128)-1的方法。

 

检查切线空间,用下面的图片检查切线空间是否连续

 

用下面的图片检查uv镜像、uv旋转的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值