法向量纹理贴图

概述:实时渲染时,在建模过程中,我们会为每个顶点设置一个特定的法向量,在这shader的时候进行光照计算。而由顶点组成的三角形面会有一个自己的法向量,因为整个面只有一个法向量,光照的时候都是按照这个法向量进行计算,渲染出来的结果是这个面很平。为了增加细节的描述,我们会改变这一现状,使用一个存储了法向量信息的纹理图来描述这个平面上每个像素点的法向量。这样渲染出来的结果就是凹凸不平的了,增加了很多真实的细节。

 

1.切线空间 :

在一个曲面上的任何一点,都有一个自己的切平面,并且有一个法向量与切平面垂直。所以,每个顶点都只有一个法向量。切平面上任意两个不平行的切向量(网上都说是一个切向量和一个副法向量,但是我觉得都是切向量),与这个法向量组成一个空间坐标系,就叫切线空间。这也说明对于曲面上任意一点,它的切线空间是有无数个的,因为两个切向量是不固定的。

 

2.切线空间坐标系:

现在将你的上方(沿着默认法线方向)作为Z轴,你的右边就是X轴,前方就是Y轴,其实这里说了个右边和前方,你站在一个切面上何为你的右?其实这里应该理解成切面上两个相互垂直的轴,这两个轴定义为X轴和Y轴。

这个图很清晰的呈现了法线坐标系,其中(0,0,1)就是法向量,而(1,0,0)和(0,1,0)分别是切向量X轴和切向量Y轴。图中的NT(0.3,0,0.8)就是纹理存储的新的法向量,其中(x,y,z)是由RGB三色分别存储的。

 

3.法向量贴图:

其实法向量贴图的工作原理上面已经说的很清楚了,就是把平面上的某一点的原本垂直平面的发向量改为一个不垂直于平面的新向量,法向量的方向改变了,这个点的切平面的方向肯定也就跟着改变了,然后计算光照还是那种方式进行计算。那么要实现发向量贴图有几个需要面对的问题:

(1)NT(0.3,0,0.8)这个坐标里面的值都是float的,而纹理存储的R为0-255的整数值,在读取RGB的时候需要转换到(-1,1)这个区间。

(2)NT(0.3,0,0.8)坐标是存在于法向量坐标系里面的一个坐标,而计算光照的其它的像光的方向,视角的方向都是世界坐标系里面的坐标。如果要进行光照计算,就必须把它们转换到同一坐标系里面。

(3)在进行转换前,我们必须找出法向量坐标系与世界坐标系之间的关系,就是求出法向量坐标系里面的X,Y,Z轴在世界坐标系里面的描述。通常用TBN来进行描述,TBN是指tagent,binormal 和normal。

 

4.RGB与TBN的映射:

我们颜色值是0-255,在hlsl里面表示的是(0,1.0)的浮点,而TBN是(-1,1),所以我们面对的是(0,1)与(-1,1)的转换而不是(0,255)与(-1,1)的转换,转换公式:

 

color * 2.0f - 1.0f

 

 这样就得到了TBN法线坐标系的坐标值。

 

5.为什么法向量不直接存储成世界坐标系?

我在网上找资料的时候看到过这个问题以及和对这个问题的解释,包括有人把纹理坐标和法向量坐标系扯到一起了(就因为这个,我硬是没整明白)。想整明白这个问题,只需要一句话解决,“是先有世界,还是先有法向量纹理?”。把这句话推广,是先有世界里面的元素,还是先有世界?很显然是先有世界里面的元素。对于一个3D虚拟世界来说,世界就是我们所说的场景,而世界坐标也就是我们所说的场景坐标。

世界坐标的意义就是告诉我们,场景中的某个点有什么,如果这个点是模型的某一个顶点,那么它会有颜色,这个颜色除了RGB以外还有(U,V)坐标映射的一个颜色值,还有TBN映射的一个法向量值(最终会生成一个颜色)。总结:我们在场景里通过世界坐标系把模型组织起来,建立起模型之间的联系。然后模型通过(U,V)把纹理和法向量组联系起来,这里又回到一个问题,为啥不把法向量存储为相对坐标。相对坐标是相对于一个模型自己都不知道自己参照谁那个点为原点建立的一个坐标系,它不关心参照的(0,0,0)点到底是在那个位置,它关心的是它与它周围点之间的关系。

不把法向量存储为相对坐标,是因为它与世界建立的联系的规则与模型不一样,模型通过简单平移缩放旋转与世界建立关系,而法向量是另一套规则。法向量最终想转换为世界坐标系里面的向量,还需要参考法向量对应的点在世界坐标中的位置等信息。(我试图把自己说明白,结果发现自己也有点不清楚了,但是我觉得是对的,就像模型有点对世界的坐标系一样。相对坐标系是用的(X,Y,Z)轴来描述,但是它不是世界坐标,它需要进行转换。而TBN只不过名字叫这个,其实它也是一个相对坐标系,只不过它的转换规则与模型的不一样)

总结:所以我按正常思维来看,应该把法线由法向量坐标系转换到世界坐标系,人家模型都是把自己相对坐标转换为世界坐标,法线为啥就搞特殊呢。

 

6.TBN坐标系的计算:

一般是通过一个三角形面的三个顶点来计算出这个面的TBN,然后每个顶点的TBN是它所有面的TBN求平均值。计算原理:

 

p = p1 + (u - u1)T + (v - v1)B; 
p = p2 + (u - u2)T + (v - v2)B; 
p = p3 + (u - u3)T + (v - v3)B;
==>
p2 - p1 = (u2 - u1)T + (v2 - v1)B; 
p3 - p1 = (u3 - u1)T + (v3 - v1)B;
==>
T = [(v3 - v1)(p2 - p1) - (v2 - v1)(p3 - p1)]/[(u2 - u1)(v3 - v1) - (u3 - u1)(v2 - v1)];  
B = [(u3 - u1)(p2 - p1) - (u2 - u1)(p3 - p1)]/[(u3 - u1)(v2 - v1) - (u2 - u1)(v3 - v1)]; 
N = Corss(T,B);

 

 N是法向量永远垂直于平面,而TB可以是切平面上任意的两条垂直的向量,所以不引入一个第三方的量来确定其中的一个向量,另外两个都不好求。这里面使用p1,p2,p3的法向量纹理的(u,v)坐标来作为参量,求出TBN来。(那个三元一次方程组是根据什么来的,没想明白)。

有了TBN后就可使用下面的矩阵将切线空间下的点转换到对象空间: 

 

[Tx    Bx    Nx ]  
[Ty    By    Ny ]  
[Tz    Bz    Nz ]

 

 使用Gram-Schmidt算法对矩阵做正交规格化,然后可以使用下面的矩阵将一个方向向量转换到切线空间下:

 

[T'x    T'y    T'z]  
[B'x    B'y    B'z]  
[Nx     Ny     Nz ]
其中: 
T' = Normalized(T - Dot(N,T)*N); 
B' = Normalized(B - Dot(N,B)*N - Dot(T',B)*T'); 

 TBN矩阵是正交矩阵,它的转置矩阵就是逆矩阵。

 

由此可见,只要一个模型的顶点相对位置和纹理坐标不变,那么这个模型绘制出的各个像素点的切线空间就不会变。 

于是将法线贴图的法线信息存放在切线坐标系下更加保险,可以保证模型经过旋转和位移之后,法线贴图中的法线依旧正确。(借鉴的别的推导结果)

 

关键代码:

 

// vs
float3x3 tangentToObject;
tangentToObject[0] = normalize(input.viBinormal); 
tangentToObject[1] = normalize(input.viTangent); 
tangentToObject[2] = normalize(input.viNormal); 
output.piTangentToWorld = mul(tangentToObject, mbWorld); 
 
// ps
//从范围[0,1]转换到[-1,1]
float3 normalT = (normalTextureColor - 0.5f)*2.0f; 
float3 N = mul(normalT, input.piTangentToWorld); 

法向量贴图关键是概念的理解,实现起来的代码并不多。

 

你可能感兴趣的:(高级渲染)