教程 26
法线贴图
原文: http://ogldev.atspace.co.uk/www/tutorial26/tutorial26.html
CSDN完整版专栏: http://blog.csdn.net/column/details/13062.html
背景
之前的我们的光线着色器类已经可以达到很不错的效果了,光线效果通过插值计算遍布到整个模型表面,使整个场景看上去比较真实,但这个效果还可以进行更好的优化。事实上,有时插值计算反而会影响场景的真实性,尤其是用材质来表现一些凹凸不平的纹理的时候,插值会让模型表面看上去太平坦了,例如下图的例子:
左边的图片效果明显比右边的要好,左边的更好的表现了石头纹理的崎岖不平,而右边的则看上去太平滑。左图的效果是使用一种叫做法线贴图(又称凹凸贴图)的技术渲染的,这边教程的主题就是这种技术。
之前我们是在三角形的三个顶点法向量之间进行平滑插值来得到三角形上每个点的法向量,这样表面会比较光滑,而法线贴图的思想是直接从‘法线贴图’上进行采样得到对应的法线方向,这样更加接近现实,因为绝大多数物体表面(尤其是游戏中)并不是那样光滑的,而是按照凹凸表面各个位置的不同方向来反射照过来的光线,不会像我们插值计算的那样平滑一致。对于每一张纹理贴图,那些表面上的所有法向量都是可以被计算并且存储在我们所谓的法线贴图里的。在片段着色器阶段进行光照计算的时候,每个像素的特定法线也是根据纹理坐标采样来获取使用。下面的图片展示了法线贴图和常规贴图中法线的不同:
现在我们有了我们的法线贴图,里面存储了真实的或者说至少是接近的表面法线。那我们可以简单的直接用它吗?不是的,考虑一下上面的带有砖块纹理的立方体,六个面贴有相同的纹理贴图,但是六张相同贴图的方向并不一样,所以对应的法向量在任意光线的照射下也应该是不一样的。如果我们直接使用同一张法线纹理,不做相应的修改,就会得到错误的结果,因为相同的法线应用到六个方向不同的表面上是不可能正确的!例如,上表面上的点的的法向量通常为(0,1,0),即使是非常凹凸不平的表面,对应底部表面上的点的法向量也都是(0,1,0)。重点是法向量是在它们私有的坐标空间中定义的,因此必要进行变换,使它们可以在世界坐标空间中正确的参与光照计算。某种意义上说,这个概念和顶点法向量的变换方式类似,顶点法向量定义在物体本地坐标空间下,然后我们通过用世界矩阵变换把他们转换到世界空间。
首先这里要定义法线的本地坐标系,坐标系需要三个正交单位向量。由于法线是2D纹理的的一部分,而2D纹理有两个正交单位向量U和V,因此通常做法是将X分量对应到U轴,而Y分量对应到V轴。注意U是从左到右的,而V是从下往上的(那个坐标系的原点位于纹理的左下角)。Z分量则是垂直于纹理,垂直于X和Y轴的了。
现在法向量就可以根据那个坐标系定义了,并存储在纹理的RGB文素中。注意即使是在非常凹凸不平的表面,我们仍然认为法线的方向是从纹理朝外的。例如:Z分量主导的一个分量,X和Y分量只能起到让其略微倾斜的作用。将XYZ向量存储在RGB文素中会使得法线纹理像下图那样偏蓝色:
下面这几个是这张法线纹理顶行的五个文素值(从左到右):(136,102,248), (144,122,255), (141,145,253), (102, 168, 244) 和 (34,130,216)。这里Z分量的主导性很明显。
接下来我们需要做的是检查模型中所有的三角面,并且按照每个顶点的纹理坐标匹配其在法线纹理上的坐标的方式,将法线纹理映射到每个三角面上。例如,如果给出的三角形的纹理坐标是(0.5,0), (1, 0.5) 和 (0,1)。那么法线纹理会按如下的方式放置:
上图中左下角的坐标系代表了物体的本地坐标系。
除了纹理坐标,三个顶点还有代表他们位置的本地空间3d坐标。当我们将纹理贴图放到上面的三角形上面后,我们实际上是给贴图的UV坐标赋了本地空间下的对应的值。如果现在我们计算出在物体本地坐标空间UV的值(同时通过U和V的差积计算出对应的法向量),我们将可以得到一个将法向量从贴图变换到物体本地坐标空间的变换矩阵,那样就可以继而变换到世界坐标空间并且参与光照计算。通常U向量我们称之为Tangent,而V向量我们称之为Bitangent,其中我们需要推导的的变换矩阵称为TBN矩阵(Tangent-Bitangent-Normal)。这些TBN向量定义了一个叫做Tangent(或者说纹理)空间的坐标系。所以,法线纹理中的法向量是存储在Tangent/纹理空间中的。接下来我们将研究如何计算物体空间下U和V向量的值。
现在看图中一个更加一般化的三角形,三角形三个顶点位于位置P0,P1和P2,对应纹理坐标为(U0,V0), (U1,V1) and (U2,V2):
我们要找到物体本地空间下的向量T(表示tangent)和B(表示bitangent)。我们可以看到两个三角形边E1和E2可以写成T和B的线性组合:
也可以写成下面的形式:
现在可以很容易的转换成矩阵公式的形式:
现在想把矩阵转换到等式的右边,为此可以两边乘以上面标红的矩阵的逆矩阵:
计算如下:
算出逆矩阵的值得到:
我们可以对网格中的每一个三角形执行上述过程,并且为每个三角形都计算出tangent向量和bitangent向量(对三角形的三个顶点来说这两个向量都是一样的)。通常的做法是为每一个顶点都保存一个tangent/bitangent值,每个顶点的tangent/bitangent值由共享这个顶点的所有三角面的平均tangent/bitangent值确定(这与顶点法线是一样的)。这样做的原因是使整个三角面的效果比较平滑,防止相邻三角面之间的不平滑过渡。这个坐标系空间的第三个分量——法线分量,是tangent和bitangent的叉乘积。这样Tangent-Bitangent-Normal三个向量就能作为纹理坐标空间的基向量,并且实现将法线由法线纹理空间到模型局部空间的转换。接下来需要做的就是将法线变换到世界坐标系之下,并使之参与光照计算。不过我们可以对此进行一点优化,即将Tangent-Bitangent-Normal坐标系变换到世界坐标系下来,这样我们就能直接将纹理中的法线变换到世界坐标系中去。
在这一节中我们需要做下面几件事:
- 将tangent向量传入到顶点着色器中;
- 将tangent向量变换到世界坐标系中并传入到片元着色器;
- 在片元着色器中使用tangent向量和法线向量(都处于世界坐标系下)来计算出bitangent向量;
- 通过tangent-bitangent-normal矩阵生成一个将法线信息变换到世界坐标系中的变换矩阵;
- 从法线纹理中采样得到法线信息;
- 通过使用上述的矩阵将法线信息变换到世界坐标系中;
- 继续和往常一样进行光照计算。
在我们的代码中有一点需要格外强调,在像素层次上,我们的tangent-bitangent-normal实际上并不是真正的正交基(三个单位向量互相垂直)。造成这种情况的原因有两个:首先对于每个顶点的tangent向量和法线向量,我们是通过对共享此顶点的所有三角面求平均值得到的;其次我们在像素层面看到的tangent向量和法线向量是经过光栅器插值得到的结果。这使得我们的tangent-bitangnet-normal矩阵丧失了他们的“正交特性”。但是为了将法线信息从纹理坐标系变换到世界坐标系我们需要一个正交基。解决方案是使用Gram-Schmidt进行处理。这个方案能够将一组基向量转换成正交基。这个方案大致如下:从基向量中选取向量 ‘A’ 并对其规范化,之后选取基向量中的向量 ‘B’ 并将其分解成两个分向量(两个分向量的和为‘B’),其中一个分向量沿着向量‘A’的方向,另一个分量则垂直于‘A’向量。现在用这个垂直于‘A’向量的分量替换‘B’向量并且对其规范化。按照这样的方法对所有基向量进行处理。
这样的最终结果是,虽然我们并没有使用数学上正确的TBN向量,但我们实现了必要的平滑效果,避免三角形边界上的明显间隔。
源代码详解
(mesh.h:33)
struct Vertex
{
Vector3f m_pos;
Vector2f m_tex;
Vector3f m_normal;
Vector3f m_tangent;
Vertex()