讲道理,这篇blog大概是我思考最久的主题了(大概花了2天的时间,期间找了很多的参考资料看),那么我现在在这里尝试着将它们说清楚。
首先是Normal Mapping法线贴图。
一般的,为了增加物体表面的细节,我们会使用法线贴图。为什么? 我们可以仔细想一下光照渲染中的各种步骤,除了ambient/diffuse, specular等纹理贴图采样,剩下的就是各种各样的光照效果,而这些光照效果都由什么计算出来呢:答案是光的位置,以及法线方向,还有视线(焦点)的位置。那么自然而然,我们就可以考虑更改原来贴图的法线方向来改变这些光照细节,从而使得渲染的效果更加棒(与其增加模型中多边形的数量,不如直接靠光照的明暗来营造细节)。
法线贴图存储高模图形的法线值,所谓高模图形,就是细节更加丰富的图形。我们可以将一些原本(已经渲染好的)图形拿出,把它们的法线方向(值)用颜色纹理的方式存起来,再给低模的图形用。
是的,用的是颜色的方式。在生成法线贴图的时候法线一般是指向正Z轴的(后面会讲到,一般法线贴图所处的空间被称作切空间Tangent Space, 也有的在object-space(既local space)或者world space中),也就是说它们的值偏向正Z轴(0, 0, 1),用颜色通道的方式存储的话就是淡蓝色(“blue-ish")。
事情到这里貌似结束了:我们给物体表面一个简单的(低模)材质贴图,然后再使用法线贴图使得物体表面细节更加的丰富(采样法线值,映射到[-1, 1],然后使用这个法线值进行各种计算)。
然而远没有这么简单,我们能用法线贴图正常进行光照渲染的前提是图形的(法线)方向和法线贴图的方向相同,一旦方向不同就会导致渲染出来的东西不符合逻辑(因为我们没有对(位于切线空间中)法线贴图里的法线方向进行变换,或者也可以暂时把这个法线贴图当作是从world space得来的)。
那么我们能用什么办法解决这个问题?(假设当前法线贴图是从世界空间中得到的,也就是说不能变换)很简单,我们只要将法线贴图改成从Object-space(local space)或者Tangent space中取得的就行了。
如果是Object space的话,法线贴图一般是五彩斑斓的(world space也是),此时我们可以通过将采样得到的normal值在Vertex Shader中进行坐标的变换(用model矩阵),之后的渲染结果是正常的。
而如果是Tangent space的话也能达到和Object space同样的渲染效果,但是Tangent space normal map不单单只是如此而已,对于一些发生形状改变(deform)的纹理(比如说用Tangent space normal map去渲染一个球形,而这个normal map原本只是从一个高模2D平面得到的)Tangent space normal map也可以做到添加细节。这就是牛叉之处了。
下面讲解Tangent space的知识:
首先Tangent Space是针对于一个渲染单元(图元Primitive)来说的,当然法线贴图一般也位于Tangent Space(这也就是说它是从高模的Tangent Space中获取的),这个空间的正Z半轴永远都是从里到外,我们称这个Z轴为N(Normal)轴,那么另外两个轴呢?一般来讲,对于简单的几何形状,我们可以自己定义剩下的两个轴(这两个轴分别叫做Tangent 和 Bitangent),只要它们相互垂直就行;而对于更加简单的三角形网格,我们可以考虑用模型顶点的位置坐标随着纹理坐标(u, v)的变化(Δu, Δv)作为切线空间剩下的两个轴(的方向)。如下图:
P.S.由于在切线空间中N轴都是从里向外的,因此我们可以只存储B T轴的方向,然后叉乘得到N轴。
那么怎么计算呢,很简单,用线性代数的相关知识就行了(TBN都为单位基向量):
[E1 = Delta U1overrightarrow{T} + Delta V1overrightarrow{B}]
[E2 = Delta U2overrightarrow{T} + Delta V2overrightarrow{B}]
写成坐标的形式就是:
[(E_{1x},E_{1y}, E_{1z}) = Delta U1(T_{x}, T_{y}, T_{z}) + Delta V1 (B_{x}, B_{y}, B_{z})]
[(E_{2x},E_{2y}, E_{2z}) = Delta U2(T_{x}, T_{y}, T_{z}) + Delta V2 (B_{x}, B_{y}, B_{z})]
再转化成矩阵的形式:
[egin{pmatrix} E_{1x} & E_{1y} & E_{1z}\ E_{2x} & E_{2y} & E_{2z} end{pmatrix} = egin{pmatrix} Delta U1 & Delta V1\ Delta U2 & Delta V2 end{pmatrix} egin{pmatrix} T_{x} & T_{y} & T_{z}\ B_{x} & B_{y} & B_{z} end{pmatrix}]
两边同乘以ΔUΔV的逆矩阵,得:
[egin{pmatrix} Delta U1 & Delta V1\ Delta U2 & Delta V2 end{pmatrix}^{-1} egin{pmatrix} E_{1x} & E_{1y} & E_{1z}\ E_{2x} & E_{2y} & E_{2z} end{pmatrix} = egin{pmatrix} T_{x} & T_{y} & T_{z}\ B_{x} & B_{y} & B_{z} end{pmatrix}]
将逆矩阵进一步化简(具体来说[A^{-1} = frac{1}{left | A ight |} A^{*}])可得:
[egin{pmatrix} T_{x} & T_{y} & T_{z}\ B_{x} & B_{y} & B_{z} end{pmatrix} = frac{1}{Delta U1 Delta V2 - Delta U2 Delta V1} egin{pmatrix} V2 & -V1 \ -U2 & U1 end{pmatrix} egin{pmatrix} E_{1x} & E_{1y} & E_{1z}\ E_{2x} & E_{2y} & E_{2z} end{pmatrix}]
有了这个等式之后,我们就可以用三角形的两条边(E1, E2)以及纹理坐标计算出单位基向量T和B了(N直接叉乘,或者不算都OK,因为N的值在Tangent space中固定为(0.0, 0.0, 1.0)。
算出来分量之后,我们需要去构造一个TBN矩阵(将其放到世界空间坐标里面,也就是将前面算出来的三个基向量分别乘以model矩阵)。
这样子,我们就得到了一个TBN轴坐标系,那么我们怎么去使用它呢?有两种方式:
1. 我们可以用得到的TBN矩阵左乘以法线贴图(中的法线)来将原本位于切线空间Tangent space中的法线转化到世界空间坐标,从而与光照向量进行正确的运算
2. 我们逆置TBN矩阵,左乘以世界空间中的相关的向量(这里相关的向量指的是光照计算中起作用的向量),从而将所有的相关向量变换到Tangent space中,同样也能进行正确的运算。
一般采用第二种方式,如果我们使用第一种方式,我们需要将每个从法线贴图中采样出来的法线变换到世界空间,这一步是在Fragment shader 中完成的,因为必须知道每个片段对应的的法线值,而不能简单的在顶点着色器中采样出来然后再插值到片段着色器中。如果我们使用第二种方式,我们会在 Vertex shader中把所需要的数据,比方说说平行光方向向量,顶点坐标,观察坐标变换到切线空间,然后在片段着色器中只需要采样出法线向量,不需要再进行其他转换就可以直接进行计算了。一般来说片段着色器执行的次数远大于顶点着色器执行的次数,所以第二种方式一般来说更高效。
尾话:
我们之前说过,Tangent space中的法线贴图可以应对变形的纹理,实际上就是因为我们可以将每个图元分别变换到Tangent Space中(当然是在UV纹理坐标的基础上)。比方说给出一个长方体(平面2D)的法线贴图,然后现在低模图形是一个球体;理论上球体纹理和法线贴图并不重合,那么在这里我们想象一下球体上的每个图元,并将其变换到和法线贴图同样的Tangent space中,那么此时我们就可以根据纹理坐标找到对应的法线贴图(的一部分,毕竟法线贴图还得根据纹理坐标采样),然后进行正确的光照计算。
参考资料:
[2]写给笨人的法线纹理贴图(里头有些值得商榷的地方,不过大体是对的)
[3]Gamasutra-Messing with Tangent Space
[4]切线空间的计算与应用